Skip to content

bpo-43669: PEP 644: Require OpenSSL 1.1.1 or newer (GH-23014) #23014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ jobs:
strategy:
fail-fast: false
matrix:
openssl_ver: [1.0.2u, 1.1.0l, 1.1.1k, 3.0.0-alpha14]
openssl_ver: [1.1.1k, 3.0.0-alpha14]
env:
OPENSSL_VER: ${{ matrix.openssl_ver }}
MULTISSL_DIR: ${{ github.workspace }}/multissl
Expand Down
1 change: 1 addition & 0 deletions Doc/using/unix.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ some Unices may not have the :program:`env` command, so you may need to hardcode

To use shell commands in your Python scripts, look at the :mod:`subprocess` module.

.. _unix_custom_openssl:

Custom OpenSSL
==============
Expand Down
6 changes: 5 additions & 1 deletion Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Summary -- Release highlights
.. PEP-sized items next.
* :pep:`644`, require OpenSSL 1.1.1 or newer


New Features
Expand Down Expand Up @@ -1438,6 +1439,10 @@ CPython bytecode changes
Build Changes
=============
* :pep:`644`: Python now requires OpenSSL 1.1.1 or newer. OpenSSL 1.0.2 is no
longer supported.
(Contributed by Christian Heimes in :issue:`43669`.)
* The C99 functions :c:func:`snprintf` and :c:func:`vsnprintf` are now required
to build Python.
(Contributed by Victor Stinner in :issue:`36020`.)
Expand Down Expand Up @@ -1483,7 +1488,6 @@ Build Changes
(Contributed by Christian Heimes in :issue:`43466`.)
C API Changes
=============
Expand Down
10 changes: 2 additions & 8 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,15 +909,12 @@ def selected_npn_protocol(self):
"""Return the currently selected NPN protocol as a string, or ``None``
if a next protocol was not negotiated or if NPN is not supported by one
of the peers."""
if _ssl.HAS_NPN:
return self._sslobj.selected_npn_protocol()

def selected_alpn_protocol(self):
"""Return the currently selected ALPN protocol as a string, or ``None``
if a next protocol was not negotiated or if ALPN is not supported by one
of the peers."""
if _ssl.HAS_ALPN:
return self._sslobj.selected_alpn_protocol()
return self._sslobj.selected_alpn_protocol()

def cipher(self):
"""Return the currently selected cipher as a 3-tuple ``(name,
Expand Down Expand Up @@ -1126,10 +1123,7 @@ def getpeercert(self, binary_form=False):
@_sslcopydoc
def selected_npn_protocol(self):
self._checkClosed()
if self._sslobj is None or not _ssl.HAS_NPN:
return None
else:
return self._sslobj.selected_npn_protocol()
return None

@_sslcopydoc
def selected_alpn_protocol(self):
Expand Down
119 changes: 26 additions & 93 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
HOST = socket_helper.HOST
IS_LIBRESSL = ssl.OPENSSL_VERSION.startswith('LibreSSL')
IS_OPENSSL_1_1_0 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0)
IS_OPENSSL_1_1_1 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 1)
IS_OPENSSL_3_0_0 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (3, 0, 0)
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
Expand Down Expand Up @@ -270,18 +269,6 @@ def handle_error(prefix):
if support.verbose:
sys.stdout.write(prefix + exc_format)

def can_clear_options():
# 0.9.8m or higher
return ssl._OPENSSL_API_VERSION >= (0, 9, 8, 13, 15)

def no_sslv2_implies_sslv3_hello():
# 0.9.7h or higher
return ssl.OPENSSL_VERSION_INFO >= (0, 9, 7, 8, 15)

def have_verify_flags():
# 0.9.8 or higher
return ssl.OPENSSL_VERSION_INFO >= (0, 9, 8, 0, 15)

def _have_secp_curves():
if not ssl.HAS_ECDH:
return False
Expand Down Expand Up @@ -372,17 +359,15 @@ def test_constants(self):
ssl.OP_SINGLE_DH_USE
if ssl.HAS_ECDH:
ssl.OP_SINGLE_ECDH_USE
if ssl.OPENSSL_VERSION_INFO >= (1, 0):
ssl.OP_NO_COMPRESSION
ssl.OP_NO_COMPRESSION
self.assertIn(ssl.HAS_SNI, {True, False})
self.assertIn(ssl.HAS_ECDH, {True, False})
ssl.OP_NO_SSLv2
ssl.OP_NO_SSLv3
ssl.OP_NO_TLSv1
ssl.OP_NO_TLSv1_3
if ssl.OPENSSL_VERSION_INFO >= (1, 0, 1):
ssl.OP_NO_TLSv1_1
ssl.OP_NO_TLSv1_2
ssl.OP_NO_TLSv1_1
ssl.OP_NO_TLSv1_2
self.assertEqual(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv23)

def test_private_init(self):
Expand Down Expand Up @@ -1161,7 +1146,6 @@ def test_python_ciphers(self):
self.assertNotIn("RC4", name)
self.assertNotIn("3DES", name)

@unittest.skipIf(ssl.OPENSSL_VERSION_INFO < (1, 0, 2, 0, 0), 'OpenSSL too old')
def test_get_ciphers(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.set_ciphers('AESGCM')
Expand All @@ -1181,15 +1165,11 @@ def test_options(self):
self.assertEqual(default, ctx.options)
ctx.options |= ssl.OP_NO_TLSv1
self.assertEqual(default | ssl.OP_NO_TLSv1, ctx.options)
if can_clear_options():
ctx.options = (ctx.options & ~ssl.OP_NO_TLSv1)
self.assertEqual(default, ctx.options)
ctx.options = 0
# Ubuntu has OP_NO_SSLv3 forced on by default
self.assertEqual(0, ctx.options & ~ssl.OP_NO_SSLv3)
else:
with self.assertRaises(ValueError):
ctx.options = 0
ctx.options = (ctx.options & ~ssl.OP_NO_TLSv1)
self.assertEqual(default, ctx.options)
ctx.options = 0
# Ubuntu has OP_NO_SSLv3 forced on by default
self.assertEqual(0, ctx.options & ~ssl.OP_NO_SSLv3)

def test_verify_mode_protocol(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
Expand Down Expand Up @@ -1327,8 +1307,6 @@ def test_security_level(self):
}
self.assertIn(ctx.security_level, security_level_range)

@unittest.skipUnless(have_verify_flags(),
"verify_flags need OpenSSL > 0.9.8")
def test_verify_flags(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# default value
Expand Down Expand Up @@ -1797,7 +1775,6 @@ class MySSLObject(ssl.SSLObject):
obj = ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO())
self.assertIsInstance(obj, MySSLObject)

@unittest.skipUnless(IS_OPENSSL_1_1_1, "Test requires OpenSSL 1.1.1")
def test_num_tickest(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
self.assertEqual(ctx.num_tickets, 2)
Expand Down Expand Up @@ -2956,8 +2933,6 @@ def test_getpeercert(self):
after = ssl.cert_time_to_seconds(cert['notAfter'])
self.assertLess(before, after)

@unittest.skipUnless(have_verify_flags(),
"verify_flags need OpenSSL > 0.9.8")
def test_crl_check(self):
if support.verbose:
sys.stdout.write("\n")
Expand Down Expand Up @@ -3859,12 +3834,7 @@ def test_version_basic(self):
self.assertIs(s.version(), None)
self.assertIs(s._sslobj, None)
s.connect((HOST, server.port))
if IS_OPENSSL_1_1_1 and has_tls_version('TLSv1_3'):
self.assertEqual(s.version(), 'TLSv1.3')
elif ssl.OPENSSL_VERSION_INFO >= (1, 0, 2):
self.assertEqual(s.version(), 'TLSv1.2')
else: # 0.9.8 to 1.0.1
self.assertIn(s.version(), ('TLSv1', 'TLSv1.2'))
self.assertEqual(s.version(), 'TLSv1.3')
self.assertIs(s._sslobj, None)
self.assertIs(s.version(), None)

Expand Down Expand Up @@ -3966,8 +3936,6 @@ def test_default_ecdh_curve(self):
# explicitly using the 'ECCdraft' cipher alias. Otherwise,
# our default cipher list should prefer ECDH-based ciphers
# automatically.
if ssl.OPENSSL_VERSION_INFO < (1, 0, 0):
context.set_ciphers("ECCdraft:ECDH")
with ThreadedEchoServer(context=context) as server:
with context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))
Expand Down Expand Up @@ -4099,15 +4067,11 @@ def test_ecdh_curve(self):
server_context.set_ciphers("ECDHE:!eNULL:!aNULL")
server_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
try:
stats = server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)
server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)
except ssl.SSLError:
pass
else:
# OpenSSL 1.0.2 does not fail although it should.
if IS_OPENSSL_1_1_0:
self.fail("mismatch curve did not fail")
self.fail("mismatch curve did not fail")

def test_selected_alpn_protocol(self):
# selected_alpn_protocol() is None unless ALPN is used.
Expand All @@ -4117,7 +4081,6 @@ def test_selected_alpn_protocol(self):
sni_name=hostname)
self.assertIs(stats['client_alpn_protocol'], None)

@unittest.skipUnless(ssl.HAS_ALPN, "ALPN support required")
def test_selected_alpn_protocol_if_server_uses_alpn(self):
# selected_alpn_protocol() is None unless ALPN is used by the client.
client_context, server_context, hostname = testing_context()
Expand All @@ -4127,7 +4090,6 @@ def test_selected_alpn_protocol_if_server_uses_alpn(self):
sni_name=hostname)
self.assertIs(stats['client_alpn_protocol'], None)

@unittest.skipUnless(ssl.HAS_ALPN, "ALPN support needed for this test")
def test_alpn_protocols(self):
server_protocols = ['foo', 'bar', 'milkshake']
protocol_tests = [
Expand All @@ -4150,22 +4112,17 @@ def test_alpn_protocols(self):
except ssl.SSLError as e:
stats = e

if (expected is None and IS_OPENSSL_1_1_0
and ssl.OPENSSL_VERSION_INFO < (1, 1, 0, 6)):
# OpenSSL 1.1.0 to 1.1.0e raises handshake error
self.assertIsInstance(stats, ssl.SSLError)
else:
msg = "failed trying %s (s) and %s (c).\n" \
"was expecting %s, but got %%s from the %%s" \
% (str(server_protocols), str(client_protocols),
str(expected))
client_result = stats['client_alpn_protocol']
self.assertEqual(client_result, expected,
msg % (client_result, "client"))
server_result = stats['server_alpn_protocols'][-1] \
if len(stats['server_alpn_protocols']) else 'nothing'
self.assertEqual(server_result, expected,
msg % (server_result, "server"))
msg = "failed trying %s (s) and %s (c).\n" \
"was expecting %s, but got %%s from the %%s" \
% (str(server_protocols), str(client_protocols),
str(expected))
client_result = stats['client_alpn_protocol']
self.assertEqual(client_result, expected,
msg % (client_result, "client"))
server_result = stats['server_alpn_protocols'][-1] \
if len(stats['server_alpn_protocols']) else 'nothing'
self.assertEqual(server_result, expected,
msg % (server_result, "server"))

def test_selected_npn_protocol(self):
# selected_npn_protocol() is None unless NPN is used
Expand All @@ -4175,31 +4132,8 @@ def test_selected_npn_protocol(self):
sni_name=hostname)
self.assertIs(stats['client_npn_protocol'], None)

@unittest.skipUnless(ssl.HAS_NPN, "NPN support needed for this test")
def test_npn_protocols(self):
server_protocols = ['http/1.1', 'spdy/2']
protocol_tests = [
(['http/1.1', 'spdy/2'], 'http/1.1'),
(['spdy/2', 'http/1.1'], 'http/1.1'),
(['spdy/2', 'test'], 'spdy/2'),
(['abc', 'def'], 'abc')
]
for client_protocols, expected in protocol_tests:
client_context, server_context, hostname = testing_context()
server_context.set_npn_protocols(server_protocols)
client_context.set_npn_protocols(client_protocols)
stats = server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)
msg = "failed trying %s (s) and %s (c).\n" \
"was expecting %s, but got %%s from the %%s" \
% (str(server_protocols), str(client_protocols),
str(expected))
client_result = stats['client_npn_protocol']
self.assertEqual(client_result, expected, msg % (client_result, "client"))
server_result = stats['server_npn_protocols'][-1] \
if len(stats['server_npn_protocols']) else 'nothing'
self.assertEqual(server_result, expected, msg % (server_result, "server"))
assert not ssl.HAS_NPN

def sni_contexts(self):
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
Expand Down Expand Up @@ -4369,8 +4303,7 @@ def test_session(self):
self.assertGreater(session.time, 0)
self.assertGreater(session.timeout, 0)
self.assertTrue(session.has_ticket)
if ssl.OPENSSL_VERSION_INFO > (1, 0, 1):
self.assertGreater(session.ticket_lifetime_hint, 0)
self.assertGreater(session.ticket_lifetime_hint, 0)
self.assertFalse(stats['session_reused'])
sess_stat = server_context.session_stats()
self.assertEqual(sess_stat['accept'], 1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement :pep:`644`. Python now requires OpenSSL 1.1.1 or newer.
22 changes: 17 additions & 5 deletions Modules/Setup
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,23 @@ _symtable symtablemodule.c
#_socket socketmodule.c

# Socket module helper for SSL support; you must comment out the other
# socket line above, and possibly edit the SSL variable:
#SSL=/usr/local/ssl
#_ssl _ssl.c \
# -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \
# -L$(SSL)/lib -lssl -lcrypto
# socket line above, and edit the OPENSSL variable:
# OPENSSL=/path/to/openssl/directory
# _ssl _ssl.c \
# -I$(OPENSSL)/include -L$(OPENSSL)/lib \
# -lssl -lcrypto
#_hashlib _hashopenssl.c \
# -I$(OPENSSL)/include -L$(OPENSSL)/lib \
# -lcrypto

# To statically link OpenSSL:
# _ssl _ssl.c \
# -I$(OPENSSL)/include -L$(OPENSSL)/lib \
# -l:libssl.a -Wl,--exclude-libs,libssl.a \
# -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a
#_hashlib _hashopenssl.c \
# -I$(OPENSSL)/include -L$(OPENSSL)/lib \
# -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a

# The crypt module is now disabled by default because it breaks builds
# on many systems (where -lcrypt is needed), e.g. Linux (I believe).
Expand Down
Loading