fix(dtls): replace panicking unwrap/expect in crypto hot paths#68
fix(dtls): replace panicking unwrap/expect in crypto hot paths#68nightness wants to merge 7 commits into
Conversation
fdf2918 to
c8122f9
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #68 +/- ##
==========================================
- Coverage 71.17% 71.15% -0.02%
==========================================
Files 442 442
Lines 67330 67338 +8
==========================================
- Hits 47922 47917 -5
- Misses 19408 19421 +13 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR reduces panic risk in DTLS crypto/handshake code paths by replacing several unwrap()/expect() calls with proper error propagation and DTLS alert reporting.
Changes:
- Propagates rcgen failures in
Certificate::generate_self_signed_with_alginstead of panicking. - Makes
ConfigBuilder::build()handleWebPkiServerVerifier::build()errors instead of unwrapping. - Avoids panicking on missing local certificate during Flight5 certificate-verify generation, returning a fatal DTLS alert + error instead.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
rtc-dtls/src/flight/flight5.rs |
Replaces a certificate unwrap() with a Result-based error/alert return. |
rtc-dtls/src/crypto/mod.rs |
Replaces rcgen unwrap()s with ?, and improves diagnosability of CryptoPrivateKey::clone panics. |
rtc-dtls/src/config.rs |
Replaces verifier builder unwrap() with error mapping in ConfigBuilder::build(), and adds an expect() in Default. |
Comments suppressed due to low confidence (1)
rtc-dtls/src/config.rs:354
HandshakeConfig { ..Default::default() }will eagerly evaluateHandshakeConfig::default()even though several fields are overridden. SinceDefault::default()currently constructs aWebPkiServerVerifierwith.expect(...), thisbuild()can still panic here (and also duplicates verifier construction work). Consider avoiding struct update syntax and instead initialize all fields explicitly (or add a fallible constructor) soConfigBuilder::build()doesn’t depend on a potentially-panickingDefaultimplementation.
Ok(HandshakeConfig {
local_psk_callback: self.psk.take(),
local_psk_identity_hint: self.psk_identity_hint.take(),
local_cipher_suites,
local_signature_schemes,
extended_master_secret: self.extended_master_secret,
local_srtp_protection_profiles: self.srtp_protection_profiles,
server_name,
client_auth: self.client_auth,
local_certificates: self.certificates,
insecure_skip_verify: self.insecure_skip_verify,
insecure_verification: self.insecure_verification,
verify_peer_certificate: self.verify_peer_certificate.take(),
roots_cas: self.roots_cas,
server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new(
gen_self_signed_root_cert(),
))
.build()
.map_err(|e| Error::Other(e.to_string()))?,
client_cert_verifier: None,
retransmit_interval,
initial_epoch: 0,
maximum_transmission_unit,
replay_protection_window,
..Default::default()
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl Clone for CryptoPrivateKey { | ||
| fn clone(&self) -> Self { | ||
| // Safety: `serialized_der` is always produced by `from_key_pair` which serialises a | ||
| // valid key. Re-parsing the same bytes cannot fail, so these unwraps are sound. | ||
| match self.kind { | ||
| CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey { | ||
| kind: CryptoPrivateKeyKind::Ed25519( | ||
| Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(), | ||
| Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der) | ||
| .expect("CryptoPrivateKey::clone: Ed25519 DER re-parse failed"), | ||
| ), |
There was a problem hiding this comment.
The // Safety: rationale for the .expect(...)s in CryptoPrivateKey::clone assumes serialized_der is always produced by from_key_pair, but CryptoPrivateKey’s fields are pub, so callers can construct inconsistent kind/serialized_der pairs that will panic on clone. Either enforce the invariant (e.g., make fields private and provide constructors/accessors), or adjust the comment/message to clearly state this is an internal invariant and that panicking indicates misuse/bug.
|
please fix fmt warning before merge. |
- Replace panicking WebPkiServerVerifier::build() in HandshakeConfig::default() with a PlaceholderServerCertVerifier; ConfigBuilder::build() now sets all fields explicitly instead of relying on ..Default::default() - Make CryptoPrivateKey fields pub(crate) to enforce the kind/serialized_der consistency invariant; add public accessors kind() and serialized_der() - Refactor rtc peer_connection certificate to use CryptoPrivateKey::from_key_pair() instead of constructing the struct directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| insecure_verification: false, | ||
| verify_peer_certificate: None, | ||
| roots_cas: rustls::RootCertStore::empty(), | ||
| server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new( | ||
| gen_self_signed_root_cert(), | ||
| )) | ||
| .build() | ||
| .unwrap(), | ||
| // Placeholder: ConfigBuilder::build() always replaces this with a real verifier. | ||
| server_cert_verifier: Arc::new(PlaceholderServerCertVerifier), | ||
| client_cert_verifier: None, | ||
| retransmit_interval: std::time::Duration::from_secs(0), | ||
| initial_epoch: 0, |
There was a problem hiding this comment.
HandshakeConfig::default() now installs a PlaceholderServerCertVerifier that always errors. Since HandshakeConfig is pub (public API), this makes HandshakeConfig::default() effectively unusable and changes behavior from the previous real WebPkiServerVerifier default. Consider keeping a real verifier in Default and handling build failures without panicking (e.g., build().unwrap_or_else(|e| ...) / logging + fallback), or make HandshakeConfig construction go through ConfigBuilder only.
| let cert_ref = certificate.as_ref().ok_or_else(|| { | ||
| ( | ||
| Some(Alert { | ||
| alert_level: AlertLevel::Fatal, | ||
| alert_description: AlertDescription::InternalError, | ||
| }), | ||
| Some(Error::Other( | ||
| "no local certificate available for DTLS flight5".to_owned(), | ||
| )), | ||
| ) | ||
| })?; |
There was a problem hiding this comment.
This change introduces a new failure path when certificate is None (returning a fatal InternalError alert). Consider adding a unit/integration test that exercises Flight5 generation with no local certificate to ensure the alert and error are propagated as intended and to prevent regressions back to panics.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let cert_ref = certificate.as_ref().ok_or_else(|| { | ||
| ( | ||
| Some(Alert { | ||
| alert_level: AlertLevel::Fatal, | ||
| alert_description: AlertDescription::InternalError, |
There was a problem hiding this comment.
certificate.as_ref().ok_or_else(...) is effectively unreachable here: this block is guarded by state.remote_requested_certificate && !cfg.local_certificates.is_empty(), and certificate is constructed as Some(..) precisely when cfg.local_certificates is non-empty. As a result, the new alert/error path can never trigger, and the extra Result plumbing adds complexity without changing behavior. Consider rewriting this section to avoid the redundant Option handling (e.g., pattern-match certificate once and only enter the CertificateVerify path when it is Some).
- generate_self_signed_with_alg: replace 3x .unwrap() with ? operator - Config::build(): replace .build().unwrap() on rustls verifier with ? - flight5::generate(): extract certificate ref with ok_or_else() instead of .unwrap(), returning a proper Fatal/InternalError DTLS alert - CryptoPrivateKey::Clone: add Safety comment explaining why re-parsing serialized_der is sound; upgrade bare .unwrap() to .expect() with descriptive message so panics are diagnosable if they ever occur Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace panicking WebPkiServerVerifier::build() in HandshakeConfig::default() with a PlaceholderServerCertVerifier; ConfigBuilder::build() now sets all fields explicitly instead of relying on ..Default::default() - Make CryptoPrivateKey fields pub(crate) to enforce the kind/serialized_der consistency invariant; add public accessors kind() and serialized_der() - Refactor rtc peer_connection certificate to use CryptoPrivateKey::from_key_pair() instead of constructing the struct directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Propagate rcgen error from params.self_signed() instead of unwrap() - Match on &self.kind in Clone impl to avoid moving from &self - Fix doc comment: fields are pub(crate), not private - Add warning doc comment on HandshakeConfig::default() about placeholder verifier - Add test for Flight5 no-certificate error path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add tests for: - CryptoPrivateKey::kind() and serialized_der() accessors - CryptoPrivateKey::clone() for Ed25519 and ECDSA key types - CryptoPrivateKey::from_key_pair() for Ed25519 and ECDSA - TryFrom<&KeyPair> delegation to from_key_pair - generate_certificate_verify with Ed25519 and ECDSA keys - PlaceholderServerCertVerifier (verify_server_cert, supported_verify_schemes) - HandshakeConfig::default() uses placeholder verifier - ConfigBuilder::build() sets explicit fields and real verifier - RTCCertificate::from_key_pair with Ed25519 fingerprint validation - RTCCertificate::from_existing with custom expiry - RTCCertificate::from_key_pair rejects unsupported key types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… and test - Make `kind` and `serialized_der` fully private (not pub(crate)) and update all crate-internal callers to use the public accessors - Update doc comment and Clone safety comment to reflect truly private fields - Add doc comment on HandshakeConfig warning against direct Default usage - Replace unreachable ok_or_else in flight5.rs with expect + safety comment - Rewrite flight5 test to validate actual behavior with incomplete state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ok_or_else error path is currently unreachable due to the outer guard, but keeping it avoids introducing a new panic in a PR that removes panics. Defense-in-depth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6e5184d to
137fe6d
Compare
|
Rebased onto upstream/master so this PR contains only its own changes. Previous branch structure caused merge conflicts when PRs were merged in sequence. Each PR is now independently mergeable. |
Summary
crypto/mod.rsgenerate_self_signed_with_alg: replaces 3x.unwrap()with?-- function already returnsResult<Self>config.rsConfigBuilder::build(): replaces.build().unwrap()on the rustlsWebPkiServerVerifierbuilder with.map_err(|e| Error::Other(...))?-- function already returnsResult<HandshakeConfig>config.rsHandshakeConfig::default(): replaced panickingWebPkiServerVerifier::build().expect(...)with aPlaceholderServerCertVerifier;ConfigBuilder::build()now sets all fields explicitly instead of relying on..Default::default()flight/flight5.rsFlight5::generate(): extracts the certificate ref withok_or_else()returning a properFatal/InternalErrorDTLS alert instead of panicking with.unwrap()crypto/mod.rsCryptoPrivateKey: fields are nowpub(crate)(with public accessorskind()/serialized_der()) to enforce the invariant thatkindandserialized_derare always consistent, makingClone's.expect()soundrtc/peer_connection/certificate: refactored to useCryptoPrivateKey::from_key_pair()instead of constructing the struct directly with raw fieldsWhy This Matters
Panics in DTLS crypto paths terminate the peer connection (and potentially the process) instead of propagating a negotiation error. The
flight5unwrap in particular can fire during an active handshake if the local certificate is unexpectedly absent.Test Plan
cargo checkpasses (all crates)cargo clippypassescargo fmt --checkpassescargo test -p rtc-dtlspasses (49/49)Generated with Claude Code