From 5dc698861c91b4aa83b284b282c0e91cdcee49a3 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Mar 2021 22:35:24 +0100 Subject: [PATCH] Add SSL_CTX_set_min_proto_version/SSL_CTX_set_max_proto_version bindings (#985) * add Context.set_*_proto_version, fix #860 * docs: add new openssl tls methods * accept the fact that nothing can be taken for granted * bump minimum required cryptography version to 3.3 * drop support for Python 3.5 * use binary wheels for cryptography * Revert "use binary wheels for cryptography" This reverts commit 91a04c612ed1d0dd9fd541dfefe21cac7c25b1c1. * docker ci: compile cryptography with rust --- .github/workflows/ci.yml | 5 ++-- CHANGELOG.rst | 5 ++++ doc/api/ssl.rst | 19 +++++++++++-- doc/introduction.rst | 2 +- setup.py | 7 +++-- src/OpenSSL/SSL.py | 60 ++++++++++++++++++++++++++++++++++++++-- tests/test_ssl.py | 28 ++++++++++++++++++- tox.ini | 4 +-- 8 files changed, 115 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b65b72824..2d45c8812 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ jobs: PYTHON: # Base builds - {VERSION: "2.7", TOXENV: "py27"} - - {VERSION: "3.5", TOXENV: "py35"} - {VERSION: "3.6", TOXENV: "py36"} - {VERSION: "3.7", TOXENV: "py37"} - {VERSION: "3.8", TOXENV: "py38"} @@ -26,7 +25,6 @@ jobs: - {VERSION: "pypy3", TOXENV: "pypy3-cryptographyMaster"} # -cryptographyMinimum - {VERSION: "2.7", TOXENV: "py27-cryptographyMinimum"} - - {VERSION: "3.5", TOXENV: "py35-cryptographyMinimum"} - {VERSION: "3.6", TOXENV: "py36-cryptographyMinimum"} - {VERSION: "3.7", TOXENV: "py37-cryptographyMinimum"} - {VERSION: "3.8", TOXENV: "py38-cryptographyMinimum"} @@ -66,13 +64,14 @@ jobs: matrix: TEST: - {CONTAINER: "stretch", TOXENV: "py27"} - - {CONTAINER: "stretch", TOXENV: "py35"} + - {CONTAINER: "ubuntu-bionic", TOXENV: "py36"} name: "${{ matrix.TEST.TOXENV }} on ${{ matrix.TEST.CONTAINER }}" steps: - uses: actions/checkout@v2 - run: tox -v env: TOXENV: ${{ matrix.TEST.TOXENV }} + RUSTUP_HOME: /root/.rustup - name: Upload coverage run: | curl -o codecov.sh -f https://codecov.io/bash || curl -o codecov.sh -f https://codecov.io/bash || curl -o codecov.sh -f https://codecov.io/bash diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 220ee0087..85fb1c3cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ The third digit is only for regressions. Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- The minimum ``cryptography`` version is now 3.3. +- Drop support for Python 3.5 + Deprecations: ^^^^^^^^^^^^^ @@ -18,6 +21,8 @@ Changes: - Raise an error when an invalid ALPN value is set. `#993 `_ +- Added ``OpenSSL.SSL.Context.set_min_proto_version`` and ``OpenSSL.SSL.Context.set_max_proto_version`` + to set the minimum and maximum supported TLS version `#985 `_. 20.0.1 (2020-12-15) ------------------- diff --git a/doc/api/ssl.rst b/doc/api/ssl.rst index ead145202..630ebfc6d 100644 --- a/doc/api/ssl.rst +++ b/doc/api/ssl.rst @@ -10,7 +10,10 @@ This module handles things specific to SSL. There are two objects defined: Context, Connection. -.. py:data:: SSLv2_METHOD +.. py:data:: TLS_METHOD + TLS_SERVER_METHOD + TLS_CLIENT_METHOD + SSLv2_METHOD SSLv3_METHOD SSLv23_METHOD TLSv1_METHOD @@ -18,11 +21,21 @@ Context, Connection. TLSv1_2_METHOD These constants represent the different SSL methods to use when creating a - context object. If the underlying OpenSSL build is missing support for any - of these protocols, constructing a :py:class:`Context` using the + context object. New code should only use ``TLS_METHOD``, ``TLS_SERVER_METHOD``, + or ``TLS_CLIENT_METHOD``. If the underlying OpenSSL build is missing support + for any of these protocols, constructing a :py:class:`Context` using the corresponding :py:const:`*_METHOD` will raise an exception. +.. py:data:: SSL3_VERSION + TLS1_VERSION + TLS1_1_VERSION + TLS1_2_VERSION + TLS1_3_VERSION + + These constants represent the different TLS versions to use when + setting the minimum or maximum TLS version. + .. py:data:: VERIFY_NONE VERIFY_PEER VERIFY_FAIL_IF_NO_PEER_CERT diff --git a/doc/introduction.rst b/doc/introduction.rst index a810fbb56..287982946 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -14,7 +14,7 @@ Other OpenSSL wrappers for Python at the time were also limited, though in diffe Later it was maintained by `Jean-Paul Calderone`_ who among other things managed to make pyOpenSSL a pure Python project which the current maintainers are *very* grateful for. Over the time the standard library's ``ssl`` module improved, never reaching the completeness of pyOpenSSL's API coverage. -Despite `PEP 466`_ many useful features remain Python 3-only and pyOpenSSL remains the only alternative for full-featured TLS code across all noteworthy Python versions from 2.7 through 3.5 and PyPy_. +Despite `PEP 466`_ many useful features remain Python 3-only and pyOpenSSL remains the only alternative for full-featured TLS code across all noteworthy Python versions from 2.7 through 3.6 and PyPy_. Development diff --git a/setup.py b/setup.py index fbd65712b..1ca7b111d 100755 --- a/setup.py +++ b/setup.py @@ -79,7 +79,6 @@ def find_meta(meta): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -90,12 +89,14 @@ def find_meta(meta): "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Networking", ], - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", + python_requires=( + ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + ), packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ # Fix cryptographyMinimum in tox.ini when changing this! - "cryptography>=3.2", + "cryptography>=3.3", "six>=1.5.2", ], extras_require={ diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index cd1e9be2d..660cd9f21 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -44,6 +44,14 @@ "TLSv1_METHOD", "TLSv1_1_METHOD", "TLSv1_2_METHOD", + "TLS_METHOD", + "TLS_SERVER_METHOD", + "TLS_CLIENT_METHOD", + "SSL3_VERSION", + "TLS1_VERSION", + "TLS1_1_VERSION", + "TLS1_2_VERSION", + "TLS1_3_VERSION", "OP_NO_SSLv2", "OP_NO_SSLv3", "OP_NO_TLSv1", @@ -139,6 +147,24 @@ class _buffer(object): TLSv1_METHOD = 4 TLSv1_1_METHOD = 5 TLSv1_2_METHOD = 6 +TLS_METHOD = 7 +TLS_SERVER_METHOD = 8 +TLS_CLIENT_METHOD = 9 + +try: + SSL3_VERSION = _lib.SSL3_VERSION + TLS1_VERSION = _lib.TLS1_VERSION + TLS1_1_VERSION = _lib.TLS1_1_VERSION + TLS1_2_VERSION = _lib.TLS1_2_VERSION + TLS1_3_VERSION = _lib.TLS1_3_VERSION +except AttributeError: + # Hardcode constants for cryptography < 3.4, see + # /~https://github.com/pyca/pyopenssl/pull/985#issuecomment-775186682 + SSL3_VERSION = 768 + TLS1_VERSION = 769 + TLS1_1_VERSION = 770 + TLS1_2_VERSION = 771 + TLS1_3_VERSION = 772 OP_NO_SSLv2 = _lib.SSL_OP_NO_SSLv2 OP_NO_SSLv3 = _lib.SSL_OP_NO_SSLv3 @@ -603,8 +629,9 @@ class Context(object): :class:`OpenSSL.SSL.Context` instances define the parameters for setting up new SSL connections. - :param method: One of SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, or - TLSv1_METHOD. + :param method: One of TLS_METHOD, TLS_CLIENT_METHOD, or TLS_SERVER_METHOD. + SSLv23_METHOD, TLSv1_METHOD, etc. are deprecated and should + not be used. """ _methods = { @@ -614,6 +641,9 @@ class Context(object): TLSv1_METHOD: "TLSv1_method", TLSv1_1_METHOD: "TLSv1_1_method", TLSv1_2_METHOD: "TLSv1_2_method", + TLS_METHOD: "TLS_method", + TLS_SERVER_METHOD: "TLS_server_method", + TLS_CLIENT_METHOD: "TLS_client_method", } _methods = dict( (identifier, getattr(_lib, name)) @@ -661,6 +691,32 @@ def __init__(self, method): self.set_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE) + def set_min_proto_version(self, version): + """ + Set the minimum supported protocol version. Setting the minimum + version to 0 will enable protocol versions down to the lowest version + supported by the library. + + If the underlying OpenSSL build is missing support for the selected + version, this method will raise an exception. + """ + _openssl_assert( + _lib.SSL_CTX_set_min_proto_version(self._context, version) == 1 + ) + + def set_max_proto_version(self, version): + """ + Set the maximum supported protocol version. Setting the maximum + version to 0 will enable protocol versions up to the highest version + supported by the library. + + If the underlying OpenSSL build is missing support for the selected + version, this method will raise an exception. + """ + _openssl_assert( + _lib.SSL_CTX_set_max_proto_version(self._context, version) == 1 + ) + def load_verify_locations(self, cafile, capath=None): """ Let SSL know where we can find trusted certificates for the certificate diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 27f2d43ed..e79d9fa15 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -48,7 +48,14 @@ from OpenSSL.crypto import dump_certificate, load_certificate from OpenSSL.crypto import get_elliptic_curves -from OpenSSL.SSL import OPENSSL_VERSION_NUMBER, SSLEAY_VERSION, SSLEAY_CFLAGS +from OpenSSL.SSL import ( + OPENSSL_VERSION_NUMBER, + SSLEAY_VERSION, + SSLEAY_CFLAGS, + TLS_METHOD, + TLS1_2_VERSION, + TLS1_1_VERSION, +) from OpenSSL.SSL import SSLEAY_PLATFORM, SSLEAY_DIR, SSLEAY_BUILT_ON from OpenSSL.SSL import SENT_SHUTDOWN, RECEIVED_SHUTDOWN from OpenSSL.SSL import ( @@ -1039,6 +1046,25 @@ def keylog(conn, line): assert all(isinstance(conn, Connection) for conn, line in called) assert all(b"CLIENT_RANDOM" in line for conn, line in called) + def test_set_proto_version(self): + server_context = Context(TLS_METHOD) + server_context.use_certificate( + load_certificate(FILETYPE_PEM, root_cert_pem) + ) + server_context.use_privatekey( + load_privatekey(FILETYPE_PEM, root_key_pem) + ) + server_context.set_min_proto_version(TLS1_2_VERSION) + + client_context = Context(TLS_METHOD) + client_context.set_max_proto_version(TLS1_1_VERSION) + + with pytest.raises(Error, match="unsupported protocol"): + self._handshake_test(server_context, client_context) + + client_context.set_max_proto_version(0) + self._handshake_test(server_context, client_context) + def _load_verify_locations_test(self, *args): """ Create a client context which will verify the peer certificate and call diff --git a/tox.ini b/tox.ini index 90a4c6aad..a4662c40f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {pypy,pypy3,py27,py35,py36,py37,py38,py39}{,-cryptographyMaster,-cryptographyMinimum}{,-randomorder},py37-twistedMaster,pypi-readme,check-manifest,flake8,docs,coverage-report +envlist = {pypy,pypy3,py27,py36,py37,py38,py39}{,-cryptographyMaster,-cryptographyMinimum}{,-randomorder},py37-twistedMaster,pypi-readme,check-manifest,flake8,docs,coverage-report [testenv] whitelist_externals = @@ -10,7 +10,7 @@ extras = deps = coverage>=4.2 cryptographyMaster: git+/~https://github.com/pyca/cryptography.git - cryptographyMinimum: cryptography==3.2 + cryptographyMinimum: cryptography==3.3 randomorder: pytest-randomly setenv = # Do not allow the executing environment to pollute the test environment