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

client: feature gate tls cert store #994

Merged
merged 4 commits into from
Feb 2, 2023
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
11 changes: 8 additions & 3 deletions client/http-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ documentation = "https://docs.rs/jsonrpsee-http-client"
async-trait = "0.1"
rustc-hash = "1"
hyper = { version = "0.14.10", features = ["client", "http1", "http2", "tcp"] }
hyper-rustls = { version = "0.23", optional = true }
hyper-rustls = { version = "0.23", optional = true, default-features = false, features = ["http1", "tls12", "logging"] }
jsonrpsee-types = { path = "../../types", version = "0.16.2" }
jsonrpsee-core = { path = "../../core", version = "0.16.2", features = ["client", "http-helpers"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
Expand All @@ -29,8 +29,13 @@ jsonrpsee-test-utils = { path = "../../test-utils" }
tokio = { version = "1.16", features = ["net", "rt-multi-thread", "macros"] }

[features]
default = ["tls"]
tls = ["hyper-rustls/webpki-tokio"]
default = ["native-tls"]
native-tls = ["hyper-rustls/native-tokio", "__tls"]
webpki-tls = ["hyper-rustls/webpki-tokio", "__tls"]

# Internal feature to indicate whether TLS is enabled.
# Does nothing on its own.
__tls = ["hyper-rustls"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with this for clarity, but just noting that as it stands, hyper-rustlswill automatically be bought in when native-tls or webpki-tls is enabled without needing it :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah but it's useful to reference this in the code, I see!


[package.metadata.docs.rs]
all-features = true
Expand Down
28 changes: 25 additions & 3 deletions client/http-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,31 @@ impl<L> HttpClientBuilder<L> {
self
}

/// Set which certificate store to use.
pub fn certificate_store(mut self, certificate_store: CertificateStore) -> Self {
self.certificate_store = certificate_store;
/// Force to use the rustls native certificate store.
///
/// Since multiple certificate stores can be optionally enabled, this option will
/// force the `native certificate store` to be used.
///
/// # Optional
///
/// This requires the optional `native-tls` feature.
#[cfg(feature = "native-tls")]
pub fn use_native_rustls(mut self) -> Self {
self.certificate_store = CertificateStore::Native;
self
}

/// Force to use the rustls webpki certificate store.
///
/// Since multiple certificate stores can be optionally enabled, this option will
/// force the `webpki certificate store` to be used.
///
/// # Optional
///
/// This requires the optional `webpki-tls` feature.
#[cfg(feature = "webpki-tls")]
pub fn use_webpki_rustls(mut self) -> Self {
self.certificate_store = CertificateStore::WebPki;
self
}

Expand Down
27 changes: 15 additions & 12 deletions client/http-client/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CONTENT_TYPE_JSON: &str = "application/json";
#[derive(Debug)]
pub enum HttpBackend<B = Body> {
/// Hyper client with https connector.
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
Https(Client<hyper_rustls::HttpsConnector<HttpConnector>, B>),
/// Hyper client with http connector.
Http(Client<HttpConnector, B>),
Expand All @@ -37,7 +37,7 @@ impl Clone for HttpBackend {
fn clone(&self) -> Self {
match self {
Self::Http(inner) => Self::Http(inner.clone()),
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
Self::Https(inner) => Self::Https(inner.clone()),
}
}
Expand All @@ -56,7 +56,7 @@ where
fn poll_ready(&mut self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self {
Self::Http(inner) => inner.poll_ready(ctx),
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
Self::Https(inner) => inner.poll_ready(ctx),
}
.map_err(Into::into)
Expand All @@ -65,7 +65,7 @@ where
fn call(&mut self, req: hyper::Request<B>) -> Self::Future {
let resp = match self {
Self::Http(inner) => inner.call(req),
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
Self::Https(inner) => inner.call(req),
};

Expand Down Expand Up @@ -112,15 +112,18 @@ where
let uri = ParsedUri::try_from(target.as_ref())?;

let client = match uri.0.scheme_str() {
#[cfg(not(feature = "__tls"))]
Some("http") => HttpBackend::Http(Client::new()),
#[cfg(feature = "tls")]
Some("https") => {
#[cfg(feature = "__tls")]
Some("https") | Some("http") => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for simplicity, just use connector based on the feature flag and yes the https connector is a bit slower on plain http than http connector.

let connector = match cert_store {
#[cfg(feature = "native-tls")]
CertificateStore::Native => hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_or_http()
.enable_http1()
.build(),
#[cfg(feature = "webpki-tls")]
CertificateStore::WebPki => hyper_rustls::HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_or_http()
Expand All @@ -131,9 +134,9 @@ where
HttpBackend::Https(Client::builder().build::<_, hyper::Body>(connector))
}
_ => {
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
let err = "URL scheme not supported, expects 'http' or 'https'";
#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
let err = "URL scheme not supported, expects 'http'";
return Err(Error::Url(err.into()));
}
Expand Down Expand Up @@ -299,7 +302,7 @@ mod tests {
assert!(matches!(err, Error::Url(_)));
}

#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
#[test]
fn https_works() {
let client = HttpTransportClient::new(
Expand All @@ -315,7 +318,7 @@ mod tests {
assert_target(&client, "localhost", "https", "/", 9933, 80);
}

#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
#[test]
fn https_fails_without_tls_feature() {
let err = HttpTransportClient::new(
Expand Down Expand Up @@ -378,7 +381,7 @@ mod tests {
u32::MAX,
"http://127.0.0.1:9999/my?name1=value1&name2=value2",
u32::MAX,
CertificateStore::WebPki,
CertificateStore::Native,
80,
HeaderMap::new(),
tower::ServiceBuilder::new(),
Expand Down Expand Up @@ -411,7 +414,7 @@ mod tests {
eighty_bytes_limit,
"http://localhost:9933",
fifty_bytes_limit,
CertificateStore::WebPki,
CertificateStore::Native,
99,
HeaderMap::new(),
tower::ServiceBuilder::new(),
Expand Down
12 changes: 10 additions & 2 deletions client/transport/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ http = { version = "0.2", optional = true }
tokio-util = { version = "0.7", features = ["compat"], optional = true }
tokio = { version = "1.16", features = ["net", "time", "macros"], optional = true }
pin-project = { version = "1", optional = true }
futures-timer = { version = "3", optional = true }

# tls
rustls-native-certs = { version = "0.6", optional = true }
webpki-roots = { version = "0.22", optional = true }
tokio-rustls = { version = "0.23", optional = true }
futures-timer = { version = "3", optional = true }

# ws
soketto = { version = "0.7.1", optional = true }
Expand All @@ -35,7 +37,13 @@ soketto = { version = "0.7.1", optional = true }
gloo-net = { version = "0.2.6", default-features = false, features = ["json", "websocket"], optional = true }

[features]
tls = ["tokio-rustls", "webpki-roots", "rustls-native-certs"]
native-tls = ["rustls-native-certs", "__tls"]
webpki-tls = ["webpki-roots", "__tls"]

# Internal feature to indicate whether TLS is enabled.
# Does nothing on its own.
__tls = ["tokio-rustls"]

ws = [
"futures-util",
"http",
Expand Down
58 changes: 41 additions & 17 deletions client/transport/src/ws/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,31 @@ impl Default for WsTransportClientBuilder {
}

impl WsTransportClientBuilder {
/// Set whether to use system certificates (default is native).
pub fn certificate_store(mut self, certificate_store: CertificateStore) -> Self {
self.certificate_store = certificate_store;
/// Force to use the rustls native certificate store.
///
/// Since multiple certificate stores can be optionally enabled, this option will
/// force the `native certificate store` to be used.
///
/// # Optional
///
/// This requires the optional `native-tls` feature.
#[cfg(feature = "native-tls")]
pub fn use_native_rustls(mut self) -> Self {
self.certificate_store = CertificateStore::Native;
self
}

/// Force to use the rustls webpki certificate store.
///
/// Since multiple certificate stores can be optionally enabled, this option will
/// force the `webpki certificate store` to be used.
///
/// # Optional
///
/// This requires the optional `webpki-tls` feature.
#[cfg(feature = "webpki-tls")]
pub fn use_webpki_rustls(mut self) -> Self {
self.certificate_store = CertificateStore::WebPki;
self
}

Expand Down Expand Up @@ -161,7 +183,7 @@ pub enum WsHandshakeError {
Transport(#[source] soketto::handshake::Error),

/// Invalid DNS name error for TLS
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
#[error("Invalid DNS name: {0}")]
InvalidDnsName(#[source] tokio_rustls::webpki::InvalidDnsNameError),

Expand Down Expand Up @@ -267,7 +289,7 @@ impl WsTransportClientBuilder {
let mut err = None;

// Only build TLS connector if `wss` in URL.
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
let mut connector = match target._mode {
Mode::Tls => Some(build_tls_config(&self.certificate_store)?),
Mode::Plain => None,
Expand All @@ -279,7 +301,7 @@ impl WsTransportClientBuilder {
// The sockaddrs might get reused if the server replies with a relative URI.
let sockaddrs = std::mem::take(&mut target.sockaddrs);
for sockaddr in &sockaddrs {
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
let tcp_stream = match connect(*sockaddr, self.connection_timeout, &target.host, connector.as_ref()).await {
Ok(stream) => stream,
Err(e) => {
Expand All @@ -289,7 +311,7 @@ impl WsTransportClientBuilder {
}
};

#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
let tcp_stream = match connect(*sockaddr, self.connection_timeout).await {
Ok(stream) => stream,
Err(e) => {
Expand Down Expand Up @@ -342,7 +364,7 @@ impl WsTransportClientBuilder {
})?;

// Only build TLS connector if `wss` in redirection URL.
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
match target._mode {
Mode::Tls if connector.is_none() => {
connector = Some(build_tls_config(&self.certificate_store)?);
Expand Down Expand Up @@ -394,7 +416,7 @@ impl WsTransportClientBuilder {
}
}

#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
async fn connect(
sockaddr: SocketAddr,
timeout_dur: Duration,
Expand Down Expand Up @@ -422,7 +444,7 @@ async fn connect(
}
}

#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
async fn connect(sockaddr: SocketAddr, timeout_dur: Duration) -> Result<EitherStream, WsHandshakeError> {
let socket = TcpStream::connect(sockaddr);
let timeout = tokio::time::sleep(timeout_dur);
Expand All @@ -444,7 +466,7 @@ impl From<io::Error> for WsHandshakeError {
}
}

#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
impl From<tokio_rustls::webpki::InvalidDnsNameError> for WsHandshakeError {
fn from(err: tokio_rustls::webpki::InvalidDnsNameError) -> WsHandshakeError {
WsHandshakeError::InvalidDnsName(err)
Expand Down Expand Up @@ -484,13 +506,13 @@ impl TryFrom<Uri> for Target {
fn try_from(uri: Uri) -> Result<Self, Self::Error> {
let _mode = match uri.scheme_str() {
Some("ws") => Mode::Plain,
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
Some("wss") => Mode::Tls,
invalid_scheme => {
let scheme = invalid_scheme.unwrap_or("no scheme");
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
let err = format!("`{scheme}` not supported, expects 'ws' or 'wss'");
#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
let err = format!("`{}` not supported, expects 'ws' ('wss' requires the tls feature)", scheme);
return Err(WsHandshakeError::Url(err.into()));
}
Expand All @@ -514,13 +536,14 @@ impl TryFrom<Uri> for Target {
}

// NOTE: this is slow and should be used sparingly.
#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
fn build_tls_config(cert_store: &CertificateStore) -> Result<tokio_rustls::TlsConnector, WsHandshakeError> {
use tokio_rustls::rustls;

let mut roots = rustls::RootCertStore::empty();

match cert_store {
#[cfg(feature = "native-tls")]
CertificateStore::Native => {
let mut first_error = None;
let certs = rustls_native_certs::load_native_certs().map_err(WsHandshakeError::CertificateStore)?;
Expand All @@ -536,6 +559,7 @@ fn build_tls_config(cert_store: &CertificateStore) -> Result<tokio_rustls::TlsCo
return Err(WsHandshakeError::CertificateStore(err));
}
}
#[cfg(feature = "webpki-tls")]
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)
Expand Down Expand Up @@ -575,14 +599,14 @@ mod tests {
assert_ws_target(target, "127.0.0.1", "127.0.0.1:9933", Mode::Plain, "/");
}

#[cfg(feature = "tls")]
#[cfg(feature = "__tls")]
#[test]
fn wss_works() {
let target = parse_target("wss://kusama-rpc.polkadot.io:443").unwrap();
assert_ws_target(target, "kusama-rpc.polkadot.io", "kusama-rpc.polkadot.io:443", Mode::Tls, "/");
}

#[cfg(not(feature = "tls"))]
#[cfg(not(feature = "__tls"))]
#[test]
fn wss_fails_with_tls_feature() {
let err = parse_target("wss://kusama-rpc.polkadot.io:443").unwrap_err();
Expand Down
Loading