Skip to content

Commit d649480

Browse files
committed
Issue #12551: Provide a get_channel_binding() method on SSL sockets so as
to get channel binding data for the current SSL session (only the "tls-unique" channel binding is implemented). This allows the implementation of certain authentication mechanisms such as SCRAM-SHA-1-PLUS. Patch by Jacek Konieczny.
1 parent 875048b commit d649480

File tree

6 files changed

+196
-0
lines changed

6 files changed

+196
-0
lines changed

Doc/library/ssl.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,13 @@ Constants
386386

387387
.. versionadded:: 3.2
388388

389+
.. data:: CHANNEL_BINDING_TYPES
390+
391+
List of supported TLS channel binding types. Strings in this list
392+
can be used as arguments to :meth:`SSLSocket.get_channel_binding`.
393+
394+
.. versionadded:: 3.3
395+
389396
.. data:: OPENSSL_VERSION
390397

391398
The version string of the OpenSSL library loaded by the interpreter::
@@ -495,6 +502,18 @@ SSL sockets also have the following additional methods and attributes:
495502
version of the SSL protocol that defines its use, and the number of secret
496503
bits being used. If no connection has been established, returns ``None``.
497504

505+
.. method:: SSLSocket.get_channel_binding(cb_type="tls-unique")
506+
507+
Get channel binding data for current connection, as a bytes object. Returns
508+
``None`` if not connected or the handshake has not been completed.
509+
510+
The *cb_type* parameter allow selection of the desired channel binding
511+
type. Valid channel binding types are listed in the
512+
:data:`CHANNEL_BINDING_TYPES` list. Currently only the 'tls-unique' channel
513+
binding, defined by :rfc:`5929`, is supported. :exc:`ValueError` will be
514+
raised if an unsupported channel binding type is requested.
515+
516+
.. versionadded:: 3.3
498517

499518
.. method:: SSLSocket.unwrap()
500519

Lib/ssl.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
import traceback
100100
import errno
101101

102+
if _ssl.HAS_TLS_UNIQUE:
103+
CHANNEL_BINDING_TYPES = ['tls-unique']
104+
else:
105+
CHANNEL_BINDING_TYPES = []
102106

103107
class CertificateError(ValueError):
104108
pass
@@ -495,6 +499,21 @@ def accept(self):
495499
self.do_handshake_on_connect),
496500
addr)
497501

502+
def get_channel_binding(self, cb_type="tls-unique"):
503+
"""Get channel binding data for current connection. Raise ValueError
504+
if the requested `cb_type` is not supported. Return bytes of the data
505+
or None if the data is not available (e.g. before the handshake).
506+
"""
507+
if cb_type not in CHANNEL_BINDING_TYPES:
508+
raise ValueError("Unsupported channel binding type")
509+
if cb_type != "tls-unique":
510+
raise NotImplementedError(
511+
"{0} channel binding type not implemented"
512+
.format(cb_type))
513+
if self._sslobj is None:
514+
return None
515+
return self._sslobj.tls_unique_cb()
516+
498517
def __del__(self):
499518
# sys.stderr.write("__del__ on %s\n" % repr(self))
500519
self._real_close()

Lib/test/test_ssl.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,25 @@ def test_server_side(self):
321321
self.assertRaises(ValueError, ctx.wrap_socket, sock, True,
322322
server_hostname="some.hostname")
323323

324+
def test_unknown_channel_binding(self):
325+
# should raise ValueError for unknown type
326+
s = socket.socket(socket.AF_INET)
327+
ss = ssl.wrap_socket(s)
328+
with self.assertRaises(ValueError):
329+
ss.get_channel_binding("unknown-type")
330+
331+
@unittest.skipUnless("tls-unique" in ssl.CHANNEL_BINDING_TYPES,
332+
"'tls-unique' channel binding not available")
333+
def test_tls_unique_channel_binding(self):
334+
# unconnected should return None for known type
335+
s = socket.socket(socket.AF_INET)
336+
ss = ssl.wrap_socket(s)
337+
self.assertIsNone(ss.get_channel_binding("tls-unique"))
338+
# the same for server-side
339+
s = socket.socket(socket.AF_INET)
340+
ss = ssl.wrap_socket(s, server_side=True, certfile=CERTFILE)
341+
self.assertIsNone(ss.get_channel_binding("tls-unique"))
342+
324343
class ContextTests(unittest.TestCase):
325344

326345
@skip_if_broken_ubuntu_ssl
@@ -826,6 +845,11 @@ def run(self):
826845
self.sslconn = None
827846
if support.verbose and self.server.connectionchatty:
828847
sys.stdout.write(" server: connection is now unencrypted...\n")
848+
elif stripped == b'CB tls-unique':
849+
if support.verbose and self.server.connectionchatty:
850+
sys.stdout.write(" server: read CB tls-unique from client, sending our CB data...\n")
851+
data = self.sslconn.get_channel_binding("tls-unique")
852+
self.write(repr(data).encode("us-ascii") + b"\n")
829853
else:
830854
if (support.verbose and
831855
self.server.connectionchatty):
@@ -1625,6 +1649,73 @@ def serve():
16251649
t.join()
16261650
server.close()
16271651

1652+
@unittest.skipUnless("tls-unique" in ssl.CHANNEL_BINDING_TYPES,
1653+
"'tls-unique' channel binding not available")
1654+
def test_tls_unique_channel_binding(self):
1655+
"""Test tls-unique channel binding."""
1656+
if support.verbose:
1657+
sys.stdout.write("\n")
1658+
1659+
server = ThreadedEchoServer(CERTFILE,
1660+
certreqs=ssl.CERT_NONE,
1661+
ssl_version=ssl.PROTOCOL_TLSv1,
1662+
cacerts=CERTFILE,
1663+
chatty=True,
1664+
connectionchatty=False)
1665+
flag = threading.Event()
1666+
server.start(flag)
1667+
# wait for it to start
1668+
flag.wait()
1669+
# try to connect
1670+
s = ssl.wrap_socket(socket.socket(),
1671+
server_side=False,
1672+
certfile=CERTFILE,
1673+
ca_certs=CERTFILE,
1674+
cert_reqs=ssl.CERT_NONE,
1675+
ssl_version=ssl.PROTOCOL_TLSv1)
1676+
s.connect((HOST, server.port))
1677+
try:
1678+
# get the data
1679+
cb_data = s.get_channel_binding("tls-unique")
1680+
if support.verbose:
1681+
sys.stdout.write(" got channel binding data: {0!r}\n"
1682+
.format(cb_data))
1683+
1684+
# check if it is sane
1685+
self.assertIsNotNone(cb_data)
1686+
self.assertEqual(len(cb_data), 12) # True for TLSv1
1687+
1688+
# and compare with the peers version
1689+
s.write(b"CB tls-unique\n")
1690+
peer_data_repr = s.read().strip()
1691+
self.assertEqual(peer_data_repr,
1692+
repr(cb_data).encode("us-ascii"))
1693+
s.close()
1694+
1695+
# now, again
1696+
s = ssl.wrap_socket(socket.socket(),
1697+
server_side=False,
1698+
certfile=CERTFILE,
1699+
ca_certs=CERTFILE,
1700+
cert_reqs=ssl.CERT_NONE,
1701+
ssl_version=ssl.PROTOCOL_TLSv1)
1702+
s.connect((HOST, server.port))
1703+
new_cb_data = s.get_channel_binding("tls-unique")
1704+
if support.verbose:
1705+
sys.stdout.write(" got another channel binding data: {0!r}\n"
1706+
.format(new_cb_data))
1707+
# is it really unique
1708+
self.assertNotEqual(cb_data, new_cb_data)
1709+
self.assertIsNotNone(cb_data)
1710+
self.assertEqual(len(cb_data), 12) # True for TLSv1
1711+
s.write(b"CB tls-unique\n")
1712+
peer_data_repr = s.read().strip()
1713+
self.assertEqual(peer_data_repr,
1714+
repr(new_cb_data).encode("us-ascii"))
1715+
s.close()
1716+
finally:
1717+
server.stop()
1718+
server.join()
16281719

16291720
def test_main(verbose=False):
16301721
if support.verbose:

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ Lenny Kneler
516516
Pat Knight
517517
Greg Kochanski
518518
Damon Kohler
519+
Jacek Konieczny
519520
Марк Коренберг
520521
Vlad Korolev
521522
Joseph Koshy

Misc/NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ Core and Builtins
234234
Library
235235
-------
236236

237+
- Issue #12551: Provide a get_channel_binding() method on SSL sockets so as
238+
to get channel binding data for the current SSL session (only the
239+
"tls-unique" channel binding is implemented). This allows the implementation
240+
of certain authentication mechanisms such as SCRAM-SHA-1-PLUS. Patch by
241+
Jacek Konieczny.
242+
237243
- Issue #665194: email.utils now has format_datetime and parsedate_to_datetime
238244
functions, allowing for round tripping of RFC2822 format dates.
239245

Modules/_ssl.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ static unsigned int _ssl_locks_count = 0;
124124
# undef HAVE_SSL_CTX_CLEAR_OPTIONS
125125
#endif
126126

127+
/* In case of 'tls-unique' it will be 12 bytes for TLS, 36 bytes for
128+
* older SSL, but let's be safe */
129+
#define PySSL_CB_MAXLEN 128
130+
131+
/* SSL_get_finished got added to OpenSSL in 0.9.5 */
132+
#if OPENSSL_VERSION_NUMBER >= 0x0090500fL
133+
# define HAVE_OPENSSL_FINISHED 1
134+
#else
135+
# define HAVE_OPENSSL_FINISHED 0
136+
#endif
137+
127138
typedef struct {
128139
PyObject_HEAD
129140
SSL_CTX *ctx;
@@ -135,6 +146,7 @@ typedef struct {
135146
SSL *ssl;
136147
X509 *peer_cert;
137148
int shutdown_seen_zero;
149+
enum py_ssl_server_or_client socket_type;
138150
} PySSLSocket;
139151

140152
static PyTypeObject PySSLContext_Type;
@@ -328,6 +340,7 @@ newPySSLSocket(SSL_CTX *ctx, PySocketSockObject *sock,
328340
SSL_set_accept_state(self->ssl);
329341
PySSL_END_ALLOW_THREADS
330342

343+
self->socket_type = socket_type;
331344
self->Socket = PyWeakref_NewRef((PyObject *) sock, NULL);
332345
return self;
333346
}
@@ -1377,6 +1390,41 @@ PyDoc_STRVAR(PySSL_SSLshutdown_doc,
13771390
Does the SSL shutdown handshake with the remote end, and returns\n\
13781391
the underlying socket object.");
13791392

1393+
#if HAVE_OPENSSL_FINISHED
1394+
static PyObject *
1395+
PySSL_tls_unique_cb(PySSLSocket *self)
1396+
{
1397+
PyObject *retval = NULL;
1398+
char buf[PySSL_CB_MAXLEN];
1399+
int len;
1400+
1401+
if (SSL_session_reused(self->ssl) ^ !self->socket_type) {
1402+
/* if session is resumed XOR we are the client */
1403+
len = SSL_get_finished(self->ssl, buf, PySSL_CB_MAXLEN);
1404+
}
1405+
else {
1406+
/* if a new session XOR we are the server */
1407+
len = SSL_get_peer_finished(self->ssl, buf, PySSL_CB_MAXLEN);
1408+
}
1409+
1410+
/* It cannot be negative in current OpenSSL version as of July 2011 */
1411+
assert(len >= 0);
1412+
if (len == 0)
1413+
Py_RETURN_NONE;
1414+
1415+
retval = PyBytes_FromStringAndSize(buf, len);
1416+
1417+
return retval;
1418+
}
1419+
1420+
PyDoc_STRVAR(PySSL_tls_unique_cb_doc,
1421+
"tls_unique_cb() -> bytes\n\
1422+
\n\
1423+
Returns the 'tls-unique' channel binding data, as defined by RFC 5929.\n\
1424+
\n\
1425+
If the TLS handshake is not yet complete, None is returned");
1426+
1427+
#endif /* HAVE_OPENSSL_FINISHED */
13801428

13811429
static PyMethodDef PySSLMethods[] = {
13821430
{"do_handshake", (PyCFunction)PySSL_SSLdo_handshake, METH_NOARGS},
@@ -1391,6 +1439,10 @@ static PyMethodDef PySSLMethods[] = {
13911439
{"cipher", (PyCFunction)PySSL_cipher, METH_NOARGS},
13921440
{"shutdown", (PyCFunction)PySSL_SSLshutdown, METH_NOARGS,
13931441
PySSL_SSLshutdown_doc},
1442+
#if HAVE_OPENSSL_FINISHED
1443+
{"tls_unique_cb", (PyCFunction)PySSL_tls_unique_cb, METH_NOARGS,
1444+
PySSL_tls_unique_cb_doc},
1445+
#endif
13941446
{NULL, NULL}
13951447
};
13961448

@@ -2221,6 +2273,14 @@ PyInit__ssl(void)
22212273
Py_INCREF(r);
22222274
PyModule_AddObject(m, "HAS_SNI", r);
22232275

2276+
#if HAVE_OPENSSL_FINISHED
2277+
r = Py_True;
2278+
#else
2279+
r = Py_False;
2280+
#endif
2281+
Py_INCREF(r);
2282+
PyModule_AddObject(m, "HAS_TLS_UNIQUE", r);
2283+
22242284
/* OpenSSL version */
22252285
/* SSLeay() gives us the version of the library linked against,
22262286
which could be different from the headers version.

0 commit comments

Comments
 (0)