具有全双工套接字通信的SSL重新协商

时间:2013-09-10 20:38:41

标签: c ssl openssl

我有一个非常简单的客户端服务器,其中一个阻塞套接字进行全双工通信。我已经为应用程序启用了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。

2 个答案:

答案 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_READWANT_WRITE

更新 ::您在阻止和非阻止方法之间存在“每个”一半的混合。这不可行。问题很简单:在调用SSL_read之前,您不知道它是否需要从套接字读取。如果您先调用poll,即使SSL_read不需要从套接字读取,也会阻止。如果您先调用SSL_read,它将阻止它是否需要从套接字读取。 SSL_pending对您无济于事。如果SSL_read需要到套接字以取得进展,SSL_pending将返回零,但调用poll将永久阻止。

你有两个明智的选择:

  1. 阻止。将套接字设置为阻塞。只需要在阅读时调用SSL_read,在要编写时调用SSL_write。他们阻止。阻止套接字可能会阻塞,这就是它们的工作方式。

  2. 无阻塞。将套接字设置为非阻塞。只需要在阅读时调用SSL_read,在要编写时调用SSL_write。他们不会阻止。如果您收到WANT_READ指示,请在读取方向上进行轮询。如果得到WANT_WRITE指示,则在写入方向上轮询。请注意,SSL_read返回WANT_WRITE是完全正常的,然后在写入方向上进行轮询。同样,SSL_write可以返回WANT_READ,然后按读取方向进行轮询。

  3. 如果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重新协商,因为它是一个非常有限和自定义的协议,我们(作为应用程序开发人员)可以决定在协议静止时进行重新协商。如果这不起作用,我将进入非阻塞套接字路由。