Skip to content

Commit

Permalink
bpo-43669: PEP 644: Require OpenSSL 1.1.1 or newer (GH-23014)
Browse files Browse the repository at this point in the history
- Remove HAVE_X509_VERIFY_PARAM_SET1_HOST check
- Update hashopenssl to require OpenSSL 1.1.1
- multissltests only OpenSSL > 1.1.0
- ALPN is always supported
- SNI is always supported
- Remove deprecated NPN code. Python wrappers are no-op.
- ECDH is always supported
- Remove OPENSSL_VERSION_1_1 macro
- Remove locking callbacks
- Drop PY_OPENSSL_1_1_API macro
- Drop HAVE_SSL_CTX_CLEAR_OPTIONS macro
- SSL_CTRL_GET_MAX_PROTO_VERSION is always defined now
- security level is always available now
- get_num_tickets is available with TLS 1.3
- X509_V_ERR MISMATCH is always available now
- Always set SSL_MODE_RELEASE_BUFFERS
- X509_V_FLAG_TRUSTED_FIRST is always available
- get_ciphers is always supported
- SSL_CTX_set_keylog_callback is always available
- Update Modules/Setup with static link example
- Mention PEP in whatsnew
- Drop 1.0.2 and 1.1.0 from GHA tests
  • Loading branch information
tiran authored Apr 17, 2021
1 parent b467d9a commit 39258d3
Show file tree
Hide file tree
Showing 17 changed files with 5,310 additions and 8,440 deletions.
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

0 comments on commit 39258d3

Please sign in to comment.