Index: docs/core/howto/ssl.rst =================================================================== --- docs/core/howto/ssl.rst (revision 44494) +++ docs/core/howto/ssl.rst (working copy) @@ -220,6 +220,44 @@ 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 for a client and server to +negotiate what application-layer protocol will be spoken once the encrypted +connection is established. This avoids the need for extra round trips once the +encrypted connection is established, instead piggybacking on the TLS handshake +to do the negotiation. + +ALPN is the newer of the two protocols, supported in OpenSSL versions 1.0.2 +onward. NPN is supported from OpenSSL version 1.0.1. + +:api:`twisted.internet.ssl.CertificateOptions` allows for selecting the +protocols your program is willing to speak after the connection is established. +Doing so is very simple: + +.. code-block:: python + + from twisted.internet.ssl import CertificateOptions + options = CertificateOptions(..., nextProtocol=['h2', '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, then +the result from ALPN will be preferred. + +For NPN, the client selects the protocol to use; for ALPN, the server does. If +Twisted is acting in either of those roles, then it will prefer the earliest +protocol in the list that is supported by both peers. + +To determine what protocol was negotiated, use +:api:`twisted.protocols.tls.TLSMemoryBIOProtocol.getNextProtocol = 0.15, + and OpenSSL version 1.0.1 or later. + """ + if skipSSL: + skip = skipSSL + elif skipNPN: + skip = skipNPN + + serverPort = clientConn = None + + sKey = None + sCert = None + cKey = None + cCert = None + + def setUp(self): + """ + Create class variables of client and server certificates. + """ + self.sKey, self.sCert = makeCertificate( + O=b"Server Test Certificate", + CN=b"server") + self.cKey, self.cCert = makeCertificate( + O=b"Client Test Certificate", + CN=b"client") + self.caCert1 = makeCertificate( + O=b"CA Test Certificate 1", + CN=b"ca1")[1] + self.caCert2 = makeCertificate( + O=b"CA Test Certificate", + CN=b"ca2")[1] + + + def tearDown(self): + if self.serverPort is not None: + self.serverPort.stopListening() + if self.clientConn is not None: + self.clientConn.disconnect() + + + def loopback(self, serverCertOpts, clientCertOpts): + """ + Create the TLS connection, and save the two ends of the connection + on the test class. + """ + self.proto = NegotiatedProtocol() + + serverFactory = protocol.ServerFactory() + serverFactory.protocol = lambda: self.proto + + clientFactory = protocol.ClientFactory() + clientFactory.protocol = NegotiatedProtocol + + self.serverPort = reactor.listenSSL(0, serverFactory, serverCertOpts) + self.clientConn = reactor.connectSSL('127.0.0.1', + self.serverPort.getHost().port, clientFactory, clientCertOpts) + + + def test_NPNAndALPNSuccess(self): + """ + Test that, when both ALPN and NPN are used, and both the client and + server have overlapping protocol choices, a protocol is successfully + negotiated. Further test that it's the first protocol in the list. + """ + protocols = [b'h2', b'http/1.1'] + self.loopback( + sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=protocols, + ), + sslverify.OpenSSLCertificateOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=protocols, + ), + ) + + return self.proto.deferred.addCallback( + self.assertEqual, b'h2') + + + def test_NPNAndALPNFailure(self): + """ + Test that when the client and server have no overlap of protocols, no + protocol is negotiated. + """ + protocols = [b'h2', b'http/1.1'] + self.loopback( + sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=protocols, + ), + sslverify.OpenSSLCertificateOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=[], + ), + ) + + return self.proto.deferred.addCallback( + self.assertEqual, None) + + + +class NPNAndALPNAbsentTest(unittest.TestCase): + """ + Test that NPN/ALPN operations fail on platforms that do not support + them. + + These tests only run on platforms that have a PyOpenSSL version < 0.15, + or an OpenSSL version earlier than 1.0.1 + """ + if skipSSL: + skip = skipSSL + elif not skipNPN: + skip = "NPN/ALPN is present on this platform" + + serverPort = clientConn = None + + sKey = None + sCert = None + cKey = None + cCert = None + + def setUp(self): + """ + Create class variables of client and server certificates. + """ + self.sKey, self.sCert = makeCertificate( + O=b"Server Test Certificate", + CN=b"server") + self.cKey, self.cCert = makeCertificate( + O=b"Client Test Certificate", + CN=b"client") + self.caCert1 = makeCertificate( + O=b"CA Test Certificate 1", + CN=b"ca1")[1] + self.caCert2 = makeCertificate( + O=b"CA Test Certificate", + CN=b"ca2")[1] + + + def tearDown(self): + if self.serverPort is not None: + self.serverPort.stopListening() + if self.clientConn is not None: + self.clientConn.disconnect() + + + def loopback(self, serverCertOpts, clientCertOpts): + """ + Create the TLS connection, and save the two ends of the connection + on the test class. + """ + self.proto = NegotiatedProtocol() + + serverFactory = protocol.ServerFactory() + serverFactory.protocol = lambda: self.proto + + clientFactory = protocol.ClientFactory() + clientFactory.protocol = NegotiatedProtocol + + self.serverPort = reactor.listenSSL(0, serverFactory, serverCertOpts) + self.clientConn = reactor.connectSSL('127.0.0.1', + self.serverPort.getHost().port, clientFactory, clientCertOpts) + + + def test_NPNAndALPNNotImplemented(self): + """ + Tests that a NotImplementedError is raised when using nextProtocols + on a platform that does not support either NPN or ALPN. + """ + protocols = [b'h2', b'http/1.1'] + serverCertOpts = sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=protocols, + ) + clientCertOpts = sslverify.OpenSSLCertificateOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=protocols, + ) + self.assertRaises( + NotImplementedError, self.loopback, serverCertOpts, clientCertOpts, + ) + + + class _NotSSLTransport: def getHandle(self): return self Index: twisted/topfiles/7860.feature =================================================================== --- twisted/topfiles/7860.feature (revision 0) +++ twisted/topfiles/7860.feature (working copy) @@ -0,0 +1 @@ +twisted.internet.ssl.CertificateOptions now takes a nextProtos parameter that enables negotiation of the next protocol to speak, after the TLS handshake has completed. This field advertises protocols over both NPN and ALPN. \ No newline at end of file