Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS 1.3 client received Unexpected Eof with early data enabled #1406

Closed
spongebob888 opened this issue Aug 17, 2023 · 4 comments
Closed

TLS 1.3 client received Unexpected Eof with early data enabled #1406

spongebob888 opened this issue Aug 17, 2023 · 4 comments

Comments

@spongebob888
Copy link

I use simple_0rtt_client with slight modification in the example to test client zero rtt.
It could setup a 0rtt connection but failed to read data from this connection and reported
a unexpected eof error. (The same code works for 1rtt connection) The code I used is below

use std::sync::Arc;

use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;

use rustls::crypto::ring::Ring;
use rustls::crypto::CryptoProvider;
use rustls::{OwnedTrustAnchor, RootCertStore};

fn start_connection(config: &Arc<rustls::ClientConfig<impl CryptoProvider>>, domain_name: &str) {
    let server_name = domain_name
        .try_into()
        .expect("invalid DNS name");
    let mut conn = rustls::ClientConnection::new(Arc::clone(config), server_name).unwrap();
    let mut sock = TcpStream::connect(format!("{}:443", domain_name)).unwrap();
    sock.set_nodelay(true).unwrap();
    let request = format!(
        "GET / HTTP/1.1\r\n\
         Host: {}\r\n\
         Connection: close\r\n\
         Accept-Encoding: identity\r\n\
         \r\n",
        domain_name
    );

    // If early data is available with this server, then early_data()
    // will yield Some(WriteEarlyData) and WriteEarlyData implements
    // io::Write.  Use this to send the request.
    if let Some(mut early_data) = conn.early_data() {
        early_data
            .write_all(request.as_bytes())
            .unwrap();
        println!("  * 0-RTT request sent");
    }

    let mut stream = rustls::Stream::new(&mut conn, &mut sock);

    // Complete handshake.
    stream.flush().unwrap();

    // If we didn't send early data, or the server didn't accept it,
    // then send the request as normal.
    if !stream.conn.is_early_data_accepted() {
        stream
            .write_all(request.as_bytes())
            .unwrap();
        println!("  * Normal request sent");
    } else {
        println!("  * 0-RTT data accepted");
    }
    let mut first_response_line = [0u8; 8024];
    let mut reader = stream;
    loop {
        let n = reader
            .read(&mut first_response_line)
            .unwrap();
        println!("len: {}", n);
        if n == 0 {
            break;
        }
    }
    //println!("  * Server response: {:?}", first_response_line);
}

fn main() {
    env_logger::init();

    let mut root_store = RootCertStore::empty();
    root_store.add_trust_anchors(
        webpki_roots::TLS_SERVER_ROOTS
            .iter()
            .map(|ta| {
                OwnedTrustAnchor::from_subject_spki_name_constraints(
                    ta.subject,
                    ta.spki,
                    ta.name_constraints,
                )
            }),
    );

    let mut config = rustls::ClientConfig::<Ring>::builder()
        .with_safe_defaults()
        .with_root_certificates(root_store)
        .with_no_client_auth();

    // Enable early data.
    config.enable_early_data = true;
    let config = Arc::new(config);

    // Do two connections. The first will be a normal request, the
    // second will use early data if the server supports it.

    println!("* Sending first request:");
    start_connection(&config, "jbp.io");
    println!("* Sending second request:");
    start_connection(&config, "jbp.io");
}

more specifically I only changed the code in the example

let mut first_response_line = String::new();
BufReader::new(stream)
.read_line(&mut first_response_line)
.unwrap();
println!(" * Server response: {:?}", first_response_line);

and changed it to

    let mut first_response_line = [0u8; 8024];
    let mut reader = stream;
    loop {
        let n = reader
            .read(&mut first_response_line)
            .unwrap();
        println!("len: {}", n);
        if n == 0 {
            break;
        }
    }
    //println!("  * Server response: {:?}", first_response_line);
@spongebob888
Copy link
Author

For 1rtt connection, zero size data can be read indicating an eof. But for 0rtt, zero size data can't be read and Unexpected Eof was reported.

@cpu
Copy link
Member

cpu commented Aug 25, 2023

@vincentliu77 Do you observe the same problem with other 0rtt enabled TLS 1.3 servers, or just jbp.io? Have you compared results with a different TLS 1.3 library?

@spongebob888
Copy link
Author

Unfortunately, I tested on codepen.io and www.bytedance.com which both supported 0rtt and the same results are observed. I didn't test other library, since I didn't know any other tls lib in rust with 0rtt feature.

@cpu
Copy link
Member

cpu commented Aug 28, 2023

Hi @vincentliu77,

I believe that Rustls is doing the correct thing in your modified example by raising an unexpected EOF error reading the response data from the 0-RTT request that the server accepted.

For 1rtt connection, zero size data can be read indicating an eof.

The reason this works for the 1rtt connection without an error is that the server-side properly signals the intent to close by sending a TLS layer close notify warning after the HTTP response application data. Because the warning is received, the Rustls client understands the EOF it reads is expected and yields a read of 0 without error.

first resp_000

But for 0rtt, zero size data can't be read and Unexpected Eof was reported.

In this case the server does not send a close notify warning, it only sends the HTTP application layer response. Because of this when Rustls reads the EOF it can't know whether this was an expected condition, or a truncation attack. Since ab685b5 Rustls' Stream abstraction properly yields an unexpected EOF error to the application allowing it to choose how to handle this event.

second resp_000

Here's the pcap I took, along with the exported SSLKEYLOGFILE in case you want to evaluate it closer:
1406.0rtt.eof.err.tar.gz

Remember you'll need to configure Wireshark with the included pre-master-secret file in order to view the decrypted records:

keyfile config

We can re-open this issue if you think my analysis is incorrect, but I believe there's nothing for us to fix on the Rustls side in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants