diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 312be186..69594e38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,42 +52,67 @@ jobs: run: | ./scripts/download_reftest_assets.sh - - name: Run Tests (ref-tests) + # --------------------------------------------------------------------- + # Test matrix + # --------------------------------------------------------------------- + # Native (Linux + macOS + Windows): + # - Test: workspace (--all-features) all crates except ref-tests + # - Test: ic-agent (default features) ship config smoke + # - Test: ic-agent (tls-ring only) alternate TLS provider + # - Test: ic-agent (no TLS, panic path) asserts the expected panic + # + # Linux-only (need extra system setup or have CI resource limits): + # - Test: WASM (ic-agent, wasm-bindgen) browser target + # - Test: ref-tests (pocket-ic) integration baseline + # - Test: ref-tests + SoftHSM HSM-backed identity + # --------------------------------------------------------------------- + + - name: Test - workspace (--all-features) + # Covers every crate except ref-tests with --all-features. For ic-agent + # this enables both TLS providers; the additivity rule selects the + # aws-lc-rs code path at runtime. + shell: bash + run: cargo test --workspace --exclude ref-tests --all-features --no-fail-fast + + - name: Test - ic-agent (default features) + # Ship-config smoke: build & test with only the default feature set + # (pem + tls-aws-lc-rs), the configuration downstream users get by + # default. Catches cfg gates that compile under --all-features but + # break with defaults alone. + shell: bash + run: cargo test -p ic-agent --no-fail-fast + + - name: Test - ic-agent (tls-ring only) + # Exercises the tls-ring code path (the minimal-feature config that + # dfinity/ic relies on to avoid rustls provider conflicts). + shell: bash + run: cargo test -p ic-agent --no-default-features --features pem,tls-ring --no-fail-fast + + - name: Test - ic-agent (no TLS, panic path) + # When no TLS feature is enabled, building the default reqwest client + # must panic with "No provider set". The integration test asserts this + # via #[should_panic]; run only that test, since the rest of ic-agent's + # lib tests would also panic for the same (expected) reason. + shell: bash + run: cargo test -p ic-agent --no-default-features --features pem --test crypto_provider_neither --no-fail-fast + + - name: Test - WASM (ic-agent, wasm-bindgen) + if: ${{ matrix.os == 'ubuntu-latest' }} + run: CARGO_TARGET_DIR=target/wasm wasm-pack test --chrome --headless ic-agent --features wasm-bindgen + + - name: Test - ref-tests (pocket-ic) # ref-tests are skipped on macOS CI: the GitHub Actions macOS runner has # a low per-process thread limit (kern.maxthreadsperproc) that cannot be # raised without root. pocket-ic spawns many OS threads per subnet, and # tokio's blocking thread pool accumulates idle threads across sequential # tests, exhausting the limit before all tests finish. This is a CI # resource constraint — ref-tests work fine on a local macOS machine. + # --test-threads=1 keeps the thread footprint manageable on Linux too. if: ${{ matrix.os == 'ubuntu-latest' }} shell: bash - run: | - cd ref-tests - cargo test --no-fail-fast -- --test-threads=1 - env: - RUST_BACKTRACE: 1 - - - name: Run Tests - shell: bash - run: | - # Test all features and no features for each package. - # ref-tests is excluded here and run separately with --test-threads=1 - # to avoid exhausting OS thread limits when pocket-ic spawns many threads. - for p in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name != "ref-tests") | .manifest_path'); do - pushd $(dirname $p) - cargo test --all-features --no-fail-fast - cargo test --no-default-features --no-fail-fast - popd - done - env: - RUST_BACKTRACE: 1 - - - name: Run Tests (WASM) - if: ${{ matrix.os == 'ubuntu-latest' }} - run: | - CARGO_TARGET_DIR=target/wasm wasm-pack test --chrome --headless ic-agent --features wasm-bindgen + run: cargo test -p ref-tests --no-fail-fast -- --test-threads=1 - - name: Run Tests (SoftHSM) + - name: Test - ref-tests + SoftHSM if: ${{ matrix.os == 'ubuntu-latest' }} run: | set -ex @@ -95,10 +120,8 @@ jobs: # create key: pkcs11-tool -k --module $HSM_PKCS11_LIBRARY_PATH --login --slot-index $HSM_SLOT_INDEX -d $HSM_KEY_ID --key-type EC:prime256v1 --pin $HSM_PIN - cd ref-tests - cargo test --all-features --no-fail-fast -- --nocapture --test-threads=1 + cargo test -p ref-tests --all-features --no-fail-fast -- --nocapture --test-threads=1 env: - RUST_BACKTRACE: 1 HSM_PKCS11_LIBRARY_PATH: /usr/lib/softhsm/libsofthsm2.so HSM_SO_PIN: 123456 HSM_PIN: 1234 diff --git a/CHANGELOG.md b/CHANGELOG.md index 309d6843..2b6713ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [0.48.0] - 2026-05-21 + +* `ic-agent`: Added cargo features `tls-aws-lc-rs` (default) and `tls-ring` to select the rustls crypto provider used by the default `reqwest::Client`. Features are additive: when both are enabled, aws-lc-rs is installed as the process-wide rustls default. Reqwest's `rustls` feature (which hardcoded aws-lc-rs) has been swapped for `rustls-no-provider`; ic-agent now installs the chosen provider via `CryptoProvider::install_default()` on the default-client path, idempotently (an application-installed provider is not overwritten). When the user supplies a client via `AgentBuilder::with_http_client`, ic-agent installs no provider. + +### Breaking Changes + +* `ic-agent`: + * Default feature set changed from `["pem"]` to `["pem", "tls-aws-lc-rs"]`. Stock-default users are unaffected (aws-lc-rs has been the only crypto provider available since 0.46.0). The `rustls` dependency is declared `cfg(not(target_family = "wasm"))`, so wasm consumers using default features are also unaffected — aws-lc-sys (which does not cross-compile to wasm) is not pulled in on wasm targets. + * Consumers using `default-features = false` on a non-wasm target must now opt into a TLS feature, otherwise `Agent::new` panics with "No provider set" when constructing the default reqwest client. + * Migration: add `tls-aws-lc-rs` (matches previous behavior) or `tls-ring` (matches reqwest 0.12 behavior) to the feature list, or supply your own `reqwest::Client` via `AgentBuilder::with_http_client`. + * Example: `ic-agent = { version = "0.48", default-features = false, features = ["pem", "tls-ring"] }`. + * Removed the deprecated `http_transport` module (`ReqwestTransport`, `AgentBuilder::with_transport`, `AgentBuilder::with_arc_transport`), deprecated since 0.38.0. + * Migration: use the dedicated `AgentBuilder` methods (`with_url`, `with_http_client`, `with_arc_route_provider`, `with_max_response_body_size`, `with_max_tcp_error_retries`). + ## [0.47.3] - 2026-05-15 * `ic-agent`: Added the `EffectiveId` enum (`Canister(Principal)` | `Subnet(Principal)`) and widened `Agent::update_signed`, `query_signed`, `request_status_signed`, `request_status_raw`, `wait`, `wait_signed`, `read_state_raw`, `verify`, and `sign_request_status` to accept `impl Into`. Passing a bare `Principal` is unchanged (treated as `EffectiveId::Canister(_)`); passing `EffectiveId::Subnet(_)` routes to the subnet-scoped HTTP endpoints (`/api/v4/subnet//call`, `/api/v3/subnet//read_state`, `/api/v3/subnet//query`) introduced in IC interface spec 0.60.0. diff --git a/Cargo.lock b/Cargo.lock index 59c300fd..dd412e15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.47.3" +version = "0.48.0" dependencies = [ "anyhow", "arc-swap", @@ -1436,7 +1436,7 @@ dependencies = [ "http-body-util", "ic-certification", "ic-ed25519", - "ic-transport-types 0.47.3", + "ic-transport-types 0.48.0", "ic-verify-bls-signature", "ic_principal", "js-sys", @@ -1451,6 +1451,7 @@ dependencies = [ "ref-tests", "reqwest 0.13.2", "ring", + "rustls", "sec1", "serde", "serde_bytes", @@ -1503,7 +1504,7 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.47.3" +version = "0.48.0" dependencies = [ "hex", "ic-agent", @@ -1555,7 +1556,7 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.47.3" +version = "0.48.0" dependencies = [ "candid", "hex", @@ -1572,7 +1573,7 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.47.3" +version = "0.48.0" dependencies = [ "async-trait", "candid", @@ -1742,7 +1743,7 @@ dependencies = [ [[package]] name = "icx" -version = "0.47.3" +version = "0.48.0" dependencies = [ "anyhow", "candid", @@ -1761,7 +1762,7 @@ dependencies = [ [[package]] name = "icx-cert" -version = "0.47.3" +version = "0.48.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 3c16e20d..6c5854fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "0.47.3" +version = "0.48.0" authors = ["DFINITY Stiftung "] edition = "2021" repository = "https://github.com/dfinity/agent-rs" @@ -30,10 +30,10 @@ license = "Apache-2.0" # a comment listing those crates). Otherwise, features are declared in the individual crate Cargo.toml. # # The path dependencies below ensure all workspace members use the same version of internal crates. -ic-agent = { path = "ic-agent", version = "0.47.3", default-features = false } -ic-identity-hsm = { path = "ic-identity-hsm", version = "0.47.3" } -ic-transport-types = { path = "ic-transport-types", version = "0.47.3" } -ic-utils = { path = "ic-utils", version = "0.47.3" } +ic-agent = { path = "ic-agent", version = "0.48.0", default-features = false } +ic-identity-hsm = { path = "ic-identity-hsm", version = "0.48.0" } +ic-transport-types = { path = "ic-transport-types", version = "0.48.0" } +ic-utils = { path = "ic-utils", version = "0.48.0" } ic-utils-bindgen = { path = "ic-utils-bindgen" } ref-tests = { path = "ref-tests" } @@ -88,6 +88,7 @@ rand = "0.10.1" rangemap = "1.7" reqwest = { version = "0.13.2", default-features = false } ring = "0.17" +rustls = { version = "0.23", default-features = false } sec1 = "0.7.2" semver = "1.0.7" serde = "1.0.215" diff --git a/ic-agent/Cargo.toml b/ic-agent/Cargo.toml index 38160d87..9e8230bb 100644 --- a/ic-agent/Cargo.toml +++ b/ic-agent/Cargo.toml @@ -19,9 +19,15 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] features = ["wasm-bindgen"] [features] -default = ["pem"] +default = ["pem", "tls-aws-lc-rs"] pem = ["dep:pem", "pkcs8/pem"] ring = ["dep:ring"] +# rustls crypto provider selection. Features are additive: if both are enabled, +# aws-lc-rs is installed as the process-wide rustls default (the ring code is +# compiled in but unused). To use ring, set `default-features = false` and +# enable `tls-ring` explicitly. +tls-aws-lc-rs = ["dep:rustls", "rustls/aws-lc-rs"] +tls-ring = ["dep:rustls", "rustls/ring"] ic_ref_tests = ["default"] # Used to separate integration tests for ic-ref which need a server running. wasm-bindgen = [ "dep:js-sys", @@ -69,7 +75,7 @@ pem = { workspace = true, optional = true } pkcs8 = { workspace = true, features = ["std"] } rand = { workspace = true } rangemap = { workspace = true } -reqwest = { workspace = true, default-features = false, features = ["blocking", "json", "rustls", "stream"] } +reqwest = { workspace = true, default-features = false, features = ["blocking", "json", "rustls-no-provider", "stream"] } ring = { workspace = true, optional = true } sec1 = { workspace = true, features = ["pem"] } serde = { workspace = true, features = ["derive"] } @@ -85,6 +91,12 @@ tracing = { workspace = true, optional = true } url = { workspace = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] +# rustls is only used off-wasm: on wasm the reqwest client is the browser's +# fetch API, and the rustls crypto-provider install is gated +# `cfg(not(target_family = "wasm"))`. Keeping the dep target-conditional means +# wasm consumers can use the default features without pulling in aws-lc-sys +# (which doesn't cross-compile to wasm32-unknown-unknown). +rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true } tokio = { workspace = true, default-features = false, features = ["time", "sync"] } [target.'cfg(target_family = "wasm")'.dependencies] @@ -103,6 +115,7 @@ tracing-subscriber = { workspace = true } [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] mockito = { workspace = true } +rustls = { workspace = true, features = ["aws-lc-rs", "ring"] } tokio = { workspace = true, features = ["full"] } [target.'cfg(target_family = "wasm")'.dev-dependencies] diff --git a/ic-agent/src/agent/http_transport/mod.rs b/ic-agent/src/agent/http_transport/mod.rs deleted file mode 100644 index bf937667..00000000 --- a/ic-agent/src/agent/http_transport/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! This module has been deprecated in favor of builder methods on `AgentBuilder`. - -#[deprecated(since = "0.38.0", note = "use the AgentBuilder methods")] -#[doc(hidden)] -pub mod reqwest_transport; -#[doc(hidden)] -#[allow(deprecated)] -pub use reqwest_transport::ReqwestTransport; diff --git a/ic-agent/src/agent/http_transport/reqwest_transport.rs b/ic-agent/src/agent/http_transport/reqwest_transport.rs deleted file mode 100644 index 5876c281..00000000 --- a/ic-agent/src/agent/http_transport/reqwest_transport.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! This module has been deprecated in favor of builder methods on `AgentBuilder`. -#![allow(deprecated)] -pub use reqwest; -use std::sync::Arc; - -use reqwest::Client; - -use crate::{ - agent::{ - route_provider::{RoundRobinRouteProvider, RouteProvider}, - AgentBuilder, - }, - AgentError, -}; - -/// A legacy configuration object. `AgentBuilder::with_transport` will apply these settings to the builder. -#[derive(Debug, Clone)] -pub struct ReqwestTransport { - route_provider: Arc, - client: Client, - max_response_body_size: Option, - max_tcp_error_retries: usize, -} - -impl ReqwestTransport { - /// Equivalent to [`AgentBuilder::with_url`]. - #[deprecated(since = "0.38.0", note = "Use AgentBuilder::with_url")] - pub fn create>(url: U) -> Result { - #[cfg(not(target_family = "wasm"))] - { - Self::create_with_client( - url, - Client::builder() - .use_rustls_tls() - .timeout(std::time::Duration::from_secs(360)) - .build() - .expect("Could not create HTTP client."), - ) - } - #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] - { - Self::create_with_client(url, Client::new()) - } - } - - /// Equivalent to [`AgentBuilder::with_url`] and [`AgentBuilder::with_http_client`]. - #[deprecated( - since = "0.38.0", - note = "Use AgentBuilder::with_url and AgentBuilder::with_http_client" - )] - pub fn create_with_client>(url: U, client: Client) -> Result { - let route_provider = Arc::new(RoundRobinRouteProvider::new(vec![url.into()])?); - Self::create_with_client_route(route_provider, client) - } - - /// Equivalent to [`AgentBuilder::with_http_client`] and [`AgentBuilder::with_route_provider`]. - #[deprecated( - since = "0.38.0", - note = "Use AgentBuilder::with_http_client and AgentBuilder::with_arc_route_provider" - )] - pub fn create_with_client_route( - route_provider: Arc, - client: Client, - ) -> Result { - Ok(Self { - route_provider, - client, - max_response_body_size: None, - max_tcp_error_retries: 0, - }) - } - - /// Equivalent to [`AgentBuilder::with_max_response_body_size`]. - #[deprecated( - since = "0.38.0", - note = "Use AgentBuilder::with_max_response_body_size" - )] - pub fn with_max_response_body_size(self, max_response_body_size: usize) -> Self { - ReqwestTransport { - max_response_body_size: Some(max_response_body_size), - ..self - } - } - - /// Equivalent to [`AgentBuilder::with_max_tcp_error_retries`]. - #[deprecated( - since = "0.38.0", - note = "Use AgentBuilder::with_max_tcp_error_retries" - )] - pub fn with_max_tcp_errors_retries(self, retries: usize) -> Self { - ReqwestTransport { - max_tcp_error_retries: retries, - ..self - } - } -} - -impl AgentBuilder { - #[doc(hidden)] - #[deprecated(since = "0.38.0", note = "Use the dedicated methods on AgentBuilder")] - pub fn with_transport(self, transport: ReqwestTransport) -> Self { - let mut builder = self - .with_arc_route_provider(transport.route_provider) - .with_http_client(transport.client) - .with_max_tcp_error_retries(transport.max_tcp_error_retries); - if let Some(max_size) = transport.max_response_body_size { - builder = builder.with_max_response_body_size(max_size); - } - builder - } - #[doc(hidden)] - #[deprecated(since = "0.38.0", note = "Use the dedicated methods on AgentBuilder")] - pub fn with_arc_transport(self, transport: Arc) -> Self { - self.with_transport((*transport).clone()) - } -} diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index 284868a2..683606b0 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -2,10 +2,6 @@ pub(crate) mod agent_config; pub mod agent_error; pub(crate) mod builder; -// delete this module after 0.40 -#[doc(hidden)] -#[deprecated(since = "0.38.0", note = "use the AgentBuilder methods")] -pub mod http_transport; pub(crate) mod nonce; pub(crate) mod response_authentication; pub mod route_provider; @@ -211,6 +207,43 @@ impl fmt::Debug for Agent { } } +/// Install a process-wide rustls [`CryptoProvider`] if none is already installed. +/// +/// Called when [`Agent::new`] builds its default [`reqwest::Client`]. Reqwest's +/// `rustls-no-provider` feature defers the provider choice to whichever +/// `CryptoProvider` is registered as the process default; this function makes +/// the choice based on the active cargo features so users don't have to install +/// one themselves. +/// +/// The call is idempotent: `install_default` returns `Err` if a default was +/// already set, which we ignore. That means an application that installs its +/// own provider before constructing an `Agent` wins. +/// +/// Feature precedence: `tls-aws-lc-rs` wins over `tls-ring` when both are +/// enabled, preserving additivity (enabling `tls-ring` on top of the default +/// never silently flips the installed provider). +#[cfg(not(target_family = "wasm"))] +pub(crate) fn install_default_crypto_provider() { + #[cfg(any(feature = "tls-aws-lc-rs", feature = "tls-ring"))] + { + // Cheap fast-path: if a default is already installed (by us on a prior + // `Agent::new`, or by the application), skip constructing a provider. + // The check has a benign TOCTOU race — `install_default()` is itself + // atomic, so concurrent installers still produce a single winner and + // the others' `Err` is discarded. + if rustls::crypto::CryptoProvider::get_default().is_some() { + return; + } + #[cfg(feature = "tls-aws-lc-rs")] + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + #[cfg(all(feature = "tls-ring", not(feature = "tls-aws-lc-rs")))] + let _ = rustls::crypto::ring::default_provider().install_default(); + } + // If neither feature is enabled, do nothing. The user is expected to either + // call `with_http_client` or install a provider themselves before building + // the agent; otherwise reqwest will panic when constructing its TLS config. +} + impl Agent { /// Create an instance of an [`AgentBuilder`] for building an [`Agent`]. This is simpler than /// using the [`AgentConfig`] and [`Agent::new()`]. @@ -225,6 +258,7 @@ impl Agent { client: config.client.unwrap_or_else(|| { #[cfg(not(target_family = "wasm"))] { + install_default_crypto_provider(); Client::builder() .use_rustls_tls() .timeout(Duration::from_secs(360)) diff --git a/ic-agent/src/agent/route_provider/dynamic_routing/mod.rs b/ic-agent/src/agent/route_provider/dynamic_routing/mod.rs index e914bcd1..7e408a5d 100644 --- a/ic-agent/src/agent/route_provider/dynamic_routing/mod.rs +++ b/ic-agent/src/agent/route_provider/dynamic_routing/mod.rs @@ -77,7 +77,11 @@ //! // Node::new("")?, //! ]; //! -//! // Build dynamic route provider with HTTP client +//! // Build dynamic route provider with HTTP client. Because the client is +//! // built directly (not via `Agent::new`), a rustls `CryptoProvider` must +//! // be installed before `reqwest::Client::new()` — `Agent::new` would +//! // normally do this for you. +//! let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); //! let http_client = Arc::new(reqwest::Client::new()); //! let route_provider = DynamicRouteProviderBuilder::new(seed_nodes, http_client, None) //! // Set how often to fetch the latest API boundary node topology diff --git a/ic-agent/tests/crypto_provider_default.rs b/ic-agent/tests/crypto_provider_default.rs new file mode 100644 index 00000000..c2712b85 --- /dev/null +++ b/ic-agent/tests/crypto_provider_default.rs @@ -0,0 +1,36 @@ +//! Verify that `Agent::new` installs the rustls `CryptoProvider` matching the +//! active cargo features (`tls-aws-lc-rs` or `tls-ring`). +//! +//! In its own integration-test file for process isolation: `CryptoProvider`'s +//! process-wide default slot is one-shot, so sharing a binary with other tests +//! that pre-install a provider would make ordering matter. + +#![cfg(not(target_family = "wasm"))] +#![cfg(any(feature = "tls-ring", feature = "tls-aws-lc-rs"))] + +use ic_agent::Agent; +use rustls::crypto::CryptoProvider; + +#[test] +fn default_client_installs_expected_provider() { + let _agent = Agent::builder() + .with_url("https://ic0.app") + .build() + .expect("Agent build should succeed with a provider feature enabled"); + + let installed = CryptoProvider::get_default().expect("a provider must be installed"); + + // aws-lc-rs wins when both features are on (additive rule). + #[cfg(feature = "tls-aws-lc-rs")] + let expected = rustls::crypto::aws_lc_rs::default_provider(); + #[cfg(all(feature = "tls-ring", not(feature = "tls-aws-lc-rs")))] + let expected = rustls::crypto::ring::default_provider(); + + // Compare the full `&'static dyn SecureRandom` wide pointer (data + vtable). + // Casting to `*const ()` would drop the vtable and could alias across + // ZST-backed impls; wide-pointer equality keeps the type identity. + assert!( + std::ptr::eq(installed.secure_random, expected.secure_random), + "installed provider does not match the one selected by active features" + ); +} diff --git a/ic-agent/tests/crypto_provider_idempotent.rs b/ic-agent/tests/crypto_provider_idempotent.rs new file mode 100644 index 00000000..5473a444 --- /dev/null +++ b/ic-agent/tests/crypto_provider_idempotent.rs @@ -0,0 +1,41 @@ +//! Verify that `Agent::new` does not overwrite a `CryptoProvider` that the +//! application installed first, and does not panic in that case. +//! +//! In its own integration-test file for process isolation: the prior test in +//! `crypto_provider_default.rs` is the inverse setup (ic-agent installs first), +//! and `CryptoProvider`'s default slot is one-shot per process. + +#![cfg(not(target_family = "wasm"))] +#![cfg(any(feature = "tls-ring", feature = "tls-aws-lc-rs"))] + +use ic_agent::Agent; +use rustls::crypto::CryptoProvider; + +#[test] +fn application_provider_wins() { + // Pick the *opposite* of what ic-agent would install, to detect overwrites. + #[cfg(feature = "tls-aws-lc-rs")] + let user_choice = rustls::crypto::ring::default_provider(); + #[cfg(all(feature = "tls-ring", not(feature = "tls-aws-lc-rs")))] + let user_choice = rustls::crypto::aws_lc_rs::default_provider(); + + let user_ptr: *const dyn rustls::crypto::SecureRandom = user_choice.secure_random; + user_choice + .install_default() + .expect("test must run before any other provider is installed"); + + // ic-agent builds its default client; should not panic, and should not + // change the installed default. + let _agent = Agent::builder() + .with_url("https://ic0.app") + .build() + .expect("Agent build should succeed"); + + let installed = CryptoProvider::get_default().expect("provider still installed"); + // Compare the full `&'static dyn SecureRandom` wide pointer (data + vtable); + // casting to `*const ()` would drop the vtable and could alias for ZSTs. + assert!( + std::ptr::eq(installed.secure_random, user_ptr), + "ic-agent overwrote a previously installed CryptoProvider" + ); +} diff --git a/ic-agent/tests/crypto_provider_neither.rs b/ic-agent/tests/crypto_provider_neither.rs new file mode 100644 index 00000000..aeaf6bf9 --- /dev/null +++ b/ic-agent/tests/crypto_provider_neither.rs @@ -0,0 +1,21 @@ +//! Verify that building a default `Agent` panics when neither `tls-ring` nor +//! `tls-aws-lc-rs` is enabled, because reqwest's `rustls-no-provider` feature +//! requires a process-wide `CryptoProvider` to be installed and ic-agent's +//! helper is a no-op in this configuration. +//! +//! Only compiled when *neither* TLS feature is active. The companion CI step +//! exercises this with `--no-default-features --features pem`. + +#![cfg(not(target_family = "wasm"))] +#![cfg(not(any(feature = "tls-ring", feature = "tls-aws-lc-rs")))] + +use ic_agent::Agent; + +#[test] +#[should_panic] +fn default_client_panics_without_provider() { + // Reqwest panics with "No provider set" when it cannot find an installed + // CryptoProvider. `Agent::builder().build()` triggers `Client::builder()` + // which is where the panic surfaces. + let _ = Agent::builder().with_url("https://ic0.app").build(); +} diff --git a/ref-tests/Cargo.toml b/ref-tests/Cargo.toml index 16d540b3..c93996d9 100644 --- a/ref-tests/Cargo.toml +++ b/ref-tests/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] candid = { workspace = true } -ic-agent = { workspace = true, features = ["pem"] } +ic-agent = { workspace = true, features = ["pem", "tls-aws-lc-rs"] } ic-certification = { workspace = true } ic-ed25519 = { workspace = true } ic-identity-hsm = { workspace = true }