Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d5d17eb

Browse files
committed
Issue #14204: The ssl module now has support for the Next Protocol Negotiation extension, if available in the underlying OpenSSL library.
Patch by Colin Marc.
1 parent a966c6f commit d5d17eb

6 files changed

Lines changed: 228 additions & 8 deletions

File tree

Doc/library/ssl.rst

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,16 @@ Constants
470470

471471
.. versionadded:: 3.2
472472

473+
.. data:: HAS_NPN
474+
475+
Whether the OpenSSL library has built-in support for *Next Protocol
476+
Negotiation* as described in the `NPN draft specification
477+
<http://tools.ietf.org/html/draft-agl-tls-nextprotoneg>`_. When true,
478+
you can use the :meth:`SSLContext.set_npn_protocols` method to advertise
479+
which protocols you want to support.
480+
481+
.. versionadded:: 3.3
482+
473483
.. data:: CHANNEL_BINDING_TYPES
474484

475485
List of supported TLS channel binding types. Strings in this list
@@ -609,6 +619,15 @@ SSL sockets also have the following additional methods and attributes:
609619

610620
.. versionadded:: 3.3
611621

622+
.. method:: SSLSocket.selected_npn_protocol()
623+
624+
Returns the protocol that was selected during the TLS/SSL handshake. If
625+
:meth:`SSLContext.set_npn_protocols` was not called, or if the other party
626+
does not support NPN, or if the handshake has not yet happened, this will
627+
return ``None``.
628+
629+
.. versionadded:: 3.3
630+
612631
.. method:: SSLSocket.unwrap()
613632

614633
Performs the SSL shutdown handshake, which removes the TLS layer from the
@@ -617,7 +636,6 @@ SSL sockets also have the following additional methods and attributes:
617636
returned socket should always be used for further communication with the
618637
other side of the connection, rather than the original socket.
619638

620-
621639
.. attribute:: SSLSocket.context
622640

623641
The :class:`SSLContext` object this SSL socket is tied to. If the SSL
@@ -715,6 +733,21 @@ to speed up repeated connections from the same clients.
715733
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
716734
give the currently selected cipher.
717735

736+
.. method:: SSLContext.set_npn_protocols(protocols)
737+
738+
Specify which protocols the socket should avertise during the SSL/TLS
739+
handshake. It should be a list of strings, like ``['http/1.1', 'spdy/2']``,
740+
ordered by preference. The selection of a protocol will happen during the
741+
handshake, and will play out according to the `NPN draft specification
742+
<http://tools.ietf.org/html/draft-agl-tls-nextprotoneg>`_. After a
743+
successful handshake, the :meth:`SSLSocket.selected_npn_protocol` method will
744+
return the agreed-upon protocol.
745+
746+
This method will raise :exc:`NotImplementedError` if :data:`HAS_NPN` is
747+
False.
748+
749+
.. versionadded:: 3.3
750+
718751
.. method:: SSLContext.load_dh_params(dhfile)
719752

720753
Load the key generation parameters for Diffie-Helman (DH) key exchange.

Lib/ssl.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
SSL_ERROR_EOF,
9191
SSL_ERROR_INVALID_ERROR_CODE,
9292
)
93-
from _ssl import HAS_SNI, HAS_ECDH
93+
from _ssl import HAS_SNI, HAS_ECDH, HAS_NPN
9494
from _ssl import (PROTOCOL_SSLv3, PROTOCOL_SSLv23,
9595
PROTOCOL_TLSv1)
9696
from _ssl import _OPENSSL_API_VERSION
@@ -209,6 +209,17 @@ def wrap_socket(self, sock, server_side=False,
209209
server_hostname=server_hostname,
210210
_context=self)
211211

212+
def set_npn_protocols(self, npn_protocols):
213+
protos = bytearray()
214+
for protocol in npn_protocols:
215+
b = bytes(protocol, 'ascii')
216+
if len(b) == 0 or len(b) > 255:
217+
raise SSLError('NPN protocols must be 1 to 255 in length')
218+
protos.append(len(b))
219+
protos.extend(b)
220+
221+
self._set_npn_protocols(protos)
222+
212223

213224
class SSLSocket(socket):
214225
"""This class implements a subtype of socket.socket that wraps
@@ -220,7 +231,7 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
220231
ssl_version=PROTOCOL_SSLv23, ca_certs=None,
221232
do_handshake_on_connect=True,
222233
family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None,
223-
suppress_ragged_eofs=True, ciphers=None,
234+
suppress_ragged_eofs=True, npn_protocols=None, ciphers=None,
224235
server_hostname=None,
225236
_context=None):
226237

@@ -240,6 +251,8 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
240251
self.context.load_verify_locations(ca_certs)
241252
if certfile:
242253
self.context.load_cert_chain(certfile, keyfile)
254+
if npn_protocols:
255+
self.context.set_npn_protocols(npn_protocols)
243256
if ciphers:
244257
self.context.set_ciphers(ciphers)
245258
self.keyfile = keyfile
@@ -340,6 +353,13 @@ def getpeercert(self, binary_form=False):
340353
self._checkClosed()
341354
return self._sslobj.peer_certificate(binary_form)
342355

356+
def selected_npn_protocol(self):
357+
self._checkClosed()
358+
if not self._sslobj or not _ssl.HAS_NPN:
359+
return None
360+
else:
361+
return self._sslobj.selected_npn_protocol()
362+
343363
def cipher(self):
344364
self._checkClosed()
345365
if not self._sslobj:
@@ -568,7 +588,8 @@ def wrap_socket(sock, keyfile=None, certfile=None,
568588
server_side=False, cert_reqs=CERT_NONE,
569589
ssl_version=PROTOCOL_SSLv23, ca_certs=None,
570590
do_handshake_on_connect=True,
571-
suppress_ragged_eofs=True, ciphers=None):
591+
suppress_ragged_eofs=True,
592+
ciphers=None):
572593

573594
return SSLSocket(sock=sock, keyfile=keyfile, certfile=certfile,
574595
server_side=server_side, cert_reqs=cert_reqs,

Lib/test/test_ssl.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,7 @@ def wrap_conn(self):
879879
try:
880880
self.sslconn = self.server.context.wrap_socket(
881881
self.sock, server_side=True)
882+
self.server.selected_protocols.append(self.sslconn.selected_npn_protocol())
882883
except ssl.SSLError as e:
883884
# XXX Various errors can have happened here, for example
884885
# a mismatching protocol version, an invalid certificate,
@@ -901,6 +902,8 @@ def wrap_conn(self):
901902
cipher = self.sslconn.cipher()
902903
if support.verbose and self.server.chatty:
903904
sys.stdout.write(" server: connection cipher is now " + str(cipher) + "\n")
905+
sys.stdout.write(" server: selected protocol is now "
906+
+ str(self.sslconn.selected_npn_protocol()) + "\n")
904907
return True
905908

906909
def read(self):
@@ -979,7 +982,7 @@ def run(self):
979982
def __init__(self, certificate=None, ssl_version=None,
980983
certreqs=None, cacerts=None,
981984
chatty=True, connectionchatty=False, starttls_server=False,
982-
ciphers=None, context=None):
985+
npn_protocols=None, ciphers=None, context=None):
983986
if context:
984987
self.context = context
985988
else:
@@ -992,6 +995,8 @@ def __init__(self, certificate=None, ssl_version=None,
992995
self.context.load_verify_locations(cacerts)
993996
if certificate:
994997
self.context.load_cert_chain(certificate)
998+
if npn_protocols:
999+
self.context.set_npn_protocols(npn_protocols)
9951000
if ciphers:
9961001
self.context.set_ciphers(ciphers)
9971002
self.chatty = chatty
@@ -1001,6 +1006,7 @@ def __init__(self, certificate=None, ssl_version=None,
10011006
self.port = support.bind_port(self.sock)
10021007
self.flag = None
10031008
self.active = False
1009+
self.selected_protocols = []
10041010
self.conn_errors = []
10051011
threading.Thread.__init__(self)
10061012
self.daemon = True
@@ -1195,6 +1201,7 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
11951201
Launch a server, connect a client to it and try various reads
11961202
and writes.
11971203
"""
1204+
stats = {}
11981205
server = ThreadedEchoServer(context=server_context,
11991206
chatty=chatty,
12001207
connectionchatty=False)
@@ -1220,12 +1227,14 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
12201227
if connectionchatty:
12211228
if support.verbose:
12221229
sys.stdout.write(" client: closing connection.\n")
1223-
stats = {
1230+
stats.update({
12241231
'compression': s.compression(),
12251232
'cipher': s.cipher(),
1226-
}
1233+
'client_npn_protocol': s.selected_npn_protocol()
1234+
})
12271235
s.close()
1228-
return stats
1236+
stats['server_npn_protocols'] = server.selected_protocols
1237+
return stats
12291238

12301239
def try_protocol_combo(server_protocol, client_protocol, expect_success,
12311240
certsreqs=None, server_options=0, client_options=0):
@@ -1853,6 +1862,43 @@ def test_dh_params(self):
18531862
if "ADH" not in parts and "EDH" not in parts and "DHE" not in parts:
18541863
self.fail("Non-DH cipher: " + cipher[0])
18551864

1865+
def test_selected_npn_protocol(self):
1866+
# selected_npn_protocol() is None unless NPN is used
1867+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
1868+
context.load_cert_chain(CERTFILE)
1869+
stats = server_params_test(context, context,
1870+
chatty=True, connectionchatty=True)
1871+
self.assertIs(stats['client_npn_protocol'], None)
1872+
1873+
@unittest.skipUnless(ssl.HAS_NPN, "NPN support needed for this test")
1874+
def test_npn_protocols(self):
1875+
server_protocols = ['http/1.1', 'spdy/2']
1876+
protocol_tests = [
1877+
(['http/1.1', 'spdy/2'], 'http/1.1'),
1878+
(['spdy/2', 'http/1.1'], 'http/1.1'),
1879+
(['spdy/2', 'test'], 'spdy/2'),
1880+
(['abc', 'def'], 'abc')
1881+
]
1882+
for client_protocols, expected in protocol_tests:
1883+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
1884+
server_context.load_cert_chain(CERTFILE)
1885+
server_context.set_npn_protocols(server_protocols)
1886+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
1887+
client_context.load_cert_chain(CERTFILE)
1888+
client_context.set_npn_protocols(client_protocols)
1889+
stats = server_params_test(client_context, server_context,
1890+
chatty=True, connectionchatty=True)
1891+
1892+
msg = "failed trying %s (s) and %s (c).\n" \
1893+
"was expecting %s, but got %%s from the %%s" \
1894+
% (str(server_protocols), str(client_protocols),
1895+
str(expected))
1896+
client_result = stats['client_npn_protocol']
1897+
self.assertEqual(client_result, expected, msg % (client_result, "client"))
1898+
server_result = stats['server_npn_protocols'][-1] \
1899+
if len(stats['server_npn_protocols']) else 'nothing'
1900+
self.assertEqual(server_result, expected, msg % (server_result, "server"))
1901+
18561902

18571903
def test_main(verbose=False):
18581904
if support.verbose:

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ Grzegorz Makarewicz
644644
David Malcolm
645645
Ken Manheimer
646646
Vladimir Marangozov
647+
Colin Marc
647648
David Marek
648649
Doug Marien
649650
Sven Marnach

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Core and Builtins
3030
Library
3131
-------
3232

33+
- Issue #14204: The ssl module now has support for the Next Protocol
34+
Negotiation extension, if available in the underlying OpenSSL library.
35+
Patch by Colin Marc.
36+
3337
- Issue #3035: Unused functions from tkinter are marked as pending peprecated.
3438

3539
- Issue #12757: Fix the skipping of doctests when python is run with -OO so

0 commit comments

Comments
 (0)