Skip to content

feat(tls): add an async-tls-rustls-no-provider backend - #2

Merged
forust merged 2 commits into
oksyd:mainfrom
stephane-segning:feat/async-tls-rustls-no-provider
Sep 24, 2026
Merged

forust merged 2 commits into
oksyd:mainfrom
stephane-segning:feat/async-tls-rustls-no-provider

Conversation

@stephane-segning

Copy link
Copy Markdown
Contributor

Closes #1.

What this does

Adds a third async rustls backend, async-tls-rustls-no-provider, that takes its
crypto provider from the process-wide default the application installed instead
of one this crate hard-selects.

Why it needed code, not just a feature

My issue proposed this as a Cargo.toml one-liner. Reading the crate, that was
wrong — the provider is chosen in code, not only in features:

// build_rustls_ring_transport
let tls_config = build_rustls_tls_config(
    TlsBackend::RustlsRing,
    rustls::crypto::ring::default_provider(),
    tls_options,
)?;

with a parallel pair for aws-lc-rs and a #[cfg(not(...))] stub each. So a
no-provider path needs its own TlsBackend variant, its own builder pair, a
dispatcher arm, and an entry in the compile_error! guard that enforces "one
async-tls-* feature must be enabled".

TlsBackend is already #[non_exhaustive], so the new variant is semver-minor
for downstream crates.

The one design decision worth reviewing

The provider is fetched, not assumed:

let provider = rustls::crypto::CryptoProvider::get_default()
    .cloned()
    .ok_or_else(|| Error::TlsBackendInit { backend: ..., message: ... })?;

rustls::ClientConfig::builder() would have been shorter, but it panics when
no provider is installed — which is precisely the situation this feature makes
reachable. A returned TlsBackendInit naming install_default seemed the only
defensible behaviour for a library. Happy to change it if you disagree.

One change outside the async path

Two matches in src/blocking_client/transport.rs are exhaustive over
TlsBackend — #[non_exhaustive] constrains downstream crates, not this one — so
they stopped compiling under any blocking feature once the variant existed.

Both are handled explicitly rather than with a _ arm: backend_is_available
returns false, and build_sync_tls_config returns TlsBackendUnavailable, since
there is no ureq counterpart. That keeps the property that adding a future
backend produces a compile error here instead of silently falling into a catch-all.

Verification

Run on the pushed commit, not just locally at some earlier point:

check result
cargo check — no-provider / ring / aws-lc-rs / native / blocking-ring / --all-features all pass
cargo fmt --check clean
cargo clippy -- -D warnings, new feature and defaults clean
cargo test --no-run builds

The claim that actually matters, with a positive control so it cannot pass
vacuously:

# production graph only, new feature
$ cargo tree --features async-tls-rustls-no-provider -e normal -i ring
warning: nothing to print.
$ cargo tree --features async-tls-rustls-no-provider -e normal -i aws-lc-rs
error: package ID specification `aws-lc-rs` did not match any packages

# same query, ring feature - proves the query detects ring when present
$ cargo tree --features async-tls-rustls-ring -e normal -i ring
ring v0.17.14

-e normal is deliberate: ring is still reachable as a dev-dependency via
rcgen, identically on every feature, which is unrelated to backend selection.

Context

We run an image-processing service that removed every C and C++ dependency and
enforces it in CI. s3 builds on reqx and
already signs SigV4 with the pure-Rust graviola, so this feature is the last
thing between that crate and a fully pure-Rust S3 client. There is also a FIPS
case: aws-lc-rs's FIPS mode needs the same escape hatch.

Precedent for the shape: reqwest ships rustls-no-provider, and ureq ships
one that this crate already depends on in blocking-tls-rustls-aws-lc-rs.

Notes

The issue offered an alternative shape — one async-tls-rustls feature plus
separate provider features, which would be cleaner but breaking. I built the
additive version since it is non-breaking; say the word if you would rather have
the other and I will rework it.

No CHANGELOG entry, since git cliff generates it from commits at release time.

stephane-segning and others added 2 commits September 22, 2026 06:54
Closes oksyd#1.

Every async rustls path currently hard-selects a crypto provider in code -
`build_rustls_ring_transport` passes `rustls::crypto::ring::default_provider()`
into `builder_with_provider`, with a parallel pair for aws-lc-rs. Because
Cargo feature unification is additive, a downstream crate cannot switch that
off, so enabling rustls at all means linking a C/assembly crypto library.

This adds a third rustls backend that takes the provider from the process-wide
default the application installed, leaving the choice to the caller: a pure-Rust
provider such as `rustls-graviola`, `aws-lc-rs` in FIPS mode, or anything else.

`TlsBackend` is `#[non_exhaustive]`, so the new variant is semver-minor for
downstream crates.

**The provider is fetched, not assumed.** `build_rustls_no_provider_transport`
calls `CryptoProvider::get_default()` and returns `Error::TlsBackendInit` with
a message naming `install_default` when nothing is installed. Using
`rustls::ClientConfig::builder()` would have been shorter, but it *panics* in
exactly that case - a returned error is the point of the feature.

Two `match`es in `src/blocking_client/transport.rs` are exhaustive over
`TlsBackend` (`#[non_exhaustive]` constrains downstream crates, not this one),
so they needed the new variant to keep compiling under any blocking feature.
Both are handled explicitly rather than with a `_` arm - `backend_is_available`
returns false and `build_sync_tls_config` returns `TlsBackendUnavailable`,
since there is no `ureq` counterpart - so a future backend still gets a
compile error here instead of silently falling into a catch-all.

`DEFAULT_TLS_BACKEND` gains a branch for the case where this is the only
feature enabled; ring keeps precedence wherever it is on.

Verified on this commit:

- `cargo check` passes for no-provider, ring, aws-lc-rs, native,
  blocking-tls-rustls-ring, and `--all-features`.
- `cargo tree --features async-tls-rustls-no-provider -e normal -i ring` has
  nothing to print, and `-i aws-lc-rs` matches no package. The same query on
  the ring feature returns `ring v0.17.14`, so the negative result is the
  query working, not the query being broken.
- `cargo fmt --check` clean; `cargo clippy -- -D warnings` clean under both
  the new feature and the defaults; `cargo test --no-run` builds.
@forust
forust merged commit 4da8139 into oksyd:main Sep 24, 2026
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.

Add an async-tls-rustls-no-provider feature so callers can supply their own rustls provider

2 participants