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

Add Rustls provider including examples #1899

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exclude = [
"esp-metadata",
"esp-println",
"esp-riscv-rt",
"esp-rustls-provider",
"esp-wifi",
"esp-storage",
"examples",
Expand Down
40 changes: 40 additions & 0 deletions esp-rustls-provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "esp-rustls-provider"
version = "0.1.0"
edition = "2021"

[dependencies]
chacha20poly1305 = { version = "0.10", default-features = false, features = [
"alloc",
] }
der = "0.7"
ecdsa = "0.16.8"
hmac = "0.12"
p256 = { version = "0.13.2", default-features = false, features = [
"alloc",
"ecdsa",
"pkcs8",
] }
pkcs8 = "0.10.2"
pki-types = { package = "rustls-pki-types", version = "1" }
rand_core = { version = "0.6", default-features = false }

rustls = { version = "0.23.21", default-features = false, features = ["tls12", "custom-provider"] }
rsa = { version = "0.9", features = ["sha2"], default-features = false }
sha2 = { version = "0.10", default-features = false }
signature = "2"
webpki = { package = "rustls-webpki", version = "0.102", features = [
"alloc",
], default-features = false }
x25519-dalek = "2"

# should this be a feature? - not really needed by this crate but re-exported for convenience
webpki-roots = "0.26.1"

esp-hal = { path = "../esp-hal", version = "0.23.1", default-features = false }
embedded-io = { version = "0.6.1", default-features = false }
log = "0.4.20"

[features]
log = ["rustls/logging"]
defmt = []
11 changes: 11 additions & 0 deletions esp-rustls-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# esp-rustls-provider

## NO support for targets w/o atomics

While most dependencies can be used with `portable-atomic` that's unfortunately not true for Rustls itself. It needs `alloc::sync::Arc` in a lot of places.

This means that ESP32-S2, ESP32-C2 and ESP32-C3 are NOT supported.

## Status
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove this and break it out into issues instead? I think we'll need a new label for the esp-rustls-provider package.

My fear is that we'll never check this again :D, if we have it in separate issues we can at least be aware.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can create the issues after the PR is merged of course, there is no rush to do it now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the TODOs here - we shouldn't forget about creating separate issues after the merge (since creating them before is kind of weird?)


This crate is currently experimental/preview. It's not available on crates.io and might be limited in functionality.
233 changes: 233 additions & 0 deletions esp-rustls-provider/src/adapter/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! Client connection wrappers

use super::ConnectionError;

/// Wrapper for [embedded_io] to be used as a client connection
pub struct ClientConnection<'s, S>
where
S: embedded_io::Read + embedded_io::Write,
{
socket: S,
conn: rustls::client::UnbufferedClientConnection,
incoming_tls: &'s mut [u8],
outgoing_tls: &'s mut [u8],
incoming_used: usize,
outgoing_used: usize,

plaintext_in: &'s mut [u8],
plaintext_in_used: usize,
plaintext_out: &'s mut [u8],
plaintext_out_used: usize,
}

impl<'s, S> ClientConnection<'s, S>
where
S: embedded_io::Read + embedded_io::Write,
{
pub fn new(
config: alloc::sync::Arc<rustls::client::ClientConfig>,
server: rustls::pki_types::ServerName<'static>,
socket: S,
incoming_tls: &'s mut [u8],
outgoing_tls: &'s mut [u8],
plaintext_in: &'s mut [u8],
plaintext_out: &'s mut [u8],
) -> Result<Self, (S, ConnectionError<S::Error>)> {
match rustls::client::UnbufferedClientConnection::new(config, server) {
Ok(conn) => Ok(Self {
socket,
conn,
incoming_tls,
outgoing_tls,
incoming_used: 0,
outgoing_used: 0,

plaintext_in,
plaintext_in_used: 0,
plaintext_out,
plaintext_out_used: 0,
}),
Err(err) => Err((socket, ConnectionError::Rustls(err))),
}
}

pub fn free(self) -> S {
self.socket
}

fn work(&mut self) -> Result<(), ConnectionError<S::Error>> {
use rustls::unbuffered::{AppDataRecord, ConnectionState};

let mut done = false;
loop {
if done {
debug!("Done work for now");
break;
}

debug!(
"Incoming used {}, outgoing used {}, plaintext_in used {}, plaintext_out used {}",
self.incoming_used,
self.outgoing_used,
self.plaintext_in_used,
self.plaintext_out_used
);

let rustls::unbuffered::UnbufferedStatus { mut discard, state } = self
.conn
.process_tls_records(&mut self.incoming_tls[..self.incoming_used]);

debug!("State {:?}", state);

match state.map_err(ConnectionError::Rustls)? {
ConnectionState::ReadTraffic(mut state) => {
while let Some(res) = state.next_record() {
let AppDataRecord {
discard: new_discard,
payload,
} = res.map_err(ConnectionError::Rustls)?;
discard += new_discard;

self.plaintext_in[self.plaintext_in_used..][..payload.len()]
.copy_from_slice(payload);
self.plaintext_in_used += payload.len();

done = true;
}
}

ConnectionState::EncodeTlsData(mut state) => {
let written = state
.encode(&mut self.outgoing_tls[self.outgoing_used..])
.map_err(ConnectionError::RustlsEncodeError)?;
self.outgoing_used += written;
}

ConnectionState::TransmitTlsData(mut state) => {
if let Some(_may_encrypt_early_data) = state.may_encrypt_early_data() {
panic!("Early data unsupported");
}

if let Some(_may_encrypt) = state.may_encrypt_app_data() {
debug!("time to sent app data")
}

debug!("Send tls");
self.socket
.write_all(&self.outgoing_tls[..self.outgoing_used])?;
self.socket.flush()?;
self.outgoing_used = 0;
state.done();
}

ConnectionState::BlockedHandshake { .. } => {
debug!("Receive tls");

let read = self
.socket
.read(&mut self.incoming_tls[self.incoming_used..])?;
debug!("Received {read}B of data");
self.incoming_used += read;
}

ConnectionState::WriteTraffic(mut may_encrypt) => {
if self.plaintext_out_used != 0 {
let written = may_encrypt
.encrypt(
&self.plaintext_out[..self.plaintext_out_used],
&mut self.outgoing_tls[self.outgoing_used..],
)
.expect("encrypted request does not fit in `outgoing_tls`");
self.outgoing_used += written;
self.plaintext_out_used = 0;

debug!("Send tls");
self.socket
.write_all(&self.outgoing_tls[..self.outgoing_used])?;
self.socket.flush()?;
self.outgoing_used = 0;
}
done = true;
}

ConnectionState::Closed => {
// connection has been cleanly closed - these might be still plaintext data to
// be consumed
done = true;
}

// other states are not expected here
_ => unreachable!(),
}

if discard != 0 {
assert!(discard <= self.incoming_used);

self.incoming_tls
.copy_within(discard..self.incoming_used, 0);
self.incoming_used -= discard;

debug!("Discarded {discard}B from `incoming_tls`");
}
}

Ok(())
}
}

impl<S> embedded_io::ErrorType for ClientConnection<'_, S>
where
S: embedded_io::Read + embedded_io::Write,
{
type Error = ConnectionError<S::Error>;
}

impl<S> embedded_io::Read for ClientConnection<'_, S>
where
S: embedded_io::Read + embedded_io::Write,
{
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
let tls_read_res = self
.socket
.read(&mut self.incoming_tls[self.incoming_used..]);

let tls_read = if let Err(err) = tls_read_res {
if self.plaintext_in_used == 0 {
Err(err)
} else {
Ok(0)
}
} else {
tls_read_res
}?;
self.incoming_used += tls_read;

self.work()?;

let l = usize::min(buf.len(), self.plaintext_in_used);
buf[0..l].copy_from_slice(&self.plaintext_in[0..l]);

self.plaintext_in.copy_within(l..self.plaintext_in_used, 0);
self.plaintext_in_used -= l;

Ok(l)
}
}

impl<S> embedded_io::Write for ClientConnection<'_, S>
where
S: embedded_io::Read + embedded_io::Write,
{
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.plaintext_out[self.plaintext_out_used..][..buf.len()].copy_from_slice(buf);
self.plaintext_out_used += buf.len();
self.work()?;
Ok(buf.len())
}

fn flush(&mut self) -> Result<(), Self::Error> {
self.socket.flush()?;
self.work()?;
Ok(())
}
}
36 changes: 36 additions & 0 deletions esp-rustls-provider/src/adapter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Useful wrappers

pub mod client;
pub mod server;

/// Errors returned by the adapters
#[derive(Debug)]
pub enum ConnectionError<E: embedded_io::Error> {
/// Error from embedded-io
Io(E),
/// Error from Rustls
Rustls(rustls::Error),
/// Error from Rustls' `encode`function
RustlsEncodeError(rustls::unbuffered::EncodeError),
}

impl<E> embedded_io::Error for ConnectionError<E>
where
E: embedded_io::Error,
{
fn kind(&self) -> embedded_io::ErrorKind {
match self {
ConnectionError::Io(err) => err.kind(),
_ => embedded_io::ErrorKind::Other,
}
}
}

impl<E> From<E> for ConnectionError<E>
where
E: embedded_io::Error,
{
fn from(value: E) -> Self {
Self::Io(value)
}
}
Loading
Loading