From da2fc3e27a75c11f5e27b646542f2ca3e851b8e0 Mon Sep 17 00:00:00 2001 From: Anthony Grondin <104731965+AnthonyGrondin@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:28:53 -0500 Subject: [PATCH] feat(esp-mbedtls): Add initial support for using esp-mbedtls in the client instead of embedded_tls `esp-mbedtls` requires a specific arch to be passed. Enable the feature using the following currently supported arch: - esp32 - esp32c3 - esp32s3 - esp32s2 --- Cargo.toml | 1 + README.md | 62 ++++++++++++++++++++++++++++++++++++++++++--- src/client.rs | 70 +++++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 14 +++++++++++ 4 files changed, 132 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e903ec..c127c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ defmt = { version = "0.3", optional = true } embedded-tls = { version = "0.17", default-features = false, optional = true } rand_chacha = { version = "0.3", default-features = false } nourl = "0.1.1" +esp-mbedtls = { git = "https://github.com/esp-rs/esp-mbedtls.git", features = ["async"], optional = true } [dev-dependencies] hyper = { version = "0.14.23", features = ["full"] } diff --git a/README.md b/README.md index 78ddaf7..bf1a743 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ traits from the `embedded-io` crate. No alloc or std lib required! It offers two sets of APIs: * A low-level `request` API which allows you to construct HTTP requests and write them to a `embedded-io` transport. -* A higher level `client` API which uses the `embedded-nal-async` (+ optional `embedded-tls`) crates to establish TCP + TLS connections. +* A higher level `client` API which uses the `embedded-nal-async` (+ optional `embedded-tls` / `esp-mbedtls`) crates to establish TCP + TLS connections. ## example @@ -30,14 +30,68 @@ let response = client .unwrap(); ``` -The client is still lacking many features, but can perform basic HTTP GET/PUT/POST/DELETE requests with payloads. However, not all content types and status codes are implemented, and are added on a need basis. For TLS, it uses `embedded-tls` as the transport. +The client is still lacking many features, but can perform basic HTTP GET/PUT/POST/DELETE requests with payloads. However, not all content types and status codes are implemented, and are added on a need basis. For TLS, it uses either `embedded-tls` or `esp-mbedtls` as the transport. NOTE: TLS verification is not supported in no_std environments for `embedded-tls`. If you are missing a feature or would like an improvement, please raise an issue or a PR. -## TLS 1.3 and Supported Cipher Suites -`reqwless` uses `embedded-tls` to establish secure TLS connections for `https://..` urls. +## TLS 1.2*, 1.3 and Supported Cipher Suites +`reqwless` uses `embedded-tls` or `esp-mbedtls` to establish secure TLS connections for `https://..` urls. + +*TLS 1.2 is only supported with `esp-mbedtls` + +:warning: Note that both features cannot be used together and will cause a compilation error. + +### esp-mbedtls +**Can only be used on esp32 boards** +`esp-mbedtls` supports TLS 1.2 and 1.3. It uses espressif's Rust wrapper over mbedtls, alongside optimizations such as hardware acceleration. + +To use, you need to enable the transitive dependency of `esp-mbedtls` for your SoC. +Currently, the supported SoCs are: + + - `esp32` + - `esp32c3` + - `esp32s2` + - `esp32s3` + +Cargo.toml: + +```toml +reqwless = { version = "0.12.0", default-features = false, features = ["esp-mbedtls", "log"] } +esp-mbedtls = { git = "https://github.com/esp-rs/esp-mbedtls.git", features = ["esp32s3"] } +``` + + +#### Example +```rust,ignore +/// ... [initialization code. See esp-wifi] +let state = TcpClientState::<1, 4096, 4096>::new(); +let mut tcp_client = TcpClient::new(stack, &state); +let dns_socket = DnsSocket::new(&stack); +let mut rsa = Rsa::new(peripherals.RSA); +let config = TlsConfig::new( + reqwless::TlsVersion::Tls1_3, + reqwless::Certificates { + ca_chain: reqwless::X509::pem(CERT.as_bytes()).ok(), + ..Default::default() + }, + Some(&mut rsa), // Will use hardware acceleration +); +let mut client = HttpClient::new_with_tls(&tcp_client, &dns_socket, config); + +let mut request = client + .request(reqwless::request::Method::GET, "https://www.google.com") + .await + .unwrap() + .content_type(reqwless::headers::ContentType::TextPlain) + .headers(&[("Host", "google.com")]) + .send(&mut buffer) + .await + .unwrap(); +``` + +### embedded-tls `embedded-tls` only supports TLS 1.3, so to establish a connection the server must have this ssl protocol enabled. An addition to the tls version requirement, there is also a negotiation of supported algorithms during the establishing phase of the secure communication between the client and server. diff --git a/src/client.rs b/src/client.rs index b479d15..86db2ce 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,10 +20,23 @@ where { client: &'a T, dns: &'a D, - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] tls: Option>, } +/// Type for TLS configuration of HTTP client. +#[cfg(feature = "esp-mbedtls")] +pub struct TlsConfig<'a> { + /// Minimum TLS version for the connection + version: crate::TlsVersion, + + /// Client certificates. See [esp_mbedtls::Certificates] + certificates: crate::Certificates<'a>, + + /// Will use hardware acceleration on the ESP32 if it contains the RSA peripheral. + rsa: Option<&'a mut esp_mbedtls::Rsa<'a>>, +} + /// Type for TLS configuration of HTTP client. #[cfg(feature = "embedded-tls")] pub struct TlsConfig<'a> { @@ -54,6 +67,21 @@ impl<'a> TlsConfig<'a> { } } +#[cfg(feature = "esp-mbedtls")] +impl<'a> TlsConfig<'a> { + pub fn new( + version: crate::TlsVersion, + certificates: crate::Certificates<'a>, + rsa: Option<&'a mut esp_mbedtls::Rsa<'a>>, + ) -> Self { + Self { + version, + certificates, + rsa, + } + } +} + impl<'a, T, D> HttpClient<'a, T, D> where T: TcpConnect + 'a, @@ -64,13 +92,13 @@ where Self { client, dns, - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] tls: None, } } /// Create a new HTTP client for a given connection handle and a target host. - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] pub fn new_with_tls(client: &'a T, dns: &'a D, tls: TlsConfig<'a>) -> Self { Self { client, @@ -99,6 +127,24 @@ where .map_err(|e| e.kind())?; if url.scheme() == UrlScheme::HTTPS { + #[cfg(feature = "esp-mbedtls")] + if let Some(tls) = self.tls.as_mut() { + let session = esp_mbedtls::asynch::Session::new( + conn, + host, + esp_mbedtls::Mode::Client, + tls.version, + tls.certificates, + // Create a inner Some(&mut Rsa) because Rsa doesn't implement Copy + tls.rsa.as_mut().map(|inner| inner as &mut esp_mbedtls::Rsa), + )? + .connect() + .await?; + Ok(HttpConnection::Tls(session)) + } else { + Ok(HttpConnection::Plain(conn)) + } + #[cfg(feature = "embedded-tls")] if let Some(tls) = self.tls.as_mut() { use embedded_tls::{TlsConfig, TlsContext}; @@ -118,7 +164,7 @@ where } else { Ok(HttpConnection::Plain(conn)) } - #[cfg(not(feature = "embedded-tls"))] + #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))] Err(Error::InvalidUrl(nourl::Error::UnsupportedScheme)) } else { #[cfg(feature = "embedded-tls")] @@ -172,9 +218,11 @@ where { Plain(C), PlainBuffered(BufferedWrite<'conn, C>), + #[cfg(feature = "esp-mbedtls")] + Tls(esp_mbedtls::asynch::AsyncConnectedSession), #[cfg(feature = "embedded-tls")] Tls(embedded_tls::TlsConnection<'conn, C, embedded_tls::Aes128GcmSha256>), - #[cfg(not(feature = "embedded-tls"))] + #[cfg(all(not(feature = "embedded-tls"), not(feature = "esp-mbedtls")))] Tls((&'conn mut (), core::convert::Infallible)), // Variant is impossible to create, but we need it to avoid "unused lifetime" warning } @@ -255,9 +303,9 @@ where match self { Self::Plain(conn) => conn.read(buf).await.map_err(|e| e.kind()), Self::PlainBuffered(conn) => conn.read(buf).await.map_err(|e| e.kind()), - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] Self::Tls(conn) => conn.read(buf).await.map_err(|e| e.kind()), - #[cfg(not(feature = "embedded-tls"))] + #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] _ => unreachable!(), } } @@ -271,9 +319,9 @@ where match self { Self::Plain(conn) => conn.write(buf).await.map_err(|e| e.kind()), Self::PlainBuffered(conn) => conn.write(buf).await.map_err(|e| e.kind()), - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] Self::Tls(conn) => conn.write(buf).await.map_err(|e| e.kind()), - #[cfg(not(feature = "embedded-tls"))] + #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] _ => unreachable!(), } } @@ -282,9 +330,9 @@ where match self { Self::Plain(conn) => conn.flush().await.map_err(|e| e.kind()), Self::PlainBuffered(conn) => conn.flush().await.map_err(|e| e.kind()), - #[cfg(feature = "embedded-tls")] + #[cfg(any(feature = "embedded-tls", feature = "esp-mbedtls"))] Self::Tls(conn) => conn.flush().await.map_err(|e| e.kind()), - #[cfg(not(feature = "embedded-tls"))] + #[cfg(not(any(feature = "embedded-tls", feature = "esp-mbedtls")))] _ => unreachable!(), } } diff --git a/src/lib.rs b/src/lib.rs index 74bdba9..e1f7740 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,9 @@ pub enum Error { /// Tls Error #[cfg(feature = "embedded-tls")] Tls(embedded_tls::TlsError), + /// Tls Error + #[cfg(feature = "esp-mbedtls")] + Tls(esp_mbedtls::TlsError), /// The provided buffer is too small BufferTooSmall, /// The request is already sent @@ -70,6 +73,17 @@ impl From for Error { } } +/// Re-export those members since they're used for [client::TlsConfig]. +#[cfg(feature = "esp-mbedtls")] +pub use esp_mbedtls::{Certificates, Rsa, TlsVersion, X509}; + +#[cfg(feature = "esp-mbedtls")] +impl From for Error { + fn from(e: esp_mbedtls::TlsError) -> Error { + Error::Tls(e) + } +} + impl From for Error { fn from(_: ParseIntError) -> Error { Error::Codec