8

Do I create one strand that all of my SSL sockets share, or one strand per SSL context (shared by any associated sockets)?

Boost.Asio SSL documentation states this, but it doesn't mention contexts. I assume that this means I must use only one strand for everything, but I think this was written before OpenSSL had multithreading support.

SSL and Threads

SSL stream objects perform no locking of their own. Therefore, it is essential that all asynchronous SSL operations are performed in an implicit or explicit strand. Note that this means that no synchronisation is required (and so no locking overhead is incurred) in single threaded programs.

I'm most likely only going to have only one SSL context, but I'm wondering if it's more proper for the strand to be owned by the SSL context, or by the global network service.

I did provide a handler to CRYPTO_set_locking_callback in case that matters.

Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
mukunda
  • 2,908
  • 15
  • 21
  • 1
    So I'm clear... you are proposing to use OpenSSL *without* its locks in a multithreaded program? You might also want to look at [CRYPTO_lock - OpenSSL thread support](https://www.openssl.org/docs/crypto/threads.html). – jww Feb 16 '15 at 02:04
  • 1
    The most common method is to use one strand per SSL connection. – David Schwartz Nov 04 '15 at 10:45
  • AFAIK boost::asio::ssl hooks in all the locking mechanisms that openSSL specifies must be used as per its own documentation. –  Jan 16 '16 at 14:22
  • I think maybe you're confusing the wording of the documentation as well. It's speaking of SSL streams, not SSL contexts. Are you asking about sharing contexts, or sharing concurrent access to a SSL stream? Also to the point David is making, look at the client/server examples in ASIO. When a new client connects, it gets its own `session` given by the handler. This session class is devoted to the client and its stream, where typically there is also a member strand. Hence, one strand per connection. –  Jan 16 '16 at 14:32

3 Answers3

9

I think there is some confusion on this thread because there's a few things that need to be clarified. Lets start by asserting that ::asio::ssl::context == SSL_CTX. The two are one.

Second, when using boost::asio::ssl, unless you're doing something crazy where you're bypassing the internal init objects, there's no need for you to manually set the crypto locking callbacks. This is done for you, as you can see in the sources here.

In fact, you may cause issues by doing this, because the destructor for the init object operates under the assumption that they have done this work internally. Take that last bit with a grain of salt, because I have not reviewed this in depth.

Third, I believe you're confusing SSL streams with SSL contexts. For simplicity, think of the SSL stream as the socket, think of the SSL context as separate object that the sockets can use for various SSL functions, such as handshaking with a particular negotiation key, or as a server, providing information about your server certificate to a connected client so that you can handshake with the client.

The mention of strands comes down to preventing possible simultaneous IO against one specific stream (socket), not contexts. Obviously attempting to read into a buffer and write from the same buffer at the same time on the same socket would be an issue. So, when you supply completion handlers wrapped by strands to the various ::asio::async_X methods, you're enforcing a specific ordering to prevent the aforementioned scenario. You can read more in this answer given by someone who knows much more about this than I.

Now as far as contexts go, David Schwartz points out in the comments and another answer he wrote I need to dig up, that whole purpose of contexts themselves are to provide information that facilitates the function of SSL across multiple SSL streams. He appears to imply that they essentially must be thread safe, given their intended purpose. I believe perhaps he is speaking within the context of ::asio::ssl::context, only because of the way that ::asio::ssl correctly employs the thread safety callbacks, or perhaps he's just speaking in the context of using openSSL correctly in a multithreaded program.

Regardless, beyond such comments and answers on SO, and my own practical experience, it's incredibly difficult to come across concrete evidence of this in documentation, or clearly defined boundaries between what is and isn't thread safe. boost::asio::ssl:context is, as David also points out, simply a very thin wrapper around SSL_CTX. I would further add that its meant to give a more "c++ ish" feel to working with the underlying structure(s). It was probably also designed with some intent of decoupling ::asio::ssl and underlying implementation library, but it doesn't achieve this, the two are tightly bound. David mentions again rightly that this thin wrapper is poorly documented, and one must look at the implementation to get insight.

If you start digging into the implementation, there's a rather easy way to find out what is and isn't thread safe when it comes to contexts. You can do a search for CRYPTO_LOCK_SSL_CTX within sources like ssl_lib.c.

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;
}

As you can see, CRYPTO_w_lock is used, which brings us back to the official page about openSSL and threads, here, which states:

OpenSSL can safely be used in multi-threaded applications provided that at least two callback functions are set, locking_function and threadid_func.

Now we come full circle to the linked asio/ssl/detail/impl/openssl_init.ipp source code in the first paragraph of this answer, where we see:

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)
  }

Note of course:

CRYPTO_set_locking_callback
CRYPTO_set_id_callback

So at least in terms of ::asio::ssl::context, thread safety here has nothing to do with strands and everything to do with openSSL working as openSSL is designed to work when used correctly in a multithreaded program.

Coming to the original question, now with all of this explained, David also gave the answer very simply in the comments with:

The most common method is to use one strand per SSL connection.

Take an example of a HTTPS server that serves up content of example.com. The server has a single context, configured with information such as the certificate for example.com. A client connects, this context is used on all connected clients to perform the handshake and such. You wrap your connected client in a new session object, where you handle that client. It is within this session that you would have a single strand, either implicit or explicit, to protect the socket, not the context.

While I'm not an expert by any means and I welcome corrections to this answer, I have put everything that I know about these subjects into practice in an open source transparent filtering HTTPS proxy. It's a little over 50% of comments to code ratio with over 17K lines total, so everything I know is written down there (be it right or wrong ;)). If you'd like to see an example of this stuff in action, you can look at the TlsCapableHttpBridge.hpp source, which acts as both a client a server on a per-host, per-connection basis.

Server contexts and certificates are spoofed/generated once and shared across all clients panning multiple threads. The only manual locking done is during storage and retrieval of the contexts. There is one strand per side of the bridge, one for the real downstream client socket and one for the upstream server connection, although they technically aren't even necessary because the order of operations creates an implicit strand anyway.

Note that the project is under development as I'm rewriting many things, (dep build instructions are not present yet), but everything is functional in terms of the MITM SSL code, so you're looking at a fully functional class and associated components.

Community
  • 1
  • 1
3

UPDATE

The gist of this answer is contested by David Schwarz, whose authority in this area I hold in high esteem.

There are reasons to expect that ssl contexts can be shared between threads - at least for some operations, if only to facilitate SSL session resumption.

I think David has experience with SSL context as OpenSSL uses it. Boost ASIO uses that in turn (at least on all platforms I know of). So, either David writes an answer sharing his knowledge, or you/me would have to spend some time with the OpenSSL documentation and Boost Asio source code to figure out the effective constraints that apply to Boost Asio's ssl::context usage.

Below are the constraints as currently documented.

[old answer text follows]

Thread Safety


In general, it is safe to make concurrent use of distinct objects, but unsafe to make concurrent use of a single object. However, types such as io_service provide a stronger guarantee that it is safe to use a single object concurrently.

Logically, because the documentation doesn't mention thread-safety of the ssl_context class in particular, you must conclude that it is not.

It doesn't matter that you know that the underlying SSL library supports this if you use some particular hooks (like you mention). This only tells you that it might not be hard to make ssl_context thread-aware.

But until you (work with the library devs to) provide this patch, it's not available.

Long story short, you access each ssl_context from a single strand.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • This either means you cannot support SSL session resumption or you must always use all your SSL connections in the same strand. – David Schwartz Nov 04 '15 at 10:44
  • @DavidSchwartz If you can explain how this follows, I'm happy to stand corrected. I've seen this question asked multiple times, and investigated the documentation and code on each occasion (including this time) and I agree the answer seems unsatisfactory. Perhaps with your argument in hand we can file an issue/request with the Asio devs? – sehe Nov 04 '15 at 10:46
  • What do you think the third alternative is? If you use the same SSL context for all SSL connections, then you're claiming you need a single strand for all SSL connections. If you use different SSL contexts, then session resumption won't work because the session cache is part of the SSL context. – David Schwartz Nov 04 '15 at 10:47
  • @DavidSchwartz I didn't have an alternative in mind. Just curious about the limitations that seem to be there, or lack of documentation for shared contexts in Boost Asio. Since there is no such documentation, I'm not confident claiming it's ok. I didn't know about _session resumption_, for one thing. – sehe Nov 04 '15 at 10:50
  • I don't think there would be any point in filing such an issue/request. If they were to take it seriously and follow it consistently they'd have to import lots of other OpenSSL documentation into their documentation. There are many cases where you have to look at the OpenSSL documentation to understand boost's SSL classes because they are a thin wrapper around OpenSSL. To understand how to use boost's SSL context class, you have to look at the OpenSSL documentation (and where that's lacking, the implementation). There's no choice. – David Schwartz Nov 04 '15 at 10:51
  • @DavidSchwartz They can just point to the OpenSSL docs (iff they use OpenSSL backend on all platforms, to start with). And they could document any additional considerations (like: stateful callback objects). _There is choice_. Right now, I can look at OpenSSL docs all I want, but there's no reason for me to safely extrapolate to Boost ASIO (except gut feeling and intuition) – sehe Nov 04 '15 at 10:52
  • My point is that this is nonsense, "*it doesn't matter that you know that the underlying SSL library supports this if you use some particular hooks (like you mention)*". This class is a thin, incompletely documented, wrapper. You *have* to understand the corresponding OpenSSL class to understand this class. Its whole purpose is to provide things like session caching safely across SSL connections. – David Schwartz Nov 04 '15 at 10:53
  • @DavidSchwartz That's the state of affairs. Of course that's the only way to arrive at an answer today. If you have vetted all of this wrapper with the implications (like the callbacks I mentioned), you should write an answer, instead of telling me I should have done this. – sehe Nov 04 '15 at 10:55
  • I felt it was much more important to point out what's wrong with your answer than to provide a different answer. I suppose I could post an answer with a "Some people say .... but that's wrong because ...." but I felt it only polite to give you an opportunity to correct your answer first. – David Schwartz Nov 04 '15 at 10:58
  • I can't correct it. For reasons I won't repeat. You seem to simply bring more experience to the table. I'm happy to up vote that. – sehe Nov 04 '15 at 11:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94211/discussion-between-sehe-and-david-schwartz). – sehe Nov 04 '15 at 14:06
  • @DavidSchwartz I've updated answer to portray as accurately as possible my current understanding. – sehe Nov 04 '15 at 14:07
  • I've written a TLS capable http proxy where contexts are generated once per upstream discovered cert. The contexts are then given a temporary EC key for negotiation, and the spoofed cert is assigned. Custom locking (done explicitly by me) only occurs for storage and retrieval of the contexts, which are then served up as necessary to multiple concurrent threads for use in the proxy with no additional locking put in place. –  Jan 16 '16 at 14:26
  • @TechnikEmpire Nice. Is this code available for reference? – sehe Jan 17 '16 at 00:38
  • @sehe It's partially on github, I'm rewriting a few things as I publish. A couple days or so and I'll come back with links. –  Jan 17 '16 at 08:46
  • @TechnikEmpire cool. These days I'm using https://mitmproxy.org/ or https://crypto.stanford.edu/ssl-mitm/ for portability. Fiddler is nice but doesn't port :) – sehe Jan 17 '16 at 12:54
  • 1
    @sehe Yeah I brought mitmproxy into a C++ app using boost python almost 3 years ago to play with it. I was developing a content filter at the time, so I needed something more robust and had to roll my own, but I knew almost nothing about it lol. So I've spent all this time learning and making one with boost::asio so it's portable. Has built in support for adblock plus filters and CSS selectors (using gumbo parser and a CSS selector lib on html content types). –  Jan 17 '16 at 15:09
  • The implementation is pretty rigidly designed around supporting just that functionality (filtering), no nice little hooks like mitmproxy proxy provides for rolling your own stuff, yet. –  Jan 17 '16 at 15:12
0

I'd say it depends on how your protocol is like. If it's HTTP, there's no need to use an (explicit) strand as you don't read and write to your socket in parallel.

In fact, what would cause problems, is code like this:

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

because here -if your io_service() has a POOL of threads associated with it-, actual read and write could be carried out in parallel by several threads.

If you only have one thread per io_service, no need for a strand. The same is true if you're implementing HTTP for example. In HTTP, you don't read and write to the socket in parallel, due to the layout of the protocol. You read a request from the client -though this might be done in several async calls-, then you somehow process request and headers, then you (async or not) send your reply.

Pretty much the same you can also read in ASIO's strand documentation.

Flo
  • 421
  • 4
  • 6