在 Twisted 中使用 TLS

概述

本文档介绍了如何在 Twisted 服务器和客户端中使用 TLS(传输层安全)——也称为 SSL(安全套接字层)——来保护您的通信。它假设您了解 TLS 是什么,使用它的主要原因是什么,以及如何生成自己的证书。它还假设您熟悉在 服务器操作指南客户端操作指南 中描述的创建 TCP 服务器和客户端。阅读本文档后,您应该能够创建可以使用 TLS 加密连接的服务器和客户端,在连接过程中从使用未加密通道切换到使用加密通道,以及要求客户端身份验证。

在 Twisted 中使用 TLS 需要您安装 pyOpenSSL。验证您是否安装了它的快速测试是在 Python 提示符下运行 from OpenSSL import SSL 并且不出现错误。

Twisted 将 TLS 支持作为一种传输提供——也就是说,作为 TCP 的替代方案。当使用 TLS 时,使用您已经熟悉的 TCP API,TCP4ClientEndpointTCP4ServerEndpoint——或者 reactor.listenTCPreactor.connectTCP——被使用并行的 TLS API(其中许多仍然使用传统的名称“SSL”,因为它们比较老或者与旧的 API 兼容)所取代。要创建 TLS 服务器,请使用 SSL4ServerEndpointlistenSSL。要创建 TLS 客户端,请使用 SSL4ClientEndpointconnectSSL

TLS 提供传输层安全,但重要的是要了解“安全”的含义。关于 TLS,它意味着三件事

  1. 身份:TLS 服务器(有时还有客户端)会提供证书,证明它们是谁,这样您就知道自己在和谁交谈。

  2. 机密性:一旦您知道自己在和谁交谈,连接的加密就能确保通信不会被任何可能在监听的第三方理解。

  3. 完整性:TLS 会检查加密的消息,以确保它们确实来自您最初验证的方。如果消息未能通过这些检查,那么它们将被丢弃,您的应用程序不会看到它们。

没有身份,机密性和完整性都是不可能的。如果您不知道自己在和谁交谈,那么您可能很容易在和您的银行交谈,或者在和想要窃取您的银行密码的小偷交谈。上面列出的每个带有“SSL”的 API 都需要一个配置对象,称为(出于历史原因)contextFactory。(请原谅这个有点尴尬的名字。)contextFactory 有三个目的

  1. 它提供材料来证明您自己的身份给连接的另一方:换句话说,您是谁。

  2. 它表达您对另一方身份的要求:换句话说,您想和谁交谈(以及您信任谁告诉您您正在和正确的人交谈)。

  3. 它允许您指定有关 TLS 协议本身操作方式的某些专门选项。

客户端和服务器的要求略有不同。两者都可以提供证书来证明自己的身份,但通常,TLS 服务器会提供证书,而 TLS 客户端会检查服务器的证书(以确保它们正在与正确的服务器交谈),然后以其他方式向服务器标识自己,通常是通过提供共享密钥(例如密码或 API 密钥),通过使用 TLS 保护的应用程序协议,而不是作为 TLS 本身的一部分。

由于这些要求略有不同,因此有不同的 API 来构建适合客户端或服务器的 contextFactory 值。

对于服务器,我们可以使用 twisted.internet.ssl.CertificateOptions。为了证明服务器的身份,您需要将 privateKeycertificate 参数传递给此对象。 twisted.internet.ssl.PrivateCertificate.options() 是一种创建 CertificateOptions 实例的便捷方法,该实例配置为使用特定的密钥和证书。

对于客户端,我们可以使用 twisted.internet.ssl.optionsForClientTLS()。它接受两个参数,hostname(指示服务器证书中必须宣传的主机名)以及可选的 trustRoot。默认情况下,optionsForClientTLS 尝试从您的平台获取信任根,但您可以指定自己的信任根。

您可以通过调用 twisted.internet.ssl.trustRootFromCertificates(),获得一个适合作为 trustRoot= 参数传递的对象,该对象包含一个显式的 twisted.internet.ssl.Certificatetwisted.internet.ssl.PrivateCertificate 实例列表。这将导致 optionsForClientTLS 接受任何连接,只要服务器证书由传递的证书中的至少一个证书签名即可。

注意

目前,Twisted 仅支持加载 OpenSSL 的默认信任根。如果您自己构建了 OpenSSL,则必须注意将这些信任根包含在适当的位置。如果您使用的是 macOS 10.5-10.9 附带的 OpenSSL,则此行为也将是正确的。如果您使用的是 Debian 或其衍生版本(如 Ubuntu),请安装 ca-certificates 包以确保您拥有可用的信任根,此行为也将是正确的。正在进行的工作是使 platformTrustoptionsForClientTLS 默认使用的 API)更加健壮。例如,platformTrust 应该在没有平台信任根可用时回退到 “certifi” 包,但它目前还没有这样做。当这种情况发生时,您不需要更改代码。

TLS 回声服务器和客户端

现在我们已经了解了理论,让我们尝试一些关于如何开始使用 TLS 服务器的实际示例。以下示例依赖于文件 server.pem(私钥和自签名证书一起)和 public.pem(服务器的公钥证书本身)。

TLS 回声服务器

echoserv_ssl.py

#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

import sys

import echoserv

from twisted.internet import defer, protocol, ssl, task
from twisted.python import log
from twisted.python.modules import getModule


def main(reactor):
    log.startLogging(sys.stdout)
    certData = getModule(__name__).filePath.sibling("server.pem").getContent()
    certificate = ssl.PrivateCertificate.loadPEM(certData)
    factory = protocol.Factory.forProtocol(echoserv.Echo)
    reactor.listenSSL(8000, factory, certificate.options())
    return defer.Deferred()


if __name__ == "__main__":
    import echoserv_ssl

    task.react(echoserv_ssl.main)

此服务器使用 listenSSL 在端口 8000 上监听 TLS 流量,使用文件 server.pem 中包含的证书和私钥。它使用与 TCP 回声服务器相同的回声示例服务器,甚至还导入其协议类。假设您可以从证书颁发机构购买自己的 TLS 证书,这是一个相当现实的 TLS 服务器。

TLS 回声客户端

echoclient_ssl.py

#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

import echoclient

from twisted.internet import defer, endpoints, protocol, ssl, task
from twisted.python.modules import getModule


@defer.inlineCallbacks
def main(reactor):
    factory = protocol.Factory.forProtocol(echoclient.EchoClient)
    certData = getModule(__name__).filePath.sibling("public.pem").getContent()
    authority = ssl.Certificate.loadPEM(certData)
    options = ssl.optionsForClientTLS("example.com", authority)
    endpoint = endpoints.SSL4ClientEndpoint(reactor, "localhost", 8000, options)
    echoClient = yield endpoint.connect(factory)

    done = defer.Deferred()
    echoClient.connectionLost = lambda reason: done.callback(None)
    yield done


if __name__ == "__main__":
    import echoclient_ssl

    task.react(echoclient_ssl.main)

此客户端使用 SSL4ClientEndpoint 连接到 echoserv_ssl.py。它还使用与 TCP 回声客户端相同的回声示例客户端。每当您拥有一个在纯文本 TCP 上监听的协议时,都可以轻松地将其运行在 TLS 上。它指定它只希望与名为 "example.com" 的主机通信,并且它信任 "public.pem" 中的证书颁发机构来确定 "example.com" 是谁。请注意,您正在连接到的主机(localhost)和您正在验证其身份的主机(example.com)可能不同。在本例中,我们的示例 server.pem 证书标识了一个名为“example.com”的主机,但您的服务器可能正在 localhost 上运行。

在现实的客户端中,您传递给连接 API(在本例中为 SSL4ClientEndpoint)和 optionsForClientTLS 的“hostname”必须相同。在本例中,我们使用“localhost”作为要连接到的主机,因为您可能在自己的计算机上运行此示例,而使用“example.com”是因为这是与 Twisted 示例代码一起分发的虚拟证书中硬编码的值。

连接到公共服务器

这是一个简短的示例,现在使用 platformTrustoptionsForClientTLS 提供的默认信任根。

check_server_certificate.py

import sys

from twisted.internet import defer, endpoints, error, protocol, ssl, task


def main(reactor, host, port=443):
    options = ssl.optionsForClientTLS(hostname=host.decode("utf-8"))
    port = int(port)

    class ShowCertificate(protocol.Protocol):
        def connectionMade(self):
            self.transport.write(b"GET / HTTP/1.0\r\n\r\n")
            self.done = defer.Deferred()

        def dataReceived(self, data):
            certificate = ssl.Certificate(self.transport.getPeerCertificate())
            print("OK:", certificate)
            self.transport.abortConnection()

        def connectionLost(self, reason):
            print("Lost.")
            if not reason.check(error.ConnectionClosed):
                print("BAD:", reason.value)
            self.done.callback(None)

    return endpoints.connectProtocol(
        endpoints.SSL4ClientEndpoint(reactor, host, port, options), ShowCertificate()
    ).addCallback(lambda protocol: protocol.done)


task.react(main, sys.argv[1:])

您可以使用此工具非常简单地从具有有效 TLS 证书的 HTTPS 服务器检索证书,方法是使用主机名运行它。例如

$ python check_server_certificate.py www.twistedmatrix.com
OK: <Certificate Subject=www.twistedmatrix.com ...>
$ python check_server_certificate.py www.cacert.org
BAD: [(... 'certificate verify failed')]
$ python check_server_certificate.py dornkirk.twistedmatrix.com
BAD: No service reference ID could be validated against certificate.

注意

要根据 RFC6125 正确 验证您的 hostname 参数,请还从 PyPI 安装 “service_identity”“idna” 包。如果没有此包,Twisted 目前将对服务器证书的正确性进行保守的猜测,但这将拒绝大量可能有效的证书。 service_identity 正确地实现了标准,它将成为 Twisted 未来版本中 TLS 的必需依赖项。

使用 startTLS

如果您想在连接过程中从未加密流量切换到加密流量,则需要使用 startTLS 在连接的双方同时通过某种约定的信号(例如接收特定消息)打开 TLS。您可以通过使用 Wireshark 等工具检查数据包有效负载来轻松验证切换到加密通道。

startTLS 服务器

starttls_server.py

from twisted.internet import defer, endpoints, protocol, ssl, task
from twisted.protocols.basic import LineReceiver
from twisted.python.modules import getModule


class TLSServer(LineReceiver):
    def lineReceived(self, line):
        print("received: ", line)
        if line == b"STARTTLS":
            print("-- Switching to TLS")
            self.sendLine(b"READY")
            self.transport.startTLS(self.factory.options)


def main(reactor):
    certData = getModule(__name__).filePath.sibling("server.pem").getContent()
    cert = ssl.PrivateCertificate.loadPEM(certData)
    factory = protocol.Factory.forProtocol(TLSServer)
    factory.options = cert.options()
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 8000)
    endpoint.listen(factory)
    return defer.Deferred()


if __name__ == "__main__":
    import starttls_server

    task.react(starttls_server.main)

startTLS 客户端

starttls_client.py

from twisted.internet import defer, endpoints, protocol, ssl, task
from twisted.protocols.basic import LineReceiver
from twisted.python.modules import getModule


class StartTLSClient(LineReceiver):
    def connectionMade(self):
        self.sendLine(b"plain text")
        self.sendLine(b"STARTTLS")

    def lineReceived(self, line):
        print("received: ", line)
        if line == b"READY":
            self.transport.startTLS(self.factory.options)
            self.sendLine(b"secure text")
            self.transport.loseConnection()


@defer.inlineCallbacks
def main(reactor):
    factory = protocol.Factory.forProtocol(StartTLSClient)
    certData = getModule(__name__).filePath.sibling("server.pem").getContent()
    factory.options = ssl.optionsForClientTLS(
        "example.com", ssl.PrivateCertificate.loadPEM(certData)
    )
    endpoint = endpoints.HostnameEndpoint(reactor, "localhost", 8000)
    startTLSClient = yield endpoint.connect(factory)

    done = defer.Deferred()
    startTLSClient.connectionLost = lambda reason: done.callback(None)
    yield done


if __name__ == "__main__":
    import starttls_client

    task.react(starttls_client.main)

startTLS 是一种传输方法,它传递一个 contextFactory。它在客户端和服务器协议的数据接收方法中约定的时间被调用。服务器使用 PrivateCertificate.options 创建一个 contextFactory,它将使用特定的证书和私钥(TLS 服务器的常见要求)。

客户端创建一个未定制的 CertificateOptions,这对于 TLS 客户端与 TLS 服务器交互来说已经足够了。

客户端身份验证

服务器端和客户端的更改以要求客户端身份验证主要属于 pyOpenSSL 的管辖范围,但网络上似乎没有多少示例,为了完整起见,这里提供了一个示例服务器和客户端。

通过客户端证书验证进行客户端身份验证的 TLS 服务器

当一个或多个证书传递给 PrivateCertificate.options 时,生成的 contextFactory 将使用这些证书作为可信机构,并要求对等方提供一个由这些机构之一锚定的有效链的证书。

服务器可以使用此来验证客户端是否提供了由这些证书颁发机构之一签名的有效证书;以下是一个此类证书的示例。

ssl_clientauth_server.py

#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

import sys

import echoserv

from twisted.internet import defer, protocol, ssl, task
from twisted.python import log
from twisted.python.modules import getModule


def main(reactor):
    log.startLogging(sys.stdout)
    certData = getModule(__name__).filePath.sibling("public.pem").getContent()
    authData = getModule(__name__).filePath.sibling("server.pem").getContent()
    authority = ssl.Certificate.loadPEM(certData)
    certificate = ssl.PrivateCertificate.loadPEM(authData)
    factory = protocol.Factory.forProtocol(echoserv.Echo)
    reactor.listenSSL(8000, factory, certificate.options(authority))
    return defer.Deferred()


if __name__ == "__main__":
    import ssl_clientauth_server

    task.react(ssl_clientauth_server.main)

带有证书的客户端

然后,以下客户端将此类证书作为 clientCertificate 参数传递给 optionsForClientTLS,同时仍然验证服务器的身份。

ssl_clientauth_client.py

#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

import echoclient

from twisted.internet import defer, endpoints, protocol, ssl, task
from twisted.python.modules import getModule


@defer.inlineCallbacks
def main(reactor):
    factory = protocol.Factory.forProtocol(echoclient.EchoClient)
    certData = getModule(__name__).filePath.sibling("public.pem").getContent()
    authData = getModule(__name__).filePath.sibling("server.pem").getContent()
    clientCertificate = ssl.PrivateCertificate.loadPEM(authData)
    authority = ssl.Certificate.loadPEM(certData)
    options = ssl.optionsForClientTLS("example.com", authority, clientCertificate)
    endpoint = endpoints.SSL4ClientEndpoint(reactor, "localhost", 8000, options)
    echoClient = yield endpoint.connect(factory)

    done = defer.Deferred()
    echoClient.connectionLost = lambda reason: done.callback(None)
    yield done


if __name__ == "__main__":
    import ssl_clientauth_client

    task.react(ssl_clientauth_client.main)

请注意,这两个示例与上面的 TLS 回声示例非常相似。实际上,您可以通过简单地运行 echoclient_ssl.py 针对 ssl_clientauth_server.py 来演示身份验证失败;您将看不到任何输出,因为服务器关闭了连接而不是回显客户端的身份验证输入。

TLS 协议选项

对于服务器,最好提供基于 Diffie-Hellman 的密钥交换,它提供完美的正向保密。密码默认情况下处于激活状态,但是需要将 DiffieHellmanParameters 的实例传递给 CertificateOptions,通过 dhParameters 选项才能使用它们。

例如,

from twisted.internet.ssl import CertificateOptions, DiffieHellmanParameters
from twisted.python.filepath import FilePath
dhFilePath = FilePath('dh_param_1024.pem')
dhParams = DiffieHellmanParameters.fromFile(dhFilePath)
options = CertificateOptions(..., dhParameters=dhParams)

CertificateOptions 可以控制的 TLS 协议的另一个部分是使用的 TLS 或 SSL 协议的版本。默认情况下,Twisted 将将其配置为使用 TLSv1.2 或更高版本,并禁用不安全的 SSLv3 协议。如果您需要支持旧的 SSLv3 系统,或者希望将其限制为仅最强大的 TLS 版本,则手动控制协议可能会有所帮助。

您可以要求 CertificateOptions 使用比 Twisted 默认值更安全的默认最小值,方法是在初始化程序中使用 raiseMinimumTo 参数

from twisted.internet.ssl import CertificateOptions, TLSVersion
options = CertificateOptions(
    ...,
    raiseMinimumTo=TLSVersion.TLSv1_3)

这将始终协商至少 TLSv1.3,但如果 Twisted 的默认值更高,则会协商更高版本。如果 Twisted 将最小值更新为某个假设的未来 TLS 版本,而不是导致您的应用程序使用您设置的现在理论上不安全的最小值,则此用法将保持安全。

如果您需要严格的 TLS 版本范围,希望 CertificateOptions 协商,您可以在初始化程序中使用 insecurelyLowerMinimumTolowerMaximumSecurityTo 参数

from twisted.internet.ssl import CertificateOptions, TLSVersion
options = CertificateOptions(
    ...,
    insecurelyLowerMinimumTo=TLSVersion.TLSv1_0,
    lowerMaximumSecurityTo=TLSVersion.TLSv1_2)

这将导致它在 TLSv1.0 和 TLSv1.2 之间协商,并且如果 Twisted 的默认最小 TLS 版本提高,它不会改变。请注意,这可能根本不起作用,因为您的 OpenSSL 版本可能会限制对已弃用或损坏的 TLS 版本的可用性。强烈建议不要设置 lowerMaximumSecurityTo,除非您有一个已知在较新 TLS 版本上行为异常的对等方,并且仅在 Twisted 的最小值不可接受时设置 insecurelyLowerMinimumTo。如果未经常检查,将这两个参数用于 CertificateOptions 可能会使您的应用程序的 TLS 不安全,不应在库中使用。

SSLv3 支持仍然可用,如果您愿意,可以启用对它的支持。例如,这支持所有 TLS 版本和 SSLv3

from twisted.internet.ssl import CertificateOptions, TLSVersion
options = CertificateOptions(
    ...,
    insecurelyLowerMinimumTo=TLSVersion.SSLv3)

未来的 OpenSSL 版本可能会完全删除协商不安全的 SSLv3 协议的能力,这将不允许您重新启用它。

此外,可以通过将 IAcceptableCiphers 对象传递给 CertificateOptions 来限制连接的可接受密码。由于 Twisted 默认使用安全的密码配置,因此除非绝对必要,否则不建议这样做。

应用层协议协商 (ALPN) 和下一协议协商 (NPN)

ALPN 和 NPN 是 TLS 扩展,客户端和服务器可以使用它们来协商在建立加密连接后将使用哪种应用层协议。这避免了在建立加密连接后需要额外的自定义往返。它作为 TLS 握手的标准部分实现。

NPN 从 OpenSSL 版本 1.0.1 开始受支持。ALPN 是这两个协议中较新的一个,在 OpenSSL 版本 1.0.2 及更高版本中受支持。这些功能需要 pyOpenSSL 版本 0.15 或更高版本。要查询系统支持的方法,请使用 twisted.internet.ssl.protocolNegotiationMechanisms()。它将返回一个标志集合,指示对 NPN 和/或 ALPN 的支持。

twisted.internet.ssl.CertificateOptionstwisted.internet.ssl.optionsForClientTLS() 允许选择程序在连接建立后愿意使用的协议。

在服务器端,您将拥有

from twisted.internet.ssl import CertificateOptions
options = CertificateOptions(..., acceptableProtocols=[b'h2', b'http/1.1'])

对于客户端

from twisted.internet.ssl import optionsForClientTLS
options = optionsForClientTLS(hostname=hostname, acceptableProtocols=[b'h2', b'http/1.1'])

Twisted 将尝试使用 ALPN 和 NPN(如果可用),以最大限度地提高与对等方的兼容性。如果对等方同时支持 ALPN 和 NPN,则优先使用 ALPN 的结果。

对于 NPN,客户端选择要使用的协议;对于 ALPN,服务器选择。如果 Twisted 充当应该选择协议的对等方,它将优先选择对等方都支持的列表中最早的协议。

要确定协商了哪个协议,在连接完成之后,请使用 TLSMemoryBIOProtocol.negotiatedProtocol。它将返回传递给 acceptableProtocols 参数的协议名称之一。如果对等方没有提供 ALPN 或 NPN,它将返回 None

如果找不到重叠并且无论如何都建立了连接,它也可能返回 None(某些对等方会这样做:Twisted 不会)。在这种情况下,应该使用的协议是如果没有尝试协商,本来会使用的协议。

警告

如果使用 ALPN 或 NPN 并且找不到重叠,则远程对等方可能会选择终止连接。这可能会导致 TLS 握手失败,或者可能导致连接在建立后立即断开。如果 Twisted 是选择对等方(即 Twisted 是服务器并且正在使用 ALPN,或者 Twisted 是客户端并且正在使用 NPN),并且找不到重叠,Twisted 将始终选择让握手失败,而不是允许建立模棱两可的连接。

使用此功能的示例可以在 示例 脚本 用于 客户端 示例 脚本 用于 服务器 中找到。

结论

阅读完本教程后,您应该能够

  • 使用 listenSSLconnectSSL 创建使用 TLS 的服务器和客户端

  • 使用 startTLS 将通道从未加密状态切换到连接过程中使用 TLS

  • 添加对客户端身份验证的服务器和客户端支持