Open marten-seemann opened 3 years ago
There are 2 options for dealing with 0-RTT rejection that come to mind:
ErrZeroRTTRefused
whenever any function on this stream is called. It's the application's responsibility to handle this error, most likely by opening a new stream and sending out the data again.I'm leaning towards option 2. While more work to implement, it would be completely transparent to the application. We don't really need to worry about the state accumulated here since 1. we only need to keep that state for the duration of the handshake (which is bounded by the handshake timeout, 5 or 10s) and 2. the amount of 0-RTT data we can send is effectively bounded by the initial congestion window.
I don't think 2 is the right option here; the application has to explicitly opt-in to 0-RTT, so it has to be prepared to handle early rejection as well. Let's take the UNIX approach and go with 1.
The question we need to answer is what most applications would want? Would they want to write different data if 0-RTT is rejected, or will (almost) every application just retransmit the data? Maybe we should think about libp2p's primary use case for 0-RTT, which is performing fast DHT operations. How would we want to use it there?
I expect that for the majority of applications not a lot will change within the one RTT the handshake takes, so they'd just retransmit that data. If the need occurs, we could implement both options here, and have the application pick between both behaviors by another option on the context.
Note: we went with this approach for relays because it was easy, not because it was the "best" way. I'm still open to doing it here, but this is kind of on the edge. I consider "allow relays" and "don't allow relays" to be hints, but 0-RTT is more than that: it's dropping a security guarantee.
I expect that for the majority of applications not a lot will change within the one RTT the handshake takes, so they'd just retransmit that data. If the need occurs, we could implement both options here, and have the application pick between both behaviors by another option on the context.
I agree. Note: we could also have an explicit "require zero rtt" if necessary, but I can't think of a use-case.
but 0-RTT is more than that: it's dropping a security guarantee.
That's why I suggested an opt-in. If an application doesn't change anything, it won't get access to a 0-RTT enabled stream, so the same strong security guarantees continue to apply.
I'm still open to doing it here, but this is kind of on the edge.
This was just a proposal to get it shipped rather sooner than later. I'm open to making an API change, if we can agree on the right course.
For QUIC, it would be sufficient to add a HandshakeComplete()
and anOpen0RTTStream
to the MuxedConn
.
For TLS 1.3 / Noise over TCP, this would probably not be enough. The primary value of using early data in that case would be to speed up the stream muxer negotiation, which brings us dangerously close to the ms2.0 discussion.
That's why I suggested an opt-in. If an application doesn't change anything, it won't get access to a 0-RTT enabled stream, so the same strong security guarantees continue to apply.
My point is that contexts are very... implicit. It would be easy for a higher-level service to say "0-RTT" is fine and add the option to their context. Then, they'd pass the context down to the lower-level service, using 0-RTT.
This is fine for now, but I'd like to eventually move to proper stream options.
Inspired by @vyzo’s and @aarshkshah1992’s recent approach of adding a value to a
context.Context
to signal if a stream should be opened via a relay, I'm wondering if a similar approach make sense for making use of QUIC’s 0-RTT feature?Background
When re-connecting to a server we've connected to before, QUIC allows us to send application data right away, without waiting for the handshake to finish. Data is encrypted with a key derived from the key used on the previous connection. However, 0-RTT data can be replayed by an attacker, and is therefore only safe to be used for idempotent data.
The doc linked in https://github.com/libp2p/go-libp2p/issues/1532 discusses approaches to make 0-RTT replay-safe. These approaches work, as long as the server properly implements them, however, there's no way for the client to enforce or even check this, so we probably don't want to rely on this. A safer approach is to only use 0-RTT for application data that's replay-safe.
Proposal
Make
Dial
return immediately when a 0-RTT connection is dialed.OpenStream
will block until the handshake is done, unless a special "0-RTT-safe" value is set on the context, in which case it will return immediately. An application can then write 0-RTT data on that stream. After writing the idempotent data, it can then use context returned byHandshakeComplete()
on the QUIC connection to wait for the QUIC handshake to complete. Note that we'd have to add a similar method to theMuxedConn
interface (or maybe to theMuxedStream
?By having application explicitly opt-in, we make sure that the default behavior of libp2p is a sane default: only send application data when the connection is properly secured and replay-safe. Furthermore, by using a special value on a context, this will naturally work on transports that don't support 0-RTT data.
Open Questions
0-RTT can be rejected by the server for various reasons. In that case, the handshake falls back to a normal 1-RTT handshake, and all data sent in 0-RTT is ignored. This also invalidates all streams opened.
cc @Stebalien