Skip to content

Commit 4cb1781

Browse files
committed
expose the client's cipher suites from the handshake (closes #23186)
1 parent e5db863 commit 4cb1781

File tree

5 files changed

+94
-21
lines changed

5 files changed

+94
-21
lines changed

Doc/library/ssl.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,17 @@ SSL sockets also have the following additional methods and attributes:
925925
version of the SSL protocol that defines its use, and the number of secret
926926
bits being used. If no connection has been established, returns ``None``.
927927

928+
.. method:: SSLSocket.shared_ciphers()
929+
930+
Return the list of ciphers shared by the client during the handshake. Each
931+
entry of the returned list is a three-value tuple containing the name of the
932+
cipher, the version of the SSL protocol that defines its use, and the number
933+
of secret bits the cipher uses. :meth:`~SSLSocket.shared_ciphers` returns
934+
``None`` if no connection has been established or the socket is a client
935+
socket.
936+
937+
.. versionadded:: 3.5
938+
928939
.. method:: SSLSocket.compression()
929940

930941
Return the compression algorithm being used as a string, or ``None``
@@ -1784,6 +1795,7 @@ provided.
17841795
- :meth:`~SSLSocket.getpeercert`
17851796
- :meth:`~SSLSocket.selected_npn_protocol`
17861797
- :meth:`~SSLSocket.cipher`
1798+
- :meth:`~SSLSocket.shared_ciphers`
17871799
- :meth:`~SSLSocket.compression`
17881800
- :meth:`~SSLSocket.pending`
17891801
- :meth:`~SSLSocket.do_handshake`

Lib/ssl.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,10 @@ def cipher(self):
572572
ssl_version, secret_bits)``."""
573573
return self._sslobj.cipher()
574574

575+
def shared_ciphers(self):
576+
"""Return the ciphers shared by the client during the handshake."""
577+
return self._sslobj.shared_ciphers()
578+
575579
def compression(self):
576580
"""Return the current compression algorithm in use, or ``None`` if
577581
compression was not negotiated or not supported by one of the peers."""
@@ -784,6 +788,12 @@ def cipher(self):
784788
else:
785789
return self._sslobj.cipher()
786790

791+
def shared_ciphers(self):
792+
self._checkClosed()
793+
if not self._sslobj:
794+
return None
795+
return self._sslobj.shared_ciphers()
796+
787797
def compression(self):
788798
self._checkClosed()
789799
if not self._sslobj:

Lib/test/test_ssl.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,11 +1698,13 @@ def test_handshake(self):
16981698
sslobj = ctx.wrap_bio(incoming, outgoing, False, 'svn.python.org')
16991699
self.assertIs(sslobj._sslobj.owner, sslobj)
17001700
self.assertIsNone(sslobj.cipher())
1701+
self.assertIsNone(sslobj.shared_ciphers())
17011702
self.assertRaises(ValueError, sslobj.getpeercert)
17021703
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES:
17031704
self.assertIsNone(sslobj.get_channel_binding('tls-unique'))
17041705
self.ssl_io_loop(sock, incoming, outgoing, sslobj.do_handshake)
17051706
self.assertTrue(sslobj.cipher())
1707+
self.assertIsNone(sslobj.shared_ciphers())
17061708
self.assertTrue(sslobj.getpeercert())
17071709
if 'tls-unique' in ssl.CHANNEL_BINDING_TYPES:
17081710
self.assertTrue(sslobj.get_channel_binding('tls-unique'))
@@ -1776,6 +1778,7 @@ def wrap_conn(self):
17761778
self.close()
17771779
return False
17781780
else:
1781+
self.server.shared_ciphers.append(self.sslconn.shared_ciphers())
17791782
if self.server.context.verify_mode == ssl.CERT_REQUIRED:
17801783
cert = self.sslconn.getpeercert()
17811784
if support.verbose and self.server.chatty:
@@ -1891,6 +1894,7 @@ def __init__(self, certificate=None, ssl_version=None,
18911894
self.flag = None
18921895
self.active = False
18931896
self.selected_protocols = []
1897+
self.shared_ciphers = []
18941898
self.conn_errors = []
18951899
threading.Thread.__init__(self)
18961900
self.daemon = True
@@ -2121,6 +2125,7 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
21212125
})
21222126
s.close()
21232127
stats['server_npn_protocols'] = server.selected_protocols
2128+
stats['server_shared_ciphers'] = server.shared_ciphers
21242129
return stats
21252130

21262131
def try_protocol_combo(server_protocol, client_protocol, expect_success,
@@ -3157,6 +3162,18 @@ def cb_wrong_return_type(ssl_sock, server_name, initial_context):
31573162
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_INTERNAL_ERROR')
31583163
self.assertIn("TypeError", stderr.getvalue())
31593164

3165+
def test_shared_ciphers(self):
3166+
server_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
3167+
client_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
3168+
client_context.set_ciphers("3DES")
3169+
server_context.set_ciphers("3DES:AES")
3170+
stats = server_params_test(client_context, server_context)
3171+
ciphers = stats['server_shared_ciphers'][0]
3172+
self.assertGreater(len(ciphers), 0)
3173+
for name, tls_version, bits in ciphers:
3174+
self.assertIn("DES-CBC3-", name)
3175+
self.assertEqual(bits, 112)
3176+
31603177
def test_read_write_after_close_raises_valuerror(self):
31613178
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
31623179
context.verify_mode = ssl.CERT_REQUIRED

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ Core and Builtins
199199
Library
200200
-------
201201

202+
- Issue #23186: Add ssl.SSLObject.shared_ciphers() and
203+
ssl.SSLSocket.shared_ciphers() to fetch the client's list ciphers sent at
204+
handshake.
205+
202206
- Issue #23143: Remove compatibility with OpenSSLs older than 0.9.8.
203207

204208
- Issue #23132: Improve performance and introspection support of comparison

Modules/_ssl.c

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,54 +1360,83 @@ If the optional argument is True, returns a DER-encoded copy of the\n\
13601360
peer certificate, or None if no certificate was provided. This will\n\
13611361
return the certificate even if it wasn't validated.");
13621362

1363-
static PyObject *PySSL_cipher (PySSLSocket *self) {
1364-
1365-
PyObject *retval, *v;
1366-
const SSL_CIPHER *current;
1367-
char *cipher_name;
1368-
char *cipher_protocol;
1369-
1370-
if (self->ssl == NULL)
1371-
Py_RETURN_NONE;
1372-
current = SSL_get_current_cipher(self->ssl);
1373-
if (current == NULL)
1374-
Py_RETURN_NONE;
1375-
1376-
retval = PyTuple_New(3);
1363+
static PyObject *
1364+
cipher_to_tuple(const SSL_CIPHER *cipher)
1365+
{
1366+
const char *cipher_name, *cipher_protocol;
1367+
PyObject *v, *retval = PyTuple_New(3);
13771368
if (retval == NULL)
13781369
return NULL;
13791370

1380-
cipher_name = (char *) SSL_CIPHER_get_name(current);
1371+
cipher_name = SSL_CIPHER_get_name(cipher);
13811372
if (cipher_name == NULL) {
13821373
Py_INCREF(Py_None);
13831374
PyTuple_SET_ITEM(retval, 0, Py_None);
13841375
} else {
13851376
v = PyUnicode_FromString(cipher_name);
13861377
if (v == NULL)
1387-
goto fail0;
1378+
goto fail;
13881379
PyTuple_SET_ITEM(retval, 0, v);
13891380
}
1390-
cipher_protocol = (char *) SSL_CIPHER_get_version(current);
1381+
1382+
cipher_protocol = SSL_CIPHER_get_version(cipher);
13911383
if (cipher_protocol == NULL) {
13921384
Py_INCREF(Py_None);
13931385
PyTuple_SET_ITEM(retval, 1, Py_None);
13941386
} else {
13951387
v = PyUnicode_FromString(cipher_protocol);
13961388
if (v == NULL)
1397-
goto fail0;
1389+
goto fail;
13981390
PyTuple_SET_ITEM(retval, 1, v);
13991391
}
1400-
v = PyLong_FromLong(SSL_CIPHER_get_bits(current, NULL));
1392+
1393+
v = PyLong_FromLong(SSL_CIPHER_get_bits(cipher, NULL));
14011394
if (v == NULL)
1402-
goto fail0;
1395+
goto fail;
14031396
PyTuple_SET_ITEM(retval, 2, v);
1397+
14041398
return retval;
14051399

1406-
fail0:
1400+
fail:
14071401
Py_DECREF(retval);
14081402
return NULL;
14091403
}
14101404

1405+
static PyObject *PySSL_shared_ciphers(PySSLSocket *self)
1406+
{
1407+
STACK_OF(SSL_CIPHER) *ciphers;
1408+
int i;
1409+
PyObject *res;
1410+
1411+
if (!self->ssl->session || !self->ssl->session->ciphers)
1412+
Py_RETURN_NONE;
1413+
ciphers = self->ssl->session->ciphers;
1414+
res = PyList_New(sk_SSL_CIPHER_num(ciphers));
1415+
if (!res)
1416+
return NULL;
1417+
for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) {
1418+
PyObject *tup = cipher_to_tuple(sk_SSL_CIPHER_value(ciphers, i));
1419+
if (!tup) {
1420+
Py_DECREF(res);
1421+
return NULL;
1422+
}
1423+
PyList_SET_ITEM(res, i, tup);
1424+
}
1425+
return res;
1426+
}
1427+
1428+
static PyObject *PySSL_cipher (PySSLSocket *self)
1429+
{
1430+
const SSL_CIPHER *current;
1431+
1432+
if (self->ssl == NULL)
1433+
Py_RETURN_NONE;
1434+
current = SSL_get_current_cipher(self->ssl);
1435+
if (current == NULL)
1436+
Py_RETURN_NONE;
1437+
return cipher_to_tuple(current);
1438+
}
1439+
14111440
static PyObject *PySSL_version(PySSLSocket *self)
14121441
{
14131442
const char *version;
@@ -2019,6 +2048,7 @@ static PyMethodDef PySSLMethods[] = {
20192048
{"peer_certificate", (PyCFunction)PySSL_peercert, METH_VARARGS,
20202049
PySSL_peercert_doc},
20212050
{"cipher", (PyCFunction)PySSL_cipher, METH_NOARGS},
2051+
{"shared_ciphers", (PyCFunction)PySSL_shared_ciphers, METH_NOARGS},
20222052
{"version", (PyCFunction)PySSL_version, METH_NOARGS},
20232053
#ifdef OPENSSL_NPN_NEGOTIATED
20242054
{"selected_npn_protocol", (PyCFunction)PySSL_selected_npn_protocol, METH_NOARGS},

0 commit comments

Comments
 (0)