Amusingly, Keith and I appear to have crossed mid-air in our respective requests to change KeyUpdate, so this will be a fun collision. Mine is a tad larger of a change I'm afraid.
We've currently implemented KeyUpdate in our in-progress TLS 1.3 code, but we've for now ignored the requirement for the receiver to send KeyUpdates to catch up with the sender. It appears that honoring that will cause significant problems. A TLS stack does not know much of the read/write patterns of the higher-level application. It must handle all kinds of flows, so a dumb filter is ideal. Some TLS APIs are even structured around this. Rather than consume and provide a socket interface, they encrypt and decrypt buffers, leaving transport business to the consumer. (And socket-based APIs are often used as if they were filters using buffers for sockets.) We may then have transport reads progress faster than transport writes, or the write channel may not even be driven most of the time. (This isn't that contrived. Unpipelined HTTP/1.1 never reads and writes in parallel. HTTP/2 may not have any response to write. Or perhaps it is processing a request while an earlier response is still being written.) The receiver can then accrue KeyUpdate obligations unboundedly. When the write channel is active again, it has to dump all those out as unbounded write overhead. This is rather a mess. There should be a tight bound (one, if not zero) on KeyUpdate obligations. I'm unclear on exactly what parts of KeyUpdate the WG finds important, so I haven't written a concrete PR yet. But here is roughly the fix I have in mind: Sending one KeyUpdate and 1,000 in a row morally should be the same. But both ends share a KeyUpdate track, so any forward secrecy goals require matching generations. So branch client-write and server-write first and have two KeyUpdate ladders starting at client_traffic_secret_0 and server_traffic_secret_0. (Note bidi KeyUpdate is asynchronous, so implementations had to store separate read and write traffic secrets anyway.) As a small bonus, we can lose the client/server split from traffic key calculations (7.3), having branched at traffic secret already. However, we lose the "free" (really at the cost of this unboundedness problem) generation-based bidirectional KeyUpdate sync. Depending on what we want out of KeyUpdate, I can imagine a few options: - Don't. KeyUpdates are unilateral. Recommend in the spec to KeyUpdate every N records or so and leave it at that. (I think this is the best option on grounds of simplicity, assuming it meets the primary needs of KeyUpdate.) - If you receive a KeyUpdate and didn't send one in the last N minutes (where N minutes >>> 1 RTT), (optionally?) flag your next write to be preceded by KeyUpdate. This is simple but needs an ad-hoc timeout to prevent ping-ponging. - Variations on sticking a please_echo boolean in the KeyUpdate message, generation counts, etc., to get synchronization with coalescing if we need it. I would much prefer the simpler options unless we truly need this. (KeyUpdate is right now a *required* feature, so simplicity should be a priority. Rare use cases are what extensions are for.) Thoughts? David PS: We've seen this before with renego (I've seen many OpenSSL consumers which lock up if the peer renegotiation), error alerts triggered on reads (often they don't get sent), and close_notify (also don't get sent in practice). Deviations from dumb filters are expensive. PPS: I'm wary of over-extending post-handshake auth for the same reason, though I haven't had time to look carefully at the latest proposal yet. Still, having TLS specify and analyze the crypto while lifting the actual messages into application-level framing would be preferable for these sorts of side protocols. The application gets to make assumptions about read/write flows and knows in which contexts what is and isn't allowed.
_______________________________________________ TLS mailing list [email protected] https://www.ietf.org/mailman/listinfo/tls
