tokio-rustls icon indicating copy to clipboard operation
tokio-rustls copied to clipboard

does TLSAcceptor read early data?

Open tahmid-23 opened this issue 1 year ago • 8 comments

hi, I was wondering if TLSAcceptor is able to read TLS1.3 early data? it looks like the early data test does not use TLSAcceptor if so, is there a reason why?

tahmid-23 avatar Apr 27 '24 16:04 tahmid-23

I think you could use TlsAcceptor::accept_with() to read early data from the ServerConnection provided to your callback FnOnce.

If you experiment and find that works a PR adding an example would be great :+1:

cpu avatar Apr 29 '24 18:04 cpu

I think I could try that. but does it not make more sense to have it automatically read early data when you read from the stream? for parity with when the tls connector writes to the stream with early data enabled

tahmid-23 avatar Apr 29 '24 20:04 tahmid-23

does it not make more sense to have it automatically read early data when you read from the stream?

Early data reads must be handled separately from regular application data reads after the handshake completes. There are security implications for early data reads that require extra care. See RFC 8446 Appendix E.5 and RFC 8470 Section 3 for more information.

cpu avatar Apr 29 '24 21:04 cpu

I was writing a CTF challenge on this so that's why I'm interested :slightly_smiling_face: but after thinking about it I realize why it's better to always separate them

  1. Here's an initial attempt
#[tokio::main]
async fn main() {
    let certs = rustls_pemfile::certs(&mut std::fs::read("localhost.direct.crt").unwrap().as_slice())
        .map(Result::unwrap)
        .collect();
    let key = rustls_pemfile::private_key(&mut std::fs::read("localhost.direct.key").unwrap().as_slice())
        .unwrap()
        .unwrap();

    let tcp_listener = TcpListener::bind("0.0.0.0:8000").await.unwrap();
    let server_config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)
        .unwrap();
    let tls_acceptor = TlsAcceptor::from(Arc::new(server_config));

    let server_task = tokio::spawn(async move {
        let (tcp_stream, _) = tcp_listener.accept().await.unwrap();
        tls_acceptor.accept_with(tcp_stream, |connection| {
            println!("{:?}", connection.early_data().is_some());
        }).await.unwrap();
    });

    server_task.await.unwrap();
}

If I run nc localhost 8000, this will immediately print False. Since early data clearly hasn't been sent at this point I imagine it's inappropriate to read early data here. Is there some other way to know that the stream has reached early data without waiting for the handshake?

  1. Early data is io::Read. Can you confirm whether that means that not all early data will become available at the same time? If so, wouldn't it make sense to have some async way to read the early data, rather than relying on the sync api?

tahmid-23 avatar Apr 30 '24 15:04 tahmid-23

I was writing a CTF challenge on this so that's why I'm interested 🙂

Cool :-)

If I run nc localhost 8000, this will immediately print False. Since early data clearly hasn't been sent at this point I imagine it's inappropriate to read early data here. Is there some other way to know that the stream has reached early data without waiting for the handshake?

It should be appropriate to read early data here. You don't need to wait for the full handshake to complete, just for the peer to have sent a full client hello.

I think the reason you always get None from early_data() is that you need to customize your server config to set the max_early_data_size to a non-zero value:

 let mut server_config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)
        .unwrap();
 server_config.max_early_data_size = xxxx;        

Early data is io::Read. Can you confirm whether that means that not all early data will become available at the same time? If so, wouldn't it make sense to have some async way to read the early data, rather than relying on the sync api?

I'm not sure off-hand. Someone more familiar with Tokio Rustls might have a better suggestion here.

cpu avatar May 01 '24 17:05 cpu

I use following code successfully, accept_with() does not help. This means we need to read early data after server side handshake.

        let mut stream = self.acceptor.accept(stream).await?;
        let buf = if let Some(mut ed) = stream.get_mut().1.early_data() {
            let mut buf = Vec::new();
            ed.read_to_end(&mut buf)?;
            Bytes::from(buf)
        } else {
            Bytes::new()
        };
        Ok(PeekableStream::new(stream, buf))

leptonyu avatar Oct 02 '24 05:10 leptonyu

I have played around with this before.. Basically we have to start with something like this:

diff --git i/src/common/handshake.rs w/src/common/handshake.rs
index d1272fd..dd75860 100644
--- i/src/common/handshake.rs
+++ w/src/common/handshake.rs
@@ -88,6 +88,12 @@ where
             }
 
             try_poll!(Pin::new(&mut tls_stream).poll_flush(cx));
+
+            if let Some(mut data) = session.early_data() {
+                let buf = Vec::new();
+                data.read_to_end(&mut buf).unwrap();
+                *state = TlsState::EarlyData(0, buf);
+            }
         }
 
         Poll::Ready(Ok(stream))

and then expose a way to read that early data to the application

valpackett avatar Nov 06 '24 22:11 valpackett

I think we can expose a fn get_session() -> Option<&mut Session> in Accept

quininer avatar Nov 07 '24 03:11 quininer