Skip to content

fix(dtls): replace panicking unwrap/expect in crypto hot paths#68

Open
nightness wants to merge 7 commits into
webrtc-rs:masterfrom
Brainwires:fix/dtls-crypto-panics
Open

fix(dtls): replace panicking unwrap/expect in crypto hot paths#68
nightness wants to merge 7 commits into
webrtc-rs:masterfrom
Brainwires:fix/dtls-crypto-panics

Conversation

@nightness
Copy link
Copy Markdown

@nightness nightness commented Apr 1, 2026

Summary

  • crypto/mod.rs generate_self_signed_with_alg: replaces 3x .unwrap() with ? -- function already returns Result<Self>
  • config.rs ConfigBuilder::build(): replaces .build().unwrap() on the rustls WebPkiServerVerifier builder with .map_err(|e| Error::Other(...))? -- function already returns Result<HandshakeConfig>
  • config.rs HandshakeConfig::default(): replaced panicking WebPkiServerVerifier::build().expect(...) with a PlaceholderServerCertVerifier; ConfigBuilder::build() now sets all fields explicitly instead of relying on ..Default::default()
  • flight/flight5.rs Flight5::generate(): extracts the certificate ref with ok_or_else() returning a proper Fatal/InternalError DTLS alert instead of panicking with .unwrap()
  • crypto/mod.rs CryptoPrivateKey: fields are now pub(crate) (with public accessors kind() / serialized_der()) to enforce the invariant that kind and serialized_der are always consistent, making Clone's .expect() sound
  • rtc/peer_connection/certificate: refactored to use CryptoPrivateKey::from_key_pair() instead of constructing the struct directly with raw fields

Why This Matters

Panics in DTLS crypto paths terminate the peer connection (and potentially the process) instead of propagating a negotiation error. The flight5 unwrap in particular can fire during an active handshake if the local certificate is unexpectedly absent.

Test Plan

  • cargo check passes (all crates)
  • cargo clippy passes
  • cargo fmt --check passes
  • cargo test -p rtc-dtls passes (49/49)

Generated with Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 37.50000% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.15%. Comparing base (9feb4a3) to head (c8122f9).

Files with missing lines Patch % Lines
rtc-dtls/src/flight/flight5.rs 23.07% 10 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_alg instead of panicking.
  • Makes ConfigBuilder::build() handle WebPkiServerVerifier::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 evaluate HandshakeConfig::default() even though several fields are overridden. Since Default::default() currently constructs a WebPkiServerVerifier with .expect(...), this build() 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) so ConfigBuilder::build() doesn’t depend on a potentially-panicking Default implementation.
        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.

Comment thread rtc-dtls/src/config.rs Outdated
Comment on lines 184 to 193
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"),
),
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@rainliu
Copy link
Copy Markdown
Member

rainliu commented Apr 4, 2026

please fix fmt warning before merge.

Copy link
Copy Markdown
Member

@rainliu rainliu left a comment

Choose a reason for hiding this comment

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

fix fmt warning

nightness added a commit to Brainwires/webrtc-rs-rtc that referenced this pull request Apr 8, 2026
- 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>
@nightness nightness requested review from Copilot and rainliu April 8, 2026 07:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread rtc/src/peer_connection/certificate/mod.rs Outdated
Comment thread rtc-dtls/src/crypto/mod.rs Outdated
Comment thread rtc-dtls/src/crypto/mod.rs Outdated
Comment thread rtc-dtls/src/config.rs
Comment on lines 492 to 499
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,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +398 to +408
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(),
)),
)
})?;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +398 to +402
let cert_ref = certificate.as_ref().ok_or_else(|| {
(
Some(Alert {
alert_level: AlertLevel::Fatal,
alert_description: AlertDescription::InternalError,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment thread rtc-dtls/src/flight/flight5.rs Outdated
Comment thread rtc-dtls/src/crypto/mod.rs Outdated
nightness and others added 7 commits April 10, 2026 00:13
- 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>
@nightness nightness force-pushed the fix/dtls-crypto-panics branch from 6e5184d to 137fe6d Compare April 10, 2026 05:14
@nightness
Copy link
Copy Markdown
Author

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.

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.

3 participants