Skip to content

RFC: Reorganize rustls Features #4408

Open

Description

@abonander

Background

When we first created SQLx, RusTLS had a hardcoded dependency on ring. There was no such thing as pluggable crypto providers.

Additionally, RusTLS comes with no TLS roots by default. We were mainly deploying containerized applications on Linux, where the base images generally don't come with the ca-certificates or openssl packages installed by default. Since no one (that I know of, anyway) ever runs in-place updates inside a container, updating ca-certificates or webpki-roots has to be done during the build process anyway, so they were effectively the same to us. It made the most sense to target rustls using webpki-roots, which simplifies the image build significantly.

Of course, later on, RusTLS introduced pluggable crypto providers with the move toward defaulting to aws-lc-rs. And users wanted support for rustls-native-certs as well, so we ended up with a jagged matrix of TLS features:

sqlx/Cargo.toml

Lines 104 to 108 in b54008a

tls-rustls = ["tls-rustls-ring"] # For backwards compatibility
tls-rustls-aws-lc-rs = ["sqlx-core/_tls-rustls-aws-lc-rs", "sqlx-macros?/_tls-rustls-aws-lc-rs"]
tls-rustls-ring = ["tls-rustls-ring-webpki"] # For backwards compatibility
tls-rustls-ring-webpki = ["sqlx-core/_tls-rustls-ring-webpki", "sqlx-macros?/_tls-rustls-ring-webpki"]
tls-rustls-ring-native-roots = ["sqlx-core/_tls-rustls-ring-native-roots", "sqlx-macros?/_tls-rustls-ring-native-roots"]

Since we want to make sure everything builds and works right, we end up needing a CI pass to test each of these features, for all of MariaDB, MySQL, and Postgres, plus additional passes testing client certificate authentication for each backend.

I'm not particularly comfortable with this situation, as it makes CI take longer, requires more review effort when multiple passes fail at once, and is super annoying to test locally.

Open Feature Requests

And naturally, users have come along wanting more options as new features:

While this isn't the thread to argue about the value of each of these requests, this situation as a whole doesn't seem tenable or scalable.

The Plan: One Convenient Default + Support for Custom Providers

The original design was meant to cater to users who just want a default that works out of the box and does the right things, without having to become an expert in TLS or cryptography or the RusTLS ecosystem. There's an argument to be made that users should make an effort to understand the choices they make in this regard, but that ideal unfortunately clashes with the ugly reality, especially in this brave new world of LLMs and vibe-coding.

I've tried to operate with the philosophy that defaults in a library should be relatively foolproof, because even a normally careful user is bound to make mistakes in areas where they don't know what they don't know. I've certainly been that user many times before.

That means we should provide at least one TLS feature that we can tell users to choose if they don't know any better, so they can trust that it'll do the right thing on supported platforms.

Default Crypto Provider: aws-lc-rs (?)

This is the default that RusTLS themselves recommend, so it would seem to be a no-brainer: https://docs.rs/rustls/0.23.45/rustls/#built-in-providers

The problem I have is that it requires installing third-party software (or trusting pre-built binary objects) on Windows. This means that a dev with a minimal dev setup on Windows (assuming they only have msvc) can't just clone and build the project. I'd argue that this makes it a bad default for a "batteries-included" config, since it's very much not batteries-included.

Updated versions are available on chocolatey, but the package on nuget hasn't been updated since 2018, which means most people will probably have to download the installer directly from the website and run it manually. Veteran Windows devs are generally going to be used to just grabbing installer .exes from sketchy-looking websites anyway, but it doesn't exactly make for the best DX regardless.

Ultimately though, I don't know what I would recommend instead. ring is apparently still maintained, but its README clearly states it is and always has been experimental software, so it's hard to recommend in this day and age for production use-cases. And I don't think any other crypto provider backend is considered production-ready yet, either.

This makes aws-lc-rs the only viable choice.

Default TLS Roots Implementation: TBD

I don't know what I want to do here, which is the main reason I'm opening this RFC. The seemingly obvious choice would be rustls-platform-verifier since it purports to do the right thing for each platform, but there's some things that bug me about it:

  1. The main argument for rustls-platform-verifier is support for OS CA constraints and revocation lists, but on Linux, arguably the most popular platform for the kind of applications SQLx would generally be used in, it offers very little over our existing default of webpki-roots (or changing the default to rustls-native-certs).
    • In fact, it pulls in webpki-roots, webpki and rustls-native-roots anyway, as platform-dependencies because of the need for a fallback, which has the possibility of further exacerbating [KNOWN ISSUE] Unexpected/unwanted crates in Cargo.lock #3211.
    • Fun fact: at least on Debian, the ca-certificates package is just a copy of Mozilla's trust store anyway, so the contents should be more or less identical to webpki-roots, just with differing paths for getting updates. native-certs does give the option for the sysadmin or image builder to override the certificate store, which is something that comes up occasionally.
  2. The reliance on OS-level APIs generally written in memory-unsafe languages would seem to defeat the purpose of choosing RusTLS in the first place. The docs argue that this is still a net positive because using only the certificate verification APIs is exposing a smaller attack surface than using the full TLS implementation. I do have to concede that this is a fair point.

Still, the guarantees offered by rustls-platform-verifier are highly conditional on the platform, and that makes me somewhat uncomfortable. It seems to be the preferred choice, though.

Otherwise, the alternative I would go with is webpki-roots for consistency across platforms.

For Everything Else...

We require users to set a rustls::ClientConfig on *ConnectOptions. This makes ssl-mode/sslMode a no-op.

If the config doesn't set a crypto provider, a process-global provider is required.

This allows maximum flexibility and links in nothing but rustls itself.

Activity

  1. pinned this issue on Sep 15, 2026
  2. manifest commented on Sep 15, 2026

    @manifest
    Contributor

    Even though updating ca-certificates or webpki-roots has to be done during the build process, updating webpki-roots requires updating related dependencies, which might not be possible. Updating ca-certificates is quite straightforward. That’s the main reason why we rely on the latter in our deployments.

  3. unownedtech commented on Oct 20, 2026

    @unownedtech
  4. LukeMathWalker commented on Oct 20, 2026

    @LukeMathWalker
    Contributor

    If a good "out of the box" experience is a priority, rustls-graviola could be considered as an alternative to aws-lc-rs.
    It depends on where you want to land on the spectrum between battle-tested and frictionless.

  5. abonander commented on Oct 21, 2026

    @abonander
    CollaboratorAuthor

    Until graviola passes through some sort of security review or certification process, I don't see it as any safer of an option to recommend than ring. It's essentially just as experimental and unproven, but at least ring has some amount of tenure to it. It's not pure Rust, but at least all it requires is MSVC build tools on Windows (which Windows devs should already have installed anyway).

    The prospect of a pure-Rust crypto backend that we can trust is very exciting, and I'd certainly consider defaulting to it in the future, but it seems like it's going to take some time and effort to get there still.

  6. markuskobler commented on Oct 21, 2026

    @markuskobler
    Contributor

    I ran into this while dealing with the recent eMudhra CA private-key CVE and put together #4474 to make default root certificates opt-in, so applications can trust only the CAs they supply. I do also have an alternative that makes it a non breaking change if that is preferred.

    This was the alternative non breaking option #4475

  7. MikeFalcon77 commented on Oct 23, 2026

    @MikeFalcon77

    Use case: FIPS 140-3 builds that select the crypto provider per OS

    Our framework installs one process-wide rustls CryptoProvider at startup. With --features fips, the provider is different on each OS:

    OS Installed provider Validated module (CMVP)
    Linux rustls::crypto::default_fips_provider() AWS-LC FIPS
    macOS provider on Apple corecrypto Apple corecrypto
    Windows rustls-cng-crypto Windows CNG

    Our other rustls clients (reqwest, hyper-rustls, kube, fred) use the installed provider. SQLx does not. sqlx-core 0.9.0 always makes its own provider (tls_rustls.rs#L99):

    let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());

    Result:

    • Linux: this is the same AWS-LC FIPS module, so the result is correct, but only because the two providers are the same.
    • macOS and Windows: database TLS uses non-FIPS AWS-LC, not the validated OS module.
    • No workaround with features: _tls-rustls without a backend feature does not compile, because provider is not defined.

    What solves our case. Either of these:

    1. The "everything else" path of this RFC: a user-supplied rustls::ClientConfig (also feat: Allow users to specify a prebuilt 'rustls' configuration for TLS #4051). This is the full solution.
    2. A small step that does not break the API and can ship before the reorganization: use the installed provider if there is one. If not, use the current default.
    let provider = rustls::crypto::CryptoProvider::get_default()
        .cloned()
        .unwrap_or_else(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()));

    Notes for option 2:

    • Do the same in the ring branch.
    • The .unwrap() on with_safe_default_protocol_versions() (L107-L109) is safe only for the built-in providers. An installed provider can have no usable cipher suites. An example is rustls-cng-crypto when Windows FIPS mode is off. Return Error::Tls there instead of a panic.
    • SemVer hazard (from Add support for using rustls without a default crypto provider or root certs #4033): if the application uses a semver-incompatible rustls, get_default() reads a different static and returns None. SQLx then uses the current default. It does not panic.
    • Behavior change: an application that installed a process-wide provider now gets that provider for database TLS. The rustls documentation recommends this behavior for libraries.

    Related: #4033, #4051.

  8. moroznah commented on Nov 4, 2026

    @moroznah

    Another use case for the custom-provider path, on MySQL/MariaDB.

    A long-running daemon wants one process-wide rustls CryptoProvider (aws-lc-rs, with the hybrid X25519MLKEM768 group first) and the OS certificate store for every TLS client it runs. Its MySQL/MariaDB connections through sqlx 0.9.0 cannot have both:

    • with tls-rustls-ring-native-roots, sqlx-core builds the configuration from rustls::crypto::ring::default_provider() (src/net/tls/tls_rustls.rs:100-109 in 0.9.0), so database TLS offers ring's groups (X25519, P-256, P-384) and never the hybrid one, and ring stays linked beside the provider the process installed;
    • TlsConfig (src/net/tls/mod.rs:60-67) has no provider or ClientConfig member;
    • the one aws-lc-rs feature, _tls-rustls-aws-lc-rs (Cargo.toml:64-68 as published), brings webpki-roots rather than the OS store (feat(tls): support native roots with the rustls AWS-LC backend #4475 would add that pairing).

    On main the provider is still chosen by feature inside connector() (sqlx-core/src/net/tls/tls_rustls.rs:96-111), and the result is kept as RustlsConnector { config: Arc<ClientConfig> }, which looks like a natural home for a caller-supplied config.

    Either path in this RFC covers the case: a user-supplied rustls::ClientConfig on MySqlConnectOptions as well as PgConnectOptions (#4051 covers Postgres only so far), or the non-breaking CryptoProvider::get_default() step proposed above. Only the first lets an application leave ring out of the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions