From d80b1d395a146ceef361664640f97c6db2ae47de Mon Sep 17 00:00:00 2001 From: ohemorange Date: Mon, 4 Aug 2025 08:08:45 -0700 Subject: [PATCH] Deprecate acme.crypto_util.probe_sni() (#10387) Fixes #10386. - Creates an internal version of `probe_sni` for `certbot-compatibility-test` use - Deprecates `acme.crypto_util.probe_sni()` --- acme/src/acme/crypto_util.py | 2 + .../certbot_compatibility_test/validator.py | 65 ++++++++++++++++++- newsfragments/10386.changed | 1 + pytest.ini | 3 + 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 newsfragments/10386.changed diff --git a/acme/src/acme/crypto_util.py b/acme/src/acme/crypto_util.py index f6d668f90..c0848c8e6 100644 --- a/acme/src/acme/crypto_util.py +++ b/acme/src/acme/crypto_util.py @@ -225,6 +225,8 @@ def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # :rtype: cryptography.x509.Certificate """ + warnings.warn("probe_sni is deprecated and will be removed in an upcoming release", + DeprecationWarning) context = SSL.Context(method) context.set_timeout(timeout) diff --git a/certbot-compatibility-test/src/certbot_compatibility_test/validator.py b/certbot-compatibility-test/src/certbot_compatibility_test/validator.py index a51339468..fc35c8b9c 100644 --- a/certbot-compatibility-test/src/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/src/certbot_compatibility_test/validator.py @@ -1,15 +1,17 @@ """Validators to determine the current webserver configuration""" +import contextlib import logging import socket from typing import cast from typing import Mapping from typing import Optional +from typing import Tuple from typing import Union from cryptography import x509 +from OpenSSL import SSL import requests -from acme import crypto_util from acme import errors as acme_errors logger = logging.getLogger(__name__) @@ -34,7 +36,7 @@ class Validator: name = name if isinstance(name, bytes) else name.encode() try: - presented_cert = crypto_util.probe_sni(name, host, port) + presented_cert = _probe_sni(name, host, port) except acme_errors.Error as error: logger.exception(str(error)) return False @@ -113,3 +115,62 @@ class Validator: def ocsp_stapling(self, name: str) -> None: """Verify ocsp stapling for domain.""" raise NotImplementedError() + + + +def _probe_sni(name: bytes, host: bytes, port: int = 443) -> x509.Certificate: + """Probe SNI server for SSL certificate. + + :param bytes name: Byte string to send as the server name in the + client hello message. + :param bytes host: Host to connect to. + :param int port: Port to connect to. + + :raises acme.errors.Error: In case of any problems. + + :returns: SSL certificate presented by the server. + :rtype: cryptography.x509.Certificate + + """ + + # Default SSL method selected here is the most compatible, while secure + # SSL method: TLSv1_METHOD is only compatible with + # TLSv1_METHOD, while TLS_method is compatible with all other + # methods, including TLSv2_METHOD (read more at + # https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _serve_sni + # should be changed to use "set_options" to disable SSLv2 and SSLv3, + # in case it's used for things other than probing/serving! + context = SSL.Context(SSL.TLS_METHOD) + context.set_timeout(300) # timeout in seconds + + # Enables multi-path probing (selection + # of source interface). See `socket.creation_connection` for more + # info. Available only in Python 2.7+. + source_address: Tuple[str, int] = ('', 0) + socket_kwargs = {'source_address': source_address} + + try: + logger.debug( + "Attempting to connect to %s:%d%s.", host, port, + " from {0}:{1}".format( + source_address[0], + source_address[1] + ) if any(source_address) else "" + ) + socket_tuple: Tuple[bytes, int] = (host, port) + sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore[arg-type] + except OSError as error: + raise acme_errors.Error(error) + + with contextlib.closing(sock) as client: + client_ssl = SSL.Connection(context, client) + client_ssl.set_connect_state() + client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13 + try: + client_ssl.do_handshake() + client_ssl.shutdown() + except SSL.Error as error: + raise acme_errors.Error(error) + cert = client_ssl.get_peer_certificate() + assert cert # Appease mypy. We would have crashed out by now if there was no certificate. + return cert.to_cryptography() diff --git a/newsfragments/10386.changed b/newsfragments/10386.changed new file mode 100644 index 000000000..5918609be --- /dev/null +++ b/newsfragments/10386.changed @@ -0,0 +1 @@ +Deprecated `acme.crypto_util.probe_sni` diff --git a/pytest.ini b/pytest.ini index 4992223b7..4785e2097 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,8 @@ # https://github.com/certbot/certbot/pull/10294. # 12) Planning to remove support for checking OCSP via OpenSSL binary. # See https://github.com/certbot/certbot/issues/10291. +# 13) Removing probe_sni from public acme, since it's only used in certbot-compatibility-test +# after TLS-ALPN support is removed. filterwarnings = error ignore:.*rsyncdir:DeprecationWarning @@ -41,3 +43,4 @@ filterwarnings = ignore:TLSALPN01 is deprecated:DeprecationWarning ignore:TLSServer is deprecated:DeprecationWarning ignore:enforce_openssl_binary_usage parameter is deprecated:DeprecationWarning + ignore:probe_sni is deprecated:DeprecationWarning