Unverified Commit 68154226 authored by dependabot[bot]'s avatar dependabot[bot] Committed by GitHub
Browse files

clients: add support for `webpki and native certificate stores` (#533)

* Update tokio-rustls requirement from 0.22 to 0.23

Updates the requirements on [tokio-rustls](https://github.com/tokio-rs/tls) to permit the latest version.
- [Release notes](https://github.com/tokio-rs/tls/releases)
- [Commits](https://github.com/tokio-rs/tls/commits

)

---
updated-dependencies:
- dependency-name: tokio-rustls
  dependency-type: direct:production
...

Signed-off-by: default avatardependabot[bot] <support@github.com>

* push fixes but requires rustls-native-certs v0.6

* update native certs to 0.6.0

* fix clippy warnings

* remove webpki roots support

* Revert "remove webpki roots support"

This reverts commit 1144d567b343049ab7c967d320fc2fe162ba0f7c.

* support both native cert store and webpki

* sort deps in Cargo.toml

* Update ws-client/src/transport.rs

Co-authored-by: David's avatarDavid <dvdplm@gmail.com>

Co-authored-by: default avatardependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Niklas Adolfsson's avatarNiklas Adolfsson <niklasadolfsson1@gmail.com>
Co-authored-by: David's avatarDavid <dvdplm@gmail.com>
parent 926f8914
......@@ -11,17 +11,17 @@ documentation = "https://docs.rs/jsonrpsee-http-client"
[dependencies]
async-trait = "0.1"
hyper-rustls = "0.22"
fnv = "1"
hyper = { version = "0.14.10", features = ["client", "http1", "http2", "tcp"] }
hyper-rustls = { version = "0.22", features = ["webpki-tokio"] }
jsonrpsee-types = { path = "../types", version = "0.4.1" }
jsonrpsee-utils = { path = "../utils", version = "0.4.1", features = ["http-helpers"] }
tracing = "0.1"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["time"] }
thiserror = "1.0"
tokio = { version = "1", features = ["time"] }
tracing = "0.1"
url = "2.2"
fnv = "1"
[dev-dependencies]
jsonrpsee-test-utils = { path = "../test-utils" }
......
......@@ -28,7 +28,7 @@ use crate::transport::HttpTransportClient;
use crate::types::{
traits::Client,
v2::{Id, NotificationSer, ParamsSer, RequestSer, Response, RpcError},
Error, RequestIdGuard, TEN_MB_SIZE_BYTES,
CertificateStore, Error, RequestIdGuard, TEN_MB_SIZE_BYTES,
};
use async_trait::async_trait;
use fnv::FnvHashMap;
......@@ -41,6 +41,7 @@ pub struct HttpClientBuilder {
max_request_body_size: u32,
request_timeout: Duration,
max_concurrent_requests: usize,
certificate_store: CertificateStore,
}
impl HttpClientBuilder {
......@@ -62,10 +63,16 @@ impl HttpClientBuilder {
self
}
/// Set which certificate store to use.
pub fn certificate_store(mut self, certificate_store: CertificateStore) -> Self {
self.certificate_store = certificate_store;
self
}
/// Build the HTTP client with target to connect to.
pub fn build(self, target: impl AsRef<str>) -> Result<HttpClient, Error> {
let transport =
HttpTransportClient::new(target, self.max_request_body_size).map_err(|e| Error::Transport(e.into()))?;
let transport = HttpTransportClient::new(target, self.max_request_body_size, self.certificate_store)
.map_err(|e| Error::Transport(e.into()))?;
Ok(HttpClient {
transport,
id_guard: RequestIdGuard::new(self.max_concurrent_requests),
......@@ -80,6 +87,7 @@ impl Default for HttpClientBuilder {
max_request_body_size: TEN_MB_SIZE_BYTES,
request_timeout: Duration::from_secs(60),
max_concurrent_requests: 256,
certificate_store: CertificateStore::Native,
}
}
}
......
......@@ -9,6 +9,7 @@
use crate::types::error::GenericTransportError;
use hyper::client::{Client, HttpConnector};
use hyper_rustls::HttpsConnector;
use jsonrpsee_types::CertificateStore;
use jsonrpsee_utils::http_helpers;
use thiserror::Error;
......@@ -27,10 +28,18 @@ pub(crate) struct HttpTransportClient {
impl HttpTransportClient {
/// Initializes a new HTTP client.
pub(crate) fn new(target: impl AsRef<str>, max_request_body_size: u32) -> Result<Self, Error> {
pub(crate) fn new(
target: impl AsRef<str>,
max_request_body_size: u32,
cert_store: CertificateStore,
) -> Result<Self, Error> {
let target = url::Url::parse(target.as_ref()).map_err(|e| Error::Url(format!("Invalid URL: {}", e)))?;
if target.scheme() == "http" || target.scheme() == "https" {
let connector = HttpsConnector::with_native_roots();
let connector = match cert_store {
CertificateStore::Native => HttpsConnector::with_native_roots(),
CertificateStore::WebPki => HttpsConnector::with_webpki_roots(),
_ => return Err(Error::InvalidCertficateStore),
};
let client = Client::builder().build::<_, hyper::Body>(connector);
Ok(HttpTransportClient { target, client, max_request_body_size })
} else {
......@@ -99,6 +108,10 @@ pub(crate) enum Error {
/// Malformed request.
#[error("Malformed request")]
Malformed,
/// Invalid certificate store.
#[error("Invalid certificate store")]
InvalidCertficateStore,
}
impl<T> From<GenericTransportError<T>> for Error
......@@ -116,18 +129,18 @@ where
#[cfg(test)]
mod tests {
use super::{Error, HttpTransportClient};
use super::{CertificateStore, Error, HttpTransportClient};
#[test]
fn invalid_http_url_rejected() {
let err = HttpTransportClient::new("ws://localhost:9933", 80).unwrap_err();
let err = HttpTransportClient::new("ws://localhost:9933", 80, CertificateStore::Native).unwrap_err();
assert!(matches!(err, Error::Url(_)));
}
#[tokio::test]
async fn request_limit_works() {
let eighty_bytes_limit = 80;
let client = HttpTransportClient::new("http://localhost:9933", 80).unwrap();
let client = HttpTransportClient::new("http://localhost:9933", 80, CertificateStore::WebPki).unwrap();
assert_eq!(client.max_request_body_size, eighty_bytes_limit);
let body = "a".repeat(81);
......
......@@ -249,3 +249,13 @@ impl RequestIdGuard {
});
}
}
/// What certificate store to use
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum CertificateStore {
/// Use the native system certificate store
Native,
/// Use WebPKI's certificate store
WebPki,
}
......@@ -10,25 +10,25 @@ homepage = "https://github.com/paritytech/jsonrpsee"
documentation = "https://docs.rs/jsonrpsee-ws-client"
[dependencies]
tokio = { version = "1", features = ["net", "time", "rt-multi-thread", "macros"] }
tokio-rustls = "0.22"
tokio-util = { version = "0.6", features = ["compat"] }
async-trait = "0.1"
fnv = "1"
futures = { version = "0.3.14", default-features = false, features = ["std"] }
http = "0.2"
jsonrpsee-types = { path = "../types", version = "0.4.1" }
pin-project = "1"
rustls-native-certs = "0.5.0"
rustls-native-certs = "0.6.0"
serde = "1"
serde_json = "1"
soketto = "0.7"
thiserror = "1"
tokio = { version = "1", features = ["net", "time", "rt-multi-thread", "macros"] }
tokio-rustls = "0.23"
tokio-util = { version = "0.6", features = ["compat"] }
tracing = "0.1"
webpki-roots = "0.22.0"
[dev-dependencies]
env_logger = "0.9"
jsonrpsee-test-utils = { path = "../test-utils" }
jsonrpsee-utils = { path = "../utils" }
tokio = { version = "1", features = ["macros"] }
\ No newline at end of file
jsonrpsee-utils = { path = "../utils", features = ["client"] }
tokio = { version = "1", features = ["macros"] }
......@@ -28,8 +28,8 @@ use crate::transport::{Receiver as WsReceiver, Sender as WsSender, WsHandshakeEr
use crate::types::{
traits::{Client, SubscriptionClient},
v2::{Id, Notification, NotificationSer, ParamsSer, RequestSer, Response, RpcError, SubscriptionResponse},
BatchMessage, Error, FrontToBack, RegisterNotificationMessage, RequestIdGuard, RequestMessage, Subscription,
SubscriptionKind, SubscriptionMessage, TEN_MB_SIZE_BYTES,
BatchMessage, CertificateStore, Error, FrontToBack, RegisterNotificationMessage, RequestIdGuard, RequestMessage,
Subscription, SubscriptionKind, SubscriptionMessage, TEN_MB_SIZE_BYTES,
};
use crate::{
helpers::{
......@@ -37,7 +37,6 @@ use crate::{
process_notification, process_single_response, process_subscription_response, stop_subscription,
},
manager::RequestManager,
transport::CertificateStore,
};
use async_trait::async_trait;
use futures::{
......
......@@ -24,7 +24,7 @@
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use crate::stream::EitherStream;
use crate::{stream::EitherStream, types::CertificateStore};
use futures::io::{BufReader, BufWriter};
use http::Uri;
use soketto::connection;
......@@ -40,12 +40,7 @@ use std::{
};
use thiserror::Error;
use tokio::net::TcpStream;
use tokio_rustls::{
client::TlsStream,
rustls::ClientConfig,
webpki::{DNSNameRef, InvalidDNSNameError},
TlsConnector,
};
use tokio_rustls::{client::TlsStream, rustls, webpki::InvalidDnsNameError, TlsConnector};
type TlsOrPlain = EitherStream<TcpStream, TlsStream<TcpStream>>;
......@@ -88,16 +83,6 @@ pub enum Mode {
Tls,
}
/// What certificate store to use
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum CertificateStore {
/// Use the native system certificate store
Native,
/// Use webPki's certificate store
WebPki,
}
/// Error that can happen during the WebSocket handshake.
///
/// If multiple IP addresses are attempted, only the last error is returned, similar to how
......@@ -122,7 +107,7 @@ pub enum WsHandshakeError {
/// Invalid DNS name error for TLS
#[error("Invalid DNS name: {0}")]
InvalidDnsName(#[source] InvalidDNSNameError),
InvalidDnsName(#[source] InvalidDnsNameError),
/// Server rejected the handshake.
#[error("Connection rejected with status code: {status_code}")]
......@@ -186,12 +171,8 @@ impl<'a> WsTransportClientBuilder<'a> {
pub async fn build(self) -> Result<(Sender, Receiver), WsHandshakeError> {
let connector = match self.target.mode {
Mode::Tls => {
let mut client_config = ClientConfig::default();
if let CertificateStore::Native = self.certificate_store {
client_config.root_store = rustls_native_certs::load_native_certs()
.map_err(|(_, e)| WsHandshakeError::CertificateStore(e))?;
}
Some(Arc::new(client_config).into())
let tls_connector = build_tls_config(&self.certificate_store)?;
Some(tls_connector)
}
Mode::Plain => None,
};
......@@ -250,16 +231,14 @@ impl<'a> WsTransportClientBuilder<'a> {
// Absolute URI.
if uri.scheme().is_some() {
target = uri.try_into()?;
tls_connector = match target.mode {
Mode::Tls => {
let mut client_config = ClientConfig::default();
if let CertificateStore::Native = self.certificate_store {
client_config.root_store = rustls_native_certs::load_native_certs()
.map_err(|(_, e)| WsHandshakeError::CertificateStore(e))?;
}
Some(Arc::new(client_config).into())
match target.mode {
Mode::Tls if tls_connector.is_none() => {
tls_connector = Some(build_tls_config(&self.certificate_store)?);
}
Mode::Tls => (),
Mode::Plain => {
tls_connector = None;
}
Mode::Plain => None,
};
}
// Relative URI.
......@@ -320,8 +299,8 @@ async fn connect(
match tls_connector {
None => Ok(TlsOrPlain::Plain(socket)),
Some(connector) => {
let dns_name = DNSNameRef::try_from_ascii_str(host)?;
let tls_stream = connector.connect(dns_name, socket).await?;
let server_name: rustls::ServerName = host.try_into().map_err(|e| WsHandshakeError::Url(format!("Invalid host: {} {:?}", host, e).into()))?;
let tls_stream = connector.connect(server_name, socket).await?;
Ok(TlsOrPlain::Tls(tls_stream))
}
}
......@@ -336,8 +315,8 @@ impl From<io::Error> for WsHandshakeError {
}
}
impl From<InvalidDNSNameError> for WsHandshakeError {
fn from(err: InvalidDNSNameError) -> WsHandshakeError {
impl From<InvalidDnsNameError> for WsHandshakeError {
fn from(err: InvalidDnsNameError) -> WsHandshakeError {
WsHandshakeError::InvalidDnsName(err)
}
}
......@@ -390,6 +369,43 @@ impl TryFrom<Uri> for Target {
}
}
// NOTE: this is slow and should be used sparingly.
fn build_tls_config(cert_store: &CertificateStore) -> Result<TlsConnector, WsHandshakeError> {
let mut roots = tokio_rustls::rustls::RootCertStore::empty();
match cert_store {
CertificateStore::Native => {
let mut first_error = None;
let certs = rustls_native_certs::load_native_certs().map_err(WsHandshakeError::CertificateStore)?;
for cert in certs {
let cert = rustls::Certificate(cert.0);
if let Err(err) = roots.add(&cert) {
first_error = first_error.or_else(|| Some(io::Error::new(io::ErrorKind::InvalidData, err)));
}
}
if roots.is_empty() {
let err = first_error
.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No valid certificate found"));
return Err(WsHandshakeError::CertificateStore(err));
}
}
CertificateStore::WebPki => {
roots.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)
}));
}
_ => {
let err = io::Error::new(io::ErrorKind::NotFound, "Invalid certificate store");
return Err(WsHandshakeError::CertificateStore(err));
}
};
let config =
rustls::ClientConfig::builder().with_safe_defaults().with_root_certificates(roots).with_no_client_auth();
Ok(Arc::new(config).into())
}
#[cfg(test)]
mod tests {
use super::{Mode, Target, Uri, WsHandshakeError};
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment