Skip to content

[Demo] [Incomplete] Allow compilation and linking against BoringSSL #116399

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

Closed
wants to merge 14 commits into from
Closed
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 Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,7 @@ SSL sockets also have the following additional methods and attributes:

.. method:: SSLSocket.shared_ciphers()

Return the list of ciphers available in both the client and server. Each
Return the list of ciphers shared by the client during the handshake. Each
entry of the returned list is a three-value tuple containing the name of the
cipher, the version of the SSL protocol that defines its use, and the number
of secret bits the cipher uses. :meth:`~SSLSocket.shared_ciphers` returns
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,9 @@ def test_tls13_pha(self):
import ssl
if not ssl.HAS_TLSv1_3:
self.skipTest('TLS 1.3 support required')
if 'BoringSSL' in ssl.OPENSSL_VERSION:
self.skipTest(
'Post Handshake Authentication is not supported by BoringSSL.')
# just check status of PHA flag
h = client.HTTPSConnection('localhost', 443)
self.assertTrue(h._context.post_handshake_auth)
Expand Down
97 changes: 74 additions & 23 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
HOST = socket_helper.HOST
IS_BORINGSSL = "BoringSSL" in ssl.OPENSSL_VERSION
IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0)
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')

Expand Down Expand Up @@ -551,7 +552,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, "AWS-LC")),
s.startswith((openssl_ver, libressl_ver, "AWS-LC", "BoringSSL")),
(s, t, hex(n))
)

Expand Down Expand Up @@ -906,6 +907,12 @@ def test_connect_ex_error(self):
self.assertIn(rc, errors)

def test_read_write_zero(self):
if IS_BORINGSSL:
# We had to revert the change which made this work for BoringSSL.
# This is because the change used APIs in OpenSSL which were
# mis-designed. When that mess is untangled, we can restore this
# test and the fix. See go/bio-read-ex-eof.
return
# empty reads and writes now work, bpo-42854, bpo-31711
client_context, server_context, hostname = testing_context()
server = ThreadedEchoServer(context=server_context)
Expand Down Expand Up @@ -1078,6 +1085,26 @@ def test_min_max_version(self):
ctx.maximum_version, ssl.TLSVersion.TLSv1_2
)

if IS_BORINGSSL:
# BoringSSL's version APIs differ from OpenSSL's slightly. Skip the
# remainder of the tests for now, until the conflicts can be
# resolved. It is unlikely these differences will affect calling
# code.
#
# - BoringSSL treats zero as the default version, not the minimum or
# maximum supported one. This works better when we are deprecating
# a version or implementing a new experimental version.
#
# - After SSL_CTX_set_min_proto_version(0), BoringSSL's
# SSL_CTX_get_min_proto_version returns the version 0 was
# interpreted as, while OpenSSL remembers it was set as 0.
#
# - BoringSSL's legacy version-locked SSL_METHODs don't reject calls
# to set the version. This is a little simpler to implement. As
# everyone, Python and OpenSSL included, have deprecated these
# APIs, this doesn't seem worth putting much effort into.
return

ctx.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
ctx.maximum_version = ssl.TLSVersion.TLSv1
self.assertEqual(
Expand Down Expand Up @@ -1190,7 +1217,7 @@ def test_load_cert_chain(self):
regex = re.compile(r"""(
key values mismatch # OpenSSL
|
KEY_VALUES_MISMATCH # AWS-LC
KEY_VALUES_MISMATCH # AWS-LC, BoringSSL
)""", re.X)
with self.assertRaisesRegex(ssl.SSLError, regex):
ctx.load_cert_chain(CAFILE_CACERT, ONLYKEY)
Expand Down Expand Up @@ -1322,12 +1349,14 @@ def test_load_verify_cadata(self):

with self.assertRaisesRegex(
ssl.SSLError,
"no start line: cadata does not contain a certificate"
"no start line: cadata does not contain a certificate" +
"|NO_START_LINE"
):
ctx.load_verify_locations(cadata="broken")
with self.assertRaisesRegex(
ssl.SSLError,
"not enough data: cadata does not contain a certificate"
"not enough data: cadata does not contain a certificate" +
"|NOT_ENOUGH_DATA"
):
ctx.load_verify_locations(cadata=b"broken")
with self.assertRaises(ssl.SSLError):
Expand Down Expand Up @@ -1671,7 +1700,7 @@ def test_lib_reason(self):
regex = "(NO_START_LINE|UNSUPPORTED_PUBLIC_KEY_TYPE)"
self.assertRegex(cm.exception.reason, regex)
s = str(cm.exception)
self.assertTrue("NO_START_LINE" in s, s)
self.assertIn("NO_START_LINE", s)

def test_subclass(self):
# Check that the appropriate SSLError subclass is raised
Expand Down Expand Up @@ -1855,7 +1884,7 @@ def test_connect_fail(self):
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
CERTIFICATE_VERIFY_FAILED # AWS-LC, BoringSSL
)""", re.X)
self.assertRaisesRegex(ssl.SSLError, regex,
s.connect, self.server_addr)
Expand Down Expand Up @@ -1929,7 +1958,7 @@ def test_connect_with_context_fail(self):
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
CERTIFICATE_VERIFY_FAILED # AWS-LC, BoringSSL
)""", re.X)
self.assertRaisesRegex(ssl.SSLError, regex,
s.connect, self.server_addr)
Expand Down Expand Up @@ -2138,6 +2167,8 @@ def test_bio_handshake(self):
incoming = ssl.MemoryBIO()
outgoing = ssl.MemoryBIO()
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# This test requests tls-unique, which is not defined in TLS 1.3.
ctx.maximum_version = ssl.TLSVersion.TLSv1_2
self.assertTrue(ctx.check_hostname)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
ctx.load_verify_locations(SIGNING_CA)
Expand All @@ -2146,15 +2177,15 @@ def test_bio_handshake(self):
self.assertIs(sslobj._sslobj.owner, sslobj)
self.assertIsNone(sslobj.cipher())
self.assertIsNone(sslobj.version())
self.assertIsNone(sslobj.shared_ciphers())
self.assertIsNotNone(sslobj.shared_ciphers())
self.assertRaises(ValueError, sslobj.getpeercert)
# 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.shared_ciphers())
self.assertIsNotNone(sslobj.version())
self.assertTrue(sslobj.getpeercert())
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES and sslobj.version() != "TLSv1.3":
Expand Down Expand Up @@ -2888,7 +2919,7 @@ def test_crl_check(self):
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
CERTIFICATE_VERIFY_FAILED # AWS-LC, BoringSSL
)""", re.X)
with server:
with client_context.wrap_socket(socket.socket(),
Expand Down Expand Up @@ -2928,7 +2959,7 @@ def test_check_hostname(self):
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
CERTIFICATE_VERIFY_FAILED # AWS-LC, BoringSSL
)""", re.X)
with server:
with client_context.wrap_socket(socket.socket(),
Expand Down Expand Up @@ -3021,6 +3052,10 @@ def test_verify_strict(self):
cert = s.getpeercert()
self.assertTrue(cert, "Can't get peer certificate.")

# TODO(gpshead): Is this accurate? If run, the test gets
# HANDSHAKE_FAILURE_ON_CLIENT_HELLO on connect().
@unittest.skipIf(ssl.OPENSSL_VERSION == "BoringSSL",
"BoringSSL does not support dual RSA ECC?")
def test_dual_rsa_ecc(self):
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.load_verify_locations(SIGNING_CA)
Expand Down Expand Up @@ -3245,7 +3280,7 @@ def test_ssl_cert_verify_error(self):
regex = re.compile(r"""(
certificate verify failed # OpenSSL
|
CERTIFICATE_VERIFY_FAILED # AWS-LC
CERTIFICATE_VERIFY_FAILED # AWS-LC, BoringSSL
)""", re.X)
self.assertRegex(repr(e), regex)

Expand Down Expand Up @@ -3908,6 +3943,8 @@ def test_tls_unique_channel_binding(self):
sys.stdout.write("\n")

client_context, server_context, hostname = testing_context()
# tls-unique is not defined in TLS 1.3.
client_context.maximum_version = ssl.TLSVersion.TLSv1_2

# tls-unique is not defined for TLSv1.3
# https://datatracker.ietf.org/doc/html/rfc8446#appendix-C.5
Expand Down Expand Up @@ -3999,6 +4036,7 @@ def test_no_legacy_server_connect(self):
sni_name=hostname)

@unittest.skipIf(Py_DEBUG_WIN32, "Avoid mixing debug/release CRT on Windows")
@unittest.skipIf(IS_BORINGSSL, "BoringSSL does not support DHE ciphers")
def test_dh_params(self):
# Check we can get a connection with ephemeral Diffie-Hellman
client_context, server_context, hostname = testing_context()
Expand Down Expand Up @@ -4181,8 +4219,14 @@ def cb_raising(ssl_sock, server_name, initial_context):
sni_name='supermessage')

# Allow for flexible libssl error messages.
regex = "(SSLV3_ALERT_HANDSHAKE_FAILURE|NO_PRIVATE_VALUE)"
self.assertRegex(regex, cm.exception.reason)
regex = re.compile(r"""(
SSLV3_ALERT_HANDSHAKE_FAILURE # OpenSSL
|
NO_PRIVATE_VALUE # AWS-LC
|
HANDSHAKE_FAILURE_ON_CLIENT_HELLO # BoringSSL? TODO(gpshead): confirm
)""", re.X)
self.assertRegex(cm.exception.reason, regex)
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)

def test_sni_callback_wrong_return_type(self):
Expand All @@ -4207,7 +4251,7 @@ def cb_wrong_return_type(ssl_sock, server_name, initial_context):
def test_shared_ciphers(self):
client_context, server_context, hostname = testing_context()
client_context.set_ciphers("AES128:AES256")
server_context.set_ciphers("AES256:eNULL")
server_context.set_ciphers("AES256")
expected_algs = [
"AES256", "AES-256",
# TLS 1.3 ciphers are always enabled
Expand Down Expand Up @@ -4251,6 +4295,12 @@ def test_sendfile(self):
self.assertEqual(s.recv(1024), TEST_DATA)

def test_session(self):
# Assertions on session_stats are skipped in BoringSSL. BoringSSL
# does not maintain those statistics for thread safety.
def maybeAssertEqual(a, b):
if not IS_BORINGSSL:
self.assertEqual(a, b)

client_context, server_context, hostname = testing_context()
# TODO: sessions aren't compatible with TLSv1.3 yet
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
Expand All @@ -4266,15 +4316,15 @@ def test_session(self):
self.assertGreater(session.ticket_lifetime_hint, 0)
self.assertFalse(stats['session_reused'])
sess_stat = server_context.session_stats()
self.assertEqual(sess_stat['accept'], 1)
self.assertEqual(sess_stat['hits'], 0)
maybeAssertEqual(sess_stat['accept'], 1)
maybeAssertEqual(sess_stat['hits'], 0)

# reuse session
stats = server_params_test(client_context, server_context,
session=session, sni_name=hostname)
sess_stat = server_context.session_stats()
self.assertEqual(sess_stat['accept'], 2)
self.assertEqual(sess_stat['hits'], 1)
maybeAssertEqual(sess_stat['accept'], 2)
maybeAssertEqual(sess_stat['hits'], 1)
self.assertTrue(stats['session_reused'])
session2 = stats['session']
self.assertEqual(session2.id, session.id)
Expand All @@ -4291,8 +4341,8 @@ def test_session(self):
self.assertNotEqual(session3.id, session.id)
self.assertNotEqual(session3, session)
sess_stat = server_context.session_stats()
self.assertEqual(sess_stat['accept'], 3)
self.assertEqual(sess_stat['hits'], 1)
maybeAssertEqual(sess_stat['accept'], 3)
maybeAssertEqual(sess_stat['hits'], 1)

# reuse session again
stats = server_params_test(client_context, server_context,
Expand All @@ -4304,8 +4354,8 @@ def test_session(self):
self.assertGreaterEqual(session4.time, session.time)
self.assertGreaterEqual(session4.timeout, session.timeout)
sess_stat = server_context.session_stats()
self.assertEqual(sess_stat['accept'], 4)
self.assertEqual(sess_stat['hits'], 2)
maybeAssertEqual(sess_stat['accept'], 4)
maybeAssertEqual(sess_stat['hits'], 2)

def test_session_handling(self):
client_context, server_context, hostname = testing_context()
Expand Down Expand Up @@ -4460,6 +4510,7 @@ def server_callback(identity):


@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
@unittest.skipIf(IS_BORINGSSL, "PHA not supported b/117801554")
class TestPostHandshakeAuth(unittest.TestCase):
def test_pha_setter(self):
protocols = [
Expand Down
35 changes: 35 additions & 0 deletions Modules/_hashopenssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ static const py_hashentry_t py_hashes[] = {
PY_HASH_ENTRY(Py_hash_sha256, "SHA256", SN_sha256, NID_sha256),
PY_HASH_ENTRY(Py_hash_sha384, "SHA384", SN_sha384, NID_sha384),
PY_HASH_ENTRY(Py_hash_sha512, "SHA512", SN_sha512, NID_sha512),
#ifndef OPENSSL_IS_BORINGSSL
/* truncated sha2 */
PY_HASH_ENTRY(Py_hash_sha512_224, "SHA512_224", SN_sha512_224, NID_sha512_224),
PY_HASH_ENTRY(Py_hash_sha512_256, "SHA512_256", SN_sha512_256, NID_sha512_256),
Expand All @@ -132,6 +133,7 @@ static const py_hashentry_t py_hashes[] = {
/* blake2 digest */
PY_HASH_ENTRY(Py_hash_blake2s, "blake2s256", SN_blake2s256, NID_blake2s256),
PY_HASH_ENTRY(Py_hash_blake2b, "blake2b512", SN_blake2b512, NID_blake2b512),
#endif
PY_HASH_ENTRY(NULL, NULL, NULL, 0),
};

Expand Down Expand Up @@ -1826,6 +1828,7 @@ typedef struct _internal_name_mapper_state {
} _InternalNameMapperState;


#ifndef OPENSSL_IS_BORINGSSL
/* A callback function to pass to OpenSSL's OBJ_NAME_do_all(...) */
static void
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
Expand Down Expand Up @@ -1854,6 +1857,7 @@ _openssl_hash_name_mapper(const EVP_MD *md, const char *from,
Py_DECREF(py_name);
}
}
#endif // !OPENSSL_IS_BORINGSSL


/* Ask OpenSSL for a list of supported ciphers, filling in a Python set. */
Expand All @@ -1862,12 +1866,42 @@ hashlib_md_meth_names(PyObject *module)
{
_InternalNameMapperState state = {
.set = PyFrozenSet_New(NULL),
#ifndef OPENSSL_IS_BORINGSSL
.error = 0
#endif // !OPENSSL_IS_BORINGSSL
};
if (state.set == NULL) {
return -1;
}

#if defined(OPENSSL_IS_BORINGSSL)
// This avoids a need to link with -ldecrepit for EVP_MD_do_all().
// TODO(gpshead): Using CPython predefined constant internal C APIs
// would be better.
const char *boringssl_hash_names[] = {
"md5",
"sha1",
"sha224",
"sha256",
"sha384",
"sha512",
NULL,
};
for (int i=0; boringssl_hash_names[i] != NULL; ++i) {
PyObject *py_name = PyUnicode_FromString(boringssl_hash_names[i]);
if (py_name == NULL) {
Py_DECREF(state.set);
return -1;
} else {
if (PySet_Add(state.set, py_name) != 0) {
Py_DECREF(py_name);
Py_DECREF(state.set);
return -1;
};
Py_DECREF(py_name);
}
}
#else
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
// get algorithms from all activated providers in default context
EVP_MD_do_all_provided(NULL, &_openssl_hash_name_mapper, &state);
Expand All @@ -1879,6 +1913,7 @@ hashlib_md_meth_names(PyObject *module)
Py_DECREF(state.set);
return -1;
}
#endif // !OPENSSL_IS_BORINGSSL

return PyModule_Add(module, "openssl_md_meth_names", state.set);
}
Expand Down
Loading