Skip to content

Ensure we always startup with a rustls CryptoProvider #600

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

Merged

Conversation

tnull
Copy link
Collaborator

@tnull tnull commented Aug 1, 2025

The rustls library recently introduced this weird behavior where they expect users to, apart from configuring the respective feature, also explictly call CryptoProvider::install_default. Otherwise they'd simply panic at runtime whenever the first network call requiring TLS would be made. While we already made a change upstream at rust-electrum-client, we also make sure here that we definitely, always, absolutley are sure that we have a CryptoProvider set on startup.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Aug 1, 2025

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull tnull requested a review from joostjager August 1, 2025 12:29
@@ -1525,6 +1527,24 @@ fn build_with_store_internal(
})
}

fn optionally_install_rustls_cryptoprovider() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add doc when this can be removed again (when a newer version of rust-electrum-client is used)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Tbh, given how messy this is, we might keep it in for a while longer just to be sure no user ever again panics at runtime because of this.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it is in the rust electrum client, is this still possible? Assuming rustls is messy, then still if we makes sure to install either in ldk-node or in rust-electrum-client, than we are equally good?

@tnull tnull force-pushed the 2025-08-fix-rustls-crypto-provider branch 2 times, most recently from d3441ab to 90c67ca Compare August 1, 2025 13:15
@tnull
Copy link
Collaborator Author

tnull commented Aug 1, 2025

Putting in draft until we're clear what's going on (give we just saw a CI job where a single test case hit the introduced assert). What a mess.

@tnull tnull marked this pull request as draft August 1, 2025 13:50
@tnull tnull force-pushed the 2025-08-fix-rustls-crypto-provider branch from 90c67ca to 7c08563 Compare August 12, 2025 12:50
@tnull tnull requested a review from TheBlueMatt August 12, 2025 13:03
@tnull tnull marked this pull request as ready for review August 12, 2025 13:03
@tnull
Copy link
Collaborator Author

tnull commented Aug 12, 2025

CI failure of CLN job is preexisting (has been fixed on main since)

src/builder.rs Outdated

// Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
if rustls::crypto::CryptoProvider::get_default().is_none() {
let res = rustls::crypto::aws_lc_rs::default_provider()

Choose a reason for hiding this comment

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

docs say this is only available with the aws_lc_rs feature and rustls::crypto::ring is only available with the ring feature. Doesn't that imply that we're somehow building with both as dependencies? Can we fix that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

docs say this is only available with the aws_lc_rs feature and rustls::crypto::ring is only available with the ring feature. Doesn't that imply that we're somehow building with both as dependencies? Can we fix that?

Yes, we have both in our tree. Preferably, we get to an aws-lc-rs-only tree, however a) we need the changes of lightningdevkit/rust-lightning#3587 (which we get as part of #462) b) we still need to switch to only use rustls-ring for Swift and possibly Kotlin bindings as aws-lc-rs fails to link properly for Uniffi Swift bindings and nobody could make it work yet.

So currently I even consider going back to a ring-only tree, even though the fact that it recently was marked unmaintained (see rustsec/advisory-db#2227 and rustsec/advisory-db#2230) isn't exactly confidence inspiring. Plus, aws-lc-rs requires cargo-bindgen to build, which is a bit annoying.

Choose a reason for hiding this comment

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

we need the changes of lightningdevkit/rust-lightning#3587

I'm not quite sure about this? cargo tree tells me bdk_electrum v23 (direct dependency) pulls in electrum-client v24 which depends on rustls (v23) with ring. Also, the direct dependency on esplora-client v11 pulls in reqwest which depends on hyper-rustls and rustls (v21) with ring. Looks like there's quite a few dependencies on it left.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Btw, note that we eventually might want to also find a fix that allows us to drop these explicit installs again, especially since our upstream libraries do not expose the particular rustls crate they're using, we might end up in a stale state where we install the crypto provider for some rustls version, even if electrum-client moved on to use more recent versions of the crate.

Choose a reason for hiding this comment

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

I mean that seems like the reason rustls docs say "libraries should use ClientConfig::builder()/ServerConfig::builder() or otherwise rely on the CryptoProvider::get_default() provider."

They want everyone to just depend on rustls without any configured crypto provider at all, and let the actual application configure it, that way you dont end up with ring and aws-lc-rs in your tree. I get what they're going for and obviously its annoying to deal with for downstream users, but it does kinda feel like maybe we should be going that way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I mean first of all we should open an issue upstream and ask^H^H^Hbeg them to just use features for this.

I don't think this is worth. There are a bunch of issues / prior discussion around this already on their github. I would cite them, if Github wouldn't be broken right now.

The current logic of "always compile both then set aws-lc-rs if the user didn't override it" is fine (its similar in practice to what rustls should be doing), but obviously totally nuts from the PoV of compiling both always. We should be focusing on not compiling either unless the downstream application opts into one, and then we can set a default like reqwest does.

The issue with the present approach is that now the user won't be able to choose their provider if LDK Node is somewhere in their tree.

Choose a reason for hiding this comment

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

Ah, hmm, went and looked and rustls actually doens't require you to call install_default as long as rustls is built without both ring and aws_lc_rs, which should be where we get to (see https://docs.rs/rustls/latest/src/rustls/crypto/mod.rs.html#265-284). So once we clean up the dep tree to not have both versions of rustls we'll be able to remove this code. This makes their stance a good bit less nonsensical (though its still, indeed, a bit weird).

The issue with the present approach is that now the user won't be able to choose their provider if LDK Node is somewhere in their tree.

Hmm? We can't override the users' pick, the API doesn't actually let us, so we can just ignore that case? I wasn't imagining we'd panic on failure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, hmm, went and looked and rustls actually doens't require you to call install_default as long as rustls is built without both ring and aws_lc_rs, which should be where we get to (see https://docs.rs/rustls/latest/src/rustls/crypto/mod.rs.html#265-284). So once we clean up the dep tree to not have both versions of rustls we'll be able to remove this code. This makes their stance a good bit less nonsensical (though its still, indeed, a bit weird).

Right, the issue is that even if we get to a single-feature tree (say ring), we can't really lean on that, because we cannot control if another crate in the tree would enable aws-lc-rs without knowing about the consequences elsewhere, no?

Hmm? We can't override the users' pick, the API doesn't actually let us, so we can just ignore that case? I wasn't imagining we'd panic on failure.

Hmm, alright. I'll drop the debug_assert.

Choose a reason for hiding this comment

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

Right, the issue is that even if we get to a single-feature tree (say ring), we can't really lean on that, because we cannot control if another crate in the tree would enable aws-lc-rs without knowing about the consequences elsewhere, no?

Sure, but that's a downstream issue, not our issue?

Copy link
Collaborator Author

@tnull tnull Aug 14, 2025

Choose a reason for hiding this comment

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

Well, yes, but it's super weird to change our behavior to the degree of suddenly panicking, just because a downstream user adds or removes another dependency. Again, they might not even know anything about rustls. Heck, they might even be two hops or more away and might not know anything about LDK Node, Electrum, etc.

@tnull
Copy link
Collaborator Author

tnull commented Aug 12, 2025

Force-pushed with the following changes:

diff --git a/src/builder.rs b/src/builder.rs
index 28cf2f7d..080d6568 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -75,5 +75,5 @@ use std::fs;
 use std::path::PathBuf;
 use std::sync::atomic::AtomicBool;
-use std::sync::{Arc, Mutex, RwLock};
+use std::sync::{Arc, Mutex, Once, RwLock};
 use std::time::SystemTime;
 use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider};
@@ -1531,19 +1531,21 @@ fn optionally_install_rustls_cryptoprovider() {
        // Acquire a global Mutex, ensuring that only one process at a time install the provider. This
        // is mostly required for running tests concurrently.
-       static _MUTEX: Mutex<()> = Mutex::new(());
-
-       // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
-       if rustls::crypto::CryptoProvider::get_default().is_none() {
-               let res = rustls::crypto::aws_lc_rs::default_provider()
-                       .install_default()
-                       .or(rustls::crypto::ring::default_provider().install_default());
-               debug_assert!(res.is_ok(), "We need to install a CryptoProvider");
-       }
+       static INIT_CRYPTO: Once = Once::new();
+
+       INIT_CRYPTO.call_once(|| {
+               // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
+               if rustls::crypto::CryptoProvider::get_default().is_none() {
+                       let res = rustls::crypto::aws_lc_rs::default_provider()
+                               .install_default()
+                               .or(rustls::crypto::ring::default_provider().install_default());
+                       debug_assert!(res.is_ok(), "We need to install a CryptoProvider");
+               }

-       // Refuse to startup without TLS support. Better to catch it now than even later at runtime.
-       assert!(
-               rustls::crypto::CryptoProvider::get_default().is_some(),
-               "We need to have a CryptoProvider"
-       );
+               // Refuse to startup without TLS support. Better to catch it now than even later at runtime.
+               assert!(
+                       rustls::crypto::CryptoProvider::get_default().is_some(),
+                       "We need to have a CryptoProvider"
+               );
+       });
 }

src/builder.rs Outdated
// Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
if rustls::crypto::CryptoProvider::get_default().is_none() {
let res = rustls::crypto::aws_lc_rs::default_provider()
.install_default()

Choose a reason for hiding this comment

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

This is not fallible. The docs should write this (can we contribute upstream here) but this fails only when there's already a default provider, so we should just swallow the error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but we don't know if the user previously installed another provider. This is why we debug_assert here, although it could be argued we should just log and continue.

Choose a reason for hiding this comment

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

I meant the fallback to try to install ring is unnecessary because the error indicates we already have a provider, not that we failed to install aws_lc_rs.

src/builder.rs Outdated

// Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
if rustls::crypto::CryptoProvider::get_default().is_none() {
let res = rustls::crypto::aws_lc_rs::default_provider()

Choose a reason for hiding this comment

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

I mean first of all we should open an issue upstream and ask^H^H^Hbeg them to just use features for this. They have a "default" in their docs, they should use it unless the application configures something different as long as the feature is set and thus aws-lc-rs is available (or some other default).

The current logic of "always compile both then set aws-lc-rs if the user didn't override it" is fine (its similar in practice to what rustls should be doing), but obviously totally nuts from the PoV of compiling both always. We should be focusing on not compiling either unless the downstream application opts into one, and then we can set a default like reqwest does.

@tnull tnull force-pushed the 2025-08-fix-rustls-crypto-provider branch from 33afe91 to 3450259 Compare August 13, 2025 11:09
@tnull tnull requested a review from TheBlueMatt August 13, 2025 11:10
@tnull
Copy link
Collaborator Author

tnull commented Aug 13, 2025

Force-pushed once more with:

diff --git a/src/builder.rs b/src/builder.rs
index 080d6568..d3cded4a 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -1536,8 +1536,7 @@ fn optionally_install_rustls_cryptoprovider() {
                // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now.
                if rustls::crypto::CryptoProvider::get_default().is_none() {
-                       let res = rustls::crypto::aws_lc_rs::default_provider()
+                       let _ = rustls::crypto::aws_lc_rs::default_provider()
                                .install_default()
                                .or(rustls::crypto::ring::default_provider().install_default());
-                       debug_assert!(res.is_ok(), "We need to install a CryptoProvider");
                }

@tnull tnull force-pushed the 2025-08-fix-rustls-crypto-provider branch from 3450259 to fba4fe8 Compare August 14, 2025 08:12
@tnull
Copy link
Collaborator Author

tnull commented Aug 14, 2025

Force-pushed the following changes:

diff --git a/src/builder.rs b/src/builder.rs
index d3cded4a..97a02b2e 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -1537,6 +1537,5 @@ fn optionally_install_rustls_cryptoprovider() {
                if rustls::crypto::CryptoProvider::get_default().is_none() {
                        let _ = rustls::crypto::aws_lc_rs::default_provider()
-                               .install_default()
-                               .or(rustls::crypto::ring::default_provider().install_default());
+                               .install_default();
                }

@tnull
Copy link
Collaborator Author

tnull commented Aug 14, 2025

The more I think about the issue, the more it seems like there's only bad choices in all direction. My current take-away is:

  1. we land this PR and Ensure we always startup with a rustls CryptoProvider (main) #601 as short-term fixes for the issue at hand
  2. As part of/post Upgrade to LDK 0.2 #462 we try to revert to a purely rustls-ring dependency tree, keeping some safeguard in place in case downstream users would enable both features.
  3. We contribute upstream to rust-electrum-client to fallback to rustls-ring by default whenever the user hasn't set a specific CryptoProvider, mirroring reqwest's behavior. I.e., we'd (partially) revert Install rustls's CryptoProvider based on features bitcoindevkit/rust-electrum-client#171 in favor of a more relaxed approach that still tries 'best effort' to pick a crypto provider.

@TheBlueMatt WDYT?

The `rustls` library recently introduced this weird behavior where they
expect users to, apart from configuring the respective feature, also
explictly call `CryptoProvider::install_default`. Otherwise they'd
simply panic at runtime whenever the first network call requiring TLS
would be made. While we already made a change upstream at
`rust-electrum-client`, we also make sure here that we definitely,
always, absolutley are sure that we have a `CryptoProvider` set on
startup.
@tnull tnull force-pushed the 2025-08-fix-rustls-crypto-provider branch from fba4fe8 to 9fa5686 Compare August 14, 2025 08:22
@TheBlueMatt
Copy link

It does seem to me like rustls has basically forced everyone to do the reqwest dance - expose features to ensure a default is set (why ring vs aws-lc-rs?) but allow the downstream app to default-features = false to pick their own. It's completely bonkers cause it pollutes basically the entire rust ecosystem with features specifically for rustls but it is where we are today (I went and read upstream issues a bit, imo we could make a stronger argument here, they haven't really grappled with this issue I don't think).

With that in mind, I agree that we should land this as long as our tree is polluted with both, but we really need to get features in place to let the downstream user pick.

@tnull tnull merged commit 201217f into lightningdevkit:release/0.6 Aug 14, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants