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 examples #1899

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
45 changes: 45 additions & 0 deletions esp-rustls-provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[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.12", default-features = false, features = [
# "logging",
# "tls12",
#] }

# there was a bug in Rustls - need a specific git commit for now
rustls = { git = "https://github.com/rustls/rustls", rev = "1177a465680cfac8c2a4b7217758d488d5d840c4", default-features = false, features = [
"logging",
"tls12",
] }
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.19.0", default-features = false }
embedded-io = { version = "0.6.1", default-features = false }
log = "0.4.20"
19 changes: 19 additions & 0 deletions esp-rustls-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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

Currently this is basically a copy of [Rustls' provider example](https://github.com/rustls/rustls/tree/main/provider-example) (minus std support, plus some helpers).

When [rustls-rustcrypto](https://crates.io/crates/rustls-rustcrypto) becomes fully no-std, this should depend on that instead.

There are still some more things to do
- Add async wrappers
- Implement optional traits from embedded-io

In future we probably want to add hardware acceleration
230 changes: 230 additions & 0 deletions esp-rustls-provider/src/adapter/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! 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, rustls::Error> {
let conn = rustls::client::UnbufferedClientConnection::new(config, server)?;

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,
})
}

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 {
log::debug!("Done work for now");
break;
}

log::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]);

log::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() {
log::debug!("time to sent app data")
}

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

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

let read = self
.socket
.read(&mut self.incoming_tls[self.incoming_used..])?;
log::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;

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

ConnectionState::Closed => {
// handle this state
}

// 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;

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

Ok(())
}
}

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

impl<'s, S> embedded_io::Read for ClientConnection<'s, 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, S> embedded_io::Write for ClientConnection<'s, 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