Skip to content

Commit

Permalink
[3.12] gh-116333: Relax error string text expectations in SSL-related…
Browse files Browse the repository at this point in the history
… tests (GH-116334) (GH-117136)

gh-116333: Relax error string text expectations in SSL-related tests (GH-116334)

* Relax error string text expectations in SSL-related tests

As suggested [here][1], this change relaxes the OpenSSL error string
text expectations in a number of tests. This was specifically done in
support of more easily building CPython [AWS-LC][2], but because AWS-LC
is a fork of [BoringSSL][3], it should increase compatibility with that
library as well.

In addition to the error string relaxations, we also add some guards
around the `tls-unique` channel binding being used with TLSv1.3, as that
feature (described in [RFC 6929][4]) is [not defined][5] for TLSv1.3.

[1]: https://discuss.python.org/t/support-building-ssl-and-hashlib-modules-against-aws-lc/44505/4
[2]: https://github.com/aws/aws-lc
[3]: https://github.com/google/boringssl
[4]: https://datatracker.ietf.org/doc/html/rfc5929GH-section-3
[5]: https://datatracker.ietf.org/doc/html/rfc8446GH-appendix-C.5
(cherry picked from commit c85d841)

Co-authored-by: Will Childs-Klein <[email protected]>
  • Loading branch information
miss-islington and WillChilds-Klein authored Mar 21, 2024
1 parent ea290f8 commit 05c5bec
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 38 deletions.
12 changes: 8 additions & 4 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,12 +1126,16 @@ def test_create_server_ssl_match_failed(self):
# incorrect server_hostname
f_c = self.loop.create_connection(MyProto, host, port,
ssl=sslcontext_client)

# Allow for flexible libssl error messages.
regex = re.compile(r"""(
IP address mismatch, certificate is not valid for '127.0.0.1' # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
with mock.patch.object(self.loop, 'call_exception_handler'):
with test_utils.disable_logger():
with self.assertRaisesRegex(
ssl.CertificateError,
"IP address mismatch, certificate is not valid for "
"'127.0.0.1'"):
with self.assertRaisesRegex(ssl.CertificateError, regex):
self.loop.run_until_complete(f_c)

# close connection
Expand Down
22 changes: 15 additions & 7 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import time
import calendar
import threading
import re
import socket

from test.support import verbose, run_with_tz, run_with_locale, cpython_only, requires_resource
Expand Down Expand Up @@ -558,9 +559,13 @@ def test_ssl_raises(self):
self.assertEqual(ssl_context.check_hostname, True)
ssl_context.load_verify_locations(CAFILE)

with self.assertRaisesRegex(ssl.CertificateError,
"IP address mismatch, certificate is not valid for "
"'127.0.0.1'"):
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
IP address mismatch, certificate is not valid for '127.0.0.1' # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
with self.assertRaisesRegex(ssl.CertificateError, regex):
_, server = self._setup(SimpleIMAPHandler)
client = self.imap_class(*server.server_address,
ssl_context=ssl_context)
Expand Down Expand Up @@ -954,10 +959,13 @@ def test_ssl_verified(self):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations(CAFILE)

with self.assertRaisesRegex(
ssl.CertificateError,
"IP address mismatch, certificate is not valid for "
"'127.0.0.1'"):
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
IP address mismatch, certificate is not valid for '127.0.0.1' # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
with self.assertRaisesRegex(ssl.CertificateError, regex):
with self.reaped_server(SimpleIMAPHandler) as server:
client = self.imap_class(*server.server_address,
ssl_context=ssl_context)
Expand Down
97 changes: 70 additions & 27 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ def test_openssl_version(self):
else:
openssl_ver = f"OpenSSL {major:d}.{minor:d}.{fix:d}"
self.assertTrue(
s.startswith((openssl_ver, libressl_ver)),
s.startswith((openssl_ver, libressl_ver, "AWS-LC")),
(s, t, hex(n))
)

Expand Down Expand Up @@ -1162,24 +1162,30 @@ def test_load_cert_chain(self):
with self.assertRaises(OSError) as cm:
ctx.load_cert_chain(NONEXISTINGCERT)
self.assertEqual(cm.exception.errno, errno.ENOENT)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_cert_chain(BADCERT)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_cert_chain(EMPTYCERT)
# Separate key and cert
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(ONLYCERT, ONLYKEY)
ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
ctx.load_cert_chain(certfile=BYTES_ONLYCERT, keyfile=BYTES_ONLYKEY)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_cert_chain(ONLYCERT)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_cert_chain(ONLYKEY)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_cert_chain(certfile=ONLYKEY, keyfile=ONLYCERT)
# Mismatching key and cert
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
with self.assertRaisesRegex(ssl.SSLError, "key values mismatch"):
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
key values mismatch # OpenSSL
|
KEY_VALUES_MISMATCH # AWS-LC
)""", re.X)
with self.assertRaisesRegex(ssl.SSLError, regex):
ctx.load_cert_chain(CAFILE_CACERT, ONLYKEY)
# Password protected key and cert
ctx.load_cert_chain(CERTFILE_PROTECTED, password=KEY_PASSWORD)
Expand Down Expand Up @@ -1247,7 +1253,7 @@ def test_load_verify_locations(self):
with self.assertRaises(OSError) as cm:
ctx.load_verify_locations(NONEXISTINGCERT)
self.assertEqual(cm.exception.errno, errno.ENOENT)
with self.assertRaisesRegex(ssl.SSLError, "PEM lib"):
with self.assertRaisesRegex(ssl.SSLError, "PEM (lib|routines)"):
ctx.load_verify_locations(BADCERT)
ctx.load_verify_locations(CERTFILE, CAPATH)
ctx.load_verify_locations(CERTFILE, capath=BYTES_CAPATH)
Expand Down Expand Up @@ -1651,9 +1657,10 @@ def test_lib_reason(self):
with self.assertRaises(ssl.SSLError) as cm:
ctx.load_dh_params(CERTFILE)
self.assertEqual(cm.exception.library, 'PEM')
self.assertEqual(cm.exception.reason, 'NO_START_LINE')
regex = "(NO_START_LINE|UNSUPPORTED_PUBLIC_KEY_TYPE)"
self.assertRegex(cm.exception.reason, regex)
s = str(cm.exception)
self.assertTrue(s.startswith("[PEM: NO_START_LINE] no start line"), s)
self.assertTrue("NO_START_LINE" in s, s)

def test_subclass(self):
# Check that the appropriate SSLError subclass is raised
Expand Down Expand Up @@ -1833,7 +1840,13 @@ def test_connect_fail(self):
s = test_wrap_socket(socket.socket(socket.AF_INET),
cert_reqs=ssl.CERT_REQUIRED)
self.addCleanup(s.close)
self.assertRaisesRegex(ssl.SSLError, "certificate verify failed",
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
self.assertRaisesRegex(ssl.SSLError, regex,
s.connect, self.server_addr)

def test_connect_ex(self):
Expand Down Expand Up @@ -1901,7 +1914,13 @@ def test_connect_with_context_fail(self):
server_hostname=SIGNED_CERTFILE_HOSTNAME
)
self.addCleanup(s.close)
self.assertRaisesRegex(ssl.SSLError, "certificate verify failed",
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
self.assertRaisesRegex(ssl.SSLError, regex,
s.connect, self.server_addr)

def test_connect_capath(self):
Expand Down Expand Up @@ -2118,14 +2137,16 @@ def test_bio_handshake(self):
self.assertIsNone(sslobj.version())
self.assertIsNone(sslobj.shared_ciphers())
self.assertRaises(ValueError, sslobj.getpeercert)
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES:
# tls-unique is not defined for TLSv1.3
# https://datatracker.ietf.org/doc/html/rfc8446#appendix-C.5
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES and sslobj.version() != "TLSv1.3":
self.assertIsNone(sslobj.get_channel_binding('tls-unique'))
self.ssl_io_loop(sock, incoming, outgoing, sslobj.do_handshake)
self.assertTrue(sslobj.cipher())
self.assertIsNone(sslobj.shared_ciphers())
self.assertIsNotNone(sslobj.version())
self.assertTrue(sslobj.getpeercert())
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES:
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES and sslobj.version() != "TLSv1.3":
self.assertTrue(sslobj.get_channel_binding('tls-unique'))
try:
self.ssl_io_loop(sock, incoming, outgoing, sslobj.unwrap)
Expand Down Expand Up @@ -2850,11 +2871,16 @@ def test_crl_check(self):
client_context.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF

server = ThreadedEchoServer(context=server_context, chatty=True)
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
with self.assertRaisesRegex(ssl.SSLError,
"certificate verify failed"):
with self.assertRaisesRegex(ssl.SSLError, regex):
s.connect((HOST, server.port))

# now load a CRL file. The CRL file is signed by the CA.
Expand Down Expand Up @@ -2885,12 +2911,16 @@ def test_check_hostname(self):

# incorrect hostname should raise an exception
server = ThreadedEchoServer(context=server_context, chatty=True)
# Allow for flexible libssl error messages.
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname="invalid") as s:
with self.assertRaisesRegex(
ssl.CertificateError,
"Hostname mismatch, certificate is not valid for 'invalid'."):
with self.assertRaisesRegex(ssl.CertificateError, regex):
s.connect((HOST, server.port))

# missing server_hostname arg should cause an exception, too
Expand Down Expand Up @@ -3094,7 +3124,7 @@ def test_wrong_cert_tls13(self):
s.connect((HOST, server.port))
with self.assertRaisesRegex(
ssl.SSLError,
'alert unknown ca|EOF occurred'
'alert unknown ca|EOF occurred|TLSV1_ALERT_UNKNOWN_CA'
):
# TLS 1.3 perform client cert exchange after handshake
s.write(b'data')
Expand Down Expand Up @@ -3158,13 +3188,21 @@ def test_ssl_cert_verify_error(self):
server_hostname=SIGNED_CERTFILE_HOSTNAME) as s:
try:
s.connect((HOST, server.port))
self.fail("Expected connection failure")
except ssl.SSLError as e:
msg = 'unable to get local issuer certificate'
self.assertIsInstance(e, ssl.SSLCertVerificationError)
self.assertEqual(e.verify_code, 20)
self.assertEqual(e.verify_message, msg)
self.assertIn(msg, repr(e))
self.assertIn('certificate verify failed', repr(e))
# Allow for flexible libssl error messages.
regex = f"({msg}|CERTIFICATE_VERIFY_FAILED)"
self.assertRegex(repr(e), regex)
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
)""", re.X)
self.assertRegex(repr(e), regex)

def test_PROTOCOL_TLS(self):
"""Connecting to an SSLv23 server with various client options"""
Expand Down Expand Up @@ -3696,7 +3734,7 @@ def test_no_shared_ciphers(self):
server_hostname=hostname) as s:
with self.assertRaises(OSError):
s.connect((HOST, server.port))
self.assertIn("no shared cipher", server.conn_errors[0])
self.assertIn("NO_SHARED_CIPHER", server.conn_errors[0])

def test_version_basic(self):
"""
Expand Down Expand Up @@ -3784,7 +3822,7 @@ def test_min_max_version_mismatch(self):
server_hostname=hostname) as s:
with self.assertRaises(ssl.SSLError) as e:
s.connect((HOST, server.port))
self.assertIn("alert", str(e.exception))
self.assertRegex("(alert|ALERT)", str(e.exception))

@requires_tls_version('SSLv3')
def test_min_max_version_sslv3(self):
Expand Down Expand Up @@ -3826,6 +3864,10 @@ def test_tls_unique_channel_binding(self):

client_context, server_context, hostname = testing_context()

# tls-unique is not defined for TLSv1.3
# https://datatracker.ietf.org/doc/html/rfc8446#appendix-C.5
client_context.maximum_version = ssl.TLSVersion.TLSv1_2

server = ThreadedEchoServer(context=server_context,
chatty=True,
connectionchatty=False)
Expand Down Expand Up @@ -3926,7 +3968,7 @@ def test_dh_params(self):
cipher = stats["cipher"][0]
parts = cipher.split("-")
if "ADH" not in parts and "EDH" not in parts and "DHE" not in parts:
self.fail("Non-DH cipher: " + cipher[0])
self.fail("Non-DH key exchange: " + cipher[0])

def test_ecdh_curve(self):
# server secp384r1, client auto
Expand Down Expand Up @@ -4093,8 +4135,9 @@ def cb_raising(ssl_sock, server_name, initial_context):
chatty=False,
sni_name='supermessage')

self.assertEqual(cm.exception.reason,
'SSLV3_ALERT_HANDSHAKE_FAILURE')
# Allow for flexible libssl error messages.
regex = "(SSLV3_ALERT_HANDSHAKE_FAILURE|NO_PRIVATE_VALUE)"
self.assertRegex(regex, cm.exception.reason)
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)

def test_sni_callback_wrong_return_type(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Tests of TLS related things (error codes, etc) were updated to be more
lenient about specific error message strings and behaviors as seen in the
BoringSSL and AWS-LC forks of OpenSSL.

0 comments on commit 05c5bec

Please sign in to comment.