我有一个非常简单的客户端服务器,其中一个阻塞套接字进行全双工通信。我已经为应用程序启用了SSL / TLS。该模型是典型的生产者 - 消费者的模型。客户端生成数据,将其发送到服务器,服务器处理它们。唯一的问题是,服务器偶尔会将数据发送回客户端,客户端会相应地处理这些数据。下面是一个非常简单的应用程序伪代码:
1 Client: 2 ------- 3 while (true) 4 { 5 if (poll(pollin, timeout=0) || 0 < SSL_pending(ssl)) 6 { 7 SSL_read(); 8 // Handle WANT_READ or WANT_WRITE appropriately. 9 // If no error, handle the received control message. 10 } 11 // produce data. 12 while (!poll(pollout)) 13 ; // Wait until the pipe is ready for a send(). 14 SSL_write(); 15 // Handle WANT_READ or WANT_WRITE appropriately. 16 if (time to renegotiate) 17 SSL_renegotiate(ssl); 18 } 19 20 Server: 21 ------- 22 while (true) 23 { 24 if (poll(pollin, timeout=1s) || 0 < SSL_pending(ssl)) 25 { 26 SSL_read(); 27 // Handle WANT_READ or WANT_WRITE appropriately. 28 // If no error, consume data. 29 } 30 if (control message needs to be sent) 31 { 32 while (!poll(pollout)) 33 ; // Wait until the pipe is ready for a send(). 34 SSL_write(); 35 // Handle WANT_READ or WANT_WRITE appropriately. 36 } 37 }
出于测试目的,我强制进行SSL重新协商(第16-17行)。会话开始很简单,但过了一段时间,我得到以下错误:
Client:
-------
error:140940F5:SSL routines:SSL3_READ_BYTES:unexpected record
Server:
-------
error:140943F2:SSL routines:SSL3_READ_BYTES:sslv3 alert unexpected message
事实证明,在客户端启动重新协商的同时(第14行),服务器最终将应用程序数据发送到客户端(第34行)。作为重新协商过程的一部分的客户端接收此应用程序数据并发出“意外记录”错误的炸弹。类似地,当服务器执行后续接收(第26行)时,它在期望应用程序数据时最终会收到重新协商数据。
我做错了什么?我应该如何使用全双工通道处理/测试SSL重新协商。请注意,没有涉及线程。这是一个简单的单线程模型,在套接字的两端都有读/写。
UPDATE :为了验证我编写的应用程序没有任何问题,我甚至可以使用OpenSSL的s_client和s_server实现来轻松地重现这一点。我启动了一个s_server,一旦s_client连接到服务器,我就会以编程方式将一堆应用程序数据从服务器发送到客户端,并从客户端向服务器发送一堆“R”(重新协商请求)。最终,它们都以与上述完全相同的方式失败。
s_client:
RENEGOTIATING
4840:error:140940F5:SSL routines:SSL3_READ_BYTES:unexpected record:s3_pkt.c:1258:
s_server:
Read BLOCK
ERROR
4838:error:140943F2:SSL routines:SSL3_READ_BYTES:sslv3 alert unexpected message:s3_pkt.c:1108:SSL alert number 10
4838:error:140940E5:SSL routines:SSL3_READ_BYTES:ssl handshake failure:s3_pkt.c:1185:
更新2: 好。正如David所建议的那样,我重新设计了测试应用程序以使用非阻塞套接字,并且总是首先执行SSL_read和SSL_write并根据它们返回的内容进行选择,并且在重新协商期间仍然会遇到相同的错误(SSL_write最终从应用程序中获取应用程序数据)在重新谈判中的另一边)。问题是,在任何时候,如果SSL_read返回WANT_READ,我可以假设它是因为管道中没有任何东西并继续使用SSL_write,因为我有东西要写吗?如果没有,那可能就是我最终出错的原因。要不然,或者我正在进行重新谈判。注意,如果SSL_read返回WANT_WRITE,我总是进行选择并再次调用SSL_read。
答案 0 :(得分:5)
您正试图“浏览”SSL黑匣子。这是一个很大的错误。
if (poll(pollin, timeout=0) || 0 < SSL_pending(ssl))
{
SSL_read();
您假设为了使SSL_read
前进,它需要从套接字读取数据。这是一个可能是错误的假设。例如,如果正在进行重新协商,则SSL引擎可能需要接下来发送数据,而不是读取数据。
while (!poll(pollout))
; // Wait until the pipe is ready for a send().
SSL_write();
您如何知道SSL引擎想要将数据写入管道?它给你一个WANT_WRITE
指示吗?如果没有,可能需要读取重新协商数据才能发送。
要在非阻止模式下使用SSL,只需尝试您要执行的操作即可。如果要读取解密数据,请致电SSL_read
。如果要发送加密数据,请致电SSL_write
。只有在SSL引擎告诉您时才会调用poll
,并显示WANT_READ
或WANT_WRITE
。
更新 ::您在阻止和非阻止方法之间存在“每个”一半的混合。这不可行。问题很简单:在调用SSL_read
之前,您不知道它是否需要从套接字读取。如果您先调用poll
,即使SSL_read
不需要从套接字读取,也会阻止。如果您先调用SSL_read
,它将阻止它是否需要从套接字读取。 SSL_pending
对您无济于事。如果SSL_read
需要写到套接字以取得进展,SSL_pending
将返回零,但调用poll
将永久阻止。
你有两个明智的选择:
阻止。将套接字设置为阻塞。只需要在阅读时调用SSL_read
,在要编写时调用SSL_write
。他们将阻止。阻止套接字可能会阻塞,这就是它们的工作方式。
无阻塞。将套接字设置为非阻塞。只需要在阅读时调用SSL_read
,在要编写时调用SSL_write
。他们不会阻止。如果您收到WANT_READ
指示,请在读取方向上进行轮询。如果得到WANT_WRITE
指示,则在写入方向上轮询。请注意,SSL_read
返回WANT_WRITE
是完全正常的,然后在写入方向上进行轮询。同样,SSL_write
可以返回WANT_READ
,然后按读取方向进行轮询。
如果SSL_read的实现基本上是“读取一些数据然后解密”并且SSL_write“加密一些数据并发送它”,那么你的代码(大部分)都会工作。问题是,这些函数实际上运行了一个复杂的状态机,可以根据需要读取和写入套接字,最终会产生解密数据或加密数据并发送数据的效果。
答案 1 :(得分:4)
花了一些时间用OpenSSL调试我的应用程序后,我想出了我最初发布的问题的答案。我在这里分享它,以防它像我一样帮助其他人。
我最初发布的问题与OpenSSL的明确错误有关,表明它正在握手中接收应用程序数据。我无法理解的是,当OpenSSL在握手过程中收到应用程序数据时会感到困惑。接收/发送应用程序数据时接收握手数据是好的,但不是相反(至少使用OpenSSL)。这是我没有意识到的事情。这也是大多数支持SSL的应用程序运行正常的原因,因为它们中的大多数本质上是半双工的(例如HTTPS),它隐含地保证在握手时不会异步到达应用程序数据。
这意味着如果您正在设计一个自定义客户端 - 服务器全双工协议(我就是这种情况)并希望将SSL打到它上面,那么当两端都没有结束时,应用程序有责任启动重新协商。发送任何数据。这在Mozilla's NSS API中有明确记载。更不用说在OpenSSL的bug存储库中有关于此问题的open ticket。当我没有让客户端/服务器彼此说话时,我改变应用程序以启动握手的那一刻,我不再遇到上述错误。
另外,我同意David关于阻止套接字的评论,我也在OpenSSL邮件列表中阅读了他的许多论点。但是,令人遗憾的是,大多数遗留应用程序都是围绕轮询和阻塞套接字构建的,它们是“Just Work Fine(TM)”。处理SSL重新协商时会出现问题。我仍然相信至少我的应用程序可以在阻塞套接字存在的情况下处理SSL重新协商,因为它是一个非常有限和自定义的协议,我们(作为应用程序开发人员)可以决定在协议静止时进行重新协商。如果这不起作用,我将进入非阻塞套接字路由。