diff --git a/.gitignore b/.gitignore index 4fffb2f..4c790d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 9f7df9f..89bf8e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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 = [ @@ -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", diff --git a/src/client.rs b/src/client.rs index 71bc84f..52d66af 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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. @@ -48,6 +56,34 @@ pub struct TlsConfig<'a> { verify: TlsVerify<'a>, } +#[cfg(feature = "embedded-tls")] +struct Provider { + rng: rand_chacha::ChaCha8Rng, + verifier: CertVerifier, +} + +#[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, TlsError> { + Ok(&mut self.verifier) + } + + fn signer(&mut self, key_der: &[u8]) -> Result<(impl SignerMut, 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> { @@ -55,6 +91,15 @@ pub enum TlsVerify<'a> { 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")] @@ -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::(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)) @@ -682,7 +758,7 @@ mod tests { } async fn flush(&mut self) -> Result<(), Self::Error> { - Ok(()) + self.0.flush().await } } diff --git a/src/fmt.rs b/src/fmt.rs index 2fc6f9d..f8db3d0 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -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."); diff --git a/src/lib.rs b/src/lib.rs index cd6d88c..6abaefd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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))] @@ -127,7 +135,7 @@ where async fn try_fill_buf(&mut self) -> Option> { // 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())); } diff --git a/tests/certs/ca-cert.der b/tests/certs/ca-cert.der new file mode 100644 index 0000000..39dc35c Binary files /dev/null and b/tests/certs/ca-cert.der differ diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem deleted file mode 100644 index 4e75082..0000000 --- a/tests/certs/cert.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICKDCCAc6gAwIBAgIUaptPaaO7FrO+ER4qLqOns4SiCoswCgYIKoZIzj0EAwIw -QjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwT -RGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yMjEwMTAxMzM5MjlaGA8yMDUyMTEyMTEz -MzkyOVowcjELMAkGA1UEBhMCTk8xDjAMBgNVBAgMBUhhbWFyMQ4wDAYDVQQHDAVI -YW1hcjEYMBYGA1UECgwPR2xvYmFsIFNlY3VyaXR5MRUwEwYDVQQLDAxIb2xzZXRi -YWtrZW4xEjAQBgNVBAMMCTEyNy4wLjAuMTBZMBMGByqGSM49AgEGCCqGSM49AwEH -A0IABMrludMdhxnYWA+hMGVBZBkctvDQtJNr5/f+nu+5R4hSN55aRygzLQIOe3cj -rmnyS5D72m+Y31jKC9P8FPy3/8yjcDBuMB8GA1UdIwQYMBaAFLhJjHTOs/fIADvD -Bd2I+hXp89lOMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxv -Y2FsaG9zdDAdBgNVHQ4EFgQUhVW31o5frrZoYqV7xZqEnNiYKe4wCgYIKoZIzj0E -AwIDSAAwRQIgCkr4VgZ9TWvxLzUuTnzcjZ14FKESp8e5lkgbMwAc1hoCIQCXt+kg -35L2/0F3h+kDKKT3drkR5huYHnx++ds9RKF2tg== ------END CERTIFICATE----- diff --git a/tests/certs/client-cert.pem b/tests/certs/client-cert.pem new file mode 100644 index 0000000..cfd1176 --- /dev/null +++ b/tests/certs/client-cert.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBzTCCAXSgAwIBAgIUGQYrxI6lMa1yflVNpTO7VPPEwSgwCgYIKoZIzj0EAwIw +RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu +dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMTUxMDA0MTVaFw0yNjEyMTAx +MDA0MTVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD +VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAATBRnKMD+6BTcFuurE4Qt4pMgjUaWLOP/kTdGzyaZkVlPfp0fIKTRKv +EgHJlmTjsZfHkIm7nVD078BrfRoP6DqIo0IwQDAdBgNVHQ4EFgQUghgnu06bRUBN +bZHPn38zSTpb70UwHwYDVR0jBBgwFoAUws5fFFtipdISXEgJlc9P0ysgnVwwCgYI +KoZIzj0EAwIDRwAwRAIgOu6eFOYVbuWpIyDs2WrLXqHybAYlv4y4qqD6LZtITawC +IHNgKyB1PNV0CN7VOexBVJQv4edB8etbVxAF+WqztDT+ +-----END CERTIFICATE----- diff --git a/tests/certs/client-key.pem b/tests/certs/client-key.pem new file mode 100644 index 0000000..a956640 --- /dev/null +++ b/tests/certs/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIFllWPnIPExTk23tY4nSbss9UJ3EgDG91qZqajC/FBrkoAoGCCqGSM49 +AwEHoUQDQgAEwUZyjA/ugU3BbrqxOELeKTII1Glizj/5E3Rs8mmZFZT36dHyCk0S +rxIByZZk47GXx5CJu51Q9O/Aa30aD+g6iA== +-----END EC PRIVATE KEY----- diff --git a/tests/certs/key.pem b/tests/certs/key.pem deleted file mode 100644 index 65cf620..0000000 --- a/tests/certs/key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+N1TvmZPYJn+Zr/H -MnAA+Tj9E3d80dfBkMi0771MO5ChRANCAATK5bnTHYcZ2FgPoTBlQWQZHLbw0LST -a+f3/p7vuUeIUjeeWkcoMy0CDnt3I65p8kuQ+9pvmN9YygvT/BT8t//M ------END PRIVATE KEY----- diff --git a/tests/certs/server-chain-cert.pem b/tests/certs/server-chain-cert.pem new file mode 100644 index 0000000..53cacd7 --- /dev/null +++ b/tests/certs/server-chain-cert.pem @@ -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----- diff --git a/tests/certs/server-key.pem b/tests/certs/server-key.pem new file mode 100644 index 0000000..44b8ea2 --- /dev/null +++ b/tests/certs/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMYMdQBdr2qBRqPn222Jf4KvN8j7ajEVCREdwwmV9EvuoAoGCCqGSM49 +AwEHoUQDQgAEHeAmKW50/TvYerJshW3Mo4SazBYUR0ZRAa5FuzDYsoAOo4mgdGGu +UGu//9ys/nbr+TkdsFHyfkwp8dfFJul6rg== +-----END EC PRIVATE KEY----- diff --git a/tests/client.rs b/tests/client.rs index e8f6c74..42db6e5 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -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}; @@ -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; @@ -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() @@ -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(); @@ -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, _ => {} } diff --git a/tests/connection.rs b/tests/connection.rs index 17f9e69..6e5bc6d 100644 --- a/tests/connection.rs +++ b/tests/connection.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use embedded_io_adapters::tokio_1::FromTokio; use embedded_io_async::{ErrorType, Read, Write}; use embedded_nal_async::AddrType; @@ -5,6 +7,14 @@ use reqwless::TryBufRead; use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs}; use tokio::net::TcpStream; +impl core::fmt::Display for TestError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl core::error::Error for TestError {} + #[derive(Debug)] pub struct TestError; @@ -44,10 +54,10 @@ impl embedded_nal_async::Dns for StdDns { for address in (host, 0).to_socket_addrs()? { match address { SocketAddr::V4(a) if addr_type == AddrType::IPv4 || addr_type == AddrType::Either => { - return Ok(IpAddr::V4(a.ip().octets().into())) + return Ok(IpAddr::V4(a.ip().octets().into())); } SocketAddr::V6(a) if addr_type == AddrType::IPv6 || addr_type == AddrType::Either => { - return Ok(IpAddr::V6(a.ip().octets().into())) + return Ok(IpAddr::V6(a.ip().octets().into())); } _ => {} }