【问题标题】:SSL renegotiation with full duplex socket communication使用全双工套接字通信的 SSL 重新协商
【发布时间】:2013-09-14 17:16:58
【问题描述】:

我有一个非常简单的客户端-服务器,其中一个阻塞套接字进行全双工通信。我已经为应用程序启用了 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 重新协商。请注意,不涉及线程。这是一个简单的单线程模型,读取/写入发生在套接字的任一端。

更新:为了验证我编写的应用程序没有任何问题,我什至可以使用 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。

【问题讨论】:

    标签: c ssl openssl


    【解决方案1】:

    您正试图“看穿” 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,然后你在读取方向进行轮询。

    如果 SSL_read 的实现基本上是“读取一些数据然后解密它”并且 SSL_write 是“加密一些数据并发送它”,那么您的代码将(大部分)工作。问题是,这些函数实际上运行了一个复杂的状态机,它根据需要读取和写入套接字,最终导致为您提供解密数据或加密数据并发送它的效果。

    【讨论】:

    • 正如我在问题中提到的,我拥有的套接字具有阻塞性质,并且使用阻塞套接字,poll() 是一种知道您最终不会阻塞 recv()/send( )。如果我不使用 poll() 并在阻塞套接字上调用 SSL_read,我一定会阻塞,直到对方发送一些东西。那么,如何处理 SSL 重新协商?
    • @Karthik:您必须选择是使用阻塞套接字还是非阻塞套接字并坚持选择。如果您想使用非阻塞套接字,请将它们设置为非阻塞并按照我的说明进行操作。如果你想使用阻塞套接字,不要调用poll。由于我解释的原因,“每个”混合的“一半” 工作 - 在 socket 上进行轮询不会告诉您 SSL 连接 的准备情况。
    • 没有“各一半”的混合体。双方都使用阻塞套接字,对于阻塞套接字,poll() 是不可避免的。另外,请参阅我的更新。在执行 SSL_read() 或 SSL_write 之前,我可以使用 OpenSSL 的 s_client 和 s_server 获得相同的行为,它们都使用阻塞套接字和 select()。
    • @Karthik 哦,那你有一个不同的错误。你拒绝打电话给SSL_read,即使它可以因为套接字上没有收到数据而取得进展。如果SSL_read 必须写入 到套接字才能继续前进怎么办?
    • 有了这样的模型,除非使用非阻塞套接字,否则不可能支持 SSL 重新协商 -- 是否阻塞?使用阻塞套接字是不可能的,但也不能阻塞。如果您不想阻塞,则必须使用非阻塞套接字。
    【解决方案2】:

    在使用 OpenSSL 调试我的应用程序后,我找到了我最初发布的问题的答案。我在这里分享它以防它帮助像我这样的其他人。

    我最初发布的问题与 OpenSSL 的一个明显错误有关,表明它在握手过程中接收应用程序数据。我不明白的是,OpenSSL 在握手过程中接收应用程序数据时会感到困惑。在接收/发送应用程序数据时接收握手数据是可以的,但反之则不行(至少使用 OpenSSL)。那是我没有意识到的事情。这也是大多数启用 SSL 的应用程序运行良好的原因,因为它们中的大多数本质上是半双工的(例如 HTTPS),这隐含地保证在握手时没有应用程序数据异步到达。

    这意味着,如果您正在设计一个自定义的客户端-服务器全双工协议(我就是这种情况)并且想要在其上添加 SSL,那么应用程序有责任在两端都没有的情况下发起重新协商发送任何数据。这在Mozilla's NSS API 中有明确记录。更不用说在 OpenSSL 的 bug 存储库中有一个关于这个问题的 open ticket。当我更改我的应用程序以在客户端/服务器之间没有什么可说的情况下发起握手时,我不再遇到上述错误。

    另外,我同意 David 关于阻塞套接字的 cmets,我也在 OpenSSL 邮件列表中阅读了他的许多论点。但是,可悲的是,大多数遗留应用程序都是围绕轮询和阻塞套接字构建的,它们“Just Work Fine (TM)”。处理 SSL 重新协商时会出现此问题。我仍然相信至少我的应用程序可以在存在阻塞套接字的情况下处理 SSL 重新协商,因为它是一个非常受限和自定义的协议,我们(作为应用程序开发人员)可以决定在协议静止时进行重新协商。如果这不起作用,我将走非阻塞套接字路由。

    【讨论】:

    • HTTPS 也可能发生这种情况。您可以流式传输请求(通常是 GET)并获得它们的响应,而不会在每个请求/响应对之间阻塞。在这中间重新谈判会导致类似的问题。
    • 救了我...进行了数据交换和握手...而不是等待握手完成。
    猜你喜欢
    • 2012-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-02
    相关资源
    最近更新 更多