[TLS 1.3] Consolidate the Session Manager#3150
Conversation
c07679d to
e2dba40
Compare
b3e1971 to
3312899
Compare
Codecov ReportBase: 88.05% // Head: 88.16% // Increases project coverage by
📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more Additional details and impacted files@@ Coverage Diff @@
## master #3150 +/- ##
==========================================
+ Coverage 88.05% 88.16% +0.11%
==========================================
Files 608 615 +7
Lines 68674 69479 +805
Branches 6840 6906 +66
==========================================
+ Hits 60470 61256 +786
+ Misses 5343 5340 -3
- Partials 2861 2883 +22
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
2c78ddc to
ac56132
Compare
I haven't seen this before in C++ tbh, but I'm generally a fan of newtypes in Rust which seem to be basically the same idea. Fine to proceed along these lines.
The underlying storage mechanism is going to be the same though, and we'd be duplicating that if we split the managers.
I think we should provide toggles where someone might reasonably have differing needs, but in general people just want something to work and they shouldn't have to understand what any of this is doing unless they are doing something tricky or unusual. If in doubt we should just use whatever default parameters that say BoringSSL or OpenSSL uses. |
|
This seems to finally converge. Frankly, I underestimated the intricacies of consolidating 1.2 and 1.3 behind the same API, but I'm fairly happy with this now. Commit history will need cleanup and potentially, I'll also split this into two or three patch sets. Main improvements from an (advanced) user's perspective:
Still to do
Questions
|
IMO no. TLS sessions are transitory. We should just make sure it's clearly called out in the release notes and migration guide. |
7a12ab8 to
a257f5a
Compare
c0a4e81 to
3adece2
Compare
3adece2 to
c1f1198
Compare
343ee18 to
29f551e
Compare
|
Sorry for leaving this dormant for a while. I waited for a continuous time slot to finish this up as it required some more focused refactoring and RFC pondering. Anyway, this should be the last oversized PR for TLS 1.3 😄. Turns out that @randombit was right, that storage and policy should be somewhat separated:
I didn't split the managers into a client and a server part. Instead I added a layer of indirection in the Final touches... that might be done in follow-ups as this PR is becoming ridiculously hard to reason about. Mainly because the API changes of
|
src/lib/tls/tls_session_manager.cpp
Outdated
| const auto ticket_age = callbacks.tls_current_timestamp() - session->start_time(); | ||
| const bool expired = ticket_age > policy_lifetime; |
There was a problem hiding this comment.
This applies the same expiry filter to both tickets and TLS 1.2 session IDs. Previously, there were two lifetimes:
Policy::session_ticket_lifetime()andSession_Manager_Memory/SQL(..., session_lifetime).
TLS 1.3 doesn't make this distinction anyway, but we could introduce a Policy::tls12_session_id_lifetime() to allow the same flexibility as before or at least rename session_ticket_lifetime() to session_lifetime().
Note that derived Session_Manager classes each used to implement the expiry logic themselveds. That is still possible (by overriding ::retrieve() and ::find()) but not necessary anymore. For applications to use the default implementation, they would just need to override the "internal storage methods" ::retrieve_one() and ::find_all().
This introduces a recursive mutex into the base class and implements 'Lockable' for the base class. Derived classes can therefore just lock *this using a scoped lock and don't have to maintain their own mutex.
b1018b8 to
558f220
Compare
|
rebased to master to solve conflicts with enum adaptions. |
e9e2b49 to
4c1c299
Compare
lieser
left a comment
There was a problem hiding this comment.
First incomplete round of review.
| BOTAN_ASSERT(session.side() == Connection_Side::Server, | ||
| "Client tried to establish a session"); |
There was a problem hiding this comment.
Not completely happy with the combined API of client and server session manager. Having to do this kind of misuse checks indicates to me that this is not optimal.
But since the API of the Session_Manager is more intended for internal use (i.e. a user should not really need to call any of the problematic methods directly himself) I am OK with keeping as is if a split would complicate things to much.
There was a problem hiding this comment.
I tend to agree. Let's spike an alternative API together.
As discussed: let's revisit after merging.
|
Thanks @lieser for the thorough review! You certainly found quite a few places with potential for improvement. Not yet addressed are (IMHO): |
randombit
left a comment
There was a problem hiding this comment.
Did not do a terribly careful review here, but leaving an approval to indicate I didn't notice any immediate problems / no objections to merging.
|
Done from my side. Waiting for CI and @lieser's approval. |
🥳 |
I wanted to comment on this as one of my use/test cases is a client-side test application which opens tens of thousands of TLS connections to a server. To force it to store separate session records for each client, despite connecting to the same host and port, I plan to append an id to the host name prior to the look-up. It seems unfortunate to me not to be able to leverage the multi-threading capbilities of SQLite, and that we essentially have to serialise these look-ups. Is it really necessary to lock a mutex around every single SQL look-up? Under Botan 2.0 I had written my own session manager, but I figured with all the changes to the session manager API for 3.0, rather than retrofit what I have I'd give the SQLite one a go and see how it performed. Perhaps it can be optional as a template argument to the session manager allowing you to supply a no-op mutex or something... |
I see this mainly as a safety precaution. But perhaps you are right and that's a bit too broad. Maybe we could use |
|
Reading up a little bit on SQLite, my understanding is that there are three settings.
Actually, in my use case, where I have a fixed number of worker threads servicing connections then it would seem that something could be possible with option 2 above in conjunction with a thread_local helper to manage the database connection lifetime. Maybe it would be a custom/derived SQLite Session_Manager, but there is still some locking going on in the base Session_Manager and Session_Manager_SQL classes which would need addressing somehow...so maybe something along the lines of being able to provide the mutex type (or a no-op) for the Session_Manager base class as a template argument and then the current Session_Manager_SQLite would pass in recursive_mutex_type, and some new Session_Manager_SQLite_TL (thread local) would pass in the noop_mutex but then would change how the DB is created, to do some sort of thread local magic to ensure one database connection exists per thread, plus also specifying to sqlite3_open that it can operate in multithreaded mode. Just some thoughts anyway. |
Pull Request Dependencies
Introduce a strong-type template #3165Feature: Generalization of RandomNumberGenerator::random_vec() #3201[TLS 1.3] Prepare for Session Consolidation #3244Strong Types
Those are the first code changes that make use of the strong-type helper introduced in #3165. Particularly for
Session_IDandSession_Ticket, which are otherwise simplystd::vector<uint8_t>containers.class SessionThis is now a dumb data-sink that can (un)marshal itself (and knows how to encrypt/decrypt itself). In particular it doesn't have a notion about its own ID or ticket anymore. These are the realm of the
Session_Manager.struct Session_HandleA descriptor for a session. As in the example above: It contains either a (TLS 1.2-ish) ID or a ticket (TLS 1.2 or 1.3) but never both. These invariants are ensured by the class. The
Session_Manageremits aSession_Handlefor new sessions and spits them out again given aSession_Handle. It can also store aSessionalong with an existing session handle.class Session_ManagerI put some documentation in the header, so won't repeat that here. Main point: There is a sharp distinction between APIs useful for the server and the client:
Servers create
Sessionobjects and pass them toSession_Manager::establish(Session) -> Session_Handle. Hence, theSession_Manageris in full control of both theSession_IDand theSession_Ticket. Servers will retrieveSessionobjects directly viaSession_Manager::retrieve(Session_Handle) -> Session(TLS 1.2) or viaSession_Manager::choose_from_offered_tickets. Both are high-level methods implemented directly in the base class. They take care of session policy validation (e.g. expiry). Low-level storage methods must be implemented by all sub-classes.Clients on the other hand always receive a
Session_Handlefrom the server (that's just how TLS works). They useSession_Manager::store(Session, Session_Handle)to associate their sessions with the handles from the server. Usually, clients will then useSession_Manager::find(Server_Information) -> std::vector<std::pair<Session, Session_Handle>>to retrieve potential sessions for resumption. Again,findis a high-level method implemented in the base class in terms of a low-level abstract storage method.Note that clients may retrieve multiple sessions for resumption. TLS 1.3 allows clients to provide several tickets (as PSK identities) at once. The
Session_Managercan opt to always return a singleSession,Session_Handlepair, of course. On the server-sideSession_Manager::choose_from_offered_tickets()receives the tickets offered by the client and selects the "most favorable": The default implementation simply selects the first that matches the server's needs.I'm hoping that typical users won't need to override the high-level (policy-enforcing) methods. They are marked as
virtualthough and can be overridden if needed.Session deletion (e.g. after usage by the client/server) is done with
Session_Manager::remove(Session_Handle).class Session_Manager_StatelessA strawman manager for a stateless server. It generates random Session IDs and creates tickets via
Session::encrypt(). The sessions are not persisted andSession_Manager::retrieve(Session_ID)will always fail. To stay compatible with the current architectureSession_Manager_StatelessusesCredentials_Manager::psk()to obtain the necessary ticket key.class Session_Manager_In_MemoryLike before, this keeps sessions in-memory in a
std::map<>. Note that theSessionIDstrong-type acts as a key! This implementation will never emit session tickets.class Session_Manager_SQLiteI adapted the implementation to the new
Session_ManagerAPI. Most notably, the implementation now detects old (and/or corrupted) databases and resets them transparently. Some more clean-ups could be done regarding the SQL database and statements wrapper. As the size of this patch is already getting out of hand, that will come later.class Session_Manager_HybridThis internally combines a
Session_Manager_Statelesswith an arbitrary (stateful)Session_Managerimplementation. It automatically creates either IDs or tickets depending on whether the TLS 1.2 peer can handle tickets. Applications can choose whether to prefer tickets or IDs at construction time. This is likely the most useful flavor for TLS servers.What's next
First: Thanks for reading through those thoughts. 😄
A few open questions:
Strong types: yay? or nay?Should we go as far and separate theNow that the API seems almost fully disjoint (apart fromSession_Managerinto a server and a client part?::remove()).Required technical understanding of the application developer: is this taking it too far?