#![allow(clippy::module_name_repetitions)]
use std::io::{self, BufRead as _};
use std::sync::Arc;
use thiserror::Error;
const REQUIRED_ALPN: &[u8] = b"http/1.1";
#[derive(Debug, Error)]
pub enum TlsError {
#[error("invalid server name: {0}")]
InvalidServerName(String),
#[error("rustls error: {0}")]
Rustls(#[from] rustls::Error),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("server negotiated unexpected ALPN: {0:?}")]
BadAlpn(Vec<u8>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TlsCryptoProvider {
AwsLc,
Ring,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum TlsCipherPreference {
#[default]
ProviderDefault,
Aes128GcmFirst,
Aes256GcmFirst,
Chacha20First,
}
impl Default for TlsCryptoProvider {
fn default() -> Self {
Self::Ring
}
}
impl std::fmt::Display for TlsCryptoProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AwsLc => f.write_str("aws-lc"),
Self::Ring => f.write_str("ring"),
}
}
}
impl std::str::FromStr for TlsCryptoProvider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"aws-lc" | "aws_lc" | "aws-lc-rs" | "aws_lc_rs" => Ok(Self::AwsLc),
"ring" => Ok(Self::Ring),
_ => Err(format!(
"invalid tls provider {s:?}; expected aws-lc or ring"
)),
}
}
}
impl std::fmt::Display for TlsCipherPreference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ProviderDefault => f.write_str("default"),
Self::Aes128GcmFirst => f.write_str("aes128"),
Self::Aes256GcmFirst => f.write_str("aes256"),
Self::Chacha20First => f.write_str("chacha"),
}
}
}
impl std::str::FromStr for TlsCipherPreference {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"default" => Ok(Self::ProviderDefault),
"aes128" | "aes-128" | "aes128-gcm" | "aes-128-gcm" => Ok(Self::Aes128GcmFirst),
"aes256" | "aes-256" | "aes256-gcm" | "aes-256-gcm" => Ok(Self::Aes256GcmFirst),
"chacha" | "chacha20" | "chacha20-poly1305" => Ok(Self::Chacha20First),
_ => Err(format!(
"invalid tls cipher preference {s:?}; expected default, aes128, aes256, or chacha"
)),
}
}
}
pub struct TlsAdapter {
conn: rustls::ClientConnection,
peer_closed_notify: bool,
}
impl std::fmt::Debug for TlsAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TlsAdapter")
.field("is_handshaking", &self.conn.is_handshaking())
.finish()
}
}
impl TlsAdapter {
pub fn new_client(server_name: &str) -> Result<Self, TlsError> {
Self::new_client_with_provider(server_name, TlsCryptoProvider::default())
}
pub fn new_client_with_provider(
server_name: &str,
provider: TlsCryptoProvider,
) -> Result<Self, TlsError> {
Self::new_client_with_config(
server_name,
Arc::new(client_config(
provider,
TlsCipherPreference::ProviderDefault,
)?),
)
}
pub fn client_config(provider: TlsCryptoProvider) -> Result<rustls::ClientConfig, TlsError> {
client_config(provider, TlsCipherPreference::ProviderDefault)
}
pub fn client_config_with_cipher_preference(
provider: TlsCryptoProvider,
preference: TlsCipherPreference,
) -> Result<rustls::ClientConfig, TlsError> {
client_config(provider, preference)
}
#[must_use]
pub fn negotiated_cipher_suite(&self) -> Option<rustls::CipherSuite> {
self.conn
.negotiated_cipher_suite()
.map(|suite| suite.suite())
}
pub fn new_client_with_config(
server_name: &str,
config: Arc<rustls::ClientConfig>,
) -> Result<Self, TlsError> {
let name = rustls::pki_types::ServerName::try_from(server_name.to_owned())
.map_err(|_| TlsError::InvalidServerName(server_name.to_owned()))?;
let conn = rustls::ClientConnection::new(config, name)?;
Ok(Self {
conn,
peer_closed_notify: false,
})
}
#[must_use]
pub fn is_handshaking(&self) -> bool {
self.conn.is_handshaking()
}
pub fn verify_alpn(&self) -> Result<(), TlsError> {
match self.conn.alpn_protocol() {
None => Ok(()),
Some(p) if p == REQUIRED_ALPN => Ok(()),
Some(other) => Err(TlsError::BadAlpn(other.to_vec())),
}
}
#[must_use]
pub fn received_close_notify(&self) -> bool {
self.peer_closed_notify
}
pub fn send_close_notify(&mut self) {
self.conn.send_close_notify();
}
pub fn ingest_ciphertext<F>(
&mut self,
mut src: &[u8],
dst_ciphertext: &mut Vec<u8>,
mut on_plaintext: F,
) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
let mut processed = false;
while !src.is_empty() {
if !self.conn.wants_read() {
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
if !self.conn.wants_read() {
return Ok(());
}
}
let before = src.len();
let n = self.conn.read_tls(&mut src)?;
if n == 0 || src.len() == before {
break;
}
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
processed = true;
}
if !processed {
self.process_and_drain(dst_ciphertext, &mut on_plaintext)?;
}
Ok(())
}
fn process_and_drain<F>(
&mut self,
dst_ciphertext: &mut Vec<u8>,
on_plaintext: &mut F,
) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
let io_state = self.conn.process_new_packets()?;
self.peer_closed_notify |= io_state.peer_has_closed();
self.drain_plaintext(on_plaintext)?;
self.drain_ciphertext(dst_ciphertext)?;
Ok(())
}
fn drain_plaintext<F>(&mut self, on_plaintext: &mut F) -> Result<(), TlsError>
where
F: FnMut(&[u8]),
{
let mut reader = self.conn.reader();
loop {
match reader.fill_buf() {
Ok([]) => break,
Ok(chunk) => {
let n = chunk.len();
on_plaintext(chunk);
reader.consume(n);
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) => return Err(TlsError::Io(e)),
}
}
Ok(())
}
fn drain_ciphertext(&mut self, dst_ciphertext: &mut Vec<u8>) -> Result<(), TlsError> {
while self.conn.wants_write() {
let n = self.conn.write_tls(dst_ciphertext)?;
if n == 0 {
break;
}
}
Ok(())
}
pub fn egress_plaintext(
&mut self,
src: &[u8],
dst_ciphertext: &mut Vec<u8>,
) -> Result<(), TlsError> {
if !src.is_empty() {
std::io::Write::write_all(&mut self.conn.writer(), src)?;
}
self.drain_ciphertext(dst_ciphertext)
}
}
fn client_config(
provider: TlsCryptoProvider,
preference: TlsCipherPreference,
) -> Result<rustls::ClientConfig, TlsError> {
let root_store = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
let mut provider = match provider {
TlsCryptoProvider::AwsLc => rustls::crypto::aws_lc_rs::default_provider(),
TlsCryptoProvider::Ring => rustls::crypto::ring::default_provider(),
};
apply_cipher_preference(&mut provider, preference);
let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(provider))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
config.alpn_protocols = vec![REQUIRED_ALPN.to_vec()];
Ok(config)
}
fn apply_cipher_preference(
provider: &mut rustls::crypto::CryptoProvider,
preference: TlsCipherPreference,
) {
let preferred = match preference {
TlsCipherPreference::ProviderDefault => return,
TlsCipherPreference::Aes128GcmFirst => &[
rustls::CipherSuite::TLS13_AES_128_GCM_SHA256,
rustls::CipherSuite::TLS13_AES_256_GCM_SHA384,
rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
][..],
TlsCipherPreference::Aes256GcmFirst => &[
rustls::CipherSuite::TLS13_AES_256_GCM_SHA384,
rustls::CipherSuite::TLS13_AES_128_GCM_SHA256,
rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
][..],
TlsCipherPreference::Chacha20First => &[
rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
rustls::CipherSuite::TLS13_AES_128_GCM_SHA256,
rustls::CipherSuite::TLS13_AES_256_GCM_SHA384,
][..],
};
let mut reordered = Vec::with_capacity(provider.cipher_suites.len());
for target in preferred {
if let Some(suite) = provider
.cipher_suites
.iter()
.copied()
.find(|suite| suite.suite() == *target)
{
reordered.push(suite);
}
}
for suite in provider.cipher_suites.iter().copied() {
if reordered
.iter()
.all(|existing| existing.suite() != suite.suite())
{
reordered.push(suite);
}
}
provider.cipher_suites = reordered;
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn construct_with_valid_name() {
let r = TlsAdapter::new_client("www.example.com");
assert!(r.is_ok());
}
#[test]
fn handshake_initially_pending() {
let t = TlsAdapter::new_client("www.example.com").unwrap();
assert!(t.is_handshaking());
}
#[test]
fn invalid_server_name_rejected() {
let r = TlsAdapter::new_client("");
assert!(matches!(r, Err(TlsError::InvalidServerName(_))));
}
}