Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ktls/src/ktls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ where
return task::Poll::Ready(Ok(()));
}

let filled_before = buf.filled().len();
let read_res = this.inner.as_mut().poll_read(cx, buf);
if let task::Poll::Ready(Err(e)) = &read_res {
// 5 is a generic "input/output error", it happens when
Expand Down Expand Up @@ -258,6 +259,18 @@ where
}
}

if let task::Poll::Ready(Ok(())) = &read_res {
if buf.filled().len() == filled_before {
// A transport EOF before the peer's close_notify means the stream was
// truncated--possibly by an attacker, since a TCP FIN is not
// authenticated. Report it as an error.
return task::Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"peer closed connection without sending TLS close_notify",
)));
}
}

read_res
}
}
Expand Down
62 changes: 62 additions & 0 deletions ktls/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,65 @@ async fn read_returns_eof_when_close_notify_reply_would_block() {
// alert rather than a reset.
jh.await.unwrap();
}

#[tokio::test]
async fn missing_close_notify_is_unexpected_eof() {
let cipher_suite = KtlsCipherSuite {
version: KtlsVersion::TLS13,
typ: KtlsCipherType::AesGcm128,
};

let ckey = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();

let mut server_config =
ServerConfig::builder_with_provider(single_suite_provider(cipher_suite))
.with_protocol_versions(&[cipher_suite.version.as_supported_version()])
.unwrap()
.with_no_client_auth()
.with_single_cert(
vec![ckey.cert.der().clone()],
rustls::pki_types::PrivatePkcs8KeyDer::from(ckey.key_pair.serialize_der()).into(),
)
.unwrap();
server_config.enable_secret_extraction = true;

let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config));
let ln = TcpListener::bind("[::]:0").await.unwrap();
let addr = ln.local_addr().unwrap();

let mut root_store = RootCertStore::empty();
root_store.add(ckey.cert.der().clone()).unwrap();
let client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let tls_connector = TlsConnector::from(Arc::new(client_config));

let server = async {
let (stream, _) = ln.accept().await.unwrap();
let stream = CorkStream::new(stream);
let stream = acceptor.accept(stream).await.unwrap();
ktls::config_ktls_server(stream).await.unwrap()
};
let client = async {
let stream = TcpStream::connect(addr).await.unwrap();
tls_connector
.connect("localhost".try_into().unwrap(), stream)
.await
.unwrap()
};
let (mut server, mut client) = tokio::join!(server, client);

// 1. Sanity round trip.
client.write_all(b"hello").await.unwrap();
client.flush().await.unwrap();
let mut buf = [0u8; 5];
server.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"hello");

// 2. The client sends a bare TCP FIN, bypassing the TLS shutdown.
client.get_mut().0.shutdown().await.unwrap();

// 3. The server must report truncation, not end-of-stream.
let err = server.read(&mut buf).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof, "{err}");
}
Loading