Boost.Asio SSL线程安全

时间:2015-02-13 17:06:27

标签: c++ multithreading boost openssl boost-asio

我是否创建了一条所有我的SSL套接字共享的链,或者每个SSL上下文一条链(由任何相关的套接字共享)?

Boost.Asio SSL文档说明了这一点,但它没有提到上下文。我认为这意味着我必须只使用一个链,但我认为这是在OpenSSL支持多线程之前编写的。

  

SSL和主题

     

SSL流对象不执行自己的锁定。因此,必须在隐式或显式链中执行所有异步SSL操作。请注意,这意味着在单线程程序中不需要同步(因此不会产生锁定开销)。

我很可能只有一个SSL上下文,但我想知道它是否适合SSL上下文或全球网络拥有服务。

如果重要的话,我确实为CRYPTO_set_locking_callback提供了一个处理程序。

3 个答案:

答案 0 :(得分:5)

我认为这个帖子存在一些混乱,因为有一些事情需要澄清。让我们先断言::asio::ssl::context == SSL_CTX。这两个是一个。

其次,在使用boost::asio::ssl时,除非您在绕过内部init对象时做了一些疯狂的事情,否则您无需手动设置加密锁定回调。这样做是为了您,您可以在来源here中看到。

事实上,这样做可能会导致问题,因为init对象的析构函数在假设他们已在内部完成此工作的情况下运行。最后一点用盐,因为我没有深入审查。

第三,我相信您会将SSL流与SSL上下文混淆。为简单起见,将SSL流视为套接字,将SSL上下文视为套接字可用于各种SSL功能的单独对象,例如使用特定协商密钥进行握手,或作为服务器,提供有关服务器证书的信息到连接的客户端,以便您可以与客户端握手。

提到线索可以防止针对一个特定流(套接字)的可能同时IO,而不是上下文。显然,尝试读取缓冲区并在同一个套接字上同时从同一缓冲区写入将是一个问题。因此,当您提供由各种::asio::async_X方法包裹的完成处理程序时,您将执行特定排序以防止上述情况。您可以在this answer中阅读更多内容,由比我更了解此事的人提供。

现在,就上下文而言,David Schwartz在评论中指出了他写的另一个我需要挖掘的答案,上下文本身的全部目的是提供有助于跨多个SSL流的SSL功能的信息。他似乎暗示,鉴于其预期目的,他们基本上必须是线程安全的。我相信也许他是在::asio::ssl::context的上下文中发言,只是因为::asio::ssl正确使用线程安全回调的方式,或者他只是在正确使用openSSL的上下文中发言在多线程程序中。

无论如何,除了SO上的这些评论和答案,以及我自己的实践经验之外,在文档中找到具体的证据,或者在什么是和不是线程之间明确界定的界限是非常困难的。安全。正如大卫所指出的那样,boost::asio::ssl:context只是SSL_CTX周围的一个非常薄的包装器。我想进一步补充一点,它意味着给予更多" c ++ ish"感觉与底层结构一起工作。可能还设计了一些解耦::asio::ssl和底层实现库的意图,但它没有实现这一点,两者紧密绑定。 David再次正确地提到这个薄的包装器文档很少,并且必须查看实现以获得洞察力。

如果你开始深入研究实现,那么在上下文中找到线程安全的东西是一种相当简单的方法。您可以在ssl_lib.c等来源中搜索CRYPTO_LOCK_SSL_CTX

int SSL_CTX_set_generate_session_id(SSL_CTX *ctx, GEN_SESSION_CB cb)
{
    CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX);
    ctx->generate_session_id = cb;
    CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX);
    return 1;
}

正如您所看到的,使用了CRYPTO_w_lock,它将我们带回到关于openSSL和线程的官方页面here,其中声明:

  

OpenSSL可以安全地用于所提供的多线程应用程序   至少设置了两个回调函数, locking_function 和   的 threadid_func

现在我们来看看本答案第一段中链接的asio/ssl/detail/impl/openssl_init.ipp源代码,我们看到了:

do_init()
  {
    ::SSL_library_init();
    ::SSL_load_error_strings();        
    ::OpenSSL_add_all_algorithms();

    mutexes_.resize(::CRYPTO_num_locks());
    for (size_t i = 0; i < mutexes_.size(); ++i)
      mutexes_[i].reset(new boost::asio::detail::mutex);
    ::CRYPTO_set_locking_callback(&do_init::openssl_locking_func);
    ::CRYPTO_set_id_callback(&do_init::openssl_id_func);

#if !defined(SSL_OP_NO_COMPRESSION) \
  && (OPENSSL_VERSION_NUMBER >= 0x00908000L)
    null_compression_methods_ = sk_SSL_COMP_new_null();
#endif // !defined(SSL_OP_NO_COMPRESSION)
       // && (OPENSSL_VERSION_NUMBER >= 0x00908000L)
  }

当然要注意:

  

CRYPTO_set_的 locking_callback
  CRYPTO_set_的 id_callback

至少在::asio::ssl::context方面,线程安全与线程无关,而与openSSL相关的一切与openSSL相关,只有在多线程程序中正确使用时才能正常工作。

回到最初的问题,现在解释了所有这些问题,大卫也在评论中非常简单地给出了答案:

  

最常见的方法是每个SSL连接使用一个链。

举例说明提供example.com内容的HTTPS服务器。服务器具有单个上下文,配置有example.com证书等信息。客户端连接,此上下文用于所有连接的客户端以执行握手等。将已连接的客户端包装在新的session对象中,您可以在该对象中处理该客户端。在此会话中,您可以使用隐式或显式的单一链来保护套接字,而不是上下文。

虽然我无论如何都不是专家,我欢迎对此答案进行更正,但我已将所有关于这些主题的知识应用于开源透明过滤HTTPS代理中。对于超过17K行的代码比率,它有超过50%的评论,所以我知道的所有内容都写在那里(无论是对还是错;))。如果您希望查看此操作的示例,您可以查看TlsCapableHttpBridge.hpp源,它在每个主机的每个连接的基础上充当客户端和服务器。

服务器上下文和证书被欺骗/生成一次,并在平移多个线程的所有客户端之间共享。唯一的手动锁定是在存储和检索上下文期间。桥的每个有一个链,一个用于真正的下游客户端套接字,一个用于上游服务器连接,尽管它们在技术上甚至不是必需的,因为操作顺序会产生隐含但无论如何。

请注意,该项目正在开发中,因为我正在重写很多东西,(dep构建指令尚不存在),但所有内容都在MITM SSL代码方面有用,因此您需要查看一个功能齐全的类和相关组件。

答案 1 :(得分:2)

<强>更新

这个答案的要点受到大卫施瓦茨的质疑,他在这个领域的权威我受到高度重视。

有理由期​​望在线程之间共享ssl contexts - 至少对于某些操作来说,如果只是为了促进SSL会话恢复。

我认为David拥有SSL上下文的经验,因为OpenSSL使用它。 Boost ASIO依次使用 (至少在我所知道的所有平台上)。所以,要么大卫写一个答案来分享他的知识,要么你/我将不得不花一些时间使用OpenSSL文档和Boost Asio源代码来找出适用于Boost Asio ssl::context的有效约束。的使用。

以下是目前记录的限制因素。

[旧答案文本如下]

  

Thread Safety


  

通常,并行使用不同的对象是安全的,但同时使用单个对象是不安全的。但是,诸如io_service之类的类型提供了更强的保证,即同时使用单个对象是安全的。

逻辑上,因为文档没有特别提及ssl_context类的线程安全性,所以必须得出结论不是。

如果您使用某些特定的钩子(如您所述),您知道底层SSL库支持此功能并不重要。此告诉您使 ssl_context线程感知可能并不困难。

但是直到你(与图书馆开发人员合作)提供这个补丁,它才可用。

长话短说,您可以从单个链中访问每个ssl_context

答案 2 :(得分:1)

我会说这取决于你的协议是怎样的。如果它是HTTP,则不需要使用(显式)链,因为您不会并行读取和写入套接字。

事实上,会导致问题的是这样的代码:

void func()
{
    async_write(...);
    async_read(...);
}

因为这里 - 如果你的io_service()有一个与之关联的线程池,那么实际的读写可以由几个线程并行执行。

如果每个io_service只有一个线程,则不需要一个链。例如,如果您正在实施HTTP,情况也是如此。在HTTP中,由于协议的布局,您不会并行读取和写入套接字。您从客户端读取请求 - 虽然这可以在几个异步调用中完成 - 然后您以某种方式处理请求和标头,然后您(异步或不同步)发送您的回复。

您也可以在ASIO的文档文档中阅读相同的内容。