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:
Piepmatz
2025-05-23 22:45:15 +02:00
committed by GitHub
parent 60cb13c493
commit cc8b623ff8
11 changed files with 479 additions and 25 deletions

View File

@ -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"]

View File

@ -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);

View File

@ -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;

View 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![],
})
}

View 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,
]
}
}

View 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::*;

View 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.

View File

@ -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);