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

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

Merged
merged 10 commits into from Oct 29, 2021
8 changes: 4 additions & 4 deletions http-client/Cargo.toml
Expand Up @@ -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" }
Expand Down
14 changes: 11 additions & 3 deletions http-client/src/client.rs
Expand Up @@ -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;
Expand All @@ -41,6 +41,7 @@ pub struct HttpClientBuilder {
max_request_body_size: u32,
request_timeout: Duration,
max_concurrent_requests: usize,
certificate_store: CertificateStore,
}

impl HttpClientBuilder {
Expand All @@ -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),
Expand All @@ -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,
}
}
}
Expand Down
23 changes: 18 additions & 5 deletions http-client/src/transport.rs
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions types/src/client.rs
Expand Up @@ -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,
}
14 changes: 7 additions & 7 deletions ws-client/Cargo.toml
Expand Up @@ -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"] }
jsonrpsee-utils = { path = "../utils", features = ["client"] }
tokio = { version = "1", features = ["macros"] }
5 changes: 2 additions & 3 deletions ws-client/src/client.rs
Expand Up @@ -28,16 +28,15 @@ 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::{
build_unsubscribe_message, call_with_timeout, process_batch_response, process_error_response,
process_notification, process_single_response, process_subscription_response, stop_subscription,
},
manager::RequestManager,
transport::CertificateStore,
};
use async_trait::async_trait;
use futures::{
Expand Down
90 changes: 53 additions & 37 deletions ws-client/src/transport.rs
Expand Up @@ -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;
Expand All @@ -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>>;

Expand Down Expand Up @@ -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
Expand All @@ -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}")]
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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};
Expand Down