mirror of
https://github.com/nushell/nushell.git
synced 2025-08-14 10:09:06 +02:00
Add rustls
for TLS (#15810)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> closes #14041 # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR switches our default TLS backend from `native-tls` to `rustls`. Cross-compiles, `musl`, and other targets build smoother because we drop the OpenSSL requirement. `native-tls` is still available as an opt-in on `nu-command` via the `native-tls` feature. WASM + `network` still fails for unrelated crates, but the OpenSSL roadblock is gone. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> No changes to the Nushell API. If you embed Nushell you now need to pick a [`rustls::crypto::CryptoProvider`](https://docs.rs/rustls/0.23.27/rustls/crypto/struct.CryptoProvider.html) at startup: ```rust use nu_command::tls::CRYPTO_PROVIDER; // common case CRYPTO_PROVIDER.default(); // or supply your own CRYPTO_PROVIDER.set(|| Ok(my_provider())); ``` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> * 🟢 `toolkit fmt` * 🟢 `toolkit clippy` * 🟢 `toolkit test` * 🟢 `toolkit test stdlib` # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
This commit is contained in:
@ -91,6 +91,8 @@ rusqlite = { workspace = true, features = [
|
||||
"backup",
|
||||
"chrono",
|
||||
], optional = true }
|
||||
rustls = { workspace = true, optional = true }
|
||||
rustls-native-certs = { workspace = true, optional = true }
|
||||
rmp = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@ -108,7 +110,6 @@ ureq = { workspace = true, default-features = false, features = [
|
||||
"charset",
|
||||
"gzip",
|
||||
"json",
|
||||
"native-tls",
|
||||
], optional = true }
|
||||
url = { workspace = true }
|
||||
uu_cp = { workspace = true, optional = true }
|
||||
@ -131,6 +132,7 @@ which = { workspace = true, optional = true }
|
||||
unicode-width = { workspace = true }
|
||||
data-encoding = { version = "2.9.0", features = ["alloc"] }
|
||||
web-time = { workspace = true }
|
||||
webpki-roots = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { workspace = true }
|
||||
@ -165,7 +167,7 @@ features = [
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["os"]
|
||||
default = ["os", "rustls-tls"]
|
||||
os = [
|
||||
# include other features
|
||||
"js",
|
||||
@ -197,11 +199,24 @@ js = ["getrandom", "getrandom/js", "rand", "uuid"]
|
||||
# interface requires openssl which is not easy to embed into wasm,
|
||||
# using rustls could solve this issue.
|
||||
network = [
|
||||
# these two don't require openssl
|
||||
"multipart-rs",
|
||||
"native-tls",
|
||||
"update-informer/native-tls",
|
||||
"ureq",
|
||||
"uuid",
|
||||
"ureq",
|
||||
"update-informer"
|
||||
]
|
||||
|
||||
native-tls = [
|
||||
"dep:native-tls",
|
||||
"update-informer/native-tls",
|
||||
"ureq/native-tls",
|
||||
]
|
||||
rustls-tls = [
|
||||
"dep:rustls",
|
||||
"dep:rustls-native-certs",
|
||||
"dep:webpki-roots",
|
||||
"update-informer/rustls-tls",
|
||||
"ureq/tls", # ureq 3 will has the feature rustls instead
|
||||
]
|
||||
|
||||
plugin = ["nu-parser/plugin", "os"]
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::formats::value_to_json_value;
|
||||
use crate::{formats::value_to_json_value, network::tls::tls};
|
||||
use base64::{
|
||||
Engine, alphabet,
|
||||
engine::{GeneralPurpose, general_purpose::PAD},
|
||||
@ -56,20 +56,9 @@ pub fn http_client(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<ureq::Agent, ShellError> {
|
||||
let tls = native_tls::TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(allow_insecure)
|
||||
.build()
|
||||
.map_err(|e| ShellError::GenericError {
|
||||
error: format!("Failed to build network tls: {}", e),
|
||||
msg: String::new(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})?;
|
||||
|
||||
let mut agent_builder = ureq::builder()
|
||||
.user_agent("nushell")
|
||||
.tls_connector(std::sync::Arc::new(tls));
|
||||
.tls_connector(std::sync::Arc::new(tls(allow_insecure)?));
|
||||
|
||||
if let RedirectMode::Manual | RedirectMode::Error = redirect_mode {
|
||||
agent_builder = agent_builder.redirects(0);
|
||||
|
@ -2,6 +2,8 @@
|
||||
mod http;
|
||||
#[cfg(feature = "network")]
|
||||
mod port;
|
||||
#[cfg(feature = "network")]
|
||||
pub mod tls;
|
||||
mod url;
|
||||
#[cfg(feature = "network")]
|
||||
mod version_check;
|
||||
|
16
crates/nu-command/src/network/tls/impl_native_tls.rs
Normal file
16
crates/nu-command/src/network/tls/impl_native_tls.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use nu_protocol::ShellError;
|
||||
use ureq::TlsConnector;
|
||||
|
||||
#[doc = include_str!("./tls.rustdoc.md")]
|
||||
pub fn tls(allow_insecure: bool) -> Result<impl TlsConnector, ShellError> {
|
||||
native_tls::TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(allow_insecure)
|
||||
.build()
|
||||
.map_err(|e| ShellError::GenericError {
|
||||
error: format!("Failed to build network tls: {}", e),
|
||||
msg: String::new(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
251
crates/nu-command/src/network/tls/impl_rustls.rs
Normal file
251
crates/nu-command/src/network/tls/impl_rustls.rs
Normal file
@ -0,0 +1,251 @@
|
||||
use std::{
|
||||
ops::Deref,
|
||||
sync::{Arc, LazyLock, OnceLock},
|
||||
};
|
||||
|
||||
use nu_engine::command_prelude::IoError;
|
||||
use nu_protocol::ShellError;
|
||||
use rustls::{
|
||||
DigitallySignedStruct, RootCertStore, SignatureScheme,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::CryptoProvider,
|
||||
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||
};
|
||||
use ureq::TlsConnector;
|
||||
|
||||
// TODO: replace all these generic errors with proper errors
|
||||
|
||||
/// Stores the crypto provider used by `rustls`.
|
||||
///
|
||||
/// This struct lives in the [`CRYPTO_PROVIDER`] static.
|
||||
/// It can't be created manually.
|
||||
///
|
||||
/// ## Purpose
|
||||
///
|
||||
/// Nushell does **not** use the global `rustls` crypto provider.
|
||||
/// You **must** set a provider here—otherwise, any networking command
|
||||
/// that uses `rustls` won't be able to build a TLS connector.
|
||||
///
|
||||
/// This only matters if the **`rustls-tls`** feature is enabled.
|
||||
/// Builds with **`native-tls`** ignore this completely.
|
||||
///
|
||||
/// ## How to set the provider
|
||||
///
|
||||
/// * [`NuCryptoProvider::default`]
|
||||
/// Uses a built-in provider that works with official `nu` builds.
|
||||
/// This might change in future versions.
|
||||
///
|
||||
/// * [`NuCryptoProvider::set`]
|
||||
/// Lets you provide your own `CryptoProvider` using a closure:
|
||||
///
|
||||
/// ```rust
|
||||
/// use nu_command::tls::CRYPTO_PROVIDER;
|
||||
///
|
||||
/// // Call once at startup
|
||||
/// CRYPTO_PROVIDER.set(|| Ok(rustls::crypto::aws_lc_rs::default_provider()));
|
||||
/// ```
|
||||
///
|
||||
/// Only the first successful call takes effect. Later calls do nothing and return `false`.
|
||||
#[derive(Debug)]
|
||||
pub struct NuCryptoProvider(OnceLock<Result<Arc<CryptoProvider>, ShellError>>);
|
||||
|
||||
/// Global [`NuCryptoProvider`] instance.
|
||||
///
|
||||
/// When the **`rustls-tls`** feature is active, call
|
||||
/// [`CRYPTO_PROVIDER.default()`](NuCryptoProvider::default) or
|
||||
/// [`CRYPTO_PROVIDER.set(...)`](NuCryptoProvider::set) once at startup
|
||||
/// to pick the [`CryptoProvider`] that [`rustls`] will use.
|
||||
///
|
||||
/// Later TLS code gets the provider using [`get`](NuCryptoProvider::get).
|
||||
/// If no provider was set or the closure returned an error, `get` returns a [`ShellError`].
|
||||
pub static CRYPTO_PROVIDER: NuCryptoProvider = NuCryptoProvider(OnceLock::new());
|
||||
|
||||
impl NuCryptoProvider {
|
||||
/// Returns the current [`CryptoProvider`].
|
||||
///
|
||||
/// Comes from the first call to [`default`](Self::default) or [`set`](Self::set).
|
||||
///
|
||||
/// # Errors
|
||||
/// - If no provider was set.
|
||||
/// - If the `set` closure returned an error.
|
||||
pub fn get(&self) -> Result<Arc<CryptoProvider>, ShellError> {
|
||||
// we clone here as the Arc for Ok is super cheap and basically all APIs expect an owned
|
||||
// ShellError, so we might as well clone here already
|
||||
match self.0.get() {
|
||||
Some(val) => val.clone(),
|
||||
None => Err(ShellError::GenericError {
|
||||
error: "tls crypto provider not found".to_string(),
|
||||
msg: "no crypto provider for rustls was defined".to_string(),
|
||||
span: None,
|
||||
help: Some("ensure that nu_command::tls::CRYPTO_PROVIDER is set".to_string()),
|
||||
inner: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a custom [`CryptoProvider`].
|
||||
///
|
||||
/// Call once at startup, before any TLS code runs.
|
||||
/// The closure runs immediately and the result (either `Ok` or `Err`) is stored.
|
||||
/// Returns whether the provider was stored successfully.
|
||||
pub fn set(&self, f: impl FnOnce() -> Result<CryptoProvider, ShellError>) -> bool {
|
||||
let value = f().map(Arc::new);
|
||||
self.0.set(value).is_ok()
|
||||
}
|
||||
|
||||
/// Sets a default [`CryptoProvider`] used in official `nu` builds.
|
||||
///
|
||||
/// Should work on most systems, but may not work in every setup.
|
||||
/// If it fails, use [`set`](Self::set) to install a custom one.
|
||||
/// Returns whether the provider was stored successfully.
|
||||
pub fn default(&self) -> bool {
|
||||
self.set(|| Ok(rustls::crypto::aws_lc_rs::default_provider()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
static ROOT_CERT_STORE: LazyLock<Result<Arc<RootCertStore>, ShellError>> = LazyLock::new(|| {
|
||||
let mut roots = RootCertStore::empty();
|
||||
|
||||
let native_certs = rustls_native_certs::load_native_certs();
|
||||
|
||||
let errors: Vec<_> = native_certs
|
||||
.errors
|
||||
.into_iter()
|
||||
.map(|err| match err.kind {
|
||||
rustls_native_certs::ErrorKind::Io { inner, path } => ShellError::Io(
|
||||
IoError::new_internal_with_path(inner, err.context, nu_protocol::location!(), path),
|
||||
),
|
||||
rustls_native_certs::ErrorKind::Os(error) => ShellError::GenericError {
|
||||
error: error.to_string(),
|
||||
msg: err.context.to_string(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
rustls_native_certs::ErrorKind::Pem(error) => ShellError::GenericError {
|
||||
error: error.to_string(),
|
||||
msg: err.context.to_string(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
_ => ShellError::GenericError {
|
||||
error: String::from("unknown error loading native certs"),
|
||||
msg: err.context.to_string(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
if !errors.is_empty() {
|
||||
return Err(ShellError::GenericError {
|
||||
error: String::from("error loading native certs"),
|
||||
msg: String::from("could not load native certs"),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: errors,
|
||||
});
|
||||
}
|
||||
|
||||
for cert in native_certs.certs {
|
||||
roots.add(cert).map_err(|err| ShellError::GenericError {
|
||||
error: err.to_string(),
|
||||
msg: String::from("could not add root cert"),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Arc::new(roots))
|
||||
});
|
||||
|
||||
#[cfg(not(feature = "os"))]
|
||||
static ROOT_CERT_STORE: LazyLock<Result<Arc<RootCertStore>, ShellError>> = LazyLock::new(|| {
|
||||
Ok(Arc::new(rustls::RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
}))
|
||||
});
|
||||
|
||||
#[doc = include_str!("./tls.rustdoc.md")]
|
||||
pub fn tls(allow_insecure: bool) -> Result<impl TlsConnector, ShellError> {
|
||||
let crypto_provider = CRYPTO_PROVIDER.get()?;
|
||||
|
||||
let make_protocol_versions_error = |err: rustls::Error| ShellError::GenericError {
|
||||
error: err.to_string(),
|
||||
msg: "crypto provider is incompatible with protocol versions".to_string(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
};
|
||||
|
||||
let client_config = match allow_insecure {
|
||||
false => rustls::ClientConfig::builder_with_provider(crypto_provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(make_protocol_versions_error)?
|
||||
.with_root_certificates(ROOT_CERT_STORE.deref().clone()?)
|
||||
.with_no_client_auth(),
|
||||
true => rustls::ClientConfig::builder_with_provider(crypto_provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(make_protocol_versions_error)?
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(UnsecureServerCertVerifier))
|
||||
.with_no_client_auth(),
|
||||
};
|
||||
|
||||
Ok(Arc::new(client_config))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UnsecureServerCertVerifier;
|
||||
|
||||
impl ServerCertVerifier for UnsecureServerCertVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA1,
|
||||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
26
crates/nu-command/src/network/tls/mod.rs
Normal file
26
crates/nu-command/src/network/tls/mod.rs
Normal file
@ -0,0 +1,26 @@
|
||||
//! TLS support for networking commands.
|
||||
//!
|
||||
//! This module is available when the `network` feature is enabled. It requires
|
||||
//! either the `native-tls` or `rustls-tls` feature to be selected.
|
||||
//!
|
||||
//! See [`tls`] for how to get a TLS connector.
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[path = "impl_native_tls.rs"]
|
||||
mod impl_tls;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[path = "impl_rustls.rs"]
|
||||
mod impl_tls;
|
||||
|
||||
#[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))]
|
||||
compile_error!(
|
||||
"No TLS backend enabled. Please enable either the `native-tls` or `rustls-tls` feature."
|
||||
);
|
||||
|
||||
#[cfg(all(feature = "native-tls", feature = "rustls-tls"))]
|
||||
compile_error!(
|
||||
"Multiple TLS backends enabled. Please enable only one of `native-tls` or `rustls-tls`, not both."
|
||||
);
|
||||
|
||||
pub use impl_tls::*;
|
31
crates/nu-command/src/network/tls/tls.rustdoc.md
Normal file
31
crates/nu-command/src/network/tls/tls.rustdoc.md
Normal file
@ -0,0 +1,31 @@
|
||||
Provide a [`TlsConnector`] for [`ureq`].
|
||||
|
||||
This is used by Nushell's networking commands (`http`) to handle secure
|
||||
(or optionally insecure) HTTP connections.
|
||||
The returned connector enables `ureq` to perform HTTPS requests.
|
||||
If `allow_insecure` is set to `true`, certificate verification is disabled.
|
||||
|
||||
This function is only available when the `network` feature is enabled,
|
||||
and requires exactly one of the `native-tls` or `rustls-tls` features to
|
||||
be active.
|
||||
|
||||
# With `native-tls`
|
||||
|
||||
When built with `native-tls`, this uses the platform TLS backend:
|
||||
- OpenSSL on most Unix systems
|
||||
- SChannel on Windows
|
||||
|
||||
These are mature and widely-deployed TLS implementations.
|
||||
Expect strong platform integration.
|
||||
|
||||
# With `rustls-tls`
|
||||
|
||||
When built with `rustls-tls`, this uses the pure-Rust [`rustls`] library for TLS.
|
||||
This has several benefits:
|
||||
- Easier cross-compilation (no need for OpenSSL headers or linker setup)
|
||||
- Works with `musl` targets out of the box
|
||||
- Can be compiled to WASM
|
||||
|
||||
A [`NuCryptoProvider`] must be configured before calling this function.
|
||||
Use [`CRYPTO_PROVIDER.default()`](NuCryptoProvider::default) or
|
||||
[`CRYPTO_PROVIDER.set(...)`](NuCryptoProvider::set) to initialize it.
|
@ -6,6 +6,8 @@ use update_informer::{
|
||||
registry,
|
||||
};
|
||||
|
||||
use super::tls::tls;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VersionCheck;
|
||||
|
||||
@ -92,7 +94,7 @@ impl HttpClient for NativeTlsHttpClient {
|
||||
headers: update_informer::http_client::HeaderMap,
|
||||
) -> update_informer::Result<T> {
|
||||
let agent = ureq::AgentBuilder::new()
|
||||
.tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new()?))
|
||||
.tls_connector(std::sync::Arc::new(tls(false)?))
|
||||
.build();
|
||||
|
||||
let mut req = agent.get(url).timeout(timeout);
|
||||
|
Reference in New Issue
Block a user