Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/Cargo.lock
.DS_Store
23 changes: 15 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "reqwless"
version = "0.13.0"
edition = "2021"
version = "0.14.0"
edition = "2024"
resolver = "2"
rust-version = "1.77"
rust-version = "1.91"
description = "HTTP client for embedded devices"
documentation = "https://docs.rs/reqwless"
readme = "README.md"
Expand All @@ -14,18 +14,24 @@ keywords = ["embedded", "async", "http", "no_std"]
exclude = [".github"]

[dependencies]
buffered-io = { version = "0.6.0" }
buffered-io = { version = "0.6" }
embedded-io = { version = "0.7" }
embedded-io-async = { version = "0.7" }
embedded-nal-async = "0.9"
embedded-nal-async = "0.9.0"
httparse = { version = "1.8.0", default-features = false }
heapless = "0.9"
hex = { version = "0.4", default-features = false }
base64 = { version = "0.21.0", default-features = false }
rand_core = { version = "0.6", default-features = true }
rand_core = { version = "0.6.3", default-features = true }
log = { version = "0.4", optional = true }
defmt = { version = "0.3", optional = true }
embedded-tls = { version = "0.18", default-features = false, optional = true }
pkcs8 = "0.10.2"
p256 = { version = "0.13", default-features = false, features = [
"ecdh",
"ecdsa",
"sha256",
] }
embedded-tls = { version = "0.18", default-features = false, features = ["rustpki"], optional = true }
rand_chacha = { version = "0.3", default-features = false }
nourl = "0.1.2"
esp-mbedtls = { version = "0.1", git = "https://github.com/esp-rs/esp-mbedtls.git", features = [
Expand All @@ -40,13 +46,14 @@ futures-util = { version = "0.3" }
embedded-io-async = { version = "0.7", features = ["std"] }
embedded-io-adapters = { version = "0.7", features = ["std", "tokio-1"] }
rustls-pemfile = "1.0"
env_logger = "0.10"
env_logger = "0.11"
log = "0.4"
rand = "0.8"

[features]
default = ["embedded-tls"]
alloc = ["embedded-tls?/alloc"]
rsa = ["embedded-tls?/rsa"]
defmt = [
"dep:defmt",
"embedded-io/defmt",
Expand Down
96 changes: 86 additions & 10 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
use crate::Error;
/// Client using embedded-nal-async traits to establish connections and perform HTTP requests.
///
use crate::body_writer::{BufferingChunkedBodyWriter, ChunkedBodyWriter, FixedBodyWriter};
use crate::headers::ContentType;
use crate::request::*;
use crate::response::*;
use crate::Error;
use buffered_io::asynch::BufferedWrite;
use core::net::SocketAddr;
use embedded_io::Error as _;
use embedded_io::ErrorType;
use embedded_io_async::{Read, Write};
use embedded_nal_async::{Dns, TcpConnect};
#[cfg(feature = "embedded-tls")]
use embedded_tls::{
Aes128GcmSha256, CryptoProvider, NoClock, SignatureScheme, TlsError, TlsVerifier, pki::CertVerifier,
};
use nourl::{Url, UrlScheme};
#[cfg(feature = "embedded-tls")]
use p256::ecdsa::{DerSignature, signature::SignerMut};
#[cfg(feature = "embedded-tls")]
use rand_core::CryptoRngCore;

/// An async HTTP client that can establish a TCP connection and perform
/// HTTP requests.
Expand Down Expand Up @@ -48,13 +56,50 @@ pub struct TlsConfig<'a> {
verify: TlsVerify<'a>,
}

#[cfg(feature = "embedded-tls")]
struct Provider {
rng: rand_chacha::ChaCha8Rng,
verifier: CertVerifier<Aes128GcmSha256, NoClock, 4096>,
}

#[cfg(feature = "embedded-tls")]
impl CryptoProvider for Provider {
type CipherSuite = Aes128GcmSha256;
type Signature = DerSignature;

fn rng(&mut self) -> impl CryptoRngCore {
&mut self.rng
}

fn verifier(&mut self) -> Result<&mut impl TlsVerifier<Self::CipherSuite>, TlsError> {
Ok(&mut self.verifier)
}

fn signer(&mut self, key_der: &[u8]) -> Result<(impl SignerMut<Self::Signature>, SignatureScheme), TlsError> {
use p256::{SecretKey, ecdsa::SigningKey};

let secret_key = SecretKey::from_sec1_der(key_der).map_err(|_| TlsError::InvalidPrivateKey)?;

Ok((SigningKey::from(&secret_key), SignatureScheme::EcdsaSecp256r1Sha256))
}
}

/// Supported verification modes.
#[cfg(feature = "embedded-tls")]
pub enum TlsVerify<'a> {
/// No verification of the remote host
None,
/// Use pre-shared keys for verifying
Psk { identity: &'a [u8], psk: &'a [u8] },
/// Use certificates for verifying
/// ca: CA cert in DER format
/// cert: Optional client cert in DER format (needed only for client verification)
/// key: Optional client privkey in DER format (needed only for client verification)
Certificate {
ca: &'a [u8],
cert: Option<&'a [u8]>,
key: Option<&'a [u8]>,
},
}

#[cfg(feature = "embedded-tls")]
Expand Down Expand Up @@ -153,17 +198,48 @@ where
if let Some(tls) = self.tls.as_mut() {
use embedded_tls::{TlsConfig, TlsContext, UnsecureProvider};
use rand_chacha::ChaCha8Rng;
use rand_core::{RngCore, SeedableRng};
let mut rng = ChaCha8Rng::seed_from_u64(tls.seed);
tls.seed = rng.next_u64();
use rand_core::SeedableRng;
let rng = ChaCha8Rng::seed_from_u64(tls.seed);
let mut config = TlsConfig::new().with_server_name(url.host());
if let TlsVerify::Psk { identity, psk } = tls.verify {
config = config.with_psk(psk, &[identity]);
}

let mut conn: embedded_tls::TlsConnection<'conn, T::Connection<'conn>, embedded_tls::Aes128GcmSha256> =
embedded_tls::TlsConnection::new(conn, tls.read_buffer, tls.write_buffer);
conn.open(TlsContext::new(&config, UnsecureProvider::new::<embedded_tls::Aes128GcmSha256>(rng)))
.await?;

match tls.verify {
TlsVerify::None => {
use embedded_tls::UnsecureProvider;
conn.open(TlsContext::new(&config, UnsecureProvider::new(rng))).await?;
}
TlsVerify::Psk { identity, psk } => {
use embedded_tls::UnsecureProvider;
config = config.with_psk(psk, &[identity]);
conn.open(TlsContext::new(&config, UnsecureProvider::new(rng))).await?;
}
TlsVerify::Certificate { ca, cert, key } => {
use embedded_tls::Certificate;

config = config.with_ca(Certificate::X509(ca));

if let Some(cert) = cert {
config = config.with_cert(Certificate::X509(cert));
}

if let Some(key) = key {
let k = pkcs8::PrivateKeyInfo::try_from(key).map_err(|_| TlsError::InvalidPrivateKey)?;
config = config.with_priv_key(k.private_key);
}

conn.open(TlsContext::new(
&config,
Provider {
rng: rng,
verifier: embedded_tls::pki::CertVerifier::new(),
},
))
.await?;
}
}

Ok(HttpConnection::Tls(conn))
} else {
Ok(HttpConnection::Plain(conn))
Expand Down Expand Up @@ -682,7 +758,7 @@ mod tests {
}

async fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
self.0.flush().await
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/fmt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![macro_use]
#![allow(unused)]
#![allow(unused_macros)]
#![allow(dead_code)]

#[cfg(all(feature = "defmt", feature = "log"))]
compile_error!("You may not enable both `defmt` and `log` features.");
Expand Down
10 changes: 9 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ mod reader;
pub mod request;
pub mod response;

impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self)
}
}

impl core::error::Error for Error {}

/// Errors that can be returned by this library.
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
Expand Down Expand Up @@ -127,7 +135,7 @@ where
async fn try_fill_buf(&mut self) -> Option<Result<&[u8], Self::Error>> {
// embedded-tls has its own internal buffer, let's prefer that if we can
#[cfg(feature = "embedded-tls")]
if let Self::Tls(ref mut tls) = self {
if let Self::Tls(ref mut tls) = *self {
use embedded_io_async::{BufRead, Error};
return Some(tls.fill_buf().await.map_err(|e| e.kind()));
}
Expand Down
Binary file added tests/certs/ca-cert.der
Binary file not shown.
14 changes: 0 additions & 14 deletions tests/certs/cert.pem

This file was deleted.

12 changes: 12 additions & 0 deletions tests/certs/client-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBzTCCAXSgAwIBAgIUGQYrxI6lMa1yflVNpTO7VPPEwSgwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMTUxMDA0MTVaFw0yNjEyMTAx
MDA0MTVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD
VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAATBRnKMD+6BTcFuurE4Qt4pMgjUaWLOP/kTdGzyaZkVlPfp0fIKTRKv
EgHJlmTjsZfHkIm7nVD078BrfRoP6DqIo0IwQDAdBgNVHQ4EFgQUghgnu06bRUBN
bZHPn38zSTpb70UwHwYDVR0jBBgwFoAUws5fFFtipdISXEgJlc9P0ysgnVwwCgYI
KoZIzj0EAwIDRwAwRAIgOu6eFOYVbuWpIyDs2WrLXqHybAYlv4y4qqD6LZtITawC
IHNgKyB1PNV0CN7VOexBVJQv4edB8etbVxAF+WqztDT+
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions tests/certs/client-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFllWPnIPExTk23tY4nSbss9UJ3EgDG91qZqajC/FBrkoAoGCCqGSM49
AwEHoUQDQgAEwUZyjA/ugU3BbrqxOELeKTII1Glizj/5E3Rs8mmZFZT36dHyCk0S
rxIByZZk47GXx5CJu51Q9O/Aa30aD+g6iA==
-----END EC PRIVATE KEY-----
5 changes: 0 additions & 5 deletions tests/certs/key.pem

This file was deleted.

23 changes: 23 additions & 0 deletions tests/certs/server-chain-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIBoDCCAUWgAwIBAgIUbZSPQ61vbraGQo0KKv00HChYiXIwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yNTEyMDQxNTIxMjRaGA8yMDUzMDQy
MTE1MjEyNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZI
zj0DAQcDQgAEHeAmKW50/TvYerJshW3Mo4SazBYUR0ZRAa5FuzDYsoAOo4mgdGGu
UGu//9ys/nbr+TkdsFHyfkwp8dfFJul6rqNCMEAwHQYDVR0OBBYEFKKVGY/EPl4k
9YUooTnAMeDMZrypMB8GA1UdIwQYMBaAFIkID0HrvDvBc05UTiImahVtXwS1MAoG
CCqGSM49BAMCA0kAMEYCIQCVc/NoapOXCXUZZpYqeqrVn7u7nQMN8yFHnBEpGfCq
ygIhAKhFjj2/nsyd8EKuZejK9ipZhCGPeKpvUmPJ8tVS0BRs
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIB0DCCAXagAwIBAgIUPScLNgV5OP8s7UzHUX1Oc4h7h0kwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yNTEyMDQxNTIxMjRaGA8yMDUzMDQy
MTE1MjEyNFowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAf
BgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABM6ezqRkVMnA+Ob41GHeCnLQP5t9flFY2iqRHqIJMFn3+6jyaEtL
jhDGUzVuNLWH7FK/Yeq0O9yqsKH15jpSUkyjQjBAMB0GA1UdDgQWBBSJCA9B67w7
wXNOVE4iJmoVbV8EtTAfBgNVHSMEGDAWgBTCzl8UW2Kl0hJcSAmVz0/TKyCdXDAK
BggqhkjOPQQDAgNIADBFAiAZc6gCaTfwIDBPrmu5KxDFpoKC17tJJ36qSQPXUo1v
WwIhAKo+4CV82HUlZTH2WmcoItKl+iF151XYa2Z5x/aR+Y4b
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions tests/certs/server-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMYMdQBdr2qBRqPn222Jf4KvN8j7ajEVCREdwwmV9EvuoAoGCCqGSM49
AwEHoUQDQgAEHeAmKW50/TvYerJshW3Mo4SazBYUR0ZRAa5FuzDYsoAOo4mgdGGu
UGu//9ys/nbr+TkdsFHyfkwp8dfFJul6rg==
-----END EC PRIVATE KEY-----
22 changes: 17 additions & 5 deletions tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use embedded_io_async::{BufRead, Write};
use hyper::server::conn::Http;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Server};
use rand::rngs::OsRng;
use rand::RngCore;
use rand::rngs::OsRng;
use reqwless::client::HttpClient;
use reqwless::headers::ContentType;
use reqwless::request::{Method, RequestBody, RequestBuilder};
Expand All @@ -12,8 +12,8 @@ use std::net::SocketAddr;
use std::sync::Once;
use tokio::net::TcpListener;
use tokio::sync::oneshot;
use tokio_rustls::rustls;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::rustls;

mod connection;

Expand Down Expand Up @@ -165,8 +165,8 @@ async fn test_resource_rustls() {
let addr: SocketAddr = ([127, 0, 0, 1], 0).into();

let test_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
let certs = load_certs(&test_dir.join("certs").join("cert.pem"));
let privkey = load_private_key(&test_dir.join("certs").join("key.pem"));
let certs = load_certs(&test_dir.join("certs").join("server-chain-cert.pem"));
let privkey = load_private_key(&test_dir.join("certs").join("server-key.pem"));

let versions = &[&rustls::version::TLS13];
let config = rustls::ServerConfig::builder()
Expand Down Expand Up @@ -199,13 +199,24 @@ async fn test_resource_rustls() {
}
});

let ca = include_bytes!("certs/ca-cert.der");

let mut tls_read_buf: [u8; 16384] = [0; 16384];
let mut tls_write_buf: [u8; 16384] = [0; 16384];
let url = format!("https://localhost:{}", addr.port());
let mut client = HttpClient::new_with_tls(
&TCP,
&LOOPBACK_DNS,
TlsConfig::new(OsRng.next_u64(), &mut tls_read_buf, &mut tls_write_buf, TlsVerify::None),
TlsConfig::new(
OsRng.next_u64(),
&mut tls_read_buf,
&mut tls_write_buf,
TlsVerify::Certificate {
ca: ca,
cert: None,
key: None,
},
),
);
let mut rx_buf = [0; 4096];
let mut resource = client.resource(&url).await.unwrap();
Expand Down Expand Up @@ -403,6 +414,7 @@ fn load_private_key(filename: &std::path::PathBuf) -> rustls::PrivateKey {
match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") {
Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::ECKey(key)) => return rustls::PrivateKey(key),
None => break,
_ => {}
}
Expand Down
Loading
Loading