use std::io::{Read, Write};
use std::sync::Arc;
use ring::aead::{
AES_128_GCM, AES_256_GCM, Aad, CHACHA20_POLY1305, LessSafeKey, Nonce, UnboundKey,
};
use rustls::client::{ClientConnectionData, UnbufferedClientConnection};
use rustls::kernel::KernelConnection;
use rustls::pki_types::ServerName;
use rustls::unbuffered::{ConnectionState, UnbufferedStatus};
use rustls::{ClientConfig, ConnectionTrafficSecrets, RootCertStore};
use crate::error::{Error, Result};
#[doc(hidden)]
pub const HEADER_LEN: usize = 5;
pub(crate) const TAG_LEN: usize = 16;
pub(crate) const MAX_RECORD: usize = HEADER_LEN + (1 << 14) + 256;
pub(crate) const MAX_HS_MSG: usize = 1 << 18;
const CT_ALERT: u8 = 21;
const CT_HANDSHAKE: u8 = 22;
pub(crate) const CT_APPDATA: u8 = 23;
const HS_NEW_SESSION_TICKET: u8 = 4;
const HS_KEY_UPDATE: u8 = 24;
pub fn client_config(roots: RootCertStore) -> Result<Arc<ClientConfig>> {
client_config_with(
roots,
Arc::new(rustls::crypto::ring::default_provider()),
true,
)
}
pub fn client_config_with(
roots: RootCertStore,
provider: Arc<rustls::crypto::CryptoProvider>,
extract_secrets: bool,
) -> Result<Arc<ClientConfig>> {
client_config_with_store(
roots,
provider,
extract_secrets,
Arc::new(NonBlockingSessionStore::new(256)),
)
}
pub fn client_config_with_store(
roots: RootCertStore,
provider: Arc<rustls::crypto::CryptoProvider>,
extract_secrets: bool,
store: Arc<dyn rustls::client::ClientSessionStore>,
) -> Result<Arc<ClientConfig>> {
let mut cfg = ClientConfig::builder_with_provider(provider)
.with_protocol_versions(&[&rustls::version::TLS13])?
.with_root_certificates(roots)
.with_no_client_auth();
cfg.enable_secret_extraction = extract_secrets;
cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
cfg.resumption = rustls::client::Resumption::store(store);
Ok(Arc::new(cfg))
}
pub(crate) fn check_client_config(cfg: &ClientConfig) -> Result<()> {
if cfg.enable_secret_extraction {
Ok(())
} else {
Err(Error::Config(
"the rustls ClientConfig needs enable_secret_extraction = true and TLS 1.3 only: \
build it with ergon_io::tls::client_config"
.into(),
))
}
}
#[derive(Debug)]
pub struct NonBlockingSessionStore(std::sync::Mutex<rustls::client::ClientSessionMemoryCache>);
impl NonBlockingSessionStore {
#[must_use]
pub fn new(size: usize) -> Self {
const SESSIONS_PER_SERVER: usize = 8;
let servers = size.max(2);
Self(std::sync::Mutex::new(
rustls::client::ClientSessionMemoryCache::new(servers * SESSIONS_PER_SERVER),
))
}
fn with<T>(&self, f: impl FnOnce(&rustls::client::ClientSessionMemoryCache) -> T) -> Option<T> {
self.0.try_lock().ok().map(|c| f(&c))
}
}
impl rustls::client::ClientSessionStore for NonBlockingSessionStore {
fn set_kx_hint(&self, server_name: ServerName<'static>, group: rustls::NamedGroup) {
self.with(|c| c.set_kx_hint(server_name, group));
}
fn kx_hint(&self, server_name: &ServerName<'_>) -> Option<rustls::NamedGroup> {
self.with(|c| c.kx_hint(server_name)).flatten()
}
fn set_tls12_session(
&self,
server_name: ServerName<'static>,
value: rustls::client::Tls12ClientSessionValue,
) {
self.with(|c| c.set_tls12_session(server_name, value));
}
fn tls12_session(
&self,
server_name: &ServerName<'_>,
) -> Option<rustls::client::Tls12ClientSessionValue> {
self.with(|c| c.tls12_session(server_name)).flatten()
}
fn remove_tls12_session(&self, server_name: &ServerName<'static>) {
self.with(|c| c.remove_tls12_session(server_name));
}
fn insert_tls13_ticket(
&self,
server_name: ServerName<'static>,
value: rustls::client::Tls13ClientSessionValue,
) {
self.with(|c| c.insert_tls13_ticket(server_name, value));
}
fn take_tls13_ticket(
&self,
server_name: &ServerName<'static>,
) -> Option<rustls::client::Tls13ClientSessionValue> {
self.with(|c| c.take_tls13_ticket(server_name)).flatten()
}
}
#[cfg(feature = "webpki-roots")]
#[must_use]
pub fn webpki_roots() -> RootCertStore {
RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
}
}
struct Direction {
key: LessSafeKey,
iv: [u8; 12],
seq: u64,
}
fn default_key_update_at(limit: u64) -> u64 {
if limit == u64::MAX {
1 << 62
} else {
(limit / 2).max(1)
}
}
type HsMessage<'a> = Option<(u8, &'a [u8], &'a [u8])>;
fn split_hs_message(buf: &[u8]) -> Result<HsMessage<'_>> {
if buf.len() < 4 {
return Ok(None);
}
let len = usize::from(buf[1]) << 16 | usize::from(buf[2]) << 8 | usize::from(buf[3]);
if len > MAX_HS_MSG - 4 {
return Err(Error::Record("post-handshake message too large"));
}
let total = 4 + len;
if buf.len() < total {
return Ok(None);
}
Ok(Some((buf[0], &buf[4..total], &buf[total..])))
}
impl Direction {
fn new(seq: u64, secrets: &ConnectionTrafficSecrets) -> Result<Self> {
let (alg, key, iv) = match secrets {
ConnectionTrafficSecrets::Aes128Gcm { key, iv } => {
(&AES_128_GCM, key.as_ref(), iv.as_ref())
}
ConnectionTrafficSecrets::Aes256Gcm { key, iv } => {
(&AES_256_GCM, key.as_ref(), iv.as_ref())
}
ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv } => {
(&CHACHA20_POLY1305, key.as_ref(), iv.as_ref())
}
_ => return Err(Error::Record("unsupported cipher suite")),
};
let key =
LessSafeKey::new(UnboundKey::new(alg, key).map_err(|_| Error::Record("bad key"))?);
let mut ivb = [0u8; 12];
if iv.len() != 12 {
return Err(Error::Record("unexpected IV length"));
}
ivb.copy_from_slice(iv);
Ok(Self { key, iv: ivb, seq })
}
fn next_nonce(&mut self) -> Nonce {
let mut n = self.iv;
for (b, s) in n[4..].iter_mut().zip(self.seq.to_be_bytes()) {
*b ^= s;
}
self.seq += 1;
Nonce::assume_unique_for_key(n)
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Opened {
AppData(usize),
Control,
}
#[doc(hidden)]
pub struct RecordLayer {
rx: Direction,
tx: Direction,
kconn: KernelConnection<ClientConnectionData>,
pub records_in: u64,
hs: Vec<u8>,
limit: u64,
key_update_at: u64,
key_updates: u64,
rx_rekey_requested: bool,
}
impl RecordLayer {
pub fn from_parts(
secrets: &rustls::ExtractedSecrets,
kconn: KernelConnection<ClientConnectionData>,
) -> Result<Self> {
let limit = kconn
.negotiated_cipher_suite()
.tls13()
.map_or(u64::MAX, |s| s.common.confidentiality_limit);
let key_update_at = default_key_update_at(limit);
Ok(Self {
rx: Direction::new(secrets.rx.0, &secrets.rx.1)?,
tx: Direction::new(secrets.tx.0, &secrets.tx.1)?,
kconn,
records_in: 0,
hs: Vec::with_capacity(MAX_HS_MSG),
limit,
key_update_at,
key_updates: 0,
rx_rekey_requested: false,
})
}
#[must_use]
pub const fn record_limit(&self) -> u64 {
self.limit
}
#[must_use]
pub const fn key_update_at(&self) -> u64 {
self.key_update_at
}
pub fn set_key_update_at(&mut self, n: u64) -> &mut Self {
self.key_update_at = if n == u64::MAX {
u64::MAX
} else {
n.min(self.limit.saturating_sub(1))
};
self
}
pub fn set_record_limit(&mut self, n: u64) -> &mut Self {
self.limit = n.max(1);
self.set_key_update_at(self.key_update_at);
self
}
#[must_use]
pub const fn key_updates(&self) -> u64 {
self.key_updates
}
pub fn complete_record_len(buf: &[u8]) -> Result<Option<usize>> {
if buf.len() < HEADER_LEN {
return Ok(None);
}
if buf[0] != CT_APPDATA && buf[0] != CT_ALERT {
return Err(Error::Record("unexpected outer content type"));
}
let len = usize::from(u16::from_be_bytes([buf[3], buf[4]]));
if len > MAX_RECORD - HEADER_LEN {
return Err(Error::Record("record too long"));
}
let total = HEADER_LEN + len;
Ok((buf.len() >= total).then_some(total))
}
pub fn open_in_place(&mut self, record: &mut [u8], tx_out: &mut Vec<u8>) -> Result<Opened> {
if self.rx.seq >= self.limit {
return Err(Error::Record("confidentiality limit reached"));
}
let mut aad = [0u8; HEADER_LEN];
aad.copy_from_slice(&record[..HEADER_LEN]);
let nonce = self.rx.next_nonce();
let plain = self
.rx
.key
.open_in_place(nonce, Aad::from(aad), &mut record[HEADER_LEN..])
.map_err(|_| Error::Record("AEAD open failed"))?;
self.records_in += 1;
let mut end = plain.len();
while end > 0 && plain[end - 1] == 0 {
end -= 1;
}
if end == 0 {
return Err(Error::Record("record has no content type"));
}
let inner = plain[end - 1];
let n = end - 1;
match inner {
CT_APPDATA => Ok(Opened::AppData(n)),
CT_HANDSHAKE => {
let body = &plain[..n];
self.handle_handshake(body, tx_out)?;
Ok(Opened::Control)
}
CT_ALERT => Err(Error::Closed),
_ => Err(Error::Record("unexpected inner content type")),
}
}
fn handle_handshake(&mut self, msgs: &[u8], tx_out: &mut Vec<u8>) -> Result<()> {
if self.hs.is_empty() {
let mut rest = msgs;
while !rest.is_empty() {
if let Some((typ, body, next)) = split_hs_message(rest)? {
self.dispatch_hs(typ, body, tx_out)?;
rest = next;
} else {
self.append_hs(rest)?;
return Ok(());
}
}
return Ok(());
}
self.append_hs(msgs)?;
self.drain_hs(tx_out)
}
fn append_hs(&mut self, more: &[u8]) -> Result<()> {
if self.hs.len() + more.len() > MAX_HS_MSG {
return Err(Error::Record("post-handshake message too large"));
}
self.hs.extend_from_slice(more);
Ok(())
}
fn drain_hs(&mut self, tx_out: &mut Vec<u8>) -> Result<()> {
let mut buf = std::mem::take(&mut self.hs);
let r = self.drain_hs_from(&mut buf, tx_out);
self.hs = buf;
r
}
fn drain_hs_from(&mut self, buf: &mut Vec<u8>, tx_out: &mut Vec<u8>) -> Result<()> {
while !buf.is_empty() {
let Some((typ, body, next)) = split_hs_message(buf)? else {
return Ok(());
};
let consumed = buf.len() - next.len();
self.dispatch_hs(typ, body, tx_out)?;
buf.drain(..consumed);
}
Ok(())
}
fn dispatch_hs(&mut self, typ: u8, body: &[u8], tx_out: &mut Vec<u8>) -> Result<()> {
match typ {
HS_NEW_SESSION_TICKET => Ok(self.kconn.handle_new_session_ticket(body)?),
HS_KEY_UPDATE => {
if body.len() != 1 {
return Err(Error::Record("malformed key update"));
}
let (seq, secrets) = self.kconn.update_rx_secret()?;
self.rx = Direction::new(seq, &secrets)?;
self.key_updates += 1;
self.rx_rekey_requested = false;
if body[0] == 1 {
self.seal_inner(CT_HANDSHAKE, &[HS_KEY_UPDATE, 0, 0, 1, 0], tx_out)?;
let (seq, secrets) = self.kconn.update_tx_secret()?;
self.tx = Direction::new(seq, &secrets)?;
self.key_updates += 1;
}
Ok(())
}
_ => Err(Error::Record("unexpected post-handshake message")),
}
}
fn maybe_rekey(&mut self, out: &mut Vec<u8>) -> Result<()> {
let tx_due = self.tx.seq >= self.key_update_at;
let rx_due = self.rx.seq >= self.key_update_at && !self.rx_rekey_requested;
if !tx_due && !rx_due {
return Ok(());
}
self.seal_inner(
CT_HANDSHAKE,
&[HS_KEY_UPDATE, 0, 0, 1, u8::from(rx_due)],
out,
)?;
let (seq, secrets) = self.kconn.update_tx_secret()?;
self.tx = Direction::new(seq, &secrets)?;
self.key_updates += 1;
self.rx_rekey_requested |= rx_due;
Ok(())
}
pub fn seal(&mut self, inner_type: u8, payload: &[u8], out: &mut Vec<u8>) -> Result<()> {
self.maybe_rekey(out)?;
self.seal_inner(inner_type, payload, out)
}
fn seal_inner(&mut self, inner_type: u8, payload: &[u8], out: &mut Vec<u8>) -> Result<()> {
if payload.len() > 1 << 14 {
return Err(Error::Record("payload larger than one record"));
}
if self.tx.seq >= self.limit {
return Err(Error::Record("confidentiality limit reached"));
}
let start = out.len();
let ct_len = payload.len() + 1 + TAG_LEN;
let len = u16::try_from(ct_len).map_err(|_| Error::Record("record too long"))?;
out.extend_from_slice(&[CT_APPDATA, 0x03, 0x03]);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(payload);
out.push(inner_type);
let mut aad = [0u8; HEADER_LEN];
aad.copy_from_slice(&out[start..start + HEADER_LEN]);
let nonce = self.tx.next_nonce();
let tag = self
.tx
.key
.seal_in_place_separate_tag(nonce, Aad::from(aad), &mut out[start + HEADER_LEN..])
.map_err(|_| Error::Record("AEAD seal failed"))?;
out.extend_from_slice(tag.as_ref());
Ok(())
}
pub fn seal_build(
&mut self,
out: &mut Vec<u8>,
build: impl FnOnce(&mut Vec<u8>) -> Result<()>,
) -> Result<()> {
self.maybe_rekey(out)?;
self.seal_build_inner(out, build)
}
fn seal_build_inner(
&mut self,
out: &mut Vec<u8>,
build: impl FnOnce(&mut Vec<u8>) -> Result<()>,
) -> Result<()> {
if self.tx.seq >= self.limit {
return Err(Error::Record("confidentiality limit reached"));
}
let start = out.len();
out.extend_from_slice(&[CT_APPDATA, 0x03, 0x03, 0, 0]);
if let Err(e) = build(out) {
out.truncate(start);
return Err(e);
}
let plain_len = out.len() - start - HEADER_LEN;
if plain_len > 1 << 14 {
out.truncate(start);
return Err(Error::Record("payload larger than one record"));
}
out.push(CT_APPDATA);
let len =
u16::try_from(plain_len + 1 + TAG_LEN).map_err(|_| Error::Record("record too long"))?;
out[start + 3..start + 5].copy_from_slice(&len.to_be_bytes());
let mut aad = [0u8; HEADER_LEN];
aad.copy_from_slice(&out[start..start + HEADER_LEN]);
let nonce = self.tx.next_nonce();
let tag = self
.tx
.key
.seal_in_place_separate_tag(nonce, Aad::from(aad), &mut out[start + HEADER_LEN..])
.map_err(|_| Error::Record("AEAD seal failed"))?;
out.extend_from_slice(tag.as_ref());
Ok(())
}
pub fn seal_app(&mut self, payload: &[u8], out: &mut Vec<u8>) -> Result<()> {
self.seal(CT_APPDATA, payload, out)
}
}
#[doc(hidden)]
pub fn handshake<S: Read + Write>(
stream: &mut S,
config: Arc<ClientConfig>,
server_name: &str,
) -> Result<(RecordLayer, Vec<u8>)> {
let mut hs = TlsHandshake::new(config, server_name)?;
while !hs.step(stream)? {}
hs.finish()
}
#[doc(hidden)]
pub fn handshake_raw<S: Read + Write>(
stream: &mut S,
config: Arc<ClientConfig>,
server_name: &str,
) -> Result<(
rustls::ExtractedSecrets,
KernelConnection<ClientConnectionData>,
Vec<u8>,
)> {
let mut hs = TlsHandshake::new(config, server_name)?;
while !hs.step(stream)? {}
hs.finish_raw()
}
pub(crate) struct TlsHandshake {
conn: UnbufferedClientConnection,
inc: Vec<u8>,
inc_len: usize,
out: Vec<u8>,
out_len: usize,
out_sent: usize,
done: bool,
}
enum Next {
Read,
Done,
Continue,
}
impl TlsHandshake {
pub fn new(config: Arc<ClientConfig>, server_name: &str) -> Result<Self> {
check_client_config(&config)?;
let name = ServerName::try_from(server_name.to_owned())
.map_err(|e| Error::Config(e.to_string()))?;
Ok(Self {
conn: UnbufferedClientConnection::new(config, name)?,
inc: vec![0u8; 1 << 16],
inc_len: 0,
out: vec![0u8; 1 << 16],
out_len: 0,
out_sent: 0,
done: false,
})
}
pub fn step<S: Read + Write>(&mut self, stream: &mut S) -> Result<bool> {
loop {
if !self.flush(stream)? {
return Ok(false);
}
if self.done {
return Ok(true);
}
let UnbufferedStatus { discard, state } =
self.conn.process_tls_records(&mut self.inc[..self.inc_len]);
let next = match state? {
ConnectionState::EncodeTlsData(mut e) => {
self.out_len += e
.encode(&mut self.out[self.out_len..])
.map_err(|e| Error::Http(format!("encode: {e}")))?;
Next::Continue
}
ConnectionState::TransmitTlsData(t) => {
t.done();
Next::Continue
}
ConnectionState::BlockedHandshake => Next::Read,
ConnectionState::WriteTraffic(_) => Next::Done,
ConnectionState::PeerClosed | ConnectionState::Closed => {
return Err(Error::Closed);
}
other => {
return Err(Error::Http(format!("unexpected handshake state {other:?}")));
}
};
if discard > 0 {
self.inc.copy_within(discard..self.inc_len, 0);
self.inc_len -= discard;
}
match next {
Next::Continue => {}
Next::Done => self.done = true,
Next::Read => {
if self.inc_len == self.inc.len() {
return Err(Error::Record("handshake buffer full"));
}
match stream.read(&mut self.inc[self.inc_len..]) {
Ok(0) => return Err(Error::Closed),
Ok(n) => self.inc_len += n,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(false),
Err(e) => return Err(e.into()),
}
}
}
}
}
fn flush<S: Write>(&mut self, stream: &mut S) -> Result<bool> {
while self.out_sent < self.out_len {
match stream.write(&self.out[self.out_sent..self.out_len]) {
Ok(0) => return Err(Error::Closed),
Ok(n) => self.out_sent += n,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(false),
Err(e) => return Err(e.into()),
}
}
self.out_len = 0;
self.out_sent = 0;
Ok(true)
}
pub fn finish(self) -> Result<(RecordLayer, Vec<u8>)> {
let (secrets, kconn, rest) = self.finish_raw()?;
Ok((RecordLayer::from_parts(&secrets, kconn)?, rest))
}
pub fn finish_raw(
mut self,
) -> Result<(
rustls::ExtractedSecrets,
KernelConnection<ClientConnectionData>,
Vec<u8>,
)> {
if !self.done {
return Err(Error::Record("handshake not complete"));
}
if self.conn.protocol_version() != Some(rustls::ProtocolVersion::TLSv1_3) {
return Err(Error::Config(
"the server negotiated TLS 1.2; the record layer needs TLS 1.3 \
(restrict the ClientConfig to TLS 1.3, as ergon_io::tls::client_config does)"
.into(),
));
}
let (secrets, kconn) = self.conn.dangerous_into_kernel_connection()?;
self.inc.truncate(self.inc_len);
Ok((secrets, kconn, self.inc))
}
}