Supporting QUIC
Closed this issue · 11 comments
QUIC requires TLS 1.3 without the transport feature.
To support QUIC, a lot of refactorings and new APIs are necessary.
I'm thinking 4 steps to merge my QUIC implemenatation:
- Adding CH and SH in encodeHandshake13' and decodeHandshake13' #390
- Adding new types for secrets for refactoring #393
- Moving many local functions to top-level for transport free #395
- Adding QUIC APIs https://github.com/kazu-yamamoto/hs-tls/tree/quic-api (this branch is fragile)
Edited: the approach was changed. The new approach is found in #419.
From a look at draft-ietf-quic-tls-23 it seems this is all about using a different record layer.
I'm wondering if it is possible to push more into infrastructure code instead of duplicating handshake code. Especially:
- Making the interface between QUIC and TLS more general. i.e. it would exchange any sequence of messages and the key updates, and remain driven by
tls
for the entire handshake sequence. - Using a callback design, similar to the Backend API, but one level above (before record protection).
Having a more general API is desirable because it could be used or tested independently of QUIC. We need to be able to maintain and refactor for TLS without impacting QUIC.
I'm also wondering what is the story for HelloRetryRequest.
From a look at draft-ietf-quic-tls-23 it seems this is all about using a different record layer.
Yes. Historically speaking, old drafts designed to use backend with the TLS record layer. Since it is inefficient, the current design was taken.
I'm wondering if it is possible to push more into infrastructure code instead of duplicating handshake code. Especially:
- Making the interface between QUIC and TLS more general. i.e. it would exchange any sequence of messages and the key updates, and remain driven by
tls
for the entire handshake sequence.- Using a callback design, similar to the Backend API, but one level above (before record protection).
Having a more general API is desirable because it could be used or tested independently of QUIC. We need to be able to maintain and refactor for TLS without impacting QUIC.
Did you suggest the following?
- Introducing a new backend for record layer.
- An encoder encrypts payload and put it into a specific record.
- An decoder extracts payload from the record and decrypts it.
- For QUIC
- Implementing IO backend using packet queues or someting (to not send to/receive from the outside)
- Implementing record layer backend carrying out only encryption and decryption. The record layer is empty.
I'm also wondering what is the story for HelloRetryRequest.
Good point. Since quicly
sends HRR in the handshake phase, I should support it quickly. :-)
If a QUIC client calls handshake
, it should be return both CH and CF separately. An example implementation is:
- A caller spawns a new Haskell thread to execute
handshake
. - The caller receives CH which
handshake
sends viabackendSend
. - The caller gives SH..SF so that the thread can receive them via
backendRecv
- The caller receives CF which
handshake
sends viabackendSend
.
Is this what you want?
I cited Backend as example of pluggable module API we already have. If interface between QUIC and TLS needs to exchange packet plaintext, it should not be backendSend/backendRecv but a higher-level API dealing with something like Record Plaintext
or even Packet(13)
. QUIC transport replaces the record layer and performs encryption/decryption with cipher and keys provided by TLS.
I don't know which side is supposed to take care of packet fragmentation.
What I don't understand too is the call model that is actually required. Does QUIC need to pull a Client Hello out of TLS in its calling thread, or is it possible to push the Client Hello to QUIC from an external thread executing handshake
.
I'm now exploring your idea:
https://github.com/kazu-yamamoto/hs-tls/tree/record-layer
I will bring my experience here soon.
I don't know which side is supposed to take care of packet fragmentation.
It is a job of QUIC CRYPTO frame which provides the offset field.
Good news: the new approach works elegantly.
- Approach one: dividing handshake functions to make them transport-free
- Pros: APIs are synchronous, so easy-to-use. No additional Haskell thread is necessary.
- Cons: we need to provide many APIs for cases such as HRR and NST
- Approach two: introducing a record layer
- Pros: we can reuse the handshake code without drastic modifications. So, HRR and NST can be implemented free
- Cons: we need to spawn a Haskell thread for negotiation. So, APIs are asynchronous and hard-to-use
- Appoarch three: introducing handshake controller for synchronization in addition to the record layer
- Pros: no drastic modification, HRR and NST are free, easy-to-use APIs and Haskell thread can be terminated safely
- Cons: we need to spawn a Haskell thread for negotiation.
I think the approach 3) is promising. In this sample code, a client and a server negotiates with HRR and NST. I also confirmed that this APIs works well for my QUIC client.
- https://github.com/kazu-yamamoto/hs-tls/tree/send-recv contains refactoring for
Sending
andReceiving
. They were not symmetric. For instance,Sending
usesengage
stuff butdisengage
is used inIO
. This branch refactors them andIO
symmetrically. - The commit of
introducing record layer
introduces the record layer. My conclusion is:
data RecordLayer = RecordLayer {
-- Sending.hs and Sending13.hs
encodeRecord :: Record Plaintext -> IO (Either TLSError ByteString)
-- IO.hs
, sendBytes :: ByteString -> IO ()
-- IO.hs
, recvRecord :: IO (Either TLSError (Record Plaintext))
}
- https://github.com/kazu-yamamoto/hs-tls/tree/handshake-controller introduces the handshake controller and provides QUIC APIs in
Network.TLS.QUIC
.
I don't have to hurry for merging since these branches are not drastic changes. But I would like to merge send-recv
as soon as possible to make my work easy.
Any comments?
record-layer
and handshake-controller
were rebased onto master
.
Cons: we need to spawn a Haskell thread for negotiation.
How much of an issue is this in practice?
With some effort, I think the encoding I gave in #380 could be extended and then provide
synchronous execution while keeping the existing code structure.
How much of an issue is this in practice?
I'm not sure. Probably, its cost is cheap.
With some effort, I think the encoding I gave in #380 could be extended and then provide
synchronous execution while keeping the existing code structure.
I don't understand this idea completely. Anyway, I'm satisfied with the current implementation: https://github.com/kazu-yamamoto/hs-tls/tree/handshake-controller
This code was tested with other QUIC implementations.
I have spent much time to implement this and would like to spend time to explore other stuffs of QUIC than the handshake. Would you compare my implementation and your idea by yourself, if necessary?
Todo items identified after reviewing #419, will work on this over time:
- Add documentation to the QUIC module, possibly taking content from my notes → #428
- Allow arbitrary exceptions to go across layers, i.e. QUIC record layer should be able to throw a
QUICError
and get it back as the other end wrapped in anTLSException
, not converted toString
. - See if it is possible to avoid repeating the TLS cipher in the
SecretInfo
data types. Similarly, handshake mode and negotiated protocol could be available from the TLS context through API. - Verify if the new handshake ACK logic gives expected result. Unclear if the frame should be sent before or after new receive. → kazu-yamamoto/quic#4
- Verify if
quic
IORefs modified by the TLS to QUIC callbacks need atomic modify or not. → kazu-yamamoto/quic#5