Index: docs/core/howto/ssl.rst =================================================================== --- docs/core/howto/ssl.rst (revision 44951) +++ docs/core/howto/ssl.rst (working copy) @@ -220,6 +220,45 @@ 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. These functions also +require pyOpenSSL version 0.15 or higher. + +: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=[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, 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. + + @param serverCertOpts: Certificate options for the server side. + @param clientCertOpts: Certificate options for the client side. + """ + 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): + """ + When both ALPN and NPN are used, and both the client and server have + overlapping protocol choices, a protocol is successfully negotiated. + Further, the negotiated protocol is the first one 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_NPNAndALPNDifferent(self): + """ + Client and server have different protocol lists: only the common + element is chosen. + """ + serverProtocols = [b'h2', b'http/1.1', b'spdy/2'] + clientProtocols = [b'spdy/3', b'http/1.1'] + self.loopback( + sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=serverProtocols, + ), + sslverify.OpenSSLCertificateOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=clientProtocols, + ), + ) + + return self.proto.deferred.addCallback( + self.assertEqual, b'http/1.1') + + + def test_NPNAndALPNFailure(self): + """ + 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) + + + def test_NPNRespectsClientPreference(self): + """ + When NPN is used, the client's protocol preference is preferred. + """ + serverProtocols = [b'http/1.1', b'h2'] + clientProtocols = [b'h2', b'http/1.1'] + self.loopback( + sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=serverProtocols, + ), + NPNOnlyOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=clientProtocols, + ), + ) + + return self.proto.deferred.addCallback( + self.assertEqual, b'h2') + + + +class ALPNTest(unittest.TestCase): + """ + ALPN protocol selection. + + These tests only run on platforms that have a PyOpenSSL version >= 0.15, + and OpenSSL version 1.0.2 or later. + + This covers only the ALPN specific logic, as any platform that has ALPN + will also have NPN and so will run the NPNAndALPNTest suite as well. + """ + if skipSSL: + skip = skipSSL + elif skipALPN: + skip = skipALPN + + 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. + + @param serverCertOpts: Certificate options for the server side. + @param clientCertOpts: Certificate options for the client side. + """ + 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_ALPNRespectsServerPreference(self): + """ + When ALPN is used, the server's protocol preference is preferred. + """ + serverProtocols = [b'http/1.1', b'h2'] + clientProtocols = [b'h2', b'http/1.1'] + self.loopback( + sslverify.OpenSSLCertificateOptions( + privateKey=self.sKey, + certificate=self.sCert, + verify=True, + requireCertificate=True, + caCerts=[self.cCert], + nextProtocols=serverProtocols, + ), + ALPNOnlyOptions( + privateKey=self.cKey, + certificate=self.cCert, + verify=True, + requireCertificate=True, + caCerts=[self.sCert], + nextProtocols=clientProtocols, + ), + ) + + return self.proto.deferred.addCallback( + self.assertEqual, b'http/1.1') + + + +class NPNAndALPNAbsentTest(unittest.TestCase): + """ + 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. + + @param serverCertOpts: Certificate options for the server side. + @param clientCertOpts: Certificate options for the client side. + """ + 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): + """ + 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