Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-M2Crypto for openSUSE:Factory 
checked in at 2026-10-06 19:13:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-M2Crypto (Old)
 and      /work/SRC/openSUSE:Factory/.python-M2Crypto.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-M2Crypto"

Tue Oct  6 19:13:23 2026 rev:61 rq:1382336 version:0.50.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-M2Crypto/python-M2Crypto.changes  
2026-08-21 16:50:47.023543728 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-M2Crypto.new.3698736/python-M2Crypto.changes 
    2026-10-06 19:13:27.634058516 +0200
@@ -1,0 +2,16 @@
+Sun Oct  4 15:00:00 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.50.0:
+  * switch to Codefloe
+  * isolate OpenSSL providers in private contexts
+  * disable legacy ENGINE bindings with OpenSSL 3 deprecations
+  * manage BIO ownership when attaching to SSL connections
+  * repair Windows CI pipeline and improve setup.py robustness
+  * record security-related behavior changes
+  * emit DeprecationWarning when RC4 cipher is instantiated
+  * disable TLSv1.0 and TLSv1.1 by default
+  * prevent out-of-bounds read in BN conversions
+  * preserve errno from gettimeofday failures
+  * restore timed read retries
+
+-------------------------------------------------------------------

Old:
----
  m2crypto-0.49.0.tar.gz

New:
----
  m2crypto-0.50.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-M2Crypto.spec ++++++
--- /var/tmp/diff_new_pack.51N6JP/_old  2026-10-06 19:13:28.420091401 +0200
+++ /var/tmp/diff_new_pack.51N6JP/_new  2026-10-06 19:13:28.421091443 +0200
@@ -21,7 +21,7 @@
 %define skip_python312 1
 %{?sle15allpythons}
 Name:           python-M2Crypto
-Version:        0.49.0
+Version:        0.50.0
 Release:        0
 Summary:        Crypto and SSL toolkit for Python
 License:        BSD-2-Clause

++++++ m2crypto-0.49.0.tar.gz -> m2crypto-0.50.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/CHANGES new/m2crypto-0.50.0/CHANGES
--- old/m2crypto-0.49.0/CHANGES 2026-08-16 00:26:35.166126700 +0200
+++ new/m2crypto-0.50.0/CHANGES 2026-09-09 22:27:01.087097200 +0200
@@ -1,3 +1,18 @@
+0.50.0 - 2026-09-09
+-------------------
+
+- switch to Codefloe
+- isolate OpenSSL providers in private contexts
+- disable legacy ENGINE bindings with OpenSSL 3 deprecations
+- manage BIO ownership when attaching to SSL connections
+- repair Windows CI pipeline and improve setup.py robustness
+- record security-related behavior changes
+- emit DeprecationWarning when RC4 cipher is instantiated
+- disable TLSv1.0 and TLSv1.1 by default
+- prevent out-of-bounds read in BN conversions
+- preserve errno from gettimeofday failures
+- restore timed read retries
+
 0.49.0 - 2026-08-15
 -------------------
 
@@ -6,6 +21,9 @@
 release. Also, it resolves compatiblility with new Fedora OpenSSL 4.0
 and Swig 4.5.
 
+- prevent out-of-bounds reads in BIGNUM hexadecimal and decimal conversions
+- disable TLS 1.0 and TLS 1.1 by default
+- deprecate RC4 and warn when it is instantiated
 - defer unavailable ENGINE constant lookup
 - fixes problematic behaviour of pkcs11-provider
 - support TLS 1.0 contexts with modern OpenSSL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/PKG-INFO new/m2crypto-0.50.0/PKG-INFO
--- old/m2crypto-0.49.0/PKG-INFO        2026-08-16 00:26:58.389001100 +0200
+++ new/m2crypto-0.50.0/PKG-INFO        2026-09-09 22:27:40.382026200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: M2Crypto
-Version: 0.49.0
+Version: 0.50.0
 Summary: A Python crypto and SSL toolkit
 Author: Ng Pheng Siong
 Author-email: [email protected]
@@ -8,10 +8,10 @@
 Maintainer-email: [email protected]
 License: BSD-2-Clause
 Project-URL: home_page, https://sr.ht/~mcepl/m2crypto/
-Project-URL: source, https://codeberg.org/mcepl/m2crypto/
+Project-URL: source, https://codefloe.com/mcepl/m2crypto/
 Project-URL: documentation, https://m2crypto.readthedocs.io/en/latest/
 Project-URL: report, https://lists.sr.ht/~mcepl/m2crypto
-Project-URL: changelog, 
https://codeberg.org/mcepl/m2crypto/src/branch/master/CHANGES
+Project-URL: changelog, 
https://codefloe.com/mcepl/m2crypto/src/branch/master/CHANGES
 Keywords: cryptography,openssl
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
@@ -41,7 +41,7 @@
   <https://todo.sr.ht/~mcepl/m2crypto> (R/O, please\
   report issues via the email list).\
 **maintainer**: "Matěj Cepl"\
-**web-Site**: <https://codeberg.org/mcepl/m2crypto/>
+**web-Site**: <https://codefloe.com/mcepl/m2crypto/>
 
 # M2Crypto
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/README.md 
new/m2crypto-0.50.0/README.md
--- old/m2crypto-0.49.0/README.md       2026-08-16 00:26:35.166126700 +0200
+++ new/m2crypto-0.50.0/README.md       2026-09-09 22:27:01.088097300 +0200
@@ -5,7 +5,7 @@
   <https://todo.sr.ht/~mcepl/m2crypto> (R/O, please\
   report issues via the email list).\
 **maintainer**: "Matěj Cepl"\
-**web-Site**: <https://codeberg.org/mcepl/m2crypto/>
+**web-Site**: <https://codefloe.com/mcepl/m2crypto/>
 
 # M2Crypto
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/setup.cfg 
new/m2crypto-0.50.0/setup.cfg
--- old/m2crypto-0.49.0/setup.cfg       2026-08-16 00:26:58.390001000 +0200
+++ new/m2crypto-0.50.0/setup.cfg       2026-09-09 22:27:40.383026100 +0200
@@ -31,10 +31,10 @@
        Programming Language :: Python :: 3.12
 project_urls = 
        home_page = https://sr.ht/~mcepl/m2crypto/
-       source = https://codeberg.org/mcepl/m2crypto/
+       source = https://codefloe.com/mcepl/m2crypto/
        documentation = https://m2crypto.readthedocs.io/en/latest/
        report = https://lists.sr.ht/~mcepl/m2crypto
-       changelog = 
https://codeberg.org/mcepl/m2crypto/src/branch/master/CHANGES
+       changelog = 
https://codefloe.com/mcepl/m2crypto/src/branch/master/CHANGES
 
 [options]
 zip_safe = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/setup.py new/m2crypto-0.50.0/setup.py
--- old/m2crypto-0.49.0/setup.py        2026-08-16 00:26:35.173126700 +0200
+++ new/m2crypto-0.50.0/setup.py        2026-09-09 22:27:01.108097300 +0200
@@ -115,7 +115,7 @@
         else:
             crypt_lib = ctypes.cdll.LoadLibrary("libssl.so")
         log.info(f"crypt_lib = {crypt_lib}")
-    except (AttributeError, FileNotFoundError):
+    except Exception:
         ver = None
         file = os.path.join(ossldir, "include", "openssl", "opensslv.h")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/BIO.py 
new/m2crypto-0.50.0/src/M2Crypto/BIO.py
--- old/m2crypto-0.49.0/src/M2Crypto/BIO.py     2026-08-16 00:26:35.174126600 
+0200
+++ new/m2crypto-0.50.0/src/M2Crypto/BIO.py     2026-09-09 22:27:01.109097200 
+0200
@@ -31,6 +31,11 @@
         _close_cb: Optional[Callable] = None,
     ) -> None:
         self.bio = bio
+        # Whether this Python wrapper owns the native BIO pointer. A value of
+        # 1 makes __del__() call m2.bio_free() for self.bio; a value of 0
+        # means that another object, usually OpenSSL after a BIO is passed to
+        # SSL_set_bio(), owns the pointer. In the latter case the wrapper must
+        # stay alive if its pointer is still in use, but must not free it.
         self._pyfree = _pyfree
         self._close_cb = _close_cb
         self.closed = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/Provider.py 
new/m2crypto-0.50.0/src/M2Crypto/Provider.py
--- old/m2crypto-0.49.0/src/M2Crypto/Provider.py        2026-08-16 
00:26:35.174126600 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto/Provider.py        2026-09-09 
22:27:01.110097200 +0200
@@ -1,11 +1,14 @@
-"""
-M2Crypto wrapper for OpenSSL PROVIDER API.
+"""M2Crypto wrapper for the OpenSSL provider API.
+
+Each provider uses a private OpenSSL library context so its implementations do
+not affect algorithm selection in the process-wide default context.
 """
 
 import re
 from typing import Optional
 
 from M2Crypto import EVP, X509, m2
+from M2Crypto import types as C
 
 
 class ProviderError(ValueError):
@@ -27,29 +30,64 @@
         :param _id: A string identifying the OpenSSL provider to load.
         :raises ProviderError: If the provider fails to load.
         """
+        self._context = m2.provider_context_new()
+        self._supporting_providers = []
+        self._ptr = None
+        if self._context is None:
+            raise ProviderError("Failed to create an OpenSSL library context")
+
         try:
-            self._ptr = m2.provider_load(_id)
-        except ProviderError:
+            # Explicitly loading any provider disables OpenSSL's automatic
+            # loading of the default provider in this library context. Load
+            # the providers needed for encoders, digests, and other support
+            # operations alongside the requested provider.
+            for name in ("default", "base"):
+                if name != _id:
+                    supporting_provider = m2.provider_load(self._context, name)
+                    if supporting_provider is None:
+                        raise ProviderError(f"Failed to load OpenSSL provider 
'{name}'")
+                    self._supporting_providers.append(supporting_provider)
+
+            self._ptr = m2.provider_load(self._context, _id)
+            if self._ptr is None:
+                raise ProviderError(f"Failed to load OpenSSL provider '{_id}'")
+        except Exception:
+            self._close()
             raise
 
-        if self._ptr is None:
-            raise ProviderError(f"Failed to load OpenSSL provider '{_id}'")
-
     def __del__(self) -> None:
-        """
-        Clean up the provider by unloading it from memory.
-
-        Automatically called when the Provider object is garbage collected.
-        Ensures that the underlying OpenSSL provider is properly unloaded
-        to free system resources.
-        """
+        """Unload the providers and free their private library context."""
         try:
-            if getattr(self, "_ptr", None) is not None:
-                m2.provider_unload(self._ptr)  # type: ignore[arg-type]
+            self._close()
         except Exception:
             # Destructors must not raise.
             pass
 
+    def _close(self) -> None:
+        provider = getattr(self, "_ptr", None)
+        if provider is not None:
+            m2.provider_unload(provider)
+            self._ptr = None
+
+        supporting_providers = getattr(self, "_supporting_providers", [])
+        while supporting_providers:
+            m2.provider_unload(supporting_providers.pop())
+
+        context = getattr(self, "_context", None)
+        if context is not None:
+            m2.provider_context_free(context)
+            self._context = None
+
+    def _context_ptr(self) -> C.OSSL_LIB_CTX:
+        if self._context is None:
+            raise ProviderError("Provider is closed")
+        return self._context
+
+    def _provider_ptr(self) -> C.OSSL_PROVIDER:
+        if self._ptr is None:
+            raise ProviderError("Provider is closed")
+        return self._ptr
+
     def load_key(self, uri: str) -> EVP.PKey:
         """
         Load a private or public key from a provider using the given URI.
@@ -70,11 +108,13 @@
                 "Key URI should indicate " + "'type=private' or 'type=public'"
             )
 
-        cptr = m2.provider_load_key(uri)
+        cptr = m2.provider_load_key(self._context_ptr(), uri)
         if not cptr:
             raise ProviderError("Key or card not found")
 
-        return EVP.PKey(cptr, _pyfree=1)
+        pkey = EVP.PKey(cptr, _pyfree=1)
+        setattr(pkey, "_provider_owner", self)
+        return pkey
 
     def load_certificate(self, uri: str) -> X509.X509:
         """
@@ -88,11 +128,13 @@
         if not isinstance(uri, str):
             raise ProviderError(f"Wrong type {type(uri)} != str for uri")
 
-        cptr = m2.provider_load_certificate(uri)
+        cptr = m2.provider_load_certificate(self._context_ptr(), uri)
         if not cptr:
             raise ProviderError("Certificate or card not found")
 
-        return X509.X509(cptr, _pyfree=1)
+        certificate = X509.X509(cptr, _pyfree=1)
+        setattr(certificate, "_provider_owner", self)
+        return certificate
 
     def generate_rsa_key_pair(
         self, bits: int = 2048, exponent: int = 65537
@@ -110,11 +152,15 @@
         if not isinstance(exponent, int) or exponent < 3:
             raise ProviderError(f"Invalid exponent value: {exponent}")
 
-        cptr = m2.provider_generate_rsa_key_pair(bits, exponent, self._ptr)  # 
type: ignore[arg-type]
+        cptr = m2.provider_generate_rsa_key_pair(
+            bits, exponent, self._context_ptr(), self._provider_ptr()
+        )
         if not cptr:
             raise ProviderError("Failed to generate RSA key pair")
 
-        return EVP.PKey(cptr, _pyfree=1)
+        pkey = EVP.PKey(cptr, _pyfree=1)
+        setattr(pkey, "_provider_owner", self)
+        return pkey
 
     def generate_ec_key_pair(self, curve_name: str = "prime256v1") -> EVP.PKey:
         """
@@ -128,11 +174,15 @@
         if not isinstance(curve_name, str) or not curve_name:
             raise ProviderError(f"Invalid curve_name: {curve_name}")
 
-        cptr = m2.provider_generate_ec_key_pair(curve_name, self._ptr)  # 
type: ignore[arg-type]
+        cptr = m2.provider_generate_ec_key_pair(
+            curve_name, self._context_ptr(), self._provider_ptr()
+        )
         if not cptr:
             raise ProviderError("Failed to generate EC key pair")
 
-        return EVP.PKey(cptr, _pyfree=1)
+        pkey = EVP.PKey(cptr, _pyfree=1)
+        setattr(pkey, "_provider_owner", self)
+        return pkey
 
     def destroy_key(self, uri: str, user_pin: Optional[str] = None) -> None:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/RC4.py 
new/m2crypto-0.50.0/src/M2Crypto/RC4.py
--- old/m2crypto-0.49.0/src/M2Crypto/RC4.py     2026-08-16 00:26:35.174126600 
+0200
+++ new/m2crypto-0.50.0/src/M2Crypto/RC4.py     2026-09-09 22:27:01.110097200 
+0200
@@ -1,15 +1,34 @@
 """M2Crypto wrapper for OpenSSL RC4 API.
 
-Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved."""
+WARNING: RC4 is cryptographically broken (RFC 7465 prohibits its use
+in TLS).  This module is kept only for interoperability with legacy
+data formats and will be removed in a future release.  New code
+should use AES (e.g. EVP.Cipher with aes_256_gcm) instead.
+
+Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.
+"""
+
+import warnings
 
 from typing import Optional
 from M2Crypto import m2, types as C
 
 
 class RC4(object):
-    """Object interface to the stream cipher RC4."""
+    """Object interface to the stream cipher RC4.
+
+    .. deprecated:: 0.49
+       RC4 is insecure (RFC 7465); use AES instead.  Instantiating
+       this class emits a :class:`DeprecationWarning`.
+    """
 
     def __init__(self, key: Optional[bytes] = None) -> None:
+        warnings.warn(
+            "M2Crypto.RC4 is deprecated and will be removed in a future "
+            "release; RC4 is insecure (RFC 7465). Use AES instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         self.cipher = m2.rc4_new()
         if key:
             m2.rc4_set_key(self.cipher, key)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/SSL/Connection.py 
new/m2crypto-0.50.0/src/M2Crypto/SSL/Connection.py
--- old/m2crypto-0.49.0/src/M2Crypto/SSL/Connection.py  2026-08-16 
00:26:35.174126600 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto/SSL/Connection.py  2026-09-09 
22:27:01.111097000 +0200
@@ -62,6 +62,11 @@
         self._bio_freed = False
         self.ctx = ctx
         self.ssl: C.SSL = m2.ssl_new(self.ctx.ctx)
+        # SSL_set_bio() takes ownership of the native BIOs. Keep the Python
+        # wrappers alive while SSL owns their pointers, but prevent their
+        # destructors from freeing those pointers a second time.
+        self._readbio: Optional[BIO.BIO] = None
+        self._writebio: Optional[BIO.BIO] = None
         if sock is not None:
             self.socket = sock
         else:
@@ -115,6 +120,8 @@
             self.close()
         if self.ssl_close_flag == self.m2_bio_noclose and getattr(self, "ssl", 
None):
             self.m2_ssl_free(self.ssl)
+        self._readbio = None
+        self._writebio = None
 
     def close(self, freeBio: Optional[bool] = True) -> None:
         """
@@ -189,7 +196,13 @@
         :param readbio: BIO for reading
         :param writebio: BIO for writing.
         """
+        # SSL_set_bio() takes ownership of both native BIOs. The wrappers
+        # must remain alive, but must not free the pointers independently.
+        readbio._pyfree = 0
+        writebio._pyfree = 0
         m2.ssl_set_bio(self.ssl, readbio._ptr(), writebio._ptr())
+        self._readbio = readbio
+        self._writebio = writebio
 
     def set_client_CA_list_from_file(self, cafile: str) -> None:
         """Set the acceptable client CA list.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/SSL/Context.py 
new/m2crypto-0.50.0/src/M2Crypto/SSL/Context.py
--- old/m2crypto-0.49.0/src/M2Crypto/SSL/Context.py     2026-08-16 
00:26:35.174126600 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto/SSL/Context.py     2026-09-09 
22:27:01.111097000 +0200
@@ -67,7 +67,17 @@
         ctxmap()[id(self.ctx)] = self
         m2.ssl_ctx_set_cache_size(self.ctx, 128)
         if weak_crypto is None and protocol in ("sslv23", "tls"):
-            self.set_options(m2.SSL_OP_ALL | m2.SSL_OP_NO_SSLv2 | 
m2.SSL_OP_NO_SSLv3)
+            # SSLv2, SSLv3, TLS 1.0 and TLS 1.1 are all considered
+            # insecure and are disabled by default. Callers that need
+            # to interoperate with a legacy peer must pass weak_crypto=1
+            # when constructing the context.
+            self.set_options(
+                m2.SSL_OP_ALL
+                | m2.SSL_OP_NO_SSLv2
+                | m2.SSL_OP_NO_SSLv3
+                | m2.SSL_OP_NO_TLSv1
+                | m2.SSL_OP_NO_TLSv1_1
+            )
 
     def __del__(self) -> None:
         if getattr(self, "ctx", None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/__init__.py 
new/m2crypto-0.50.0/src/M2Crypto/__init__.py
--- old/m2crypto-0.49.0/src/M2Crypto/__init__.py        2026-08-16 
00:26:35.175126800 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto/__init__.py        2026-09-09 
22:27:01.112097300 +0200
@@ -18,7 +18,7 @@
 
 from typing import Any, Callable, Optional, Tuple, cast
 
-__version__: str = "0.49.0"
+__version__: str = "0.50.0"
 version: str = __version__
 
 VersionCtor: Optional[Callable[[str], Any]]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/_m2crypto.pyi 
new/m2crypto-0.50.0/src/M2Crypto/_m2crypto.pyi
--- old/m2crypto-0.49.0/src/M2Crypto/_m2crypto.pyi      2026-08-16 
00:26:35.175126800 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto/_m2crypto.pyi      2026-09-09 
22:27:01.112097300 +0200
@@ -62,6 +62,9 @@
 SSL_OP_NO_SSLv2: int
 SSL_OP_NO_SSLv3: int
 SSL_OP_NO_TLSv1: int
+SSL_OP_NO_TLSv1_1: int
+SSL_OP_NO_TLSv1_2: int
+SSL_OP_NO_TLSv1_3: int
 
 # SSL state flags
 SSL_ST_CONNECT: int
@@ -334,9 +337,9 @@
 def mpi_to_bn(s: bytes) -> C.BIGNUM: ...
 def bn_to_bin(bn: C.BIGNUM) -> bytes: ...
 def bin_to_bn(s: bytes) -> C.BIGNUM: ...
-def bn_to_hex(bn: C.BIGNUM) -> str: ...
-def hex_to_bn(s: str) -> C.BIGNUM: ...
-def dec_to_bn(s: str) -> C.BIGNUM: ...
+def bn_to_hex(bn: C.BIGNUM) -> bytes: ...
+def hex_to_bn(s: Union[bytes, bytearray, memoryview]) -> C.BIGNUM: ...
+def dec_to_bn(s: Union[bytes, bytearray, memoryview]) -> C.BIGNUM: ...
 def bn_rand(bits: int, top: int, bottom: int) -> int: ...
 def bn_rand_range(range_bn: int) -> int: ...
 
@@ -940,14 +943,21 @@
 
 # --- Provider ---
 def provider_init_error(provider_err: Type[BaseException]) -> None: ...
-def provider_load(name: str) -> Optional[C.OSSL_PROVIDER]: ...
-def provider_load_key(uri: str) -> Optional[C.EVP_PKEY]: ...
-def provider_load_certificate(uri: str) -> Optional[C.X509]: ...
+def provider_context_new() -> Optional[C.OSSL_LIB_CTX]: ...
+def provider_context_free(libctx: C.OSSL_LIB_CTX) -> None: ...
+def provider_load(libctx: C.OSSL_LIB_CTX, name: str) -> 
Optional[C.OSSL_PROVIDER]: ...
+def provider_load_key(libctx: C.OSSL_LIB_CTX, uri: str) -> 
Optional[C.EVP_PKEY]: ...
+def provider_load_certificate(libctx: C.OSSL_LIB_CTX, uri: str) -> 
Optional[C.X509]: ...
 def provider_unload(provider: C.OSSL_PROVIDER) -> None: ...
 def provider_generate_rsa_key_pair(
-    bits: int, exponent: int, provider: Optional[C.OSSL_PROVIDER]
+    bits: int,
+    exponent: int,
+    libctx: C.OSSL_LIB_CTX,
+    provider: Optional[C.OSSL_PROVIDER],
 ) -> Optional[C.EVP_PKEY]: ...
 def provider_generate_ec_key_pair(
-    curve_name: str, provider: Optional[C.OSSL_PROVIDER]
+    curve_name: str,
+    libctx: C.OSSL_LIB_CTX,
+    provider: Optional[C.OSSL_PROVIDER],
 ) -> Optional[C.EVP_PKEY]: ...
 def provider_destroy_key(uri: str, user_pin: Optional[str]) -> int: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto/types.py 
new/m2crypto-0.50.0/src/M2Crypto/types.py
--- old/m2crypto-0.49.0/src/M2Crypto/types.py   2026-08-16 00:26:35.175126800 
+0200
+++ new/m2crypto-0.50.0/src/M2Crypto/types.py   2026-09-09 22:27:01.113097200 
+0200
@@ -31,6 +31,7 @@
 EVP_PKEY_CTX = NewType("EVP_PKEY_CTX", object)
 EVP_PKEY = NewType("EVP_PKEY", object)
 HMAC_CTX = NewType("HMAC_CTX", object)
+OSSL_LIB_CTX = NewType("OSSL_LIB_CTX", object)
 OSSL_PROVIDER = NewType("OSSL_PROVIDER", object)
 PKCS7 = NewType("PKCS7", object)
 RC4_KEY = NewType("RC4_KEY", object)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/M2Crypto.egg-info/PKG-INFO 
new/m2crypto-0.50.0/src/M2Crypto.egg-info/PKG-INFO
--- old/m2crypto-0.49.0/src/M2Crypto.egg-info/PKG-INFO  2026-08-16 
00:26:58.315001500 +0200
+++ new/m2crypto-0.50.0/src/M2Crypto.egg-info/PKG-INFO  2026-09-09 
22:27:40.168025700 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: M2Crypto
-Version: 0.49.0
+Version: 0.50.0
 Summary: A Python crypto and SSL toolkit
 Author: Ng Pheng Siong
 Author-email: [email protected]
@@ -8,10 +8,10 @@
 Maintainer-email: [email protected]
 License: BSD-2-Clause
 Project-URL: home_page, https://sr.ht/~mcepl/m2crypto/
-Project-URL: source, https://codeberg.org/mcepl/m2crypto/
+Project-URL: source, https://codefloe.com/mcepl/m2crypto/
 Project-URL: documentation, https://m2crypto.readthedocs.io/en/latest/
 Project-URL: report, https://lists.sr.ht/~mcepl/m2crypto
-Project-URL: changelog, 
https://codeberg.org/mcepl/m2crypto/src/branch/master/CHANGES
+Project-URL: changelog, 
https://codefloe.com/mcepl/m2crypto/src/branch/master/CHANGES
 Keywords: cryptography,openssl
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
@@ -41,7 +41,7 @@
   <https://todo.sr.ht/~mcepl/m2crypto> (R/O, please\
   report issues via the email list).\
 **maintainer**: "Matěj Cepl"\
-**web-Site**: <https://codeberg.org/mcepl/m2crypto/>
+**web-Site**: <https://codefloe.com/mcepl/m2crypto/>
 
 # M2Crypto
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/SWIG/_engine.i 
new/m2crypto-0.50.0/src/SWIG/_engine.i
--- old/m2crypto-0.49.0/src/SWIG/_engine.i      2026-08-16 00:26:35.176126700 
+0200
+++ new/m2crypto-0.50.0/src/SWIG/_engine.i      2026-09-09 22:27:01.114097000 
+0200
@@ -9,17 +9,23 @@
  */
 %{
 #include <openssl/opensslconf.h>
+#include <openssl/opensslv.h>
 #ifndef OPENSSL_NO_ENGINE
+#if !defined(OPENSSL_NO_DEPRECATED_3_0) && OPENSSL_VERSION_NUMBER < 0x30500000L
 #include <openssl/engine.h>
 #endif
+#endif
 #include <openssl/ui.h>
 #include <stdio.h>
 #include <stdbool.h>
 %}
 
 %include <openssl/opensslconf.h>
+%include <openssl/opensslv.h>
 
-#if defined(OPENSSL_NO_ENGINE)
+#if defined(OPENSSL_NO_ENGINE) \
+        || defined(OPENSSL_NO_DEPRECATED_3_0) \
+        || OPENSSL_VERSION_NUMBER >= 0x30500000L
     %constant bool is_engine_available = false;
 #else
     %constant bool is_engine_available = true;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/SWIG/_lib.i 
new/m2crypto-0.50.0/src/SWIG/_lib.i
--- old/m2crypto-0.49.0/src/SWIG/_lib.i 2026-08-16 00:26:35.176126700 +0200
+++ new/m2crypto-0.50.0/src/SWIG/_lib.i 2026-09-09 22:27:01.114097000 +0200
@@ -566,25 +566,43 @@
 BIGNUM *hex_to_bn(PyObject *value) {
     Py_buffer vbuf;
     BIGNUM *bn;
+    char *tmp;
 
+    /* m2_PyObject_GetBufferInt also bounds vbuf.len <= INT_MAX, which
+     * matches what OpenSSL's BN_hex2bn can safely consume and keeps
+     * the +1 below from overflowing on a 32-bit size_t. */
+    if (m2_PyObject_GetBufferInt(value, &vbuf, PyBUF_SIMPLE) == -1)
+        return NULL;
 
-    if (m2_PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) == -1)
+    if ((bn = BN_new()) == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "hex_to_bn: BN_new");
+        m2_PyBuffer_Release(value, &vbuf);
         return NULL;
+    }
 
-    if ((bn=BN_new())==NULL) {
-        PyErr_SetString(PyExc_MemoryError, "hex_to_bn");
+    /* BN_hex2bn requires a NUL-terminated C string.  Python bytes
+     * (and bytearray slices, memoryview, etc.) are not guaranteed
+     * to be NUL-terminated, so copy into a temporary buffer that
+     * we control.  Embedded NULs will still truncate parsing -- that
+     * matches BN_hex2bn's documented behaviour. */
+    tmp = PyMem_Malloc((size_t)vbuf.len + 1);
+    if (tmp == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "hex_to_bn: tmp buffer");
+        BN_free(bn);
         m2_PyBuffer_Release(value, &vbuf);
         return NULL;
     }
+    memcpy(tmp, vbuf.buf, (size_t)vbuf.len);
+    tmp[vbuf.len] = '\0';
 
-    /* BN_hex2bn expects a null-terminated string/buffer */
-    /* We pass vbuf.buf assuming input is correctly formatted (e.g. bytes from 
hex string) */
-    if (BN_hex2bn(&bn, (const char *)vbuf.buf) <= 0) {
+    if (BN_hex2bn(&bn, tmp) <= 0) {
         m2_PyErr_Msg(PyExc_RuntimeError);
         BN_free(bn);
+        PyMem_Free(tmp);
         m2_PyBuffer_Release(value, &vbuf);
         return NULL;
     }
+    PyMem_Free(tmp);
     m2_PyBuffer_Release(value, &vbuf);
     return bn;
 }
@@ -592,23 +610,37 @@
 BIGNUM *dec_to_bn(PyObject *value) {
     Py_buffer vbuf;
     BIGNUM *bn;
+    char *tmp;
 
-    if (m2_PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) == -1)
+    if (m2_PyObject_GetBufferInt(value, &vbuf, PyBUF_SIMPLE) == -1)
         return NULL;
 
-    if ((bn=BN_new())==NULL) {
-      PyErr_SetString(PyExc_MemoryError, "dec_to_bn");
-      m2_PyBuffer_Release(value, &vbuf);
-      return NULL;
+    if ((bn = BN_new()) == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "dec_to_bn: BN_new");
+        m2_PyBuffer_Release(value, &vbuf);
+        return NULL;
     }
 
-    /* BN_dec2bn expects a null-terminated string/buffer */
-    if ((BN_dec2bn(&bn, (const char *)vbuf.buf) <= 0)) {
-      m2_PyErr_Msg(PyExc_RuntimeError);
-      BN_free(bn);
-      m2_PyBuffer_Release(value, &vbuf);
-      return NULL;
+    /* See hex_to_bn() above for the rationale behind the NUL-terminated
+     * temporary copy. */
+    tmp = PyMem_Malloc((size_t)vbuf.len + 1);
+    if (tmp == NULL) {
+        PyErr_SetString(PyExc_MemoryError, "dec_to_bn: tmp buffer");
+        BN_free(bn);
+        m2_PyBuffer_Release(value, &vbuf);
+        return NULL;
+    }
+    memcpy(tmp, vbuf.buf, (size_t)vbuf.len);
+    tmp[vbuf.len] = '\0';
+
+    if (BN_dec2bn(&bn, tmp) <= 0) {
+        m2_PyErr_Msg(PyExc_RuntimeError);
+        BN_free(bn);
+        PyMem_Free(tmp);
+        m2_PyBuffer_Release(value, &vbuf);
+        return NULL;
     }
+    PyMem_Free(tmp);
     m2_PyBuffer_Release(value, &vbuf);
     return bn;
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/SWIG/_provider.i 
new/m2crypto-0.50.0/src/SWIG/_provider.i
--- old/m2crypto-0.49.0/src/SWIG/_provider.i    2026-08-16 00:26:35.176126700 
+0200
+++ new/m2crypto-0.50.0/src/SWIG/_provider.i    2026-09-09 22:27:01.115097300 
+0200
@@ -17,6 +17,7 @@
 #include <openssl/provider.h>
 #include <openssl/params.h>
 #else
+typedef void OSSL_LIB_CTX;
 typedef void OSSL_PROVIDER;
 #endif
 %}
@@ -63,21 +64,32 @@
     }
 }
 
-EVP_PKEY *provider_load_key(const char *uri)
+EVP_PKEY *provider_load_key(OSSL_LIB_CTX *libctx, const char *uri)
 {
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+    (void)libctx;
+    (void)uri;
+    PyErr_SetString(provider_exc(), "OpenSSL provider API requires OpenSSL >= 
3.0");
+    return NULL;
+#else
     OSSL_STORE_CTX *store;
     OSSL_STORE_INFO *info;
     EVP_PKEY *key = NULL;
     int want_private = 0;
     int want_public = 0;
 
+    if (!libctx) {
+        PyErr_SetString(provider_exc(), "Invalid NULL library context");
+        return NULL;
+    }
+
     if (!uri) {
         raise_ossl_error(_provider_err, "Invalid NULL uri");
         return NULL;
     }
 
     ERR_clear_error();
-    store = OSSL_STORE_open(uri, NULL, NULL, NULL, NULL);
+    store = OSSL_STORE_open_ex(uri, libctx, NULL, NULL, NULL, NULL, NULL, 
NULL);
     if (store == NULL) {
         raise_ossl_error(_provider_err, "Failed to open store: %s", uri);
         return NULL;
@@ -180,21 +192,33 @@
     OSSL_STORE_close(store);
 
     return key;
+#endif
 }
 
-X509 *provider_load_certificate(const char *uri)
+X509 *provider_load_certificate(OSSL_LIB_CTX *libctx, const char *uri)
 {
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+    (void)libctx;
+    (void)uri;
+    PyErr_SetString(provider_exc(), "OpenSSL provider API requires OpenSSL >= 
3.0");
+    return NULL;
+#else
     OSSL_STORE_CTX *store;
     OSSL_STORE_INFO *info;
     X509 *cert = NULL;
 
+    if (!libctx) {
+        PyErr_SetString(provider_exc(), "Invalid NULL library context");
+        return NULL;
+    }
+
     if (!uri) {
         raise_ossl_error(_provider_err, "Invalid NULL uri");
         return NULL;
     }
 
     ERR_clear_error();
-    store = OSSL_STORE_open(uri, NULL, NULL, NULL, NULL);
+    store = OSSL_STORE_open_ex(uri, libctx, NULL, NULL, NULL, NULL, NULL, 
NULL);
     if (store == NULL) {
         raise_ossl_error(_provider_err, "Failed to open store: %s", uri);
         return NULL;
@@ -228,24 +252,54 @@
     OSSL_STORE_close(store);
 
     return cert;
+#endif
+}
+
+OSSL_LIB_CTX *provider_context_new(void)
+{
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+    PyErr_SetString(provider_exc(), "OpenSSL provider API requires OpenSSL >= 
3.0");
+    return NULL;
+#else
+    OSSL_LIB_CTX *libctx = OSSL_LIB_CTX_new();
+    if (libctx == NULL) {
+        raise_ossl_error(provider_exc(), "Failed to create OpenSSL library 
context");
+    }
+    return libctx;
+#endif
+}
+
+void provider_context_free(OSSL_LIB_CTX *libctx)
+{
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+    (void)libctx;
+#else
+    OSSL_LIB_CTX_free(libctx);
+#endif
 }
 
-OSSL_PROVIDER *provider_load(const char *name)
+OSSL_PROVIDER *provider_load(OSSL_LIB_CTX *libctx, const char *name)
 {
 #if OPENSSL_VERSION_NUMBER < 0x30000000L
+    (void)libctx;
     (void)name;
     PyErr_SetString(provider_exc(), "OpenSSL provider API requires OpenSSL >= 
3.0");
     return NULL;
 #else
     OSSL_PROVIDER *provider = NULL;
 
+    if (!libctx) {
+        PyErr_SetString(provider_exc(), "Invalid NULL library context");
+        return NULL;
+    }
+
     ERR_clear_error();
     /* Load providers */
 #if OPENSSL_VERSION_NUMBER >= 0x30500000L // OpenSSL 3.5.0
-    provider = OSSL_PROVIDER_load_ex(NULL, name, NULL);
+    provider = OSSL_PROVIDER_load_ex(libctx, name, NULL);
 #else
     /* Use the older function for OpenSSL < 3.5 */
-    provider = OSSL_PROVIDER_load(NULL, name);
+    provider = OSSL_PROVIDER_load(libctx, name);
 #endif
     if (!provider) {
         raise_ossl_error(provider_exc(), "Failed to load provider '%s'", name);
@@ -266,11 +320,12 @@
 #endif
 }
 
-EVP_PKEY *provider_generate_rsa_key_pair(int bits, int exponent, OSSL_PROVIDER 
*provider)
+EVP_PKEY *provider_generate_rsa_key_pair(int bits, int exponent, OSSL_LIB_CTX 
*libctx, OSSL_PROVIDER *provider)
 {
 #if OPENSSL_VERSION_NUMBER < 0x30000000L
     (void)bits;
     (void)exponent;
+    (void)libctx;
     (void)provider;
     PyErr_SetString(provider_exc(), "Key generation requires OpenSSL >= 3.0");
     return NULL;
@@ -283,6 +338,11 @@
     char propq[128];
     const char *pname = NULL;
 
+    if (libctx == NULL) {
+        PyErr_SetString(provider_exc(), "Invalid NULL library context");
+        return NULL;
+    }
+
     if (provider == NULL) {
         PyErr_SetString(provider_exc(), "Invalid NULL provider");
         return NULL;
@@ -299,7 +359,7 @@
     ERR_clear_error();
 
     /* Create a context for RSA key generation from the requested provider. */
-    ctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", propq);
+    ctx = EVP_PKEY_CTX_new_from_name(libctx, "RSA", propq);
     if (ctx == NULL) {
         raise_ossl_error(provider_exc(), "Failed to create RSA key generation 
context");
         return NULL;
@@ -355,10 +415,11 @@
 #endif
 }
 
-EVP_PKEY *provider_generate_ec_key_pair(const char *curve_name, OSSL_PROVIDER 
*provider)
+EVP_PKEY *provider_generate_ec_key_pair(const char *curve_name, OSSL_LIB_CTX 
*libctx, OSSL_PROVIDER *provider)
 {
 #if OPENSSL_VERSION_NUMBER < 0x30000000L
     (void)curve_name;
+    (void)libctx;
     (void)provider;
     PyErr_SetString(provider_exc(), "Key generation requires OpenSSL >= 3.0");
     return NULL;
@@ -372,6 +433,11 @@
 
     ERR_clear_error();
 
+    if (libctx == NULL) {
+        PyErr_SetString(provider_exc(), "Invalid NULL library context");
+        return NULL;
+    }
+
     if (!curve_name) {
         raise_ossl_error(provider_exc(), "Invalid NULL curve_name");
         return NULL;
@@ -391,7 +457,7 @@
     snprintf(propq, sizeof(propq), "provider=%s", pname);
 
     /* Create a context for EC key generation from the requested provider. */
-    ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", propq);
+    ctx = EVP_PKEY_CTX_new_from_name(libctx, "EC", propq);
     if (ctx == NULL) {
         raise_ossl_error(provider_exc(), "Failed to create EC key generation 
context");
         return NULL;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/src/SWIG/_ssl.i 
new/m2crypto-0.50.0/src/SWIG/_ssl.i
--- old/m2crypto-0.49.0/src/SWIG/_ssl.i 2026-08-16 00:26:35.176126700 +0200
+++ new/m2crypto-0.50.0/src/SWIG/_ssl.i 2026-09-09 22:27:01.115097300 +0200
@@ -246,6 +246,9 @@
 %constant int SSL_OP_NO_SSLv2             = 0x01000000L;
 %constant int SSL_OP_NO_SSLv3             = 0x02000000L;
 %constant int SSL_OP_NO_TLSv1             = 0x04000000L;
+%constant int SSL_OP_NO_TLSv1_1           = 0x10000000L;
+%constant int SSL_OP_NO_TLSv1_2           = 0x08000000L;
+%constant int SSL_OP_NO_TLSv1_3           = 0x20000000L;
 %constant int SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x00000800L;
 
 %constant int SSL_MODE_ENABLE_PARTIAL_WRITE = SSL_MODE_ENABLE_PARTIAL_WRITE;
@@ -555,7 +558,7 @@
 again:
     if (gettimeofday(&now, NULL) != 0) {
         // Should not happen, but handle defensively
-        PyErr_SetString(PyExc_OSError, "gettimeofday failed");
+        PyErr_SetFromErrno(PyExc_OSError);
         return -1;
     }
 
@@ -664,7 +667,7 @@
     // Get start time only if a timeout is specified
     if (has_timeout) {
         if (gettimeofday(&tv_start, NULL) != 0) {
-             PyErr_SetString(PyExc_OSError, "gettimeofday failed");
+             PyErr_SetFromErrno(PyExc_OSError);
              return NULL;
         }
     }
@@ -790,6 +793,11 @@
     int r;
     struct timeval tv;
 
+    if (timeout > 0 && gettimeofday(&tv, NULL) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        return NULL;
+    }
+
     if (!(buf = PyMem_Malloc(num))) {
         PyErr_SetString(PyExc_MemoryError, "ssl_read");
         return NULL;
@@ -825,14 +833,13 @@
                         goto cleanup;
                     }
 
-                    // NOTE: ssl_sleep_with_timeout must now check and enforce 
the timeout
-                    // This function should return 0 on success/retry, and -1 
on timeout or error
-                    if (ssl_sleep_with_timeout(ssl, &tv, timeout, ssl_err) == 
0) {
+                    // ssl_sleep_with_timeout returns 0 when the socket is
+                    // ready and -1 after setting an exception.
+                    if (ssl_sleep_with_timeout(ssl, &tv, timeout, ssl_err) != 
0) {
                         obj = NULL;
                         goto cleanup;
                     }
-                    // If it returns 0 (success/retry), loop continues.
-                    break;
+                    continue;
                 /* Some non-recoverable, fatal I/O error occurred. If this
                  * error occurs then no further I/O operations should be
                  * performed on the connection and SSL_shutdown() must not
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_bio_ssl.py 
new/m2crypto-0.50.0/tests/test_bio_ssl.py
--- old/m2crypto-0.49.0/tests/test_bio_ssl.py   2026-08-16 00:26:35.178126600 
+0200
+++ new/m2crypto-0.50.0/tests/test_bio_ssl.py   2026-09-09 22:27:01.120097200 
+0200
@@ -34,13 +34,13 @@
         try:
             ctx = SSL.Context()
             ctx.load_cert_chain("tests/server.pem")
-            conn = SSL.Connection(ctx)
+            self.conn = SSL.Connection(ctx)
             sslbio = BIO.SSLBio()
             readbio = BIO.MemoryBuffer()
             writebio = BIO.MemoryBuffer()
-            sslbio.set_ssl(conn)
-            conn.set_bio(readbio, writebio)
-            conn.set_connect_state()
+            sslbio.set_ssl(self.conn)
+            self.conn.set_bio(readbio, writebio)
+            self.conn.set_connect_state()
             sock.settimeout(_HANDSHAKE_TIMEOUT)
             sock.connect((self.host, self.port))
 
@@ -121,12 +121,12 @@
     def test_do_handshake_succeed(self):  # XXX leaks 196/26586 bytes
         ctx = SSL.Context()
         ctx.load_cert_chain("tests/server.pem")
-        conn = SSL.Connection(ctx)
-        self.sslbio.set_ssl(conn)
+        self.conn = SSL.Connection(ctx)
+        self.sslbio.set_ssl(self.conn)
         readbio = BIO.MemoryBuffer()
         writebio = BIO.MemoryBuffer()
-        conn.set_bio(readbio, writebio)
-        conn.set_accept_state()
+        self.conn.set_bio(readbio, writebio)
+        self.conn.set_accept_state()
         handshake_complete = False
         srv_port = allocate_srv_port()
         sock = socket.socket()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_bn.py 
new/m2crypto-0.50.0/tests/test_bn.py
--- old/m2crypto-0.49.0/tests/test_bn.py        2026-08-16 00:26:35.178126600 
+0200
+++ new/m2crypto-0.50.0/tests/test_bn.py        2026-09-09 22:27:01.120097200 
+0200
@@ -9,7 +9,7 @@
 import re
 import warnings
 
-from M2Crypto import BN, Rand
+from M2Crypto import BN, Rand, m2
 from tests import unittest
 
 loops = 16
@@ -71,8 +71,60 @@
             assert m.match(r)
 
 
+class BNConvTestCase(unittest.TestCase):
+    """Regression tests for m2.hex_to_bn / m2.dec_to_bn.
+
+    These wrap OpenSSL's BN_hex2bn / BN_dec2bn, which require a
+    NUL-terminated C string.  The Python wrappers must copy the
+    incoming buffer into a NUL-terminated scratch area; passing the
+    raw buffer pointer is an out-of-bounds read on hostile input.
+    """
+
+    def test_hex_to_bn_roundtrip(self):
+        bn = m2.hex_to_bn(b"deadbeef")
+        # bn_to_hex returns bytes (the .pyi annotation is incorrect)
+        self.assertEqual(m2.bn_to_hex(bn).upper(), b"DEADBEEF")
+
+    def test_dec_to_bn_roundtrip(self):
+        bn_dec = m2.dec_to_bn(b"3735928559")  # 0xDEADBEEF
+        bn_hex = m2.hex_to_bn(b"deadbeef")
+        self.assertEqual(m2.bn_to_bin(bn_dec), m2.bn_to_bin(bn_hex))
+
+    def test_hex_to_bn_non_nul_terminated(self):
+        """The bytearray slice has no trailing NUL: must not OOB-read.
+
+        Before the fix, BN_hex2bn would read past the end of the
+        buffer until it found a NUL or a non-hex byte.  After the fix,
+        the wrapper copies into its own NUL-terminated scratch buffer,
+        so the result must depend only on the slice contents.
+        """
+        big = bytearray(b"deadbeef" + b"ZZZZZZZZ")
+        view = memoryview(big)[:8]
+        bn = m2.hex_to_bn(view)
+        self.assertEqual(m2.bn_to_hex(bn).upper(), b"DEADBEEF")
+
+    def test_dec_to_bn_non_nul_terminated(self):
+        big = bytearray(b"12345" + b"9999")
+        view = memoryview(big)[:5]
+        bn = m2.dec_to_bn(view)
+        self.assertEqual(int(m2.bn_to_hex(bn), 16), 12345)
+
+    def test_hex_to_bn_rejects_str(self):
+        # Note: the underlying TypeError is wrapped in SystemError by
+        # SWIG's BIGNUM* output typemap (pre-existing behaviour).
+        with self.assertRaises((TypeError, SystemError)):
+            m2.hex_to_bn("deadbeef")
+
+    def test_dec_to_bn_rejects_str(self):
+        with self.assertRaises((TypeError, SystemError)):
+            m2.dec_to_bn("12345")
+
+
 def suite():
-    return unittest.TestLoader().loadTestsFromTestCase(BNTestCase)
+    t_suite = unittest.TestSuite()
+    t_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BNTestCase))
+    
t_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BNConvTestCase))
+    return t_suite
 
 
 if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_provider.py 
new/m2crypto-0.50.0/tests/test_provider.py
--- old/m2crypto-0.49.0/tests/test_provider.py  2026-08-16 00:26:35.179126700 
+0200
+++ new/m2crypto-0.50.0/tests/test_provider.py  2026-09-09 22:27:01.121097000 
+0200
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 """Test file for M2Crypto.Provider."""
 
+import gc
 import hashlib
 import os
 import platform
@@ -70,6 +71,17 @@
         openssl_module_pkcs11 = cls.getenv("M2CRYPTO_OPENSSL_MODULE_PKCS11")
         pkcs11_module_path = cls.getenv("M2CRYPTO_PKCS11_MODULE_PATH")
 
+        cls.original_environment = {
+            name: os.environ.get(name)
+            for name in (
+                "OPENSSL_CONF",
+                "OPENSSL_MODULES",
+                "SOFTHSM2_CONF",
+                "PKCS11_MODULE_PATH",
+                "PKCS11_PIN",
+            )
+        }
+
         # Use mkdtemp + explicit cleanup for Python 3.6+ compatibility.
         cls.tempdir = tempfile.mkdtemp(prefix="m2crypto-provider-")
         openssl_conf = os.path.join(cls.tempdir, "openssl.conf")
@@ -268,9 +280,24 @@
 
     @classmethod
     def tearDownClass(cls):
+        # Release provider-backed objects before unloading the provider and
+        # removing the module, token, and configuration files they use.
+        cls.privkey = None
+        cls.pubkey = None
+        cls.cert = None
+        cls.provider = None
+        gc.collect()
+
+        for name, value in cls.original_environment.items():
+            if value is None:
+                os.environ.pop(name, None)
+            else:
+                os.environ[name] = value
+
         # Best-effort cleanup; tests may create extra files under tempdir.
         if getattr(cls, "tempdir", None):
             shutil.rmtree(cls.tempdir, ignore_errors=True)
+            cls.tempdir = None
 
     def test_public_key_der_comparison(self):
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_rc4.py 
new/m2crypto-0.50.0/tests/test_rc4.py
--- old/m2crypto-0.49.0/tests/test_rc4.py       2026-08-16 00:26:35.179126700 
+0200
+++ new/m2crypto-0.50.0/tests/test_rc4.py       2026-09-09 22:27:01.121097000 
+0200
@@ -4,6 +4,8 @@
 
 Copyright (c) 2009 Heikki Toivonen. All rights reserved."""
 
+import warnings
+
 from M2Crypto import RC4, Rand
 from binascii import hexlify
 
@@ -30,20 +32,34 @@
             ),
         )
 
-        rc4 = RC4.RC4()
-        for key, plaintext, ciphertext in vectors:
-            rc4.set_key(key)
-            self.assertEqual(hexlify(rc4.update(plaintext)).upper(), 
ciphertext)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", DeprecationWarning)
+            rc4 = RC4.RC4()
+            for key, plaintext, ciphertext in vectors:
+                rc4.set_key(key)
+                self.assertEqual(hexlify(rc4.update(plaintext)).upper(), 
ciphertext)
 
-        self.assertEqual(rc4.final(), "")
+            self.assertEqual(rc4.final(), "")
 
     @unittest.skipIf(fips_mode, "Can't be run in FIPS mode")
     def test_bad(self):
         if fips_mode:
             return
-        rc4 = RC4.RC4(b"foo")
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", DeprecationWarning)
+            rc4 = RC4.RC4(b"foo")
         self.assertNotEqual(hexlify(rc4.update(b"bar")).upper(), b"45678")
 
+    @unittest.skipIf(fips_mode, "Can't be run in FIPS mode")
+    def test_deprecation_warning(self):
+        """RC4() must emit DeprecationWarning per RFC 7465."""
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always", DeprecationWarning)
+            RC4.RC4(b"key")
+        deprecations = [w for w in caught if issubclass(w.category, 
DeprecationWarning)]
+        self.assertEqual(len(deprecations), 1)
+        self.assertIn("RC4", str(deprecations[0].message))
+
 
 def suite():
     return unittest.TestLoader().loadTestsFromTestCase(RC4TestCase)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_ssl.py 
new/m2crypto-0.50.0/tests/test_ssl.py
--- old/m2crypto-0.49.0/tests/test_ssl.py       2026-08-16 00:26:35.179126700 
+0200
+++ new/m2crypto-0.50.0/tests/test_ssl.py       2026-09-09 22:27:01.122097300 
+0200
@@ -1211,12 +1211,13 @@
 class ChunkedHTTPSServer(threading.Thread):
     """Simple HTTPS server that sends a chunked response."""
 
-    def __init__(self, host, port, certfile, keyfile):
+    def __init__(self, host, port, certfile, keyfile, response_delay=0):
         super().__init__(daemon=True)
         self.host = host
         self.port = port
         self.certfile = certfile
         self.keyfile = keyfile
+        self.response_delay = response_delay
         self.ready = threading.Event()
         self.stop_event = threading.Event()
 
@@ -1224,6 +1225,7 @@
         self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 
1)
         self.server_socket.bind((self.host, self.port))
+        self.port = self.server_socket.getsockname()[1]
         self.server_socket.listen(1)
         self.server_socket.settimeout(0.5)
 
@@ -1247,6 +1249,9 @@
                 # Read the request
                 request = ssl_conn.recv(4096)
 
+                if self.response_delay:
+                    time.sleep(self.response_delay)
+
                 # Send properly formatted chunked response
                 response = (
                     b"HTTP/1.1 200 OK\r\n"
@@ -1279,6 +1284,39 @@
             pass
 
 
+class SSLReadTimeoutTestCase(unittest.TestCase):
+    """Test SSL reads which must wait for the socket to become ready."""
+
+    def setUp(self):
+        self.server = ChunkedHTTPSServer(
+            srv_bind_host,
+            0,
+            certfile="tests/server.pem",
+            keyfile="tests/server_key.pem",
+            response_delay=0.2,
+        )
+        self.server.start()
+        if not self.server.ready.wait(timeout=5):
+            self.fail("HTTPS server failed to start")
+
+    def tearDown(self):
+        self.server.stop()
+        self.server.join(timeout=2)
+
+    def test_read_retries_after_socket_becomes_ready(self):
+        ctx = SSL.Context()
+        conn = SSL.Connection(ctx)
+        try:
+            conn.connect((srv_host, self.server.port))
+            conn.write(b"GET / HTTP/1.0\r\n\r\n")
+            conn.settimeout(2.0)
+
+            self.assertEqual(conn.read(1), b"H")
+        finally:
+            conn.close()
+            ctx.close()
+
+
 class Urllib2TEChunkedSSLClientTestCase(BaseSSLClientTestCase):
     """Test a response with "Transfer-Encoding: chunked"."""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/m2crypto-0.49.0/tests/test_ssl_offline.py 
new/m2crypto-0.50.0/tests/test_ssl_offline.py
--- old/m2crypto-0.49.0/tests/test_ssl_offline.py       2026-08-16 
00:26:35.179126700 +0200
+++ new/m2crypto-0.50.0/tests/test_ssl_offline.py       2026-09-09 
22:27:01.122097300 +0200
@@ -7,7 +7,7 @@
 
 import doctest
 
-from M2Crypto import Rand, SSL, X509
+from M2Crypto import Rand, SSL, X509, m2
 from tests import unittest
 from tests.test_ssl import srv_host
 
@@ -56,6 +56,36 @@
         store = ctx.get_cert_store()
         self.assertIsInstance(store, X509.X509_Store)
 
+    def test_default_options_disable_legacy_protocols(self):
+        """Default Context() must disable SSLv2, SSLv3, TLS 1.0 and TLS 1.1.
+
+        Regression test for the security-audit finding that TLS 1.0 / 1.1
+        were enabled by default.
+        """
+        # set_options(0) returns the currently-set bitmask without
+        # changing it (OR-ing in zero is a no-op).
+        ctx = SSL.Context()
+        opts = ctx.set_options(0)
+        for flag_name in (
+            "SSL_OP_NO_SSLv2",
+            "SSL_OP_NO_SSLv3",
+            "SSL_OP_NO_TLSv1",
+            "SSL_OP_NO_TLSv1_1",
+        ):
+            flag = getattr(m2, flag_name)
+            self.assertTrue(
+                opts & flag,
+                "Default Context() must have %s set (mask=0x%x)" % (flag_name, 
opts),
+            )
+
+    def test_weak_crypto_keeps_legacy_protocols(self):
+        """Context(weak_crypto=1) preserves the legacy escape hatch."""
+        ctx = SSL.Context(weak_crypto=1)
+        opts = ctx.set_options(0)
+        # weak_crypto=1 must not gratuitously set the NO_TLSv1* bits.
+        self.assertFalse(opts & m2.SSL_OP_NO_TLSv1)
+        self.assertFalse(opts & m2.SSL_OP_NO_TLSv1_1)
+
 
 def suite():
     t_suite = unittest.TestSuite()

Reply via email to