第九章 建立TLS连接并通过它们发送数据

在本章中,我们将了解传输层安全Transport Layer SecurityTLS)协议。TLS协议是安全套接字层Secure Sockets LayerSSL)协议的继承者,用于安全的网络通信,并作为更高级别协议的基础,如超文本传输协议安全Hypertext Transfer Protocol SecureHTTPS)和简单邮件传输协议安全Simple Mail Transfer Protocol SecureSMTPS)。TLS协议在万维网上最为常见,但也用于其他应用程序,如文件传输、电子邮件、即时消息、IP语音、远程访问、数据库连接、金融数据传输以及许多其他需要加密通信的应用程序。

我们将学习TLS协议的基础知识,并快速浏览一下它的历史。在本章的实践部分,我们将学习如何建立TLS连接,通过它们发送数据,并正确关闭它们。将有命令行和C代码示例来说明如何通过TLS进行通信。

第九章 建立TLS连接并通过它们发送数据技术要求理解TLS协议理解TLS握手TLS握手后发生什么?TLS协议的历史在命令行上建立TLS客户端连接为TLS服务器连接准备证书在命令行上接受TLS服务器连接理解 OpenSSL BIOs以编程方式建立TLS客户端连接实现tls-client程序运行tls-client程序以编程方式接受TLS服务器连接实现tls-server程序运行tls-server程序总结

技术要求

本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要带有OpenSSL动态库的 openssl 命令行工具。为了构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。

我们将在本章中实施一些示例程序,以练习我们正在学习的内容。这些程序的完整源代码可以在这里找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter09

理解TLS协议

TLS是一种提供安全通信的通用协议。在撰写本文时,TLS协议的最新版本是1.3。TLS提供以下安全方面:

什么是PFS?要了解PFS是什么,我们必须了解密钥交换。TLS中的密钥交换或密钥协商key agreement)是就对称加密密钥达成一致的过程,该密钥将用于在TLS会话中加密用户数据。最古老的密钥交换方法是RSA密钥交换RSA key exchange),其中客户端生成对称密钥并用服务器证书公钥对其进行加密。较新的方法是Diffie-HellmanDH)密钥交换的变体,其中客户端和服务器都有一个非对称密钥对,并从自己的私钥和对等方的公钥中导出对称密钥。早期的TLS版本允许非PFS密钥交换方法,如RSA密钥交换和静态DH/EDCH密钥交换。当使用非PFS密钥交换方法时,服务器证书密钥对用于身份验证和密钥交换。在这种情况下,如果攻击者记录了TLS会话并随后窃取了服务器证书私钥,则攻击者可以使用它来恢复TLS会话中使用的对称会话密钥,并随后解密TLS会话期间发送的用户数据。当使用PFS密钥交换方法(如DHE或ECDHE)时,客户端和服务器会生成临时(短暂)密钥对进行密钥交换。这些短暂的密钥对只需要用于密钥交换,之后可以销毁。在这种情况下,即使服务器证书密钥被盗,攻击者也无法解密TLS会话,因为他们将没有来自该TLS会话的任何临时私钥。正如我们所看到的,当使用PFS密钥交换方法时,证书密钥对用于身份验证,临时密钥对用于密钥交换。

我们可以得出结论,TLS中的PFS是密钥交换方法的一个属性,即使服务器证书密钥被泄露,也可以防止攻击者解密记录的TLS会话。

TLS通常运行在可靠的传输协议之上,如传输控制协议Transmission Control ProtocolTCP)。TLS通常与运行在TLS之上的应用层协议(如超文本传输协议Hypertext Transfer ProtocolHTTP))一起使用。这个典型用例的协议层可以直观地表示如下:

9.1

通常将运行在TLS之上的应用程序协议视为该应用程序协议的安全变体。例如,HTTPS协议是HTTP协议的安全变体。HTTPS是在TLS之上运行的HTTP,而SMTPS是在TLS上运行的SMTP。正如我们所观察到的,应用程序协议的安全变体的名称中有S后缀。应用程序协议的普通和启用TLS的变体通常在不同的端口上侦听。例如,默认情况下,HTTP在端口 80 上侦听,HTTPS在端口 443 上侦听。但是,一些协议在相同的端口上支持非TLS和TLS变体。例如,SMTP支持 STARTTLS 命令,该命令将当前的TCP连接升级到SMTPS。这种显式启动TLS会话的方法称为机会TLSopportunistic TLS)。

TLS协议还有一个修改,称为数据报传输层安全Datagram Transport Layer SecurityDTLS),旨在在不可靠的传输协议(如用户数据报协议User Datagram ProtocolUDP))之上运行。值得一提的是,TCP上的TLS比UDP上的DTLS使用得更广泛。然而,确实存在使用DTLS的流行软件。一个值得注意的例子是OpenVPN。OpenVPN可以使用UDP上的DTLS,也可以像往常一样使用TCP上的TLS。

与TCP一样,TLS是一种客户端-服务器协议。要开始通过TLS发送数据,客户端和服务器必须首先建立TCP连接并执行TLS握手。

理解TLS握手

客户端通常在建立TCP连接后立即开始TLS握手,但在机会TLS的情况下,客户端和服务器首先以未加密的形式交换一些数据。

TLS握手的确切方式取决于TLS协议版本,但以下是握手过程中执行的重要操作:

TLS握手非常重要,但它不是TLS连接的唯一部分。让我们来了解一下握手后会发生什么。

TLS握手后发生什么?

TLS握手成功后,客户端和服务器可以相互发送数据,并将其封装到TLS记录(TLS records)中。传输的数据使用会话密钥进行加密,并通过经过身份验证的加密标签或HMAC进行身份验证,如果旧的TLS协议与非身份验证加密模式一起使用。接收方检查每个TLS记录的身份验证,如果检测到真实性验证错误,则中止连接。

当TLS连接的一方检测到错误时,它应该使用TLS警报消息将错误传达给另一方。警报消息可以指示致命错误或警告。在收到致命错误后,接收方必须停止通过连接发送和接收更多数据。

可能的警报消息之一是 close_notify 。此警报消息用于正确关闭TLS连接。通过此警告级别消息,连接的一方通知另一方,第一方正在关闭TLS连接的写入部分,并且不会发送更多数据。发送 close_notify 警报的连接方仍然可以从TLS 1.3中的TLS连接读取数据。较旧的TLS版本要求收到 close_notify 警报的另一方放弃其挂起的写入,并发送 close_notiify 警报,从而导致TLS连接关闭。TLS协议规范要求客户端和服务器在TLS连接到达其逻辑端并且不应发送或接收更多数据时发送 close_notify 消息。此要求减轻了对连接的截断攻击。如果两者都收到了对方的 close_notify 消息,则可以确定没有MITM截断了连接。请注意,关闭TLS连接并不自动意味着关闭底层TCP连接。TCP连接必须在TLS连接关闭后单独关闭——当然,只有在不再发送数据的情况下。

请注意,我们之前学到的各种加密概念是如何用于构建TLS协议的。用户数据加密需要对称加密。加密哈希函数是HMAC、数字签名和证书的基础。经过身份验证的加密和HMAC用于对用户和服务数据进行身份验证。密钥推导函数用于生成密钥、会话密钥和IV。数字签名用于确认X.509证书的有效性和所有权。X.509证书用于身份验证和缓解MITM攻击。我们现在所知道的TLS协议是许多人多年来研究和经验的结果,它结合了许多加密技术,为我们提供了安全的网络通信手段。

在本节中,我们多次提到TLS 1.3和旧版本协议之间的差异。让我们在下一节中更多地讨论TLS版本和TLS协议历史之间的差异。

TLS协议的历史

TLS协议是SSL协议的继承者。SSL协议最初是在20世纪90年代由网景通信公司开发的。

由于协议中的安全漏洞,SSL 1.0版本规范从未向公众发布或用于已知的软件产品。SSL 1.0仅在Netscape内部使用。

SSL 2.0于1995年发布。它支持DES、3DES、RC2、RC4和IDEA对称密码、基于MD5的MAC(不是HMAC)、RSA密钥交换和基于RSA的证书。安全研究人员很快发现了该协议中的许多安全漏洞。SSL 2.0的MAC身份验证较弱,握手不受保护,并且被发现容易受到长度扩展、截断、密码降级和MITM攻击。SSL 2.0没有得到太多的普及,很快就被第二年出现的SSL 3.0所取代。尽管SSL 2.0的使用并不多,但直到2011年才被正式弃用。

SSL 3.0于1996年发布,是对SSL 2.0中发现的缺陷的回应。它对SSL协议进行了全面的重新设计。所有现有的TLS协议版本仍然建立在SSL 3.0的基础上,与SSL 3.0有很多共同之处。SSL 3.0修复了SSL 2.0的设计缺陷,并引入了许多新功能,包括支持临时和非临时DH密钥交换、基于SHA-1的MAC、DSA签名证书、压缩、会话重用、会话重新协商、握手身份验证和协议版本协商。放弃了对弱对称密码的支持,如单DES、RC2和40位RC4。SSL 3.0从未正式支持AES密码,尽管包括OpenSSL在内的一些库支持AES和SSL 3.0作为非标准扩展。SSL 3.0流行了很长一段时间,但随着时间的推移,人们发现它容易受到某些攻击,例如针对SSL/TLS的浏览器漏洞利用Browser Exploit Against SSL/TLSBEAST)、压缩比信息泄露变得容易Compression Ratio Info-Leak Made EasyCRIME),尤其是在降级的传统加密上填充OraclePadding Oracle On Downgraded Legacy EncryptionPOODLE)。值得注意的是,SSL库可以通过记录拆分和压缩禁用来减轻这些SSL协议缺陷。SSL 3.0也使用了有缺陷的MAC然后加密结构。SSL 3.0的另一个缺点是它不支持TLS扩展。这导致了一个实际问题,即web浏览器在协商SSL 3.0协议时,无法使用重要的SNI扩展来指定共享主机上的web服务器名称。SSL 3.0最终在2015年被弃用。

TLS 1.0协议于1999年发布,作为SSL 3.0的升级。TLS 1.0规范不是由Netscape开发的,而是由互联网工程任务组Internet Engineering Task ForceIETF)组织的TLS工作组TLS Working GroupTLS WG)开发的。该协议已从SSL重命名为TLS,以避免Netscape的法律问题。TLS 1.0引入了对TLS扩展的支持,并增强了协议的各个方面,如PRF、MAC、Finished 消息和警报消息。与SSL不同,TLS 1.0使用HMAC进行TLS记录身份验证。后来,TLS 1.0扩展了更多对称密码,如AES、Camellia、SEED和GOST89,以及椭圆曲线支持,特别是对ECDSA签名证书和ECDH/ECDHE密钥协商方法的支持。TLS 1.0与SSL 3.0一样容易受到BEAST和CRIME攻击。最初的POODLE攻击不适用于TLS 1.0,但后来发现,一些TLS 1.0服务器使用的TLS库存在一个错误,使得修改后的POODLE攻击成为可能。TLS 1.0在2021年被弃用。

TLS 1.1于2006年发布,是TLS 1.0的进一步发展。它具有显式加密IV,而不是早期协议版本中的隐式IV,因此不易受到BEAST攻击。TLS 1.1还包括对填充错误的改进处理,以减轻与CBC填充相关的攻击,更好的会话重用和其他增强功能。尽管有这些增强功能,TLS 1.1不支持较新的加密算法,如AEAD加密模式和SHA-2哈希函数。TLS 1.1依赖于易受填充预言攻击的基于CBC的密码套件,现在已弃用MAC和PRF的MD5和SHA-1哈希函数。TLS 1.1于2021与TLS 1.0一起被正式弃用。

TLS 1.2于2008年发布。它增加了对AEAD加密模式的支持,如GCM和CCM,以及对MAC和PRF的更安全的哈希函数,如SHA-256和SHA-384。与此同时,TLS 1.2删除了对旧算法的支持,如DES、IDEA、MD5和SHA-1。后来,TLS 1.2得到了扩展,支持ARIA、ChaCha20和Poly1305算法。AEAD加密模式完全缓解了填充预言机攻击,如POODLE。与往常一样,新协议版本对协议的各个方面进行了许多小的增强。在撰写本文时,TLS 1.2尚未被弃用。

2018年发布的TLS 1.3是对TLS协议的一次重大清理。在早期的TLS版本中添加越来越多的功能积累了大量的遗留问题。TLS 1.3放弃了对许多旧的不安全或过时的算法和功能的支持,如非AEAD(non-AEAD)密码套件、非PFS密钥交换方法,如RSA和非临时DH/EDCH、DSA签名证书、弱和较少使用的椭圆曲线、椭圆曲线点格式协商、自定义DHE组、ChangeCipherSpec协议、压缩和重新协商。TLS 1.3仅支持AES-GCM、AES-CCM、ChaCha20-Poly1305对称AEAD密码、基于SHA-256和SHA-384的HMAC以及DHE/ECDHE密钥交换方法。HKDF用作PRF。支持基于RSA、ECDSA和PureEdDSA的证书。基于RSA和ECDSA的证书应使用SHA-256、SHA-384或SHA-512哈希函数进行签名;然而,作为一种特殊的向后兼容性措施,仍然支持使用SHA-1消息摘要签名的证书。由于旧功能的删除,TLS 1.3对CRIME和POODLE攻击都免疫。TLS 1.3中的握手比以前的TLS版本更快,因为握手现在只需要一次数据往返,而不是像早期版本那样需要两次数据往返。作为一项主要的加速功能,TLS 1.3还支持零往返时间zero round-trip time0-RTT)握手模式,如果可以重用较早的TLS会话,则允许TLS客户端在从客户端到服务器的第一次数据飞行中发送加密的用户数据。TLS 1.3有一个新的会话重用机制,支持早期会话恢复和预共享密钥。TLS 1.3还包含了其他不如前面提到的重要的改进。

哪个TLS版本最好?当然是最新的!但是,您可能需要保留对旧版本的支持,以便与使用旧通信软件的各方兼容。对于不再接收软件更新的硬件,如旧手机、平板电脑、路由器或物联网Internet-of-ThingsIoT)设备,情况往往如此。

我们现在已经了解了TLS的基础知识和协议的历史。让我们继续本章的实践部分,我们将学习如何在命令行上以编程方式进行TLS连接。

在命令行上建立TLS客户端连接

要建立TLS客户端连接,我们将使用openssl工具的s_client子命令。其文档可以在 man 页上找到:

以互联网上的HTTPS服务器为例,https://example.org/。让我们通过TLS连接到它并获取其主页:

openssl 工具将输出大量关于TLS握手过程的信息,使用了哪些加密算法,甚至将打印base64编码的服务器证书。

我们还可以通过添加 -verify_return_error-verify_hostname 命令行选项来请求验证服务器证书及其主机名:

如果你想验证服务器证书,你需要在OpenSSL期望找到它们的目录中拥有受信任的CA证书。例如,在我的Ubuntu Linux系统上,安装了OpenSSL的系统在 /etc/ssl/certs 目录中查找受信任的证书。例如,如果您将自己的OpenSSL版本安装到 /opt/openssl-3.0.0 目录中,则该版本的OpenSSL将在 /opt/opensl-3.0.0/ssl/certs 目录中查找证书。在这种情况下,您可以使用以下任何方法向OpenSSL提供受信任的CA证书:

如果您使用的操作系统没有系统OpenSSL证书存储,如Windows,您可以从curl实用程序网站下载Mozilla的受信任CA证书包。在撰写本文时,证书包URL为https://curl.se/ca/cacert.pem.例如,您可以使用curl实用程序本身下载它:

然后,您可以使用以下任何方法将下载的证书包提供给OpenSSL:

成功建立TLS连接后,我们可以键入或复制并粘贴任何数据到连接中。服务器发送的任何数据都将打印到您的终端。让我们键入以下内容以发出获取服务器主页的HTTP请求:

我们将得到一个HTTP响应,其中包含HTTP标头和服务器主页的HTML代码。服务器将关闭连接,因为我们在HTTP请求中指定了 connection: close 标头。您将返回到命令行。如果省略该请求标头,服务器将不会关闭连接,您将留在连接中。如果您想在正确关闭TLS和TCP连接的情况下完成连接,请键入 Q ,然后按 Enter ,或者在类Unix系统上按 Ctrl+D 或在Windows系统上按 Ctrl+Z+Enter 在终端上执行文件结束End of FileEOF)。如果你想在没有正确关闭连接的情况下强制退出 openssl s_client ,你可以按 Ctrl+C

这是多么容易。现在让我们尝试建立TLS服务器连接。TLS服务器连接将需要服务器证书及其相应的私钥。因此,我们必须先生成几个证书。

为TLS服务器连接准备证书

要接受TLS服务器连接,我们需要生成两个密钥对和证书:服务器证书和自签名 CA 证书,后者对服务器证书进行签名。好奇的读者可能会问,为什么我们不能生成一个自签名的服务器证书?因为使用自签名证书作为服务器证书被大多数TLS客户端和库(包括OpenSSL库)认为是错误的。您可能会遇到的另一个问题是:为什么我们不能为我们的TLS服务器重用操作系统证书存储中的证书?因为我们没有这些证书的私钥。

与【第8章X.509证书和PKI】一样,我们将使用 openssl reqopenssl x509 子命令进行密钥对和证书生成。但是,这一次,我们也将使用组合生成命令来演示它们。

我们必须先生成CA证书。这一次,我们将使用一个命令,该命令将密钥对、证书签名请求Certificate Signing RequestCSR)和证书的生成组合在一个命令中。这种命令结合了上述三个操作,只能用于生成自签名证书:

现在,我们有了CA密钥对和证书,可以创建和签署服务器证书。这一次,我们也将使用组合命令,但我们不能组合所有三个操作。我们只能组合两个操作,即生成服务器密钥对和CSR:

下一个命令将创建(或颁发)由CA证书签名的服务器证书:

请注意,我们在服务器证书主题字段中使用了服务器主机名(localhost)。如果TLS客户端决定执行证书的主机名验证,则需要通过该验证。

现在我们的证书已经准备就绪,让我们尝试建立TLS服务器连接。

在命令行上接受TLS服务器连接

按照以下步骤在命令行上接受TLS服务器连接:

  1. 要接受TLS服务器连接,我们将使用 openssl 工具的 s_server 子命令。其文档可以在手册页上找到:

  2. 我们将向 openssl s_server 提供端口号、服务器证书和相应的服务器密钥对。这就是我们启动TLS服务器的方式:

  3. 为了检查我们的TLS服务器是否可以接受连接并通过它们发送和接收数据,我们可以在另一个终端窗口中启动TLS客户端并连接到我们的TLS server:

    请注意,这一次,我们为 openssl s_client 提供了一个 -CAfile 开关,以便它可以找到受信任的CA证书并验证服务器证书。

  4. 现在,运行在不同终端窗口中的TLS客户端和TLS服务器已连接,可以相互发送数据。尝试在一个终端窗口中键入内容,按 Enter 键,您将看到键入的字符串将出现在另一个终端窗中。您可以通过发送 EOF 或按 Ctrl+C 来完成连接。

我们现在已经学习了如何在命令行上建立TLS客户端和服务器连接,并通过它们发送数据。现在,让我们学习如何以编程方式完成它。

理解 OpenSSL BIOs

为了建立TLS连接并通过它们发送数据,我们将使用OpenSSL基本输入/输出Basic Input/OutputBIO)对象。BIOs提供相同的应用程序编程接口Application Programming InterfaceAPI),用于处理不同类型的输入/输出Input/OutputI/O)通道,如文件、套接字和TLS流。

BIOs可分为两类:源或宿BIOssource or sink BIOs)和 过滤器BIOsfilter BIOs)。源或宿BIO表示I/O端点,如文件或套接字。过滤器BIO转换通过BIO的数据。例如,密码BIO在写入数据时加密数据,在读取数据时解密数据。BIO可以连接并形成BIO链。例如,SSL BIO可以连接到套接字BIO,在套接字上提供TLS通信。

OpenSSL支持以下源或宿BIOs:

OpenSSL还支持以下过滤器BIOs:

为了使用BIO,您使用BIO API,该API由带有 BIO_ 前缀的函数组成。

您可以使用 BIO_new() 函数或特定于BIO类型的函数(如 BIO_new_socket()BIO_new_accept() )创建BIO。有些函数甚至可以创建短的BIO链。例如, BIO_new_buffer_ssl_connect() 函数创建了一个由缓冲BIO、SSL BIO和连接BIO组成的BIO链。

从BIO读取和写入BIO可以使用 BIO_read()BIO_write()BIOs _read_ex()BIO_write_ex()BIO_gets()BOD_get_line()BIO_puts() 等函数完成。

当需要将以null结尾的字符串写入BIO时, BIO_puts() 函数非常方便。请注意,与标准C库中的 put() 函数不同, BIO_puts() 不会在输出中添加新行。相反, BIO_puts() 的工作方式与 fput() 一样,它也不会添加额外的新行。

BIO_gets() 从BIO读取一行,并与 fgets() 一样,在读取的输入数据中保留新行字符。 BIO_gets() 返回读取的字节数,但有以下警告:如果读取的数据包含空字符,即使读取了更多的字节, BIO_get() 也会返回到第一个读取的空字符为止的字节数。因此,最好使用OpenSSL 3.0中引入的 BIO_get_line() 函数,该函数始终返回读取字节数,而不管遇到空字符。

大多数BIOs都支持读写功能。但一些BIOs也支持BIO特定的功能。例如,SSL BIOs具有执行TLS握手的 BIO_do_handshake() 函数。

当BIO操作期间发生错误时,通常会通过函数返回代码和添加到OpenSSL错误队列中的错误来发出错误信号。因此,必须在BIO操作后检查、处理和清除OpenSSL错误队列。

当不再需要BIO对象时,必须通过 BIO_free() 函数释放它。当调用 BIO_free() 时,它可能会对底层I/O结构产生影响,例如,文件或网络连接可能会被关闭。还有 BIO_free_all() 函数,它释放了整个BIO链。

有关BIOs的更多信息,请参阅相关手册页。以下是其中一些:

在下一节中,我们将使用BIOs。我们将使用BIO建立TLS连接,通过它发送和接收一些数据,并正确关闭它。

以编程方式建立TLS客户端连接

我们将开发一个小型 tls-client 程序,该程序将通过tls连接到HTTPS服务器,发出HTTP请求,并从服务器读取响应。

为此,我们将使用OpenSSL BIO API和SSL API。BIO API将帮助我们建立TLS连接,通过连接发送和接收数据,并正确关闭连接。SSL API将帮助我们设置服务器证书验证,检测连接是否仍然有效,并区分连接上的错误类型。如前所述,由于历史原因,OpenSSL在使用TLS的对象和函数名称中仍然使用SSL子字符串而不是TLS。

有关SSL API的更多信息,请参见OpenSSL手册页。以下是一些相关页面:

我们的程序将接受三个命令行参数:

  1. 服务器主机名
  2. 服务器端口
  3. 可选参数:包含一个或多个用于服务器证书验证的受信任CA证书的文件的名称

我们的高层实施计划如下:

  1. 创建SSL上下文(context),即 SSL_CTX 对象。
  2. 将受信任的CA证书加载到SSL上下文中。
  3. 在SSL上下文中启用服务器证书验证。
  4. 从SSL上下文创建SSL BIO。
  5. 与服务器建立TLS连接。
  6. 向服务器发送 HTTP 请求。
  7. 读取服务器响应。
  8. 关闭TLS连接。

实现tls-client程序

让我们根据我们的计划实现 tls-client 程序:

  1. 首先,为缓冲区分配读写权限:

  2. 接下来,创建SSL上下文:

    SSL上下文是一个对象,用于存储TLS会话建立的常用设置和数据。常见数据的一个例子是可信CA证书的集合。一个常见设置的示例是对等证书验证标志。我们将在代码中使用这两个示例。许多可以设置为SSL上下文的设置也可以设置为单个TLS连接,由SSL类型的对象表示。如果 SSL_CTX 对象和SSL对象都设置了相同的设置,则SSL对象中的设置优先。

    请注意,我们使用 TLS_client_method() 作为 SSL_CTX_new() 的参数。这就是我们如何指定在此上下文中创建的TLS连接将是TLS客户端连接。

  3. 下一步是加载受信任的证书。如果向我们的程序提供了第三个参数,我们将从提供的文件加载证书。否则,我们将从默认位置加载证书,例如Ubuntu Linux上的 /etc/ssl/certsSSL_CERT_DIRSSL_CERT_FILE 环境变量指定的路径:

  4. 让我们在SSL上下文中设置一些设置:

    设置 SSL_VERIFY_PEER 标志可以验证对等证书。在我们的例子中,对等方是TLS服务器,因此将验证服务器证书。如果验证失败,TLS握手也将失败,TLS连接将中止。

    设置 SSL_MODE_AUTO_RETRY 标志简化了TLS I/O编程。有时,当我们想将一些用户数据写入TLS连接时,TLS协议希望读取一些服务数据以继续,而写入尝试会导致 SSL_ERROR_WANT_READ 错误。在这种情况下,我们必须从连接中读取,然后重试写入。反之亦然,有时我们想读取,但TLS协议想写入,然后我们会得到 SSL_ERROR_WANT_WRITE 错误。当 SSL_MODE_AUTO_RETRY 标志被设置时,OpenSSL将根据需要重试读取或写入,并且不需要处理 SSL_ERROR_WANT_READSSL_ERROR_WANT_WRITE 错误。

  5. 下一步是使用SSL上下文创建SSL BIO,并设置远程TLS服务器的主机名和端口:

    SSL BIO包含一个 SSL 对象,表示TLS连接。

  6. 我们必须获取 SSL 对象并为其设置一些参数:

    SSL_set_tlsext_host_name() 函数设置SNI扩展的主机名,以便可以连接到共享主机上的正确服务器。 SSL_set1_host() 函数设置对等证书验证的主机名。在握手过程中,OpenSSL将尝试将提供的主机名与证书通用名Common NameCN)和主题备选名称Subject Alternative NamesSANs)相匹配。如果主机名不匹配,TLS握手将失败,TLS连接将中止。如果未调用 SSL_set1_host() ,则不会进行主机名验证,只会进行证书链验证。

  7. 下一步是建立TLS连接:

    BIO_do_connect() 调用将尝试执行TCP和TLS握手。为了方便起见,SSL BIO允许我们跳过显式的 BIO_do_connect() 调用。TCP和TLS握手也将在SSL BIO上的第一次读写尝试时尝试。但是,显式建立TLS连接并在调用后处理可能的连接错误通常是有意义的。

  8. 下一步是发送HTTP请求:

  9. 接下来,读取服务器响应:

  10. 我们正在从服务器读取数据,直到服务器关闭其一侧的TLS连接。然后,最好也关闭我们这边的连接:

  11. 关闭连接后,我们必须释放已使用的对象和缓冲区:

  12. 最后,我们必须检查并清除OpenSSL错误队列:

我们 tls-client 程序的完整源代码可以在GitHub上找到 tls-client.c 文件:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter09/tls-client.c

运行tls-client程序

让我们在example.org网站上运行我们的程序:

正如我们所观察到的,我们的 tls-client 程序可以成功连接到服务器,发送和接收一些数据,并成功关闭连接。

让我们还检查一下 tls-client 中的错误处理是如何工作的。为此,让我们尝试对来自同一服务器的非tls端口运行 tls-client

正如我们所看到的,我们的 tls-client 程序检测到错误并报告了错误。我们可以得出结论, tls-client 程序既可以与TLS服务器通信,也可以处理错误。

在下一节中,我们将学习如何建立TLS服务器连接并通过它们发送数据。

以编程方式接受TLS服务器连接

我们将开发一个小型 tls-server 程序,该程序将接受TLS连接,从连接的TLS客户端读取HTTP请求,并将HTTP响应发送回客户端。

我们的程序将接受三个命令行参数:

  1. 服务器端口
  2. 包含TLS服务器密钥对的文件的名称
  3. 包含TLS服务器证书链的文件的名称

在我们的例子中,证书链文件将只包含一个证书——服务器证书。但是,如果我们有中间CA证书,我们可以将它们包含在服务器证书之后的文件中,以帮助TLS客户端进行服务器证书验证。在证书链文件中包含根CA证书没有多大意义,因为TLS客户端必须在受信任的证书中拥有根CA证书才能验证服务器证书。

我们的高层实施计划如下:

  1. 创建SSL上下文,即 SSL_CTX 对象。
  2. 将TLS服务器密钥对加载到SSL上下文中。
  3. 将TLS服务器证书链加载到SSL上下文中。
  4. 检查加载的密钥对是否与加载的服务器证书匹配。
  5. 从SSL上下文创建接受BIO,并开始接受传入的TCP连接。
  6. 当TCP连接被接受时,接受BIO将创建一个新的套接字BIO,它将表示接受的连接。我们将把该套接字BIO从接受BIO中分离出来。
  7. 创建一个新的SSL BIO并将其与套接字BIO链接。
  8. 使用形成的 SSL-socket BIO链处理已接受的连接。我们将很快介绍连接处理计划。
  9. 处理连接后,继续侦听端口并接受传入连接。

以下是如何处理已接受连接的计划:

  1. 对已接受的连接执行TLS握手。
  2. 从TLS客户端读取 HTTP 请求。
  3. 发送服务器响应。
  4. 关闭TLS连接。

实现tls-server程序

让我们根据我们的计划实现 tls-server 程序:

  1. 首先,创建 SSL 上下文:

    请注意,我们这次使用 TLS_server_method() 作为 SSL_CTX_new() 的参数。这就是我们如何指定在此上下文中创建的TLS连接将是TLS服务器连接。

  2. 接下来,加载服务器密钥对:

  3. 接下来,加载服务器证书链:

  4. 接下来,检查加载的密钥对是否与服务器证书匹配,服务器证书是加载的证书链中的第一个证书:

  5. 接下来,设置与上一节【“以编程方式建立tls客户端连接”】中开发 tls-client 时相同的 SSL_MODE_AUTO_RETRY 标志:

    在服务器模式下,我们不会请求和检查对等(客户端)证书,因此,我们不会启用 SSL_VERIFY_PEER 选项。

  6. 接下来,创建一个新的accept BIO并开始在指定端口上侦听:

  7. 接下来,组织一个循环来监听传入的TCP连接:

  8. 在循环中,尝试接受传入的TCP连接:

    请注意,我们第二次调用 BIO_do_accept() 。这就是accept-BIO的工作原理。第一个 BIO_do_accept() 调用设置监听套接字。它相当于调用 bind()listen() 系统调用。第二个 BIO_do_accept() 调用实际上在侦听套接字上接受传入的TCP连接。这相当于调用 accept() 系统调用。

    当TCP连接被成功接受时,accept BIO将创建一个新的套接字BIO,类似于 accept() 系统调用创建新连接套接字的方式。acceptBIO还将附加到新创建的套接字BIO,创建一个 accept-socket BIO链。新创建的BIO代表已接受的连接,而不是监听套接字或BIO。我们将分离套接字BIO,在套接字BIO前面创建并附加一个SSL BIO,并在新创建的SSL BIO上进行其余的通信。让我们一步一步地做。

  9. 将套接字BIO从接受BIO上拆下:

  10. 创建新的SSL BIOS:

  11. 将SSL BIO连接到插座BIO的前面:

  12. 我们将在一个名为 handle_accepted_connection() 的单独函数中完成SSL BIO上的其余通信:

  13. 请记住,我们仍然处于监听 while() 循环中,所以让我们关闭它:

handle_accepted_connection() 函数将执行TLS握手,接收和发送连接上的数据,关闭连接,处理错误,并释放SSL BIO和套接字BIO。如果需要, handle_accepted_connection() 功能可以在单独的线程中运行。在这种情况下,可以同时处理多个传入连接。accept BIO已经从套接字BIO中分离出来,因此accept BIOS可以接受下一个连接,而不会影响正在运行的连接。为了简单和可移植性,我们将在这个例子中运行一个线程。

是时候实现 handle_accepted_connection() 函数了:

  1. 首先,分配读取缓冲区:

  2. 接下来,执行TLS握手:

  3. 接下来,从客户端读取 HTTP 请求。为简单起见,我们不会彻底解析请求,但当我们遇到第一个空行时,我们将停止从客户端读取:

  4. 读取客户端请求后,我们将发送服务器响应:

  5. 在将响应发送到客户端并且不再发送更多数据后,关闭我们这边的TLS连接是一个很好的做法:

  6. 通信结束后,我们必须释放已使用的对象和缓冲区:

  7. 最后,让我们检查并清除OpenSSL错误队列。请注意,我们对每个传入连接而不是每个应用程序运行进行错误队列处理:

    这将是 handle_accepted_connection() 函数的结束。 handle_accepted_connection() 函数完成后,执行返回到接受传入连接的 while() 循环,可以接受和处理新的传入连接。

我们的 tls-server 程序的完整源代码可以在GitHub上找到 tls-server.c 文件:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter09/tls-server.c

运行tls-server程序

让我们运行 tls-server 程序并处理一些传入连接:

  1. 打开终端窗口并启动 tls-server 程序:

    服务器现在正在运行并接受连接。

  2. 打开另一个终端窗口,对正在运行的 tls-server 程序运行之前开发的 tls-client 程序:

    正如我们所观察到的,从 tls-client 的角度来看,TLS连接进展得很好。请注意,这次我们向 tls-client 提供了第三个命令行参数,即包含服务器CA证书的文件,以便 tls-client 可以验证服务器证书。

  3. 切换到第一个终端窗口并检查 tls-server 输出:

    正如我们所看到的, tls-server 已经成功处理了来自 tls-client 的连接,并继续监听下一个传入的连接。

  4. 让我们再尝试一件事,另一个TLS客户端, curl 实用程序。让我们在第二个终端窗口中运行以下命令:

    正如我们所观察到的, tls-server 也与 curl 兼容!第一个终端窗口中的 tls-server 输出确认我们的 tls-server 程序刚刚为curl提供了服务。请参阅请求中的 User-Agent 标头:

    tls-server 将运行并接受连接,直到它被中止。例如,可以通过在运行它的终端窗口中按 Ctrl+C 来完成。

我们已经完成了本章的实践部分。我们已经学习了如何建立TLS连接和使用BIOs,并编写了两个使用这些功能的优秀示例程序。

让我们继续进行总结。

总结

在本章中,我们了解了TLS协议的重要性、为什么需要它以及在哪里使用它。我们还了解了协议在高层是如何工作的,TLS握手中包含了什么,以及握手后会发生什么。我们通过查看TLS协议的历史、了解其发展以及SSL和TLS的新旧版本之间的差异来完成理论部分。

在本章的实践部分,我们学习了如何在命令行上和使用C代码以编程方式建立客户端和服务器TLS连接。我们还了解了OpenSSL BIOs。

在下一章中,我们将了解X.509证书在TLS中的更高级用法,包括用户控制的证书验证和客户端TLS证书。