Skip to content

Commit

Permalink
Merge tls-alpn-npn-7860-25: Add NPN and ALPN support for the TLS tran…
Browse files Browse the repository at this point in the history
…sport.

Author: lukasa
Reviewers: hawkowl, adiroiban, glyph
Fixes: twisted#7860

git-svn-id: svn://svn.twistedmatrix.com/svn/Twisted/trunk@46146 bbbe8e31-12d6-0310-92fd-ac37d47ddeeb
  • Loading branch information
adiroiban committed Nov 12, 2015
1 parent 19fbb8e commit 2f67bc3
Show file tree
Hide file tree
Showing 10 changed files with 823 additions and 30 deletions.
2 changes: 2 additions & 0 deletions docs/core/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,5 @@ Miscellaneous
- :download:`wxacceptance.py` - acceptance tests for wxreactor
- :download:`postfix.py` - test application for PostfixTCPMapServer
- :download:`udpbroadcast.py` - broadcasting using UDP
- :download:`tls_alpn_npn_client.py` - example of TLS next-protocol negotiation on the client side using NPN and ALPN.
- :download:`tls_alpn_npn_server.py` - example of TLS next-protocol negotiation on the server side using NPN and ALPN.
110 changes: 110 additions & 0 deletions docs/core/examples/tls_alpn_npn_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
tls_alpn_npn_client
~~~~~~~~~~~~~~~~~~~
This test script demonstrates the usage of the acceptableProtocols API as a
client peer.
It performs next protocol negotiation using NPN and ALPN.
It will print what protocol was negotiated and exit.
The global variables are provided as input values.
This is set up to run against the server from
tls_alpn_npn_server.py from the directory that contains this example.
It assumes that you have a self-signed server certificate, named
`server-cert.pem` and located in the working directory.
"""
from twisted.internet import ssl, protocol, endpoints, task, defer
from twisted.python.filepath import FilePath

# The hostname the remote server to contact.
TARGET_HOST = u'localhost'

# The port to contact.
TARGET_PORT = 8080

# The list of protocols we'd be prepared to speak after the TLS negotiation is
# complete.
# The order of the protocols here is an order of preference: most servers will
# attempt to respect our preferences when doing the negotiation. This indicates
# that we'd prefer to use HTTP/2 if possible (where HTTP/2 is using the token
# 'h2'), but would also accept HTTP/1.1.
# The bytes here are sent literally on the wire, and so there is no room for
# ambiguity about text encodings.
# Try changing this list by adding, removing, and reordering protocols to see
# how it affects the result.
ACCEPTABLE_PROTOCOLS = [b'h2', b'http/1.1']

# Some safe initial data to send. This data is specific to HTTP/2: it is part
# of the HTTP/2 client preface (see RFC 7540 Section 3.5). This is used to
# signal to the remote server that it is aiming to speak HTTP/2, and to prevent
# a remote HTTP/1.1 server from expecting a 'proper' HTTP/1.1 request.
#
# FIXME: https://twistedmatrix.com/trac/ticket/6024
# This is only required because there is no event that fires when the TLS
# handshake is done. Instead, we wait for one that is implicitly after the
# TLS handshake is done: dataReceived. To trigger the remote peer to send data,
# we send some ourselves.
TLS_TRIGGER_DATA = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'


def main(reactor):
certData = FilePath('server-cert.pem').getContent()
serverCertificate = ssl.Certificate.loadPEM(certData)
options = ssl.optionsForClientTLS(
hostname=TARGET_HOST,
trustRoot=serverCertificate,
# `acceptableProtocols` is the targetted option for this example.
acceptableProtocols=ACCEPTABLE_PROTOCOLS,
)

class BasicH2Request(protocol.Protocol):
def connectionMade(self):
print("Connection made")
# Add a deferred that fires where we're done with the connection.
# This deferred is returned to the reactor, and when we call it
# back the reactor will clean up the protocol.
self.complete = defer.Deferred()

# Write some data to trigger the SSL handshake.
self.transport.write(TLS_TRIGGER_DATA)

def dataReceived(self, data):
# We can only safely be sure what the next protocol is when we know
# the TLS handshake is over. This is generally *not* in the call to
# connectionMade, but instead only when we've received some data
# back.
print('Next protocol is: %s' % self.transport.negotiatedProtocol)
self.transport.loseConnection()

# If this is the first data write, we can tell the reactor we're
# done here by firing the callback we gave it.
if self.complete is not None:
self.complete.callback(None)
self.complete = None

def connectionLost(self, reason):
# If we haven't received any data, an error occurred. Otherwise,
# we lost the connection on purpose.
if self.complete is not None:
print("Connection lost due to error %s" % (reason,))
self.complete.callback(None)
else:
print("Connection closed cleanly")

return endpoints.connectProtocol(
endpoints.SSL4ClientEndpoint(
reactor,
TARGET_HOST,
TARGET_PORT,
options
),
BasicH2Request()
).addCallback(lambda protocol: protocol.complete)

task.react(main)
106 changes: 106 additions & 0 deletions docs/core/examples/tls_alpn_npn_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
tls_alpn_npn_server
~~~~~~~~~~~~~~~~~~~
This test script demonstrates the usage of the acceptableProtocols API as a
server peer.
It performs next protocol negotiation using NPN and ALPN.
It will print what protocol was negotiated for each connection that is made to
it.
To exit the server, use CTRL+C on the command-line.
Before using this, you should generate a new RSA private key and an associated
X.509 certificate and place it in the working directory as `server-key.pem`
and `server-cert.pem`.
You can generate a self signed certificate using OpenSSL:
openssl req -new -newkey rsa:2048 -days 3 -nodes -x509 \
-keyout server-key.pem -out server-cert.pem
To test this, use OpenSSL's s_client command, with either or both of the
-nextprotoneg and -alpn arguments. For example:
openssl s_client -connect localhost:8080 -alpn h2,http/1.1
openssl s_client -connect localhost:8080 -nextprotoneg h2,http/1.1
Alternatively, use the tls_alpn_npn_client.py script found in the examples
directory.
"""
from OpenSSL import crypto

from twisted.internet.endpoints import SSL4ServerEndpoint
from twisted.internet.protocol import Protocol, Factory
from twisted.internet import reactor, ssl
from twisted.python.filepath import FilePath


# The list of protocols we'd be prepared to speak after the TLS negotiation is
# complete.
# The order of the protocols here is an order of preference. This indicates
# that we'd prefer to use HTTP/2 if possible (where HTTP/2 is using the token
# 'h2'), but would also accept HTTP/1.1.
# The bytes here are sent literally on the wire, and so there is no room for
# ambiguity about text encodings.
# Try changing this list by adding, removing, and reordering protocols to see
# how it affects the result.
ACCEPTABLE_PROTOCOLS = [b'h2', b'http/1.1']

# The port that the server will listen on.
LISTEN_PORT = 8080



class NPNPrinterProtocol(Protocol):
"""
This protocol accepts incoming connections and waits for data. When
received, it prints what the negotiated protocol is, echoes the data back,
and then terminates the connection.
"""
def connectionMade(self):
self.complete = False
print("Connection made")


def dataReceived(self, data):
print(self.transport.negotiatedProtocol)
self.transport.write(data)
self.complete = True
self.transport.loseConnection()


def connectionLost(self, reason):
# If we haven't received any data, an error occurred. Otherwise,
# we lost the connection on purpose.
if self.complete:
print("Connection closed cleanly")
else:
print("Connection lost due to error %s" % (reason,))



class ResponderFactory(Factory):
def buildProtocol(self, addr):
return NPNPrinterProtocol()



privateKeyData = FilePath('server-key.pem').getContent()
privateKey = crypto.load_privatekey(crypto.FILETYPE_PEM, privateKeyData)
certData = FilePath('server-cert.pem').getContent()
certificate = crypto.load_certificate(crypto.FILETYPE_PEM, certData)

options = ssl.CertificateOptions(
privateKey=privateKey,
certificate=certificate,
acceptableProtocols=ACCEPTABLE_PROTOCOLS,
)
endpoint = SSL4ServerEndpoint(reactor, LISTEN_PORT, options)
endpoint.listen(ResponderFactory())
reactor.run()
50 changes: 50 additions & 0 deletions docs/core/howto/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,56 @@ Additionally, it is possible to limit the acceptable ciphers for your connection
Since Twisted uses a secure cipher configuration by default, it is discouraged to do so unless absolutely necessary.


Application Layer Protocol Negotiation (ALPN) and Next Protocol Negotiation (NPN)
---------------------------------------------------------------------------------

ALPN and NPN are TLS extensions that can be used by clients and servers to negotiate what application-layer protocol will be spoken once the encrypted connection is established.
This avoids the need for extra custom round trips once the encrypted connection is established. It is implemented as a standard part of the TLS handshake.

NPN is supported from OpenSSL version 1.0.1.
ALPN is the newer of the two protocols, supported in OpenSSL versions 1.0.2 onward.
These functions require pyOpenSSL version 0.15 or higher.
To query the methods supported by your system, use :api:`twisted.internet.ssl.protocolNegotiationMechanisms`.
It will return a collection of flags indicating support for NPN and/or ALPN.

:api:`twisted.internet.ssl.CertificateOptions` and :api:`twisted.internet.ssl.optionsForClientTLS` allow for selecting the protocols your program is willing to speak after the connection is established.

On the server=side you will have:

.. code-block:: python
from twisted.internet.ssl import CertificateOptions
options = CertificateOptions(..., acceptableProtocols=[b'h2', b'http/1.1'])
and for clients:

.. code-block:: python
from twisted.internet.ssl import optionsForClientTLS
options = optionsForClientTLS(hostname=hostname, acceptableProtocols=[b'h2', b'http/1.1'])
Twisted will attempt to use both ALPN and NPN, if they're available, to maximise compatibility with peers.
If both ALPN and NPN are supported by the peer, the result from ALPN is preferred.

For NPN, the client selects the protocol to use;
For ALPN, the server does.
If Twisted is acting as the peer who is supposed to select the protocol, it will prefer the earliest protocol in the list that is supported by both peers.

To determine what protocol was negotiated, after the connection is done, use :api:`twisted.protocols.tls.TLSMemoryBIOProtocol.negotiatedProtocol <TLSMemoryBIOProtocol.negotiatedProtocol>`.
It will return one of the protocol names passed to the ``acceptableProtocols`` parameter.
It will return ``None`` if the peer did not offer ALPN or NPN.

It can also return ``None`` if no overlap could be found and the connection was established regardless (some peers will do this: Twisted will not).
In this case, the protocol that should be used is whatever protocol would have been used if negotiation had not been attempted at all.

.. warning::
If ALPN or NPN are used and no overlap can be found, then the remote peer may choose to terminate the connection.
This may cause the TLS handshake to fail, or may result in the connection being torn down immediately after being made.
If Twisted is the selecting peer (that is, Twisted is the server and ALPN is being used, or Twisted is the client and NPN is being used), and no overlap can be found, Twisted will always choose to fail the handshake rather than allow an ambiguous connection to set up.

An example of using this functionality can be found in :download:`this example script for clients </core/examples/tls_alpn_npn_client.py>` and :download:`this example script for servers </core/examples/tls_alpn_npn_server.py>`.


Related facilities
------------------

Expand Down
Loading

0 comments on commit 2f67bc3

Please sign in to comment.