Skip to content

Commit a1b4f7f

Browse files
authored
Attempt to prohibit mutating a Context after its in use (#1416)
1 parent 56d20e7 commit a1b4f7f

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

CHANGELOG.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ Changelog
44
Versions are year-based with a strict backward-compatibility policy.
55
The third digit is only for regressions.
66

7+
UNRELEASED
8+
----------
9+
10+
Backward-incompatible changes:
11+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
Deprecations:
14+
^^^^^^^^^^^^^
15+
16+
- Attempting using any methods that mutate an ``OpenSSL.SSL.Context`` after it
17+
has been used to create an ``OpenSSL.SSL.Connection`` will emit a warning. In
18+
a future release, this will raise an exception.
19+
20+
Changes:
21+
^^^^^^^^
22+
23+
724
25.0.0 (2025-01-12)
825
-------------------
926

src/OpenSSL/SSL.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,26 @@ class Session:
827827
_session: Any
828828

829829

830+
F = TypeVar("F", bound=Callable[..., Any])
831+
832+
833+
def _require_not_used(f: F) -> F:
834+
@wraps(f)
835+
def inner(self: Context, *args: Any, **kwargs: Any) -> Any:
836+
if self._used:
837+
warnings.warn(
838+
(
839+
"Attempting to mutate a Context after a Connection was "
840+
"created. In the future, this will raise an exception"
841+
),
842+
DeprecationWarning,
843+
stacklevel=2,
844+
)
845+
return f(self, *args, **kwargs)
846+
847+
return typing.cast(F, inner)
848+
849+
830850
class Context:
831851
"""
832852
:class:`OpenSSL.SSL.Context` instances define the parameters for setting
@@ -870,6 +890,7 @@ def __init__(self, method: int) -> None:
870890
context = _ffi.gc(context, _lib.SSL_CTX_free)
871891

872892
self._context = context
893+
self._used = False
873894
self._passphrase_helper: _PassphraseHelper | None = None
874895
self._passphrase_callback: _PassphraseCallback[Any] | None = None
875896
self._passphrase_userdata: Any | None = None
@@ -898,6 +919,7 @@ def __init__(self, method: int) -> None:
898919
self.set_min_proto_version(version)
899920
self.set_max_proto_version(version)
900921

922+
@_require_not_used
901923
def set_min_proto_version(self, version: int) -> None:
902924
"""
903925
Set the minimum supported protocol version. Setting the minimum
@@ -911,6 +933,7 @@ def set_min_proto_version(self, version: int) -> None:
911933
_lib.SSL_CTX_set_min_proto_version(self._context, version) == 1
912934
)
913935

936+
@_require_not_used
914937
def set_max_proto_version(self, version: int) -> None:
915938
"""
916939
Set the maximum supported protocol version. Setting the maximum
@@ -924,6 +947,7 @@ def set_max_proto_version(self, version: int) -> None:
924947
_lib.SSL_CTX_set_max_proto_version(self._context, version) == 1
925948
)
926949

950+
@_require_not_used
927951
def load_verify_locations(
928952
self,
929953
cafile: _StrOrBytesPath | None,
@@ -971,6 +995,7 @@ def wrapper(size: int, verify: bool, userdata: Any) -> bytes:
971995
FILETYPE_PEM, wrapper, more_args=True, truncate=True
972996
)
973997

998+
@_require_not_used
974999
def set_passwd_cb(
9751000
self,
9761001
callback: _PassphraseCallback[_T],
@@ -1004,6 +1029,7 @@ def set_passwd_cb(
10041029
)
10051030
self._passphrase_userdata = userdata
10061031

1032+
@_require_not_used
10071033
def set_default_verify_paths(self) -> None:
10081034
"""
10091035
Specify that the platform provided CA certificates are to be used for
@@ -1079,6 +1105,7 @@ def _fallback_default_verify_paths(
10791105
self.load_verify_locations(None, capath)
10801106
break
10811107

1108+
@_require_not_used
10821109
def use_certificate_chain_file(self, certfile: _StrOrBytesPath) -> None:
10831110
"""
10841111
Load a certificate chain from a file.
@@ -1096,6 +1123,7 @@ def use_certificate_chain_file(self, certfile: _StrOrBytesPath) -> None:
10961123
if not result:
10971124
_raise_current_error()
10981125

1126+
@_require_not_used
10991127
def use_certificate_file(
11001128
self, certfile: _StrOrBytesPath, filetype: int = FILETYPE_PEM
11011129
) -> None:
@@ -1120,6 +1148,7 @@ def use_certificate_file(
11201148
if not use_result:
11211149
_raise_current_error()
11221150

1151+
@_require_not_used
11231152
def use_certificate(self, cert: X509 | x509.Certificate) -> None:
11241153
"""
11251154
Load a certificate from a X509 object
@@ -1144,6 +1173,7 @@ def use_certificate(self, cert: X509 | x509.Certificate) -> None:
11441173
if not use_result:
11451174
_raise_current_error()
11461175

1176+
@_require_not_used
11471177
def add_extra_chain_cert(self, certobj: X509 | x509.Certificate) -> None:
11481178
"""
11491179
Add certificate to chain
@@ -1176,6 +1206,7 @@ def _raise_passphrase_exception(self) -> None:
11761206

11771207
_raise_current_error()
11781208

1209+
@_require_not_used
11791210
def use_privatekey_file(
11801211
self, keyfile: _StrOrBytesPath, filetype: int = FILETYPE_PEM
11811212
) -> None:
@@ -1200,6 +1231,7 @@ def use_privatekey_file(
12001231
if not use_result:
12011232
self._raise_passphrase_exception()
12021233

1234+
@_require_not_used
12031235
def use_privatekey(self, pkey: _PrivateKey | PKey) -> None:
12041236
"""
12051237
Load a private key from a PKey object
@@ -1234,6 +1266,7 @@ def check_privatekey(self) -> None:
12341266
if not _lib.SSL_CTX_check_private_key(self._context):
12351267
_raise_current_error()
12361268

1269+
@_require_not_used
12371270
def load_client_ca(self, cafile: bytes) -> None:
12381271
"""
12391272
Load the trusted certificates that will be sent to the client. Does
@@ -1249,6 +1282,7 @@ def load_client_ca(self, cafile: bytes) -> None:
12491282
_openssl_assert(ca_list != _ffi.NULL)
12501283
_lib.SSL_CTX_set_client_CA_list(self._context, ca_list)
12511284

1285+
@_require_not_used
12521286
def set_session_id(self, buf: bytes) -> None:
12531287
"""
12541288
Set the session id to *buf* within which a session can be reused for
@@ -1266,6 +1300,7 @@ def set_session_id(self, buf: bytes) -> None:
12661300
== 1
12671301
)
12681302

1303+
@_require_not_used
12691304
def set_session_cache_mode(self, mode: int) -> int:
12701305
"""
12711306
Set the behavior of the session cache used by all connections using
@@ -1293,6 +1328,7 @@ def get_session_cache_mode(self) -> int:
12931328
"""
12941329
return _lib.SSL_CTX_get_session_cache_mode(self._context)
12951330

1331+
@_require_not_used
12961332
def set_verify(
12971333
self, mode: int, callback: _VerifyCallback | None = None
12981334
) -> None:
@@ -1330,6 +1366,7 @@ def set_verify(
13301366
self._verify_callback = self._verify_helper.callback
13311367
_lib.SSL_CTX_set_verify(self._context, mode, self._verify_callback)
13321368

1369+
@_require_not_used
13331370
def set_verify_depth(self, depth: int) -> None:
13341371
"""
13351372
Set the maximum depth for the certificate chain verification that shall
@@ -1361,6 +1398,7 @@ def get_verify_depth(self) -> int:
13611398
"""
13621399
return _lib.SSL_CTX_get_verify_depth(self._context)
13631400

1401+
@_require_not_used
13641402
def load_tmp_dh(self, dhfile: _StrOrBytesPath) -> None:
13651403
"""
13661404
Load parameters for Ephemeral Diffie-Hellman
@@ -1382,6 +1420,7 @@ def load_tmp_dh(self, dhfile: _StrOrBytesPath) -> None:
13821420
res = _lib.SSL_CTX_set_tmp_dh(self._context, dh)
13831421
_openssl_assert(res == 1)
13841422

1423+
@_require_not_used
13851424
def set_tmp_ecdh(self, curve: _EllipticCurve | ec.EllipticCurve) -> None:
13861425
"""
13871426
Select a curve to use for ECDHE key exchange.
@@ -1421,6 +1460,7 @@ def set_tmp_ecdh(self, curve: _EllipticCurve | ec.EllipticCurve) -> None:
14211460
ec = _ffi.gc(ec, _lib.EC_KEY_free)
14221461
_lib.SSL_CTX_set_tmp_ecdh(self._context, ec)
14231462

1463+
@_require_not_used
14241464
def set_cipher_list(self, cipher_list: bytes) -> None:
14251465
"""
14261466
Set the list of ciphers to be used in this context.
@@ -1460,6 +1500,7 @@ def set_cipher_list(self, cipher_list: bytes) -> None:
14601500
],
14611501
)
14621502

1503+
@_require_not_used
14631504
def set_client_ca_list(
14641505
self, certificate_authorities: Sequence[X509Name]
14651506
) -> None:
@@ -1497,6 +1538,7 @@ def set_client_ca_list(
14971538

14981539
_lib.SSL_CTX_set_client_CA_list(self._context, name_stack)
14991540

1541+
@_require_not_used
15001542
def add_client_ca(
15011543
self, certificate_authority: X509 | x509.Certificate
15021544
) -> None:
@@ -1531,6 +1573,7 @@ def add_client_ca(
15311573
)
15321574
_openssl_assert(add_result == 1)
15331575

1576+
@_require_not_used
15341577
def set_timeout(self, timeout: int) -> None:
15351578
"""
15361579
Set the timeout for newly created sessions for this Context object to
@@ -1554,6 +1597,7 @@ def get_timeout(self) -> int:
15541597
"""
15551598
return _lib.SSL_CTX_get_timeout(self._context)
15561599

1600+
@_require_not_used
15571601
def set_info_callback(
15581602
self, callback: Callable[[Connection, int, int], None]
15591603
) -> None:
@@ -1579,6 +1623,7 @@ def wrapper(ssl, where, return_code): # type: ignore[no-untyped-def]
15791623
_lib.SSL_CTX_set_info_callback(self._context, self._info_callback)
15801624

15811625
@_requires_keylog
1626+
@_require_not_used
15821627
def set_keylog_callback(
15831628
self, callback: Callable[[Connection, bytes], None]
15841629
) -> None:
@@ -1613,6 +1658,7 @@ def get_app_data(self) -> Any:
16131658
"""
16141659
return self._app_data
16151660

1661+
@_require_not_used
16161662
def set_app_data(self, data: Any) -> None:
16171663
"""
16181664
Set the application data (will be returned from get_app_data())
@@ -1639,6 +1685,7 @@ def get_cert_store(self) -> X509Store | None:
16391685
pystore._store = store
16401686
return pystore
16411687

1688+
@_require_not_used
16421689
def set_options(self, options: int) -> int:
16431690
"""
16441691
Add options. Options set before are not cleared!
@@ -1652,6 +1699,7 @@ def set_options(self, options: int) -> int:
16521699

16531700
return _lib.SSL_CTX_set_options(self._context, options)
16541701

1702+
@_require_not_used
16551703
def set_mode(self, mode: int) -> int:
16561704
"""
16571705
Add modes via bitmask. Modes set before are not cleared! This method
@@ -1665,6 +1713,7 @@ def set_mode(self, mode: int) -> int:
16651713

16661714
return _lib.SSL_CTX_set_mode(self._context, mode)
16671715

1716+
@_require_not_used
16681717
def set_tlsext_servername_callback(
16691718
self, callback: Callable[[Connection], None]
16701719
) -> None:
@@ -1690,6 +1739,7 @@ def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
16901739
self._context, self._tlsext_servername_callback
16911740
)
16921741

1742+
@_require_not_used
16931743
def set_tlsext_use_srtp(self, profiles: bytes) -> None:
16941744
"""
16951745
Enable support for negotiating SRTP keying material.
@@ -1705,6 +1755,7 @@ def set_tlsext_use_srtp(self, profiles: bytes) -> None:
17051755
_lib.SSL_CTX_set_tlsext_use_srtp(self._context, profiles) == 0
17061756
)
17071757

1758+
@_require_not_used
17081759
def set_alpn_protos(self, protos: list[bytes]) -> None:
17091760
"""
17101761
Specify the protocols that the client is prepared to speak after the
@@ -1742,6 +1793,7 @@ def set_alpn_protos(self, protos: list[bytes]) -> None:
17421793
== 0
17431794
)
17441795

1796+
@_require_not_used
17451797
def set_alpn_select_callback(self, callback: _ALPNSelectCallback) -> None:
17461798
"""
17471799
Specify a callback function that will be called on the server when a
@@ -1786,6 +1838,7 @@ def _set_ocsp_callback(
17861838
rc = _lib.SSL_CTX_set_tlsext_status_arg(self._context, self._ocsp_data)
17871839
_openssl_assert(rc == 1)
17881840

1841+
@_require_not_used
17891842
def set_ocsp_server_callback(
17901843
self,
17911844
callback: _OCSPServerCallback[_T],
@@ -1808,6 +1861,7 @@ def set_ocsp_server_callback(
18081861
helper = _OCSPServerCallbackHelper(callback)
18091862
self._set_ocsp_callback(helper, data)
18101863

1864+
@_require_not_used
18111865
def set_ocsp_client_callback(
18121866
self,
18131867
callback: _OCSPClientCallback[_T],
@@ -1832,6 +1886,7 @@ def set_ocsp_client_callback(
18321886
helper = _OCSPClientCallbackHelper(callback)
18331887
self._set_ocsp_callback(helper, data)
18341888

1889+
@_require_not_used
18351890
def set_cookie_generate_callback(
18361891
self, callback: _CookieGenerateCallback
18371892
) -> None:
@@ -1841,6 +1896,7 @@ def set_cookie_generate_callback(
18411896
self._cookie_generate_helper.callback,
18421897
)
18431898

1899+
@_require_not_used
18441900
def set_cookie_verify_callback(
18451901
self, callback: _CookieVerifyCallback
18461902
) -> None:
@@ -1869,6 +1925,8 @@ def __init__(
18691925
if not isinstance(context, Context):
18701926
raise TypeError("context must be a Context instance")
18711927

1928+
context._used = True
1929+
18721930
ssl = _lib.SSL_new(context._context)
18731931
self._ssl = _ffi.gc(ssl, _lib.SSL_free)
18741932
# We set SSL_MODE_AUTO_RETRY to handle situations where OpenSSL returns
@@ -2000,6 +2058,7 @@ def set_context(self, context: Context) -> None:
20002058

20012059
_lib.SSL_set_SSL_CTX(self._ssl, context._context)
20022060
self._context = context
2061+
self._context._used = True
20032062

20042063
def get_servername(self) -> bytes | None:
20052064
"""

tests/test_ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,7 @@ def test_set_tlsext_use_srtp_valid(self) -> None:
18191819
It does not return anything.
18201820
"""
18211821
context = Context(SSLv23_METHOD)
1822-
assert context.set_tlsext_use_srtp(b"SRTP_AES128_CM_SHA1_80") is None # type: ignore[func-returns-value]
1822+
assert context.set_tlsext_use_srtp(b"SRTP_AES128_CM_SHA1_80") is None
18231823

18241824

18251825
class TestServerNameCallback:

0 commit comments

Comments
 (0)