Conversation
|
I don't think we should make cryptographic choices for people. This also breaks down when someone installs |
Agreed. The latest commit drops the both-features auto-pick. Both backends, or neither, now return The neither-feature panic-to-error change is unchanged. |
|
I don't quite see the point of this though, in general using |
No. encode/decode already return Result, and a miss should not consume the install slot. The previous dummy OnceLock made the first Err unrecoverable. This tip leaves the lock empty, so A test without either feature now checks that |
|
But does someone really write code like this? let token = match jsonwebtoken::encode(&header, &claims, &encoding_key) {
Ok(t) => t,
Err(e) => match e.kind() {
jsonwebtoken::errors::ErrorKind::MissingCryptoProvider => {
some_provider::install_default().unwrap();
jsonwebtoken::encode(&header, &claims, &encoding_key)?
}
_ => todo!(), // something else failed
},
};I understand that the encoding failure immediately kills the program, as that was the point. I don't understand how / why someone would write code to deal with a failure of this kind, when the simple solution is just to initialise the provider beforehand. some_provider::install_default().unwrap();
let token = jsonwebtoken::encode(&header, &claims, &encoding_key)?; // if an error, a different much more helpful one |
`get_default` was `PROCESS_DEFAULT_PROVIDER.get_or_init(from_crate_features)`, and `from_crate_features` returned a dummy `CryptoProvider` whose factory fns panic. The dummy was therefore stored in the `OnceLock` before the panic fired, so any caller that catches the unwind - `tokio::spawn`, `CatchPanicLayer`, an actix worker, libtest - kept running with the install slot consumed, and `install_default` returned `Err` from then on. The panic message's own instruction to call `install_default` was no longer possible to follow. rustls, which this module is modelled on, has the same two functions and never stores a sentinel: `from_crate_features` returns `Option<Self>` and `get_default_or_install_from_crate_features` calls `.expect()` on it, panicking before anything reaches the lock. Restore that shape, keeping the panic and its message unchanged. Callers with exactly one backend feature enabled are unaffected, since that path never touched the dummy. Also split the panic message so it names which case applies, since both backends enabled and neither enabled previously produced the same text, and add `CryptoProvider::try_get_default() -> Option<&'static CryptoProvider>` so an application can assert a backend is available at startup without consuming the install slot to find out. (rustls spells this one `get_default`; named `try_get_default` here to leave the existing internal accessor alone.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019JmrSYP2rFBMJeP1caiyAu
f1e1fd1 to
3d05401
Compare
|
You're right. A missing provider is a configuration error, there's no sensible code to write at the call site, and wrapping it in There's a separate bug underneath that I buried in the API argument, and it stands whether or not we keep the panic.
rustls, which this module is modelled on, has the same two functions and never stores a sentinel: // rustls 0.23.35, crypto/mod.rs:238-257
pub(crate) fn get_default_or_install_from_crate_features() -> &'static Arc<Self> {
if let Some(provider) = Self::get_default() {
return provider;
}
let provider = Self::from_crate_features()
.expect(r###"
Could not automatically determine the process-level CryptoProvider from Rustls crate features.
..."###);
let _ = provider.install_default();
Self::get_default().unwrap()
}
It matters anywhere unwinds get caught: I've pushed the reduced version, so the diff is the real thing rather than a description. It's now one source file:
I'll move One thing I noticed while testing and am not touching here: |
|
Split out as #541 (draft, stacked on this one — only the second commit belongs to it). Keeping it in draft until this PR is resolved, since on |
Reduced after review. The earlier version of this PR converted the missing-provider panic into
ErrorKind::MissingCryptoProvider. @arckoor pushed back — a missing provider is a configuration error, not something a caller can act on — and I agree. That change is gone. The panic stays.What's left is a bug that is independent of the panic-vs-error question.
The bug
get_defaultwas:from_crate_featuresreturns a dummyCryptoProviderwhose factory fns panic — it does not panic itself. So the dummy is stored in theOnceLockfirst, and the panic only fires later, when a factory fn is called.install_defaultisOnceLock::set, so from that point it returnsErrforever.This bites wherever unwinds are caught and the process keeps running:
tokio::spawnreturnsErr(JoinError),tower-http'sCatchPanicLayerturns it into a 500, actix-web restarts the worker thread, libtest catches it per test. In all of those the panic message's own instruction — "CallCryptoProvider::install_default()before this point" — has become impossible to follow.tests/missing_provider.rscatches the panic and assertsinstall_default()still succeeds. It fails on master and passes here.The fix
rustls, which this module is modelled on, has the same two functions and never stores a sentinel (0.23.35, crypto/mod.rs:238-257):
from_crate_featuresreturnsOption<Self>, andget_default_or_install_from_crate_featurescalls.expect()on it, so the panic fires before anything reaches the lock. We inherited rustls's panic message almost verbatim but replaced theOptionwith a stored dummy. This restores the upstream shape.static INSTANCEdummy;from_crate_featuresreturnsOption.OnceLock. Same panic, same message, same signature — no call sites change, and anyone with exactly one backend feature enabled is unaffected, since that path never touched the dummy.cfgso it names which case applies. Both-enabled and neither-enabled previously produced identical text, and the both-enabled case usually arrives via Cargo feature unification, where it is least obvious.pub fn CryptoProvider::try_get_default() -> Option<&'static CryptoProvider>for a startup assertion. There is currently no way to ask "is a provider installed?" without callinginstall_defaultand consuming the slot to find out. rustls spells this oneget_default; I usedtry_get_defaultto avoid renaming the existing internal accessor and churning every call site. Happy to drop this or split it out — it is new public API and that is @Keats's call.Validation
cargo test --features aws_lc_rscargo test --features rust_cryptocargo test --no-default-features --features use_pem --test missing_providercargo clippy --all-targets -- -D warnings, all three configscargo fmt --checkSplit out of this PR
KeyUtils::new_unimplementedreturningErrinstead of panicking — will open separately. With a custom provider that skips JWKs, whether you reach that path depends on the key the caller passes at runtime, not onCargo.toml, which reads like the same line drawn in Don't panic when computing thumbprint #524.Separately:
cargo test --features aws_lc_rs,rust_cryptofails on master today (4jwk::tests) because the dummy'sKeyUtilspanics. Identical before and after this change, so not addressed here.Ref #456