From 411194a8b2400d3c5e9de8b6ff4a6d2b9ad3c3d5 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Thu, 10 Sep 2026 22:02:02 -0400 Subject: [PATCH 1/2] test(sdk): reproduce rustls CryptoProvider panic from the device RED. The APK builds, installs and opens, but the first rustls config built from the process default panics one call deep inside Arti's rustls backend: Could not automatically determine the process-level CryptoProvider from Rustls crate features. Call CryptoProvider::install_default() before this point ... rustls 0.23 picks its provider from its own 'ring' / 'aws-lc-rs' crate features, not from which crypto crates happen to be linked. Arti reaches rustls via tor-rtcompat with default-features = false, so neither provider feature is on and rustls compiles with zero providers. 'ring' does appear in cargo tree, pulled in by snow for Noise -- unrelated and irrelevant. This commit adds rustls as an explicit dependency with no provider feature (no behaviour change: it is already in the graph that way) so the test can name the type, plus the regression test. On the host it reproduces the device's panic verbatim. The SDK lockfile is resynced with the root Cargo.toml on main (onionwire 0.2.1, rpassword, md-5, rtoolbox) in the same commit so the test build is against a consistent lock. --- crates/onionwire-sdk/Cargo.lock | 37 ++++++++++++++- crates/onionwire-sdk/Cargo.toml | 8 ++++ .../tests/provider_resolution.rs | 45 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 crates/onionwire-sdk/tests/provider_resolution.rs diff --git a/crates/onionwire-sdk/Cargo.lock b/crates/onionwire-sdk/Cargo.lock index b132956..6835896 100644 --- a/crates/onionwire-sdk/Cargo.lock +++ b/crates/onionwire-sdk/Cargo.lock @@ -2507,6 +2507,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.8.3" @@ -2750,18 +2760,21 @@ dependencies = [ [[package]] name = "onionwire" -version = "0.2.0" +version = "0.2.1" dependencies = [ "argon2", "arti-client", "chacha20poly1305", "ed25519-dalek", "futures", + "md-5", "rand 0.8.8", "ratatui", + "rpassword", "rusqlite", "safelog", "serde_json", + "sha3 0.10.9", "snow", "tokio", "tor-cell", @@ -2776,6 +2789,7 @@ version = "0.1.0" dependencies = [ "arti-client", "onionwire", + "rustls", "thiserror 2.0.20", "tokio", "uniffi", @@ -3472,6 +3486,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rpassword" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3493,6 +3518,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtoolbox" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "rusqlite" version = "0.36.0" diff --git a/crates/onionwire-sdk/Cargo.toml b/crates/onionwire-sdk/Cargo.toml index 27c9cb8..c4bb52e 100644 --- a/crates/onionwire-sdk/Cargo.toml +++ b/crates/onionwire-sdk/Cargo.toml @@ -32,6 +32,14 @@ arti-client = { version = "0.46", default-features = false, features = [ "static-sqlite", ] } tokio = { version = "1", features = ["rt-multi-thread", "time"] } +# rustls 0.23 resolves its provider from its OWN `ring` / `aws-lc-rs` features, +# never from whichever crypto crates happen to be linked in the graph. Arti +# pulls rustls in through `tor-rtcompat` with `default-features = false`, so +# unless this crate turns a provider feature on, rustls compiles with zero +# providers and the first TLS config built from the process default fails at +# runtime. (Currently: no provider feature — see the regression test in +# `tests/provider_resolution.rs`.) +rustls = { version = "0.23", default-features = false } uniffi = { version = "0.32", features = ["cli", "tokio"] } thiserror = "2" diff --git a/crates/onionwire-sdk/tests/provider_resolution.rs b/crates/onionwire-sdk/tests/provider_resolution.rs new file mode 100644 index 0000000..0b490cb --- /dev/null +++ b/crates/onionwire-sdk/tests/provider_resolution.rs @@ -0,0 +1,45 @@ +//! Regression test for the on-device failure: the APK built, installed and +//! opened, but pressing **Open** on the unlock screen died one call deep inside +//! Arti's rustls backend with +//! +//! ```text +//! Could not automatically determine the process-level CryptoProvider from +//! Rustls crate features. +//! Call CryptoProvider::install_default() before this point to select a +//! provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' +//! features is enabled. +//! ``` +//! +//! rustls 0.23 chooses its provider from its own `ring` / `aws-lc-rs` **crate +//! features**, not from which crypto crates happen to be linked. Arti reaches +//! rustls through `tor-rtcompat` with `default-features = false`, so neither +//! provider feature is on and rustls is compiled with *no* provider at all: +//! `ring` showing up in `cargo tree` (pulled in by `snow`, for Noise) proves +//! nothing. Everything compiles, the APK ships, and the process-default lookup +//! fails at runtime. +//! +//! This test is the invariant Arti relies on: the process default must be +//! resolvable **without** anyone calling `install_default()` first. +//! +//! Deliberately the only test in this file — a second test that installs a +//! provider would race with it inside the same test binary and could mask the +//! regression. + +/// Building a `rustls` config the way Arti does, straight from the process +/// default, must not panic. +#[test] +fn rustls_default_provider_resolves_without_manual_install() { + assert!( + rustls::crypto::CryptoProvider::get_default().is_none(), + "this test must start with no provider installed" + ); + + // Pre-fix this panics with the device's exact message. + let _ = rustls::ClientConfig::builder(); + + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "rustls resolved no CryptoProvider — the crate feature that selects a \ + provider is not enabled in this workspace" + ); +} From d2d3b0c827cc81e2bebce730693991f677c686de Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Thu, 10 Sep 2026 22:03:40 -0400 Subject: [PATCH 2/2] fix(sdk): enable rustls' ring provider and install it in the init path GREEN. rustls 0.23 selects its CryptoProvider from its own 'ring' / 'aws-lc-rs' crate features. Arti reaches rustls through tor-rtcompat with default-features = false and nothing in this workspace turned a provider feature on, so rustls was compiled with zero providers and the first TLS config built from the process default panicked: Could not automatically determine the process-level CryptoProvider from Rustls crate features. ... That is the 'Failed to start' screen on the unlock path. The 'ring' crate being present in the graph (via snow, for Noise) never had anything to do with it. 'ring', not 'aws-lc-rs': aws-lc-rs wants CMake and a C toolchain for the NDK and fights the cross-compile. Belt and braces: open_wire() now calls install_crypto_provider() before Node::start_with_passphrase, a OnceLock-guarded ring::default_provider().install_default() with the already-installed Err ignored. That keeps the cdylib correct regardless of how a consumer's feature graph resolves rustls. aws-lc-rs is absent from the lock, so there is no two-provider conflict to report. Evidence: cargo test --release --test provider_resolution before: panicked at rustls-0.23.44/src/crypto/mod.rs:249 (device message) after: 1 passed cargo tree -f '{p} [{f}]' -p rustls rustls v0.23.44 [log,logging,ring,std,tls12] --- crates/onionwire-sdk/Cargo.lock | 1 + crates/onionwire-sdk/Cargo.toml | 13 +++++---- crates/onionwire-sdk/src/lib.rs | 31 ++++++++++++++++++++- crates/onionwire-sdk/tests/init_provider.rs | 30 ++++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 crates/onionwire-sdk/tests/init_provider.rs diff --git a/crates/onionwire-sdk/Cargo.lock b/crates/onionwire-sdk/Cargo.lock index 6835896..0422aac 100644 --- a/crates/onionwire-sdk/Cargo.lock +++ b/crates/onionwire-sdk/Cargo.lock @@ -3588,6 +3588,7 @@ checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", diff --git a/crates/onionwire-sdk/Cargo.toml b/crates/onionwire-sdk/Cargo.toml index c4bb52e..e3847bd 100644 --- a/crates/onionwire-sdk/Cargo.toml +++ b/crates/onionwire-sdk/Cargo.toml @@ -35,11 +35,14 @@ tokio = { version = "1", features = ["rt-multi-thread", "time"] } # rustls 0.23 resolves its provider from its OWN `ring` / `aws-lc-rs` features, # never from whichever crypto crates happen to be linked in the graph. Arti # pulls rustls in through `tor-rtcompat` with `default-features = false`, so -# unless this crate turns a provider feature on, rustls compiles with zero -# providers and the first TLS config built from the process default fails at -# runtime. (Currently: no provider feature — see the regression test in -# `tests/provider_resolution.rs`.) -rustls = { version = "0.23", default-features = false } +# without this line rustls compiles with zero providers and the first TLS config +# built from the process default fails at runtime — the "Failed to start / +# Could not automatically determine the process-level CryptoProvider" screen. +# +# `ring`, not `aws-lc-rs`: aws-lc-rs needs CMake and a C toolchain for the NDK +# and fights the Android cross-compile. The `ring` crate was already in the +# graph (via `snow`, for Noise) but that is a different thing entirely. +rustls = { version = "0.23", default-features = false, features = ["ring"] } uniffi = { version = "0.32", features = ["cli", "tokio"] } thiserror = "2" diff --git a/crates/onionwire-sdk/src/lib.rs b/crates/onionwire-sdk/src/lib.rs index cd08457..e90c7fa 100644 --- a/crates/onionwire-sdk/src/lib.rs +++ b/crates/onionwire-sdk/src/lib.rs @@ -17,13 +17,38 @@ //! no TUI/ratatui type leaks into the AAR. use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use onionwire::node::Node; use onionwire::qr; uniffi::setup_scaffolding!(); +/// Install rustls's process-level default `CryptoProvider`, exactly once. +/// +/// Belt and braces. `Cargo.toml` pins `rustls` with the `ring` feature, which +/// is what actually fixes the missing-provider failure: with it, rustls +/// resolves a provider from crate features on its own. This function exists +/// because the cdylib is loaded into a process we do not own — an explicitly +/// installed provider makes the SDK independent of how the surrounding app's +/// feature graph happens to resolve rustls. +/// +/// Idempotent and cheap after the first call (`OnceLock` short-circuits it). +/// An `Err` means some provider is already installed process-wide, which is the +/// outcome we want, so it is deliberately ignored — including the case of a +/// consumer that installed `aws-lc-rs` itself. A genuine *conflict* — both +/// provider features compiled in — is a build-graph bug, not something to +/// paper over here; `Cargo.toml` enables `ring` only. +/// +/// Not exported over UniFFI: it is Rust-side plumbing, not part of the Kotlin +/// surface. +pub fn install_crypto_provider() { + static INSTALLED: OnceLock<()> = OnceLock::new(); + INSTALLED.get_or_init(|| { + let _ = rustls::crypto::ring::default_provider().install_default(); + }); +} + #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum WireError { @@ -88,6 +113,10 @@ pub struct Wire { /// that is minutes, not seconds. Call it off the main thread. #[uniffi::export(async_runtime = "tokio")] pub async fn open_wire(home: String, passphrase: String) -> WResult> { + // Before anything that can build a TLS config: Arti's rustls backend dies + // with "Could not automatically determine the process-level CryptoProvider" + // if no provider is resolvable. See `install_crypto_provider`. + install_crypto_provider(); let node = Node::start_with_passphrase(PathBuf::from(home), &passphrase) .await .map_err(WireError::new)?; diff --git a/crates/onionwire-sdk/tests/init_provider.rs b/crates/onionwire-sdk/tests/init_provider.rs new file mode 100644 index 0000000..85d8a79 --- /dev/null +++ b/crates/onionwire-sdk/tests/init_provider.rs @@ -0,0 +1,30 @@ +//! `open_wire` must not depend on how the consumer's feature graph resolves a +//! rustls provider — it installs the process default itself, first. +//! +//! This is the belt-and-braces half of the fix for the on-device "Failed to +//! start / Could not automatically determine the process-level CryptoProvider" +//! screen; the primary fix is the `rustls` `ring` feature in `Cargo.toml`, +//! covered by `tests/provider_resolution.rs`. Kept in its own test binary on +//! purpose: installing a provider here would mask that test if they shared a +//! process. + +#[test] +fn sdk_installs_the_process_default_provider() { + assert!( + rustls::crypto::CryptoProvider::get_default().is_none(), + "this test must start with no provider installed" + ); + + onionwire_sdk::install_crypto_provider(); + + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "install_crypto_provider() left the process without a default provider" + ); + + // Second call: idempotent, not a panic and not an error. + onionwire_sdk::install_crypto_provider(); + + // The call Arti makes that blew up on device. + let _ = rustls::ClientConfig::builder(); +}