tmp: initial client-side mid-level API

This will obsolete the unbuffered API.
This commit is contained in:
Joe Birr-Pixton committed 2026-01-13 13:22:50 +00:00
1 parent a80c67411d
commit 35ebb15abf
9 files changed
+503 -11

No files matched your search

+4
View File
@@ -19,6 +19,8 @@ mod tests_with_ring {
mod io;
#[path = "api/kx.rs"]
mod kx;
#[path = "api/midstate.rs"]
mod midstate;
#[path = "api/quic.rs"]
mod quic;
#[path = "api/raw_keys.rs"]
@@ -54,6 +56,8 @@ mod tests_with_aws_lc_rs {
mod io;
#[path = "api/kx.rs"]
mod kx;
#[path = "api/midstate.rs"]
mod midstate;
#[path = "api/quic.rs"]
mod quic;
#[path = "api/raw_keys.rs"]
+105
View File
@@ -0,0 +1,105 @@
//! Tests for mid-level state-based API
#![allow(clippy::disallowed_types, clippy::duplicate_mod)]
use std::io::{Read, Write};
use rustls::ServerConnection;
use rustls::client::{ClientState, ReceivedTrafficState};
use rustls::state::{ReceivedData, SliceInput};
use rustls_test::{KeyType, make_client_config, make_server_config};
use super::provider;
#[test]
fn test_client_smoke() {
let server_config = make_server_config(KeyType::Rsa2048, &provider::DEFAULT_PROVIDER);
let mut server = ServerConnection::new(server_config.into()).unwrap();
server
.writer()
.write_all(b"hello from server")
.unwrap();
let mut client_config = make_client_config(KeyType::Rsa2048, &provider::DEFAULT_PROVIDER);
client_config.max_fragment_size = Some(64);
let mut client =
ClientState::new(client_config.into(), "localhost".try_into().unwrap(), None).unwrap();
let mut server_to_client = vec![];
for _ in 0..5 {
client = match client {
ClientState::AwaitServerFlight(asf) => {
let mut slice_input = SliceInput::new(&mut server_to_client);
let next = asf
.input_data(&mut slice_input)
.unwrap();
let used = slice_input.into_used();
server_to_client.drain(0..used);
next
}
ClientState::SendClientFlight(mut scf) => {
while let Some(data) = scf.take_data() {
server
.read_tls(&mut data.as_slice())
.unwrap();
server.process_new_packets().unwrap();
server
.write_tls(&mut &mut server_to_client)
.unwrap();
println!("data send: {data:x?}");
}
scf.into_next()
}
ClientState::Traffic(mut send, recv, outputs) => {
println!("traffic kind: {:?}", outputs.handshake_kind());
if !server_to_client.is_empty() {
let mut slice_input = SliceInput::new(&mut server_to_client);
let ReceivedTrafficState::Available(avail) =
recv.read(&mut slice_input).unwrap()
else {
panic!("recieve failed");
};
println!("received data: {:?}", avail.data);
let (discard, _recv) = avail.into_next();
println!("received used: {:?}", discard);
slice_input.discard(discard);
let used = slice_input.into_used();
server_to_client.drain(0..used);
assert!(server_to_client.is_empty());
let mut client_to_server = [0u8; 128];
let sent = send
.write(b"client says hi", &mut client_to_server)
.unwrap();
server
.read_tls(&mut &client_to_server[..sent])
.unwrap();
let sent = send
.close(&mut client_to_server)
.unwrap();
server
.read_tls(&mut &client_to_server[..sent])
.unwrap();
let io_state = server.process_new_packets().unwrap();
assert_eq!(0, io_state.tls_bytes_to_write());
assert_eq!(14, io_state.plaintext_bytes_to_read());
assert!(io_state.peer_has_closed());
let mut received = vec![];
server
.reader()
.read_to_end(&mut received)
.unwrap();
println!("server received {:x?}", received);
}
break;
}
_ => todo!(),
};
}
}
+7 -3
View File
@@ -251,12 +251,12 @@ mod buffered {
}
impl super::EarlyData {
fn check_write(&mut self, sz: usize) -> io::Result<usize> {
pub(crate) fn check_write(&mut self, sz: usize) -> io::Result<usize> {
self.check_write_opt(sz)
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
}
fn bytes_left(&self) -> usize {
pub(crate) fn bytes_left(&self) -> usize {
self.left
}
}
@@ -561,7 +561,7 @@ impl core::error::Error for EarlyDataError {}
/// State associated with a client connection.
#[derive(Debug)]
pub struct ClientConnectionData {
early_data: EarlyData,
pub(crate) early_data: EarlyData,
ech_status: EchStatus,
}
@@ -572,6 +572,10 @@ impl ClientConnectionData {
ech_status: EchStatus::default(),
}
}
pub(crate) fn early_data_is_enabled(&self) -> bool {
self.early_data.is_enabled()
}
}
impl Output for ClientConnectionData {
+6
View File
@@ -42,6 +42,12 @@ pub(crate) use tls12::TLS12_HANDLER;
mod tls13;
pub(crate) use tls13::TLS13_HANDLER;
mod state;
pub use state::{
AwaitServerFlight, ClientState, ProvideCredential, ReceiveTraffic, ReceivedApplicationData,
ReceivedTrafficState, SendClientFlight, SendEarlyData, SendTraffic, VerifyServerIdentity,
};
/// Dangerous configuration that should be audited and used with extreme care.
pub mod danger {
pub use super::config::danger::{DangerousClientConfig, DangerousClientConfigBuilder};
+361
View File
@@ -0,0 +1,361 @@
use core::mem;
use core::ops::Deref;
use std::vec::Vec;
use pki_types::ServerName;
use crate::client::ClientConnectionData;
use crate::common_state::{MidState, Protocol};
use crate::conn::ConnectionCore;
use crate::crypto::cipher::Payload;
use crate::error::{ApiMisuse, ErrorWithAlert};
use crate::lock::Mutex;
use crate::msgs::deframer::{Delocator, ReceivedData};
use crate::msgs::handshake::ClientExtensionsInput;
use crate::sync::Arc;
use crate::unbuffered::EncryptError;
use crate::{ClientConfig, ConnectionCommon, ConnectionOutputs, Error, KeyingMaterialExporter};
/// "Mid-level" state-based Client TLS API.
#[non_exhaustive]
pub enum ClientState {
/// Handshake data should be transmitted to the peer.
SendClientFlight(SendClientFlight),
/// Early data capability is available for this peer and can be input into this object.
SendEarlyData(SendEarlyData),
/// We are awaiting handshake data from the peer.
AwaitServerFlight(AwaitServerFlight),
/// The peer's identity should be verified.
VerifyServerIdentity(VerifyServerIdentity),
/// Our credentials should be selected and supplied for this peer.
ProvideCredential(ProvideCredential),
/// The handshake has completed and application data may now flow.
Traffic(SendTraffic, ReceiveTraffic, ConnectionOutputs),
}
impl ClientState {
/// Create a new client connection.
///
/// If `alpn_protocols` is [`None`], then the ALPN settings are obtained
/// from [`ClientConfig::alpn_protocols`].
pub fn new(
config: Arc<ClientConfig>,
name: ServerName<'static>,
alpn_protocols: Option<Vec<Vec<u8>>>,
) -> Result<Self, Error> {
let input = ClientExtensionsInput::from_alpn(
alpn_protocols.unwrap_or_else(|| config.alpn_protocols.clone()),
);
let inner = ConnectionCommon::from(ConnectionCore::for_client(
config,
name,
input,
Protocol::Tcp,
)?);
let next = match inner.core.side.early_data_is_enabled() {
true => |inner| Self::SendEarlyData(SendEarlyData { inner }),
false => |inner| Self::AwaitServerFlight(AwaitServerFlight { inner }),
};
Ok(Self::SendClientFlight(SendClientFlight { inner, next }))
}
}
impl Deref for ClientState {
type Target = ConnectionOutputs;
fn deref(&self) -> &Self::Target {
match self {
Self::SendClientFlight(st) => &st.inner.outputs,
Self::SendEarlyData(st) => &st.inner.outputs,
Self::AwaitServerFlight(st) => &st.inner.outputs,
Self::VerifyServerIdentity(_) => todo!(),
Self::ProvideCredential(_) => todo!(),
Self::Traffic(_, _, outputs) => outputs,
}
}
}
/// A handshake state where we are required to send data to the peer.
pub struct SendClientFlight {
inner: ConnectionCommon<ClientConnectionData>,
next: fn(ConnectionCommon<ClientConnectionData>) -> ClientState,
}
impl SendClientFlight {
/// Obtain one TLS message that should be sent to the peer.
///
/// If you wish to use vectored IO, call this function repeatedly
/// to collect all messages before doing the IO.
pub fn take_data(&mut self) -> Option<Vec<u8>> {
self.inner.send.sendable_tls.pop()
}
/// Move to the next state, if possible.
///
/// This returns the current state if there is remaining data to write.
pub fn into_next(self) -> ClientState {
match self.inner.send.sendable_tls.is_empty() {
true => (self.next)(self.inner),
false => ClientState::SendClientFlight(self),
}
}
}
/// A handshake state where we are awaiting handshake data from the peer.
pub struct AwaitServerFlight {
inner: ConnectionCommon<ClientConnectionData>,
}
impl AwaitServerFlight {
/// Receive some data.
///
/// Return the next state if reached, the current state if not, and an error if things are permenantly
/// broken. If an error occurs here is is fatal to the connection.
pub fn input_data(mut self, input: &mut dyn ReceivedData) -> Result<ClientState, Error> {
self.inner
.core
.process_new_packets(input)?;
if !self.inner.send.sendable_tls.is_empty() {
return Ok(ClientState::SendClientFlight(SendClientFlight {
inner: self.inner,
next: next_state,
}));
}
Ok(next_state(self.inner))
}
}
/// An opportunity exists to send early data.
pub struct SendEarlyData {
inner: ConnectionCommon<ClientConnectionData>,
}
impl SendEarlyData {
/// Write `data` as early data.
///
/// The number of written bytes is returned.
pub fn write(&mut self, data: &[u8]) -> usize {
let early_data = &mut self.inner.core.side.early_data;
early_data
.check_write(data.len())
.map(|sz| {
self.inner
.send
.send_early_plaintext(&data[..sz])
})
.unwrap_or_default()
}
/// How many bytes may be sent as early data.
pub fn bytes_left(&self) -> usize {
self.inner
.core
.side
.early_data
.bytes_left()
}
/// TODO: copy docs
pub fn early_exporter(&mut self) -> Result<KeyingMaterialExporter, Error> {
match self.inner.outputs.early_exporter.take() {
Some(inner) => Ok(KeyingMaterialExporter { inner }),
None => Err(ApiMisuse::ExporterAlreadyUsed.into()),
}
}
/// Move to the next state, concluding writing of Early Data.
pub fn into_next(self) -> ClientState {
ClientState::AwaitServerFlight(AwaitServerFlight { inner: self.inner })
}
}
/// TODO
pub struct VerifyServerIdentity;
/// TODO
pub struct ProvideCredential;
/// TODO: should be `SendTraffic(SendPath)`.
pub struct SendTraffic(Arc<Mutex<ConnectionCommon<ClientConnectionData>>>);
impl SendTraffic {
/// Write application data to the peer.
///
/// The TLS data to send to the peer is written into `output_buffer` and
/// the number of bytes written are returned. This data should then
/// be communicated to the peer.
pub fn write(
&mut self,
application_data: &[u8],
output_buffer: &mut [u8],
) -> Result<usize, EncryptError> {
let mut inner = self.0.lock().unwrap();
inner.core.maybe_refresh_traffic_keys();
inner
.send
.write_plaintext(application_data.into(), output_buffer)
}
/// Conclude sending traffic by sending a `close_notify` alert.
///
/// The alert is written into `output_buffer` and the number of bytes written
/// is returned.
pub fn close(self, output_buffer: &mut [u8]) -> Result<usize, EncryptError> {
let mut inner = self.0.lock().unwrap();
inner
.send
.eager_send_close_notify(output_buffer)
}
}
/// TODO: should be `ReceiveTraffic(ReceivePath)`.
pub struct ReceiveTraffic(Arc<Mutex<ConnectionCommon<ClientConnectionData>>>);
impl ReceiveTraffic {
/// Receive application data from the peer.
///
/// `received_tls` is an instance of the receive buffer abstraction containing
/// TLS-protected data received from the peer.
///
/// A [`ReceivedTrafficState`] is returned on success:
///
/// - [`ReceivedTrafficState::Available`] if an application data message
/// has been received.
/// - [`ReceivedTrafficState::Await`] if more IO is required.
/// - [`ReceivedTrafficState::CloseNotify`] if the peer has cleanly
/// closed the receive direction of the connection.
///
/// An error from this function permanently breaks the ability to receive
/// data from the peer. The error may be accompanied by a TLS alert,
/// which can be obtained from the returned [`ErrorWithAlert`] and sent
/// to the peer. Following this, the underlying IO medium should be
/// closed by the application.
pub fn read<'a>(
self,
received_tls: &'a mut impl ReceivedData,
) -> Result<ReceivedTrafficState<'a>, ErrorWithAlert> {
let mut inner = self.0.lock().unwrap();
let received_plain = match inner
.core
.process_new_packets(received_tls)
{
Ok(received_plain) => received_plain,
Err(err) => return Err(ErrorWithAlert::new(err, &mut inner.core.common_state.send)),
};
if let Some((unborrowed, mut progress)) = received_plain {
let pending_discard = progress.take_discard();
let Payload::Borrowed(data) =
unborrowed.reborrow(&Delocator::new(received_tls.slice_mut()))
else {
return Err(Error::Unreachable("decrypted data should be borrowed").into());
};
drop(inner);
return Ok(ReceivedTrafficState::Available(ReceivedApplicationData {
data,
pending_discard,
rt: self,
}));
}
let closed = inner.recv.has_received_close_notify;
drop(inner);
Ok(match closed {
true => ReceivedTrafficState::CloseNotify,
false => ReceivedTrafficState::Await(self),
})
}
}
/// A state machine as a cycle between requiring further received TLS data,
/// and discharging received application data.
#[expect(clippy::exhaustive_enums)]
pub enum ReceivedTrafficState<'a> {
/// More input is required.
///
/// Collect it into your input buffer, and then call [`ReceiveTraffic::read()`] again.
Await(ReceiveTraffic),
/// Some application data has been received.
Available(ReceivedApplicationData<'a>),
/// We received a `close_notify` alert from the peer.
///
/// This means the receive path is closed cleanly.
CloseNotify,
}
/// Received application data.
pub struct ReceivedApplicationData<'a> {
/// The application data bytes.
pub data: &'a [u8],
/// How many bytes on the front of the original input buffer are associated
/// with this data.
///
/// This value should be added to the discard count of the original input
/// buffer via [`ReceivedData::discard()`].
///
/// Use [`ReceivedApplicationData::into_next()`] to obtain it while releasing
/// the borrow on `data` (from the original input buffer).
pending_discard: usize,
rt: ReceiveTraffic,
}
impl ReceivedApplicationData<'_> {
/// Finish processing this received data.
///
/// This yields the discard value that should now be applied to the originating
/// buffer, and the next `ReceiveTraffic` state.
pub fn into_next(self) -> (usize, ReceiveTraffic) {
(self.pending_discard, self.rt)
}
}
fn next_state(mut inner: ConnectionCommon<ClientConnectionData>) -> ClientState {
match inner
.core
.state
.as_ref()
.map(|s| s.mid_state())
.expect("TODO: withdraw error fusing in state box")
{
MidState::AwaitPeerFlight => ClientState::AwaitServerFlight(AwaitServerFlight { inner }),
MidState::Traffic => {
/*
* TODO: here we should deconstruct `inner` into two direction-specific objects,
* forming the interior of `SendTraffic` and `ReceiveTraffic`. At the moment this is
* difficult because we only have one `state: impl State` and both the send and
* receive directions interact with this.
*
let CommonState {
outputs,
send,
recv,
..
} = inner.core.common_state;
* Once we achieve that, we can also return `outputs` here (giving the caller ultimate
* control over the lifetime of the large allocations hung off this.) For now, we
* steal its contents which achieves the same goal but risks someone seeing the empty
* husk left behind.
*/
let outputs = mem::take(&mut inner.outputs);
let mutex = Arc::new(Mutex::new(inner));
ClientState::Traffic(SendTraffic(mutex.clone()), ReceiveTraffic(mutex), outputs)
}
}
}
+1 -1
View File
@@ -1206,7 +1206,7 @@ impl<Side: SideData> ConnectionCore<Side> {
}
/// Trigger a `refresh_traffic_keys` if required by `CommonState`.
fn maybe_refresh_traffic_keys(&mut self) {
pub(crate) fn maybe_refresh_traffic_keys(&mut self) {
if mem::take(
&mut self
.common_state
+7
View File
@@ -492,6 +492,13 @@ pub use crate::verify::{DigitallySignedStruct, DistinguishedName, SignerPublicKe
pub use crate::versions::{ALL_VERSIONS, DEFAULT_VERSIONS, SupportedProtocolVersion};
pub use crate::webpki::RootCertStore;
/// Types supporting the mid-level state-based API.
pub mod state {
pub use crate::msgs::deframer::{
DeframerSliceBuffer as SliceInput, DeframerVecBuffer as BufferedInput, ReceivedData,
};
}
/// Items for use in a client.
pub mod client;
pub use client::ClientConfig;
+10 -4
View File
@@ -147,8 +147,9 @@ impl BufferProgress {
}
}
/// TODO
#[derive(Default, Debug)]
pub(crate) struct DeframerVecBuffer {
pub struct DeframerVecBuffer {
/// Buffer of data read from the socket, in the process of being parsed into messages.
///
/// For buffer size management, checkout out the [`DeframerVecBuffer::prepare_read()`] method.
@@ -278,7 +279,7 @@ impl DeframerVecBuffer {
/// A borrowed version of [`DeframerVecBuffer`] that tracks discard operations
#[derive(Debug)]
pub(crate) struct DeframerSliceBuffer<'a> {
pub struct DeframerSliceBuffer<'a> {
// a fully initialized buffer that will be deframed
buf: &'a mut [u8],
// number of bytes to discard from the front of `buf` at a later time
@@ -286,10 +287,15 @@ pub(crate) struct DeframerSliceBuffer<'a> {
}
impl<'a> DeframerSliceBuffer<'a> {
pub(crate) fn new(buf: &'a mut [u8]) -> Self {
pub fn new(buf: &'a mut [u8]) -> Self {
Self { buf, discard: 0 }
}
/// Returns how many bytes were consumed at the start of the original buffer.
pub fn into_used(self) -> usize {
self.discard
}
/// Tracks a pending discard operation of `num_bytes`
pub(crate) fn queue_discard(&mut self, num_bytes: usize) {
self.discard += num_bytes;
@@ -315,7 +321,7 @@ impl ReceivedData for DeframerSliceBuffer<'_> {
}
/// An abstraction over received data buffers (either owned or borrowed)
pub(crate) trait ReceivedData {
pub trait ReceivedData {
/// TODO
fn slice_mut(&mut self) -> &mut [u8];
+2 -3
View File
@@ -6,9 +6,8 @@ use crate::msgs::codec::Reader;
use crate::msgs::message::{HEADER_SIZE, read_opaque_message_header};
mod buffers;
pub(crate) use buffers::{
BufferProgress, DeframerSliceBuffer, DeframerVecBuffer, Delocator, Locator, ReceivedData,
};
pub(crate) use buffers::{BufferProgress, Delocator, Locator};
pub use buffers::{DeframerSliceBuffer, DeframerVecBuffer, ReceivedData};
mod handshake;
pub(crate) use handshake::{HandshakeAlignedProof, HandshakeDeframer};