From 6326a9bd23f63adab51a5ac1ecabeba123800365 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 24 Jul 2026 01:52:33 +0200 Subject: [PATCH 01/40] fix(dtls): ignore timeout before transport starts (#135) --- src/peer_connection/handler/dtls.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index 7d4fbe63..4215b8b6 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -323,16 +323,13 @@ impl<'a> sansio::Protocol Result<()> { - let dtls_endpoint = self - .ctx - .dtls_transport - .dtls_endpoint - .as_mut() - .ok_or(Error::ErrDtlsTransportNotStarted)?; + let Some(dtls_endpoint) = self.ctx.dtls_transport.dtls_endpoint.as_mut() else { + return Ok(()); + }; let remotes: Vec = dtls_endpoint.get_connections_keys().copied().collect(); for remote in remotes { - let _ = dtls_endpoint.handle_timeout(remote, now); + dtls_endpoint.handle_timeout(remote, now)?; } while let Some(transmit) = dtls_endpoint.poll_transmit() { self.ctx.write_outs.push_back(TaggedRTCMessageInternal { @@ -433,3 +430,17 @@ impl<'a> DtlsHandler<'a> { Ok((local_context, remote_context)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn timeout_before_dtls_starts_is_a_noop() { + let mut context = DtlsHandlerContext::default(); + let mut stats = RTCStatsAccumulator::default(); + let mut handler = DtlsHandler::new(&mut context, &mut stats); + + sansio::Protocol::handle_timeout(&mut handler, Instant::now()).unwrap(); + } +} From a10cd2c18f7c4e646e83f6f00b792ef38ac3cdb2 Mon Sep 17 00:00:00 2001 From: Lann Date: Thu, 23 Jul 2026 20:14:55 -0400 Subject: [PATCH 02/40] ice: send srflx/prflx checks from the candidate base, not the mapped address (#136) Connectivity checks and data writes for a server-reflexive local candidate were tagged with the candidate's NAT-mapped address, which no local socket is bound to; drivers routing outbound transmits by transport.local_addr had to drop them, so ICE never connected when the srflx path was the only viable one. Per RFC 8445 sec 6.1.2, checks for a reflexive candidate must be sent from its base. Add Candidate::base_addr() (related address for srflx/prflx, addr() otherwise) and use it in Agent::send_stun and the peer connection ICE handler write path. --- rtc-ice/src/agent/agent_test.rs | 57 ++++++++++++++ rtc-ice/src/agent/mod.rs | 5 +- rtc-ice/src/candidate/candidate_test.rs | 99 +++++++++++++++++++++++++ rtc-ice/src/candidate/mod.rs | 23 ++++++ src/peer_connection/handler/ice.rs | 5 +- 5 files changed, 187 insertions(+), 2 deletions(-) diff --git a/rtc-ice/src/agent/agent_test.rs b/rtc-ice/src/agent/agent_test.rs index 5746d5f4..6c3bc15f 100644 --- a/rtc-ice/src/agent/agent_test.rs +++ b/rtc-ice/src/agent/agent_test.rs @@ -2879,3 +2879,60 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { Ok(()) } + +/// Regression test: connectivity checks for a server-reflexive local candidate +/// must be tagged with the candidate's base (the bound host socket), not the +/// NAT-mapped srflx address (RFC 8445 §6.1.2). Drivers that route outbound +/// transmits by `transport.local_addr` otherwise have no socket to send from +/// and drop the packet. +#[test] +fn test_send_stun_from_srflx_uses_base_addr() -> Result<()> { + let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + + let srflx_local = CandidateServerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), // NAT mapping; no local socket here + port: 60823, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), // base: bound host socket + rel_port: 5000, + ..Default::default() + } + .new_candidate_server_reflexive()?; + a.add_local_candidate(srflx_local)?; + + let host_remote = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.11.1".to_owned(), + port: 37983, + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + a.add_remote_candidate(host_remote)?; + + a.write_outs.clear(); + + let msg = Message::new(); + a.send_stun(&msg, 0, 0); + + let transmit = a.write_outs.pop_front().expect("send_stun must emit"); + assert_eq!( + transmit.transport.local_addr, + "192.168.0.2:5000".parse().unwrap(), + "srflx checks must be sent from the candidate's base, not the mapped address" + ); + assert_eq!( + transmit.transport.peer_addr, + "10.79.11.1:37983".parse().unwrap() + ); + + a.close()?; + Ok(()) +} diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index 1f06a776..b039824f 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -1381,7 +1381,10 @@ impl Agent { pub(crate) fn send_stun(&mut self, msg: &Message, local_index: usize, remote_index: usize) { let peer_addr = self.remote_candidates[remote_index].addr(); - let local_addr = self.local_candidates[local_index].addr(); + // RFC 8445 §6.1.2: checks for a (server/peer-)reflexive candidate must + // be sent from its base, the bound local socket the candidate was + // derived from; the mapped address is not a local socket. + let local_addr = self.local_candidates[local_index].base_addr(); let transport_protocol = if self.local_candidates[local_index].network_type().is_tcp() { TransportProtocol::TCP } else { diff --git a/rtc-ice/src/candidate/candidate_test.rs b/rtc-ice/src/candidate/candidate_test.rs index 09c29459..51ce52b7 100644 --- a/rtc-ice/src/candidate/candidate_test.rs +++ b/rtc-ice/src/candidate/candidate_test.rs @@ -1,5 +1,9 @@ use super::*; +use crate::candidate::candidate_host::CandidateHostConfig; use crate::candidate::candidate_pair::CandidatePairState; +use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; +use crate::candidate::candidate_relay::CandidateRelayConfig; +use crate::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; use crate::candidate::{Candidate, unmarshal_candidate}; use std::time::Instant; @@ -429,3 +433,98 @@ fn test_candidate_marshal() -> Result<()> { Ok(()) } + +/// Regression test: a candidate's base address must be the local (bound) +/// transport address. For server/peer-reflexive candidates that is the +/// related address, not the NAT-mapped candidate address (RFC 8445 §5.1.1). +#[test] +fn test_candidate_base_addr() -> Result<()> { + let host = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "192.168.0.2".to_owned(), + port: 5000, + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + assert_eq!(host.base_addr(), host.addr(), "host base is itself"); + + let srflx = CandidateServerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60823, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5000, + ..Default::default() + } + .new_candidate_server_reflexive()?; + assert_eq!( + srflx.base_addr(), + "192.168.0.2:5000".parse().unwrap(), + "srflx base must be the related (host) address" + ); + assert_ne!(srflx.base_addr(), srflx.addr()); + + let prflx = CandidatePeerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60824, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5000, + ..Default::default() + } + .new_candidate_peer_reflexive()?; + assert_eq!( + prflx.base_addr(), + "192.168.0.2:5000".parse().unwrap(), + "prflx base must be the related (host) address" + ); + + // Missing/unparseable related address falls back to the candidate address. + let prflx_no_rel = CandidatePeerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60825, + component: 1, + ..Default::default() + }, + rel_addr: "".to_owned(), + rel_port: 0, + ..Default::default() + } + .new_candidate_peer_reflexive()?; + assert_eq!(prflx_no_rel.base_addr(), prflx_no_rel.addr()); + + let relay = CandidateRelayConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "50.0.0.1".to_owned(), + port: 5000, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5001, + ..Default::default() + } + .new_candidate_relay()?; + assert_eq!( + relay.base_addr(), + relay.addr(), + "relay base is the relayed address itself" + ); + + Ok(()) +} diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index fd55fa69..f113bded 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -327,6 +327,29 @@ impl Candidate { self.resolved_addr } + /// Returns the candidate's base address: the local transport address the + /// candidate was derived from, i.e. the address packets for this candidate + /// must be sent from (RFC 8445 §5.1.1). + /// + /// For server-reflexive and peer-reflexive candidates this is the related + /// (host) address; for host and relay candidates the base is the candidate + /// address itself. + pub fn base_addr(&self) -> SocketAddr { + match self.candidate_type { + CandidateType::ServerReflexive | CandidateType::PeerReflexive => self + .related_address + .as_ref() + .and_then(|ra| { + ra.address + .parse::() + .ok() + .map(|ip| SocketAddr::new(ip, ra.port)) + }) + .unwrap_or(self.resolved_addr), + _ => self.resolved_addr, + } + } + pub fn seen(&mut self, outbound: bool) { let now = Instant::now(); diff --git a/src/peer_connection/handler/ice.rs b/src/peer_connection/handler/ice.rs index 3fd35db7..490313e3 100644 --- a/src/peer_connection/handler/ice.rs +++ b/src/peer_connection/handler/ice.rs @@ -121,7 +121,10 @@ impl<'a> sansio::Protocol Date: Wed, 29 Jul 2026 09:16:28 +0900 Subject: [PATCH 03/40] fix(data_channel): default `ordered` to true, as documented (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RTCDataChannelInit` derived `Default`, so `ordered` came out `false` — contradicting both the field's own doc comment ("The default value of `true` guarantees that data will be delivered in order") and the W3C dictionary, where `ordered` is defined as `= true`. `create_data_channel(label, None)` did not consult that default at all: it left `DataChannelParameters` on its own derived default, which is `false` too. Route `None` through `RTCDataChannelInit::default()` so the documented defaults have a single definition. An unordered channel is not merely out-of-order. Unordered chunks bypass SCTP's ordered-delivery queue, so a first message can overtake the `DATA_CHANNEL_OPEN` sent on the same stream; the peer receives user data on a stream it has not accepted yet, `RTCDataChannelInternal::accept` rejects it as a non-DCEP PPID, and that error is logged and discarded on the pipeline's read pass. The message is lost with no error reaching either side. Closes #139 --- src/data_channel/init.rs | 24 +++++++++++- src/peer_connection/mod.rs | 52 ++++++++++++------------ tests/data_channel_init_defaults.rs | 61 +++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 tests/data_channel_init_defaults.rs diff --git a/src/data_channel/init.rs b/src/data_channel/init.rs index 295ada38..f0425dbd 100644 --- a/src/data_channel/init.rs +++ b/src/data_channel/init.rs @@ -21,7 +21,7 @@ /// ..Default::default() /// }; /// ``` -#[derive(Default, Clone)] +#[derive(Clone)] pub struct RTCDataChannelInit { /// If set to `false`, data is allowed to be delivered out of order. /// @@ -77,3 +77,25 @@ pub struct RTCDataChannelInit { /// [W3C specification]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit-negotiated pub negotiated: Option, } + +impl Default for RTCDataChannelInit { + /// The defaults defined by [W3C `RTCDataChannelInit`]. + /// + /// Note `ordered`: it defaults to `true`, so `..Default::default()` yields an ordered, + /// reliable channel — what an application asking for "a plain data channel" expects. + /// A derived `Default` would make it `false`, and an unordered channel does more than + /// reorder application messages: unordered chunks bypass SCTP's ordered-delivery queue, + /// so a first message can overtake the `DATA_CHANNEL_OPEN` sent on the same stream and + /// be dropped by the peer as data on a stream it has not accepted yet. + /// + /// [W3C `RTCDataChannelInit`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit + fn default() -> Self { + Self { + ordered: true, + max_packet_life_time: None, + max_retransmits: None, + protocol: String::new(), + negotiated: None, + } + } +} diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index 7a8a811b..d15b23d7 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -1812,38 +1812,40 @@ where let mut id = self.generate_data_channel_id()?; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #19) - if let Some(options) = options { - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) - if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() { - return Err(Error::ErrRetransmitsOrPacketLifeTime); - } + // `None` means "the dictionary defaults", which is what `RTCDataChannelInit::default()` + // spells out. Taking that route rather than leaving `params` on its derived default + // keeps a single definition of those defaults — notably `ordered`, which is `true`. + let options = options.unwrap_or_default(); + + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) + if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() { + return Err(Error::ErrRetransmitsOrPacketLifeTime); + } - // Ordered indicates if data is allowed to be delivered out of order. The - // default value of true, guarantees that data will be delivered in order. - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) - params.ordered = options.ordered; + // Ordered indicates if data is allowed to be delivered out of order. The + // default value of true, guarantees that data will be delivered in order. + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) + params.ordered = options.ordered; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) - params.max_packet_life_time = options.max_packet_life_time; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) + params.max_packet_life_time = options.max_packet_life_time; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) - params.max_retransmits = options.max_retransmits; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) + params.max_retransmits = options.max_retransmits; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) - params.protocol = options.protocol; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) + params.protocol = options.protocol; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) - if params.protocol.len() > 65535 { - return Err(Error::ErrProtocolTooLarge); - } + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) + if params.protocol.len() > 65535 { + return Err(Error::ErrProtocolTooLarge); + } - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) - params.negotiated = options.negotiated; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) + params.negotiated = options.negotiated; - if let Some(negotiated_id) = ¶ms.negotiated { - id = *negotiated_id; - } + if let Some(negotiated_id) = ¶ms.negotiated { + id = *negotiated_id; } let mut data_channel = RTCDataChannelInternal::new(id, params); diff --git a/tests/data_channel_init_defaults.rs b/tests/data_channel_init_defaults.rs new file mode 100644 index 00000000..49c22c52 --- /dev/null +++ b/tests/data_channel_init_defaults.rs @@ -0,0 +1,61 @@ +//! `RTCDataChannelInit`'s defaults must match what its documentation promises. +//! +//! `ordered` is the one that matters. A derived `Default` makes it `false`, so +//! `create_data_channel(label, None)` — the natural way to ask for a plain data channel — +//! would hand back an *unordered* one, contradicting both the field's own doc comment and +//! [W3C `RTCDataChannelInit`], where `ordered` is defined as `= true`. +//! +//! The consequence is not merely out-of-order application messages. Unordered chunks bypass +//! SCTP's ordered-delivery queue, so a first message can overtake the `DATA_CHANNEL_OPEN` +//! sent on the same stream; the peer then receives user data on a stream id it has not +//! accepted yet and drops it. +//! +//! [W3C `RTCDataChannelInit`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit + +use anyhow::Result; +use rtc::data_channel::RTCDataChannelInit; +use rtc::peer_connection::RTCPeerConnectionBuilder; + +#[test] +fn default_init_is_ordered() { + assert!( + RTCDataChannelInit::default().ordered, + "the documented default for `ordered` is true" + ); +} + +/// The default reaches the channel: passing `None` must not quietly opt out of ordering. +#[test] +fn channel_created_without_options_is_ordered() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let dc = pc.create_data_channel("plain", None)?; + + assert!( + dc.ordered(), + "a channel created without options must be ordered" + ); + + Ok(()) +} + +/// Opting out still works — this is a default, not a policy. +#[test] +fn unordered_can_still_be_requested() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let dc = pc.create_data_channel( + "unordered", + Some(RTCDataChannelInit { + ordered: false, + ..Default::default() + }), + )?; + + assert!( + !dc.ordered(), + "an explicit `ordered: false` must be honored" + ); + + Ok(()) +} From a1566b794d174b4d643c16267cdbe106c35a3e18 Mon Sep 17 00:00:00 2001 From: yexiyue <72074435+yexiyue@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:16:42 +0900 Subject: [PATCH 04/40] fix(data_channel): reject sends the write path cannot carry out (#138) `RTCDataChannel::send` checked only that the channel was registered, which it is from `create_data_channel` onwards. The condition that actually matters -- whether its SCTP stream exists -- was checked later, in `DataChannelHandler::handle_write`, and that runs on the pipeline's write pass where an `Err` is logged and discarded: if let Err(err) = handler.handle_write(msg) { warn!("{}.handle_write got error: {}", handler.name(), err); } So the caller was handed `Ok(())` for a message that was dropped on the floor, with only a stray warning to show for it. Sending on an already closed channel had the same shape. Check the real condition at the send boundary instead, where the error can still reach the caller: `ErrDataChannelNotOpen` (new variant -- `Error` is `#[non_exhaustive]`) while the channel is `connecting`, `ErrDataChannelClosed` once it is gone. Keeping them distinct matters: the first is worth retrying after the channel opens, the second never is. This also stops a rejected send from charging `outstanding_bytes`. Those bytes never entered the SCTP pipeline, so nothing would ever release them, and the leaked counter would permanently shrink the channel's send window. Note this errors where W3C `send()` prescribes buffering for a `connecting` channel. Buffering is the better contract and a much larger change; erroring is the smallest step that stops the silent data loss, and it is what the write path already decided -- it just could not say so. --- rtc-shared/src/error.rs | 6 ++ src/data_channel/mod.rs | 38 ++++++++++-- tests/data_channel_send_before_open.rs | 84 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 tests/data_channel_send_before_open.rs diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index ff035c04..4465af77 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -1247,6 +1247,12 @@ pub enum Error { #[error("data channel closed")] ErrDataChannelClosed, + /// ErrDataChannelNotOpen indicates a send was attempted on a data channel + /// whose underlying SCTP stream has not been established yet — its + /// `ready_state` is still `connecting`. Wait for the channel to open. + #[error("data channel is not open yet")] + ErrDataChannelNotOpen, + /// ErrDataChannelNonExist indicates an operation executed when the data /// channel not existed. #[error("data channel not existed")] diff --git a/src/data_channel/mod.rs b/src/data_channel/mod.rs index f59b06f5..79d7e301 100644 --- a/src/data_channel/mod.rs +++ b/src/data_channel/mod.rs @@ -241,11 +241,37 @@ where } } + /// Rejects a send the write path could not carry out. + /// + /// The condition mirrors what `DataChannelHandler::handle_write` requires: the channel + /// must be registered *and* its SCTP stream established. Checking it here, synchronously, + /// is what makes the failure visible — the handler runs later, on the pipeline's write + /// pass, where an `Err` is only logged and cannot reach the caller. + fn ensure_sendable(&self) -> Result<()> { + let dc = self + .peer_connection + .data_channels + .get(&self.id) + .ok_or(Error::ErrDataChannelClosed)?; + + if dc.data_channel.is_none() { + // No stream yet: either it is still being negotiated, or it is already gone. + return Err(if dc.ready_state == RTCDataChannelState::Connecting { + Error::ErrDataChannelNotOpen + } else { + Error::ErrDataChannelClosed + }); + } + + Ok(()) + } + /// send sends the binary message to the DataChannel peer + /// + /// Returns [`Error::ErrDataChannelNotOpen`] if the channel's SCTP stream has not been + /// established yet, and [`Error::ErrDataChannelClosed`] once it is gone. pub fn send(&mut self, data: BytesMut) -> Result<()> { - if !self.peer_connection.data_channels.contains_key(&self.id) { - return Err(Error::ErrDataChannelClosed); - } + self.ensure_sendable()?; let data_len = data.len(); self.peer_connection .handle_write(RTCMessage::DataChannelMessage( @@ -265,10 +291,10 @@ where } /// send_text sends the text message to the DataChannel peer + /// + /// Error contract matches [`send`](Self::send). pub fn send_text(&mut self, s: impl Into) -> Result<()> { - if !self.peer_connection.data_channels.contains_key(&self.id) { - return Err(Error::ErrDataChannelClosed); - } + self.ensure_sendable()?; let data = BytesMut::from(s.into().as_str()); let data_len = data.len(); self.peer_connection diff --git a/tests/data_channel_send_before_open.rs b/tests/data_channel_send_before_open.rs new file mode 100644 index 00000000..547c10ff --- /dev/null +++ b/tests/data_channel_send_before_open.rs @@ -0,0 +1,84 @@ +//! `send` must not report success for data it cannot carry. +//! +//! Until the SCTP stream backing a channel exists, the write path cannot deliver anything. That +//! failure used to be invisible: `send` checked only that the channel was *registered*, so it +//! returned `Ok(())`, and the real rejection happened later inside +//! `DataChannelHandler::handle_write` — on the pipeline's write pass, where an `Err` is logged +//! and discarded rather than returned to anyone. +//! +//! The caller was therefore told its message went out while it was dropped on the floor. That is +//! the worst of the three possible contracts: buffering (what [W3C `send()`] prescribes for a +//! `connecting` channel) and erroring are both recoverable, silence is not. +//! +//! [W3C `send()`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannel-send + +use anyhow::Result; +use bytes::BytesMut; +use rtc::data_channel::{RTCDataChannelInit, RTCDataChannelState}; +use rtc::peer_connection::RTCPeerConnectionBuilder; +use rtc::shared::error::Error; + +#[test] +fn send_before_the_stream_exists_is_rejected() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + assert_eq!( + dc.ready_state(), + RTCDataChannelState::Connecting, + "a freshly created channel has no stream yet" + ); + + assert_eq!( + dc.send(BytesMut::from(&b"dropped"[..])), + Err(Error::ErrDataChannelNotOpen), + "send must not claim success before the channel opens" + ); + assert_eq!( + dc.send_text("dropped"), + Err(Error::ErrDataChannelNotOpen), + "send_text must not claim success before the channel opens" + ); + + Ok(()) +} + +/// A rejected send must not be counted against the back-pressure budget: those bytes never +/// entered the SCTP pipeline, so nothing will ever release them. Leaking the counter would +/// permanently shrink the channel's send window. +#[test] +fn rejected_send_does_not_charge_outstanding_bytes() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + + let _ = dc.send(BytesMut::from(&b"dropped"[..])); + + assert_eq!( + dc.outstanding_bytes(), + 0, + "a send that never reached SCTP must not be charged" + ); + + Ok(()) +} + +/// Sending on an id that was never registered is a different failure from sending too early, +/// and the two should stay distinguishable — a caller can retry the second but not the first. +#[test] +fn send_on_a_closed_channel_reports_closed() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + let id = dc.id(); + dc.close()?; + + let mut dc = pc.data_channel(id).expect("handle survives close"); + assert_ne!( + dc.send(BytesMut::from(&b"dropped"[..])), + Ok(()), + "send on a closed channel must fail" + ); + + Ok(()) +} From 146b756315061502ea1e88f00fea7e53798619e1 Mon Sep 17 00:00:00 2001 From: yexiyue <72074435+yexiyue@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:20:02 +0900 Subject: [PATCH 05/40] fix(dtls): honor disable_certificate_fingerprint_verification (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SettingEngine::disable_certificate_fingerprint_verification` had a field and a setter but no reader: the flag was never passed to `RTCDtlsTransport`, so the handshake always installed the fingerprint-matching `verify_peer_certificate` callback and enabling the option had no observable effect. The neighbouring `allow_insecure_verification_algorithm` is plumbed through the exact same path, which makes the omission easy to spot side by side. Pass the flag down and build the callback only when the comparison is wanted. `with_verify_peer_certificate` takes an `Option`, and leaving it out is what disables the check — `insecure_skip_verify` is already true, so this callback is the only thing standing between the peer's certificate and acceptance. It does not weaken the "a certificate must be presented" requirement: `client_auth` is `RequireAnyClientCert`, which the DTLS layer enforces on its own (flight4 rejects an empty `peer_certificates` with `ErrClientCertificateRequired` before the callback would run). This is required by protocols where the answerer cannot know the offerer's fingerprint ahead of time. libp2p's WebRTC-Direct is the canonical case: the server synthesizes the client's offer locally with a placeholder fingerprint and authenticates the peer afterwards with a Noise handshake over the data channel. Without this, the DTLS handshake fails with ErrNoMatchingCertificateFingerprint. Adds an integration test covering both directions — a mismatched fingerprint connects with the option enabled, and still fails with it left at the default. --- src/peer_connection/internal.rs | 1 + src/peer_connection/transport/dtls/mod.rs | 71 +++-- .../dtls_disable_fingerprint_verification.rs | 256 ++++++++++++++++++ 3 files changed, 302 insertions(+), 26 deletions(-) create mode 100644 tests/dtls_disable_fingerprint_verification.rs diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index 274051b8..8230f0ca 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -92,6 +92,7 @@ where setting_engine.answering_dtls_role, setting_engine.srtp_protection_profiles.clone(), setting_engine.allow_insecure_verification_algorithm, + setting_engine.disable_certificate_fingerprint_verification, setting_engine.replay_protection, )?; diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 0831ca1e..3ca524f5 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -45,6 +45,7 @@ pub(crate) struct RTCDtlsTransport { pub(crate) answering_dtls_role: RTCDtlsRole, pub(crate) srtp_protection_profiles: Vec, pub(crate) allow_insecure_verification_algorithm: bool, + pub(crate) disable_certificate_fingerprint_verification: bool, pub(crate) replay_protection: ReplayProtection, } @@ -54,6 +55,7 @@ impl RTCDtlsTransport { answering_dtls_role: RTCDtlsRole, srtp_protection_profiles: Vec, allow_insecure_verification_algorithm: bool, + disable_certificate_fingerprint_verification: bool, replay_protection: ReplayProtection, ) -> Result { if !certificates.is_empty() { @@ -79,6 +81,7 @@ impl RTCDtlsTransport { answering_dtls_role, srtp_protection_profiles, allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification, replay_protection, }) } @@ -129,31 +132,47 @@ impl RTCDtlsTransport { self.dtls_role = self.derive_role(ice_role, remote_dtls_parameters.role); let remote_fingerprints = remote_dtls_parameters.fingerprints; - let verify_peer_certificate: VerifyPeerCertificateFn = Arc::new( - move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { - if certs.is_empty() { - return Err(Error::ErrNonCertificate); - } - - for fp in &remote_fingerprints { - if fp.algorithm != "sha-256" { - return Err(Error::ErrUnsupportedFingerprintAlgorithm); - } - - let mut h = Sha256::new(); - h.update(&certs[0]); - let hashed = h.finalize(); - let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); - let remote_value = values.join(":").to_lowercase(); - - if remote_value == fp.value.to_lowercase() { - return Ok(()); - } - } - - Err(Error::ErrNoMatchingCertificateFingerprint) - }, - ); + // Leaving the callback out is what disables the check: `insecure_skip_verify` is + // already true, so this comparison is the only thing standing between the peer's + // certificate and acceptance. Dropping it does not accept a peer that presents no + // certificate at all — `client_auth` is `RequireAnyClientCert`, which the DTLS layer + // enforces on its own. + // + // Protocols where the answerer cannot know the offerer's fingerprint ahead of time + // need this. libp2p's WebRTC-Direct is the canonical case: the server synthesizes the + // client's offer locally with a placeholder fingerprint and authenticates the peer + // afterwards with a Noise handshake over the data channel. + let verify_peer_certificate: Option = + if !self.disable_certificate_fingerprint_verification { + Some(Arc::new( + move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { + if certs.is_empty() { + return Err(Error::ErrNonCertificate); + } + + for fp in &remote_fingerprints { + if fp.algorithm != "sha-256" { + return Err(Error::ErrUnsupportedFingerprintAlgorithm); + } + + let mut h = Sha256::new(); + h.update(&certs[0]); + let hashed = h.finalize(); + let values: Vec = + hashed.iter().map(|x| format! {"{x:02x}"}).collect(); + let remote_value = values.join(":").to_lowercase(); + + if remote_value == fp.value.to_lowercase() { + return Ok(()); + } + } + + Err(Error::ErrNoMatchingCertificateFingerprint) + }, + )) + } else { + None + }; let certificate = if let Some(cert) = self.certificates.first() { cert.dtls_certificate.clone() @@ -173,7 +192,7 @@ impl RTCDtlsTransport { .with_client_auth(ClientAuthType::RequireAnyClientCert) .with_insecure_skip_verify(true) .with_insecure_verification(self.allow_insecure_verification_algorithm) - .with_verify_peer_certificate(Some(verify_peer_certificate)) + .with_verify_peer_certificate(verify_peer_certificate) .with_extended_master_secret(::dtls::config::ExtendedMasterSecretType::Require) .with_replay_protection_window(self.replay_protection.dtls) .build(self.dtls_role == RTCDtlsRole::Client, None)?, diff --git a/tests/dtls_disable_fingerprint_verification.rs b/tests/dtls_disable_fingerprint_verification.rs new file mode 100644 index 00000000..ff69a5cd --- /dev/null +++ b/tests/dtls_disable_fingerprint_verification.rs @@ -0,0 +1,256 @@ +//! Regression test for `SettingEngine::disable_certificate_fingerprint_verification`. +//! +//! The setter existed but the flag was never plumbed into `RTCDtlsTransport`, so the +//! DTLS handshake always installed the fingerprint-matching `verify_peer_certificate` +//! callback and enabling the option had no effect at all. +//! +//! This matters for protocols where the answerer *cannot* know the offerer's +//! fingerprint ahead of time. libp2p's WebRTC-Direct is the canonical example: the +//! server never receives a real offer — it synthesizes one locally from the incoming +//! STUN binding request, filling the `a=fingerprint` line with a placeholder, and +//! authenticates the peer afterwards with a Noise handshake over the data channel. +//! With the flag ignored, that handshake fails with `ErrNoMatchingCertificateFingerprint`. +//! +//! The two tests below pin both directions: with the option enabled a mismatched +//! fingerprint connects, and with it left at the default the same setup still fails. + +use anyhow::Result; +use bytes::BytesMut; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::event::RTCPeerConnectionEvent; +use rtc::peer_connection::state::RTCPeerConnectionState; +use rtc::peer_connection::transport::{ + CandidateConfig, CandidateHostConfig, RTCDtlsRole, RTCIceCandidate, +}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::sansio::Protocol; +use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; + +/// A fingerprint that matches no certificate, standing in for the placeholder a +/// WebRTC-Direct server puts in the offer it synthesizes for the client. +const PLACEHOLDER_FINGERPRINT: &str = "a=fingerprint:sha-256 \ +FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:\ +FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"; + +/// Long enough for a DTLS handshake over loopback, short enough to keep the +/// negative test from dominating the suite. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +struct Peer { + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, +} + +impl Peer { + async fn new(setting_engine: SettingEngine) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration(RTCConfigurationBuilder::new().build()) + .with_setting_engine(setting_engine) + .build()?; + + // Host candidate only: the peers talk over loopback, so no STUN is needed. + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + pc, + socket: Arc::new(socket), + local_addr, + }) + } +} + +/// Replaces the `a=fingerprint` line so the answerer is told to expect a +/// certificate the offerer will never present. +fn with_placeholder_fingerprint(sdp: &str) -> String { + sdp.lines() + .map(|line| { + if line.starts_with("a=fingerprint:") { + PLACEHOLDER_FINGERPRINT + } else { + line + } + }) + .collect::>() + .join("\r\n") + + "\r\n" +} + +/// Drives both peers until each reports `Connected`, or the timeout expires. +/// +/// Returns whether the DTLS handshake completed on both ends. +async fn connect(offer: &mut Peer, answer: &mut Peer) -> Result { + let (mut offer_connected, mut answer_connected) = (false, false); + let mut offer_buf = vec![0u8; 2000]; + let mut answer_buf = vec![0u8; 2000]; + let start = Instant::now(); + + while start.elapsed() < CONNECT_TIMEOUT && !(offer_connected && answer_connected) { + while let Some(msg) = offer.pc.poll_write() { + offer + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = offer.pc.poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + offer_connected = true; + } + } + + while let Some(msg) = answer.pc.poll_write() { + answer + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = answer.pc.poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + answer_connected = true; + } + } + + let next_timeout = offer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT) + .min( + answer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT), + ); + let delay = next_timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + + if delay.is_zero() { + offer.pc.handle_timeout(Instant::now()).ok(); + answer.pc.handle_timeout(Instant::now()).ok(); + continue; + } + + let sleep = tokio::time::sleep(delay); + tokio::pin!(sleep); + tokio::select! { + _ = sleep => { + offer.pc.handle_timeout(Instant::now()).ok(); + answer.pc.handle_timeout(Instant::now()).ok(); + } + Ok((n, peer_addr)) = offer.socket.recv_from(&mut offer_buf) => { + offer.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: offer.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&offer_buf[..n]), + }).ok(); + } + Ok((n, peer_addr)) = answer.socket.recv_from(&mut answer_buf) => { + answer.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: answer.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&answer_buf[..n]), + }).ok(); + } + } + } + + Ok(offer_connected && answer_connected) +} + +/// Runs the handshake with the answerer given a fingerprint that cannot match. +/// +/// `disable_verification` selects whether the answerer opts out of fingerprint +/// checking; everything else is identical between the two tests. +async fn handshake_with_mismatched_fingerprint(disable_verification: bool) -> Result { + let mut offer_setting_engine = SettingEngine::default(); + offer_setting_engine.set_answering_dtls_role(RTCDtlsRole::Client)?; + let mut offer = Peer::new(offer_setting_engine).await?; + + // The answerer takes the DTLS server role, mirroring a WebRTC-Direct listener. + let mut answer_setting_engine = SettingEngine::default(); + answer_setting_engine.set_answering_dtls_role(RTCDtlsRole::Server)?; + answer_setting_engine.disable_certificate_fingerprint_verification(disable_verification); + let mut answer = Peer::new(answer_setting_engine).await?; + + // A data channel is needed for the m-line that carries the DTLS parameters. + offer.pc.create_data_channel("test", None)?; + + let local_offer = offer.pc.create_offer(None)?; + offer.pc.set_local_description(local_offer.clone())?; + + // The answerer is handed an offer whose fingerprint the offerer cannot satisfy. + let mut tampered = local_offer; + tampered.sdp = with_placeholder_fingerprint(&tampered.sdp); + answer.pc.set_remote_description(tampered)?; + + let local_answer = answer.pc.create_answer(None)?; + answer.pc.set_local_description(local_answer.clone())?; + offer.pc.set_remote_description(local_answer)?; + + let connected = connect(&mut offer, &mut answer).await?; + + offer.pc.close().ok(); + answer.pc.close().ok(); + + Ok(connected) +} + +/// With verification disabled, a mismatched fingerprint must not block the handshake. +#[tokio::test] +async fn disabled_verification_accepts_mismatched_fingerprint() -> Result<()> { + assert!( + handshake_with_mismatched_fingerprint(true).await?, + "DTLS should complete when disable_certificate_fingerprint_verification is set" + ); + Ok(()) +} + +/// The same setup must still fail by default — otherwise the test above would pass +/// even if the option were ignored again. +#[tokio::test] +async fn default_verification_rejects_mismatched_fingerprint() -> Result<()> { + assert!( + !handshake_with_mismatched_fingerprint(false).await?, + "DTLS must reject a certificate that does not match the signaled fingerprint" + ); + Ok(()) +} From 480f019612cf9b38ab450ba9c8c5d136510ecb74 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Tue, 28 Jul 2026 22:09:46 -0700 Subject: [PATCH 06/40] making Interceptor object-safe and introduce BoxedInterceptor so that the peer connection can have one concrete type --- rtc-interceptor/src/lib.rs | 29 +++++++++++++++++++++++++++-- rtc-interceptor/src/registry.rs | 23 ++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index 44e74561..d42dd42a 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -313,8 +313,7 @@ pub trait Interceptor: Eout = (), Time = Instant, Error = shared::error::Error, - > + Sized - + Send + > + Send + Sync + 'static { @@ -335,6 +334,7 @@ pub trait Interceptor: /// ``` fn with(self, f: F) -> O where + Self: Sized, F: FnOnce(Self) -> O, O: Interceptor, { @@ -356,6 +356,31 @@ pub trait Interceptor: fn unbind_remote_stream(&mut self, info: &StreamInfo); } +/// A type-erased interceptor chain. +/// +/// `Interceptor` is object safe, so a chain built at runtime can be erased into this one +/// concrete type. That lets an application store a `RTCPeerConnection` +/// (see [`Registry::boxed`]) instead of being generic over the chain's type. +pub type BoxedInterceptor = Box; + +impl Interceptor for Box

{ + fn bind_local_stream(&mut self, info: &StreamInfo) { + (**self).bind_local_stream(info) + } + + fn unbind_local_stream(&mut self, info: &StreamInfo) { + (**self).unbind_local_stream(info) + } + + fn bind_remote_stream(&mut self, info: &StreamInfo) { + (**self).bind_remote_stream(info) + } + + fn unbind_remote_stream(&mut self, info: &StreamInfo) { + (**self).unbind_remote_stream(info) + } +} + #[cfg(test)] mod derive_tests { use super::*; diff --git a/rtc-interceptor/src/registry.rs b/rtc-interceptor/src/registry.rs index 5a3bb51e..19095119 100644 --- a/rtc-interceptor/src/registry.rs +++ b/rtc-interceptor/src/registry.rs @@ -19,8 +19,8 @@ //! Results in: C wraps B wraps A wraps NoopInterceptor //! ``` -use crate::Interceptor; use crate::noop::NoopInterceptor; +use crate::{BoxedInterceptor, Interceptor}; /// Registry for constructing interceptor chains. /// @@ -136,6 +136,27 @@ impl Registry

{ pub fn build(self) -> P { self.inner } + + /// Erase the chain's type, turning this into a `Registry`. + /// + /// The chain an application assembles at runtime is a deep nest of generic types + /// (`TwccSender>>`), which otherwise leaks into every + /// type that holds the peer connection. Boxing it collapses that to one concrete type, so + /// a struct can store an `RTCPeerConnection` field directly. + /// + /// # Example + /// + /// ```ignore + /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + /// let pc: RTCPeerConnection = RTCPeerConnectionBuilder::new() + /// .with_interceptor_registry(registry.boxed()) + /// .build()?; + /// ``` + pub fn boxed(self) -> Registry { + Registry { + inner: Box::new(self.inner), + } + } } #[cfg(test)] From c6544977cc02fbe716a97df59635352c1602bd42 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Tue, 28 Jul 2026 22:30:53 -0700 Subject: [PATCH 07/40] add integration tests/rtcp_processing_boxed_interop.rs and examples/rtcp-processing-boxed to demonstrate how to use RTCPeerConnection directly --- Cargo.toml | 5 + examples/README.md | 4 + examples/rtcp-processing-boxed/README.md | 168 ++++ .../rtcp-processing-boxed.rs | 587 ++++++++++++ tests/README.md | 47 + tests/rtcp_processing_boxed_interop.rs | 835 ++++++++++++++++++ 6 files changed, 1646 insertions(+) create mode 100644 examples/rtcp-processing-boxed/README.md create mode 100644 examples/rtcp-processing-boxed/rtcp-processing-boxed.rs create mode 100644 tests/rtcp_processing_boxed_interop.rs diff --git a/Cargo.toml b/Cargo.toml index 663fa51a..f3d23074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -296,6 +296,11 @@ name = "rtcp-processing" path = "examples/rtcp-processing/rtcp-processing.rs" bench = false +[[example]] +name = "rtcp-processing-boxed" +path = "examples/rtcp-processing-boxed/rtcp-processing-boxed.rs" +bench = false + [[example]] name = "data-channels-simple" path = "examples/data-channels-simple/data-channels-simple.rs" diff --git a/examples/README.md b/examples/README.md index 318f5228..f01a736f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,10 @@ check [Pion Examples](https://github.com/pion/webrtc/tree/master/examples#readme track. - ✅ [RTCP Processing](rtcp-processing): The rtcp-processing example demonstrates how to create a custom RtcpForwarderInterceptor using the derive macros. This allows access to media statistics and control information. +- ✅ [RTCP Processing Boxed](rtcp-processing-boxed): The rtcp-processing-boxed example is the type-erased variant of + rtcp-processing. It holds the peer connection as `RTCPeerConnection`, so an interceptor chain + assembled at runtime (here, switched by a `--no-rtcp-forwarding` flag) does not leak its type into the application: + a plain, non-generic struct can own the peer connection, and peers built with different chains share one type. - ✅ [Save to Disk AV1](save-to-disk-av1): The save-to-disk-av1 is a simple application that shows how to save a video to disk using AV1. - ✅ [Play from Disk Playlist Control](play-from-disk-playlist-control): Streams Opus pages from multi or single track diff --git a/examples/rtcp-processing-boxed/README.md b/examples/rtcp-processing-boxed/README.md new file mode 100644 index 00000000..3fec1764 --- /dev/null +++ b/examples/rtcp-processing-boxed/README.md @@ -0,0 +1,168 @@ +# rtcp-processing-boxed + +The type-erased counterpart of the [`rtcp-processing`](../rtcp-processing) example. + +It does exactly the same thing — installs a custom `RtcpForwarderInterceptor` so RTCP packets surface through +`poll_read()`, then prints them — but it stores the peer connection as `RTCPeerConnection` instead of +letting the interceptor chain's type leak into the application. Read `rtcp-processing` first for the RTCP concepts and +the forwarder interceptor itself; this README only covers what is different. + +## The problem: the chain's type is enormous, and infectious + +`RTCPeerConnection` is generic over its interceptor chain `I`. A chain built by `register_default_interceptors` +plus one custom layer has a type like: + +```text +RtcpForwarderInterceptor>>>>> +``` + +As long as the chain flows straight into a local variable, `impl Interceptor` hides this — that is what +`rtcp-processing` does: + +```rust +fn create_rtc_peer() -> Result> { /* ... */ } +``` + +That breaks down as soon as an application does anything more than hold the peer connection in a `let`: + +| You want to… | With `impl Interceptor` | +|-------------------------------------------------------------------------|-------------------------| +| store the peer connection in your own struct | the struct must become `MyStruct`, and `I` then spreads to every impl block and helper function | +| keep peers in a `Vec` / `HashMap` where chains differ per peer | impossible — different chains are different types | +| choose the chain in an `if` / `match` at runtime | impossible — the arms have different types | +| write a trait-object-free API boundary (`fn handle(pc: &mut ...)`) | must be generic | + +The usual workaround is to hand-write an application-level `trait PeerConnection { … }` that mirrors the whole +`RTCPeerConnection` API and store a `Box` — hundreds of lines of pure forwarding boilerplate. + +## The fix: erase the chain, not the peer connection + +`Interceptor` is object safe, so the chain itself can be boxed. [`Registry::boxed`] does that, and +`BoxedInterceptor` is the alias for `Box`: + +```rust +use rtc::interceptor::{BoxedInterceptor, Registry}; + +fn build_peer_connection( + forward_rtcp: bool, + mut media_engine: MediaEngine, +) -> Result> { + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // Two different chain types, unified by `.boxed()`. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + Ok(RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?) +} +``` + +Every peer connection now has the same concrete type, so an ordinary struct can own one: + +```rust +struct RtcpSession { + peer_connection: RTCPeerConnection, // no type parameter needed + socket: Arc, + local_addr: SocketAddr, + ssrc2kind: HashMap, + rtcp_count: u64, +} + +impl RtcpSession { // a plain impl block + async fn flush_writes(&mut self) { /* ... */ } + fn drain_events(&mut self) -> Result { /* ... */ } + fn drain_reads(&mut self) { /* ... */ } +} +``` + +Compare with `rtcp-processing`, where all of this logic has to live inline in `run()` because there is no nameable +type to hang it on. + +**Cost:** one virtual call per interceptor-chain entry point (`handle_read`, `poll_write`, `handle_timeout`, …). The +chain's *interior* is untouched — the layers still call each other through static dispatch and inline as before. If +your chain is fixed at compile time, keep `RTCPeerConnection`; nothing about it changed. + +## Instructions + +### Open the rtcp-processing example page + +[jsfiddle.net](https://jsfiddle.net/zurq6j7x/) — the same page the `rtcp-processing` example uses. You should see two +text-areas, a 'Start Session' button and 'Copy browser SessionDescription to clipboard'. + +### Run + +```bash +cargo run --example rtcp-processing-boxed +``` + +Paste the browser's offer, then paste the printed answer back into the browser. RTCP packets are printed as media +flows, exactly as in `rtcp-processing`. + +### Run with the forwarder omitted + +```bash +cargo run --example rtcp-processing-boxed -- --no-rtcp-forwarding +``` + +The peer connection has the **same type** (`RTCPeerConnection`) and the same code path drives it, +but the chain was built without the RTCP forwarder — so the default interceptors consume RTCP internally and nothing +is printed. This flag is the demonstration: the two chains are different Rust types, chosen at runtime, behind one +peer connection type. + +### Other flags + +```bash +cargo run --example rtcp-processing-boxed -- --debug # debug logging +cargo run --example rtcp-processing-boxed -- --input-sdp-file offer.txt # read SDP from a file +``` + +## Example Output + +``` +Interceptor chain: defaults + RTCP forwarder (boxed) +Paste your offer here: + + +Offer received: ... +RTCP Processing (boxed) listening on 127.0.0.1:54321... + +Paste this answer in your browser: +eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoi... + +Waiting for RTCP packets... +Press Ctrl-C to stop + +Connection State has changed: connected +Connection established! Waiting for RTCP packets... + +Track has started - track_id: video-track, receiver_id: 0 + Stream ID: my-stream, Track ID: video-track, Kind: video, Codec: video/VP8 + +=== RTCP Packet #1 (Track: video-track) === + [1] Type: SenderReport, Length: 12 words + SenderReport from 1234567890 + ... + +^C +Ctrl-C received, shutting down... +Total RTCP packets received: 42 +Event loop exited +``` + +With `--no-rtcp-forwarding` the first line reads +`Interceptor chain: defaults only (boxed) — no RTCP will be printed`, and no `=== RTCP Packet ===` blocks appear. + +## See also + +- [`rtcp-processing`](../rtcp-processing) — the same example without type erasure +- `tests/rtcp_processing_boxed_interop.rs` — integration tests for the boxed chain, including two peers with + *different* chains driven out of a single `Vec` diff --git a/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs new file mode 100644 index 00000000..da526810 --- /dev/null +++ b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs @@ -0,0 +1,587 @@ +//! rtcp-processing-boxed is the type-erased counterpart of the `rtcp-processing` example. +//! +//! It does exactly the same thing — installs a custom `RtcpForwarderInterceptor` so RTCP +//! packets surface through `poll_read()`, then prints them — but it holds the peer +//! connection as `RTCPeerConnection` instead of letting the interceptor +//! chain's type leak into the application. +//! +//! # Why erase the chain? +//! +//! An interceptor chain is a nest of generic types: `register_default_interceptors` +//! followed by `.with(RtcpForwarderBuilder::new().build())` produces something like +//! +//! ```text +//! RtcpForwarderInterceptor>>>>> +//! ``` +//! +//! `RTCPeerConnection` is generic over that type. As long as the chain flows straight +//! into a local variable, `impl Interceptor` hides it (that is what `rtcp-processing` +//! does). But the moment an application wants to *store* the peer connection, the type +//! parameter propagates: every struct that owns one, and every function that touches +//! those structs, has to carry ``. And an opaque `impl Interceptor` type +//! still cannot be produced by two different branches of an `if`, nor put in a collection +//! next to a peer built with a different chain. +//! +//! [`Registry::boxed`] erases the chain to [`BoxedInterceptor`] (`Box`), +//! so every peer connection has the same concrete type no matter how its chain was +//! assembled at runtime. Two consequences are visible below: +//! +//! * [`build_peer_connection`] picks its chain from a **command-line flag** — the two +//! branches build different chain types and unify only after `.boxed()`. +//! * [`RtcpSession`] is a plain struct with **no type parameter**, and its methods are +//! ordinary non-generic methods. +//! +//! The cost is one virtual call per chain entry point; the chain's interior stays +//! statically dispatched. + +use anyhow::Result; +use bytes::BytesMut; +use clap::Parser; +use env_logger::Target; +use log::{error, trace}; +use rtc::interceptor::{ + BoxedInterceptor, Interceptor, Packet, Registry, StreamInfo, TaggedPacket, interceptor, +}; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +use rtc::peer_connection::configuration::media_engine::{ + MIME_TYPE_OPUS, MIME_TYPE_VP8, MediaEngine, +}; +use rtc::peer_connection::event::RTCTrackEvent; +use rtc::peer_connection::event::{RTCEvent, RTCPeerConnectionEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::sdp::RTCSessionDescription; +use rtc::peer_connection::state::RTCPeerConnectionState; +use rtc::peer_connection::transport::RTCIceServer; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtp_transceiver::rtp_sender::RtpCodecKind; +use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RTCRtpCodecParameters}; +use rtc::sansio::{self, Protocol}; // Required for #[interceptor] macro and Protocol trait methods +use rtc::shared::error::Error; +use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use std::collections::{HashMap, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; +use tokio::sync::mpsc::channel; + +const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(86400); // 1 day + +// ============================================================================ +// RTCP Forwarder Interceptor +// ============================================================================ +// +// Identical to the one in the `rtcp-processing` example: it forwards RTCP packets to the +// application via poll_read(). By default RTCP is consumed by the interceptor chain (for +// statistics, NACK, congestion control) and never reaches the application. + +/// Builder for the RtcpForwarderInterceptor. +pub struct RtcpForwarderBuilder

{ + _phantom: std::marker::PhantomData

, +} + +impl

Default for RtcpForwarderBuilder

{ + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +impl

RtcpForwarderBuilder

{ + /// Create a new builder. + pub fn new() -> Self { + Self::default() + } + + /// Build the interceptor. + pub fn build(self) -> impl FnOnce(P) -> RtcpForwarderInterceptor

{ + move |inner| RtcpForwarderInterceptor::new(inner) + } +} + +/// Interceptor that forwards RTCP packets to the application. +#[derive(Interceptor)] +pub struct RtcpForwarderInterceptor

{ + #[next] + next: P, + read_queue: VecDeque, +} + +impl

RtcpForwarderInterceptor

{ + /// Create a new RtcpForwarderInterceptor. + fn new(next: P) -> Self { + Self { + next, + read_queue: VecDeque::new(), + } + } +} + +#[interceptor] +impl RtcpForwarderInterceptor

{ + #[overrides] + fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> { + // If this is an RTCP packet, queue a copy for the application + if let Packet::Rtcp(rtcp_packets) = &msg.message { + self.read_queue.push_back(TaggedPacket { + now: msg.now, + transport: msg.transport, + message: Packet::Rtcp(rtcp_packets.clone()), + }); + } + // Always pass to next interceptor for normal processing + self.next.handle_read(msg) + } + + #[overrides] + fn poll_read(&mut self) -> Option { + // First return any queued RTCP packets + if let Some(pkt) = self.read_queue.pop_front() { + return Some(pkt); + } + // Then check next interceptor + self.next.poll_read() + } + + #[overrides] + fn close(&mut self) -> Result<(), Self::Error> { + self.read_queue.clear(); + self.next.close() + } +} + +// ============================================================================ +// Building the peer connection: the chain is chosen at runtime +// ============================================================================ + +/// Build a peer connection whose interceptor chain depends on `forward_rtcp`. +/// +/// This is the shape that the `impl Interceptor` return type cannot express. The two +/// branches produce chains of different Rust types, and `if`/`else` arms must agree on a +/// type — so without erasure this function would have to be split in two, and its two +/// return types would then infect everything downstream. `Registry::boxed()` collapses +/// both to [`BoxedInterceptor`], leaving one ordinary return type. +fn build_peer_connection( + forward_rtcp: bool, + mut media_engine: MediaEngine, +) -> Result> { + // Default interceptors (NACK, RTCP reports, TWCC receiver) in both cases. + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // The RTCP forwarder must be the *outermost* layer, so it sees RTCP before the rest + // of the chain consumes it. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + let config = RTCConfigurationBuilder::new() + .with_ice_servers(vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_string()], + ..Default::default() + }]) + .build(); + + let peer_connection = RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?; + + Ok(peer_connection) +} + +// ============================================================================ +// The session: a plain struct that owns the peer connection +// ============================================================================ + +/// Everything the example needs to run one session. +/// +/// The point of this struct is what is *missing* from it: a type parameter. Holding an +/// `RTCPeerConnection` would have made this `struct RtcpSession`, and +/// then every `impl` block, every helper function, and any collection of sessions would +/// have had to carry `I` too. With the chain erased, this is an ordinary struct with +/// ordinary methods — and a server could keep a `Vec` or +/// `HashMap` even if each session were configured with a +/// different chain. +struct RtcpSession { + peer_connection: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, + ssrc2kind: HashMap, + rtcp_count: u64, +} + +impl RtcpSession { + /// Bind a socket and build the peer connection with the requested chain. + async fn new(forward_rtcp: bool) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut media_engine = MediaEngine::default(); + + // Register VP8 codec for video + media_engine.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_string(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_string(), + rtcp_feedback: vec![], + }, + payload_type: 96, + }, + RtpCodecKind::Video, + )?; + + // Register Opus codec for audio + media_engine.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_OPUS.to_string(), + clock_rate: 48000, + channels: 2, + sdp_fmtp_line: "".to_string(), + rtcp_feedback: vec![], + }, + payload_type: 111, + }, + RtpCodecKind::Audio, + )?; + + let peer_connection = build_peer_connection(forward_rtcp, media_engine)?; + + Ok(Self { + peer_connection, + socket: Arc::new(socket), + local_addr, + ssrc2kind: HashMap::new(), + rtcp_count: 0, + }) + } + + /// Apply the browser's offer and produce our answer. + fn answer(&mut self, offer: RTCSessionDescription) -> Result { + self.peer_connection.set_remote_description(offer)?; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: self.local_addr.ip().to_string(), + port: self.local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + self.peer_connection + .add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + let answer = self.peer_connection.create_answer(None)?; + self.peer_connection.set_local_description(answer.clone())?; + Ok(answer) + } + + /// Send everything the peer connection wants to put on the wire. + async fn flush_writes(&mut self) { + while let Some(msg) = self.peer_connection.poll_write() { + match self + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await + { + Ok(n) => trace!( + "socket write to {} with {} bytes", + msg.transport.peer_addr, n + ), + Err(err) => error!("socket write error: {}", err), + } + } + } + + /// Drain connection/track events. Returns `false` when the session should stop. + fn drain_events(&mut self) -> Result { + while let Some(event) = self.peer_connection.poll_event() { + match event { + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { + println!("Connection State has changed: {}", state); + if state == RTCPeerConnectionState::Failed { + println!("Connection failed, exiting..."); + return Ok(false); + } else if state == RTCPeerConnectionState::Connected { + println!("Connection established! Waiting for RTCP packets...\n"); + } + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + println!( + "Track has started - track_id: {}, receiver_id: {:?}", + init.track_id, init.receiver_id + ); + + if let Some(receiver) = self.peer_connection.rtp_receiver(init.receiver_id) { + let track = receiver.track(); + let ssrc = track + .ssrcs() + .next() + .ok_or(Error::ErrRTPReceiverForSSRCTrackStreamNotFound)?; + let codec = track.codec(ssrc).ok_or(Error::ErrCodecNotFound)?; + + println!( + " Stream ID: {}, Track ID: {}, Kind: {}, Codec: {}", + track.stream_id(), + track.track_id(), + track.kind(), + codec.mime_type + ); + + self.ssrc2kind.insert(ssrc, track.kind()); + } + println!(); + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnClose(track_id)) => { + println!("Track closed: {}", track_id); + } + _ => {} + } + } + Ok(true) + } + + /// Print every RTCP packet the forwarder surfaced. + /// + /// Nothing arrives here when the session was built without the forwarder — that is + /// what `--no-rtcp-forwarding` demonstrates. + fn drain_reads(&mut self) { + while let Some(message) = self.peer_connection.poll_read() { + match message { + RTCMessage::RtpPacket(_track_id, _rtp_packet) => { + // We're not processing RTP packets in this example + trace!("Received RTP packet"); + } + RTCMessage::RtcpPacket(track_id, rtcp_packets) => { + self.rtcp_count += 1; + println!( + "=== RTCP Packet #{} (Track: {}) ===", + self.rtcp_count, track_id + ); + + for (i, packet) in rtcp_packets.iter().enumerate() { + let header = packet.header(); + println!( + " [{}] Type: {:?}, Length: {} words", + i + 1, + header.packet_type, + header.length + ); + + // The RTCP packets implement Display for human-readable output + for line in format!("{}", packet).lines() { + println!(" {}", line); + } + } + println!(); + } + RTCMessage::DataChannelMessage(_, _) => {} + } + } + } + + /// Feed a datagram read off the socket into the peer connection. + fn handle_datagram(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + self.peer_connection.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?; + Ok(()) + } +} + +// ============================================================================ +// Main Application +// ============================================================================ + +#[derive(Parser)] +#[command(name = "rtcp-processing-boxed")] +#[command(author = "Rusty Rain ")] +#[command(version = "0.1.0")] +#[command(about = "RTCP packet processing with a type-erased interceptor chain")] +struct Cli { + #[arg(short, long)] + debug: bool, + #[arg(short, long, default_value_t = format!("INFO"))] + log_level: String, + #[arg(short, long, default_value_t = format!(""))] + input_sdp_file: String, + #[arg(short, long, default_value_t = format!(""))] + output_log_file: String, + /// Build the chain *without* the RTCP forwarder. The peer connection still has type + /// `RTCPeerConnection`; it simply never surfaces RTCP to the + /// application, so no RTCP packets are printed. + #[arg(long)] + no_rtcp_forwarding: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + let input_sdp_file = cli.input_sdp_file; + let output_log_file = cli.output_log_file; + let log_level = log::LevelFilter::from_str(&cli.log_level)?; + + if cli.debug { + env_logger::Builder::new() + .target(if !output_log_file.is_empty() { + Target::Pipe(Box::new( + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(output_log_file)?, + )) + } else { + Target::Stdout + }) + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, log_level) + .init(); + } + + run(input_sdp_file, !cli.no_rtcp_forwarding).await?; + + Ok(()) +} + +async fn run(input_sdp_file: String, forward_rtcp: bool) -> Result<()> { + // The chain is decided here, at runtime — and the session type does not change. + let mut session = RtcpSession::new(forward_rtcp).await?; + if forward_rtcp { + println!("Interceptor chain: defaults + RTCP forwarder (boxed)"); + } else { + println!("Interceptor chain: defaults only (boxed) — no RTCP will be printed"); + } + + // Wait for the offer to be pasted + println!("Paste your offer here:"); + let line = if input_sdp_file.is_empty() { + signal::must_read_stdin()? + } else { + std::fs::read_to_string(&input_sdp_file)? + }; + let desc_data = signal::decode(line.as_str())?; + let offer = serde_json::from_str::(&desc_data)?; + println!("Offer received: {}", offer); + + let answer = session.answer(offer)?; + + println!( + "RTCP Processing (boxed) listening on {}...", + session.local_addr + ); + + // Output the answer + let json_str = serde_json::to_string(&answer)?; + let b64 = signal::encode(&json_str); + println!("\nPaste this answer in your browser:\n{}\n", b64); + + let (_event_tx, mut event_rx) = channel::(8); + let mut buf = vec![0; 2000]; + + println!("Waiting for RTCP packets..."); + println!("Press Ctrl-C to stop\n"); + + // Event loop — all of it non-generic, because `RtcpSession` is. + 'EventLoop: loop { + session.flush_writes().await; + if !session.drain_events()? { + break 'EventLoop; + } + session.drain_reads(); + + // Poll peer_connection to get next timeout + let eto = session + .peer_connection + .poll_timeout() + .unwrap_or(Instant::now() + DEFAULT_TIMEOUT_DURATION); + + let delay_from_now = eto + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + session.peer_connection.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now); + tokio::pin!(timer); + // Clone the socket handle out so the recv future does not borrow `session` while + // the other arms mutate it. + let socket = Arc::clone(&session.socket); + + tokio::select! { + biased; + + _ = tokio::signal::ctrl_c() => { + println!("\nCtrl-C received, shutting down..."); + println!("Total RTCP packets received: {}", session.rtcp_count); + break 'EventLoop; + } + res = event_rx.recv() => { + match res { + Some(event) => { + session.peer_connection.handle_event(event)?; + } + None => { + eprintln!("event_rx closed"); + break 'EventLoop; + } + } + } + _ = timer.as_mut() => { + session.peer_connection.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + match res { + Ok((n, peer_addr)) => { + trace!("socket read {} bytes from {}", n, peer_addr); + session.handle_datagram(&buf[..n], peer_addr)?; + } + Err(err) => { + eprintln!("socket read error {}", err); + break 'EventLoop; + } + } + } + } + } + + session.peer_connection.close()?; + println!("Event loop exited"); + Ok(()) +} diff --git a/tests/README.md b/tests/README.md index 5af95620..b73b412f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -64,6 +64,7 @@ Consider adding tests for: | 13 | `simulcast_rtc_to_webrtc_interop.rs` | Simulcast: rtc→webrtc (IGNORED) | | 14 | `offer_answer_rtc2rtc.rs` | **Pure rtc ↔ rtc (no webrtc)** | | 15 | `interceptor_rtcp_reports_interop.rs` | **RTCP report interceptor integration** | +| 16 | `rtcp_processing_boxed_interop.rs` | **Type-erased (`BoxedInterceptor`) chain** | --- @@ -741,3 +742,49 @@ Run these with: ```bash cargo test --package rtc-interceptor --test rtcp_report_integration ``` + +--- + +## Test 16: Type-Erased Interceptor Chain (`BoxedInterceptor`) + +**File:** `rtcp_processing_boxed_interop.rs` + +**Purpose:** The `RTCPeerConnection` counterpart of Test 15's `rtcp_processing_interop.rs`. It +installs the same custom `RtcpForwarderInterceptor`, but erases the chain's type so that every peer connection has the +one concrete type `RTCPeerConnection`. + +### Why + +`RTCPeerConnection` is generic over its interceptor chain. Returning `RTCPeerConnection` (what +Test 15 does) works while the value flows straight into a local, but the opaque type cannot be stored in a non-generic +struct, put in a collection next to a peer with a different chain, or produced by two branches of an `if`. Erasing the +chain with `Registry::boxed()` removes all three limits, at the cost of one virtual call per chain entry point. + +The file demonstrates both halves of that: `build_boxed_rtc_peer(forward_rtcp, is_answerer)` picks its chain at +runtime (the branches unify only after `.boxed()`), and `struct RtcpPeer` — which owns a peer connection, a socket and +the test's counters — has **no type parameter**. + +### Tests + +| Test | Description | +|----------------------------------------------------------|-------------------------------------------------------------------| +| `test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer` | webrtc sends video; the boxed chain still surfaces RTCP via `poll_read()` | +| `test_boxed_rtcp_processing_rtc_sender_receives_feedback` | the boxed-chain RTC sender receives RTCP feedback about its own stream | +| `test_boxed_rtc_to_rtc_heterogeneous_chains` | two rtc peers with **different** chains driven out of a single `Vec` | + +The third test is the one the non-erased form cannot express. The offerer streams RTP (so its +`SenderReportInterceptor` emits periodic SRs) and is built **without** the forwarder; the answerer is built **with** +it. Both live in one `Vec` and are driven by the same non-generic loop, and the assertion is that their behaviour +differs accordingly: the answerer surfaces the SRs to the application, while the offerer — same type, different chain +— never surfaces a single RTCP packet. + +### Running the Tests + +```bash +cargo test --test rtcp_processing_boxed_interop -- --nocapture +``` + +### Related + +The `rtcp-processing-boxed` example (`examples/rtcp-processing-boxed/`) is the browser-driven version of the same +pattern, with a `--no-rtcp-forwarding` flag that switches the chain at runtime. diff --git a/tests/rtcp_processing_boxed_interop.rs b/tests/rtcp_processing_boxed_interop.rs new file mode 100644 index 00000000..89ade00c --- /dev/null +++ b/tests/rtcp_processing_boxed_interop.rs @@ -0,0 +1,835 @@ +//! Integration tests for RTCP packet processing with a **type-erased** interceptor chain. +//! +//! This is the `RTCPeerConnection` counterpart of +//! `rtcp_processing_interop.rs`. Both files install the same custom +//! `RtcpForwarderInterceptor`; the difference is how the resulting peer connection is +//! *typed*, and therefore what the application can do with it. +//! +//! `rtcp_processing_interop.rs` returns `RTCPeerConnection`. That works +//! as long as the chain flows straight into a local variable, but the opaque type is +//! chosen once per return site, so it cannot: +//! +//! - be stored in a plain (non-generic) application struct, +//! - be put in a `Vec`/`HashMap` alongside peers built with a *different* chain, +//! - be produced by two different branches of an `if` in the same function. +//! +//! Erasing the chain to [`BoxedInterceptor`] removes all three limits: every peer +//! connection has the one concrete type `RTCPeerConnection` regardless +//! of how its chain was assembled at runtime. That is what lets [`RtcpPeer`] below — an +//! ordinary struct with no type parameters — own a peer connection, and what lets +//! `test_boxed_rtc_to_rtc_heterogeneous_chains` drive two peers with *different* chains +//! out of a single `Vec`. +//! +//! Test scenarios: +//! 1. webrtc (offerer sending video) + boxed-chain RTC (answerer receiving RTCP) +//! 2. boxed-chain RTC (sender) receives RTCP feedback about its own stream from webrtc +//! 3. two boxed-chain RTC peers with *different* chains, driven from one `Vec` + +use anyhow::Result; +use bytes::BytesMut; +use sansio::Protocol; +use shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use std::collections::VecDeque; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; +use tokio::time::timeout; + +use rtc::interceptor::{ + BoxedInterceptor, Interceptor, Packet, Registry, StreamInfo, TaggedPacket, interceptor, +}; +use rtc::media_stream::MediaStreamTrack; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine}; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::state::{RTCIceConnectionState, RTCPeerConnectionState}; +use rtc::peer_connection::transport::{ + CandidateConfig, CandidateHostConfig, RTCDtlsRole, RTCIceCandidate, RTCIceServer, +}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtp_transceiver::rtp_sender::{ + RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters, + RtpCodecKind, +}; +use rtc::shared::error::Error; + +use webrtc::api::APIBuilder; +use webrtc::api::interceptor_registry::register_default_interceptors as webrtc_register_default_interceptors; +use webrtc::api::media_engine::MediaEngine as WebrtcMediaEngine; +use webrtc::ice_transport::ice_server::RTCIceServer as WebrtcIceServer; +use webrtc::interceptor::registry::Registry as WebrtcRegistry; +use webrtc::peer_connection::RTCPeerConnection as WebrtcPeerConnection; +use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; + +const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); + +// ============================================================================ +// RTCP Forwarder Interceptor +// ============================================================================ + +/// Builder for the RtcpForwarderInterceptor. +pub struct RtcpForwarderBuilder

{ + _phantom: std::marker::PhantomData

, +} + +impl

Default for RtcpForwarderBuilder

{ + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +impl

RtcpForwarderBuilder

{ + pub fn new() -> Self { + Self::default() + } + + pub fn build(self) -> impl FnOnce(P) -> RtcpForwarderInterceptor

{ + move |inner| RtcpForwarderInterceptor::new(inner) + } +} + +/// Interceptor that forwards RTCP packets to the application via poll_read(). +#[derive(Interceptor)] +pub struct RtcpForwarderInterceptor

{ + #[next] + next: P, + read_queue: VecDeque, +} + +impl

RtcpForwarderInterceptor

{ + fn new(next: P) -> Self { + Self { + next, + read_queue: VecDeque::new(), + } + } +} + +#[interceptor] +impl RtcpForwarderInterceptor

{ + #[overrides] + fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> { + // If this is an RTCP packet, queue a copy for the application + if let Packet::Rtcp(rtcp_packets) = &msg.message { + self.read_queue.push_back(TaggedPacket { + now: msg.now, + transport: msg.transport, + message: Packet::Rtcp(rtcp_packets.clone()), + }); + } + // Always pass to next interceptor for normal processing + self.next.handle_read(msg) + } + + #[overrides] + fn poll_read(&mut self) -> Option { + // First return any queued RTCP packets + if let Some(pkt) = self.read_queue.pop_front() { + return Some(pkt); + } + // Then check next interceptor + self.next.poll_read() + } + + #[overrides] + fn close(&mut self) -> Result<(), Self::Error> { + self.read_queue.clear(); + self.next.close() + } +} + +// ============================================================================ +// Building a peer connection whose interceptor chain is chosen at runtime +// ============================================================================ + +/// Build a peer connection whose chain is decided at *runtime* by `forward_rtcp`. +/// +/// This is the function the `impl Interceptor` form cannot express. The two branches +/// build chains of different Rust types — +/// `RtcpForwarderInterceptor>>` versus +/// `TwccReceiver>` — so there is no single `impl Interceptor` they can +/// both satisfy, and `if`/`else` arms must agree on a type. [`Registry::boxed`] erases +/// both to [`BoxedInterceptor`], after which the branches unify and the function has one +/// ordinary return type. +fn build_boxed_rtc_peer( + forward_rtcp: bool, + is_answerer: bool, +) -> Result> { + let mut setting_engine = SettingEngine::default(); + if is_answerer { + setting_engine.set_answering_dtls_role(RTCDtlsRole::Client)?; + } + + let mut media_engine = MediaEngine::default(); + let video_codec = RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + payload_type: 96, + }; + media_engine.register_codec(video_codec, RtpCodecKind::Video)?; + + // Default interceptors (NACK, RTCP reports, TWCC receiver) in both cases. + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // The RTCP forwarder is layered on only for peers that want to see RTCP in + // `poll_read`. Both arms are erased to the same `Registry`. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + let config = RTCConfigurationBuilder::new() + .with_ice_servers(vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_owned()], + ..Default::default() + }]) + .build(); + + let pc = RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_setting_engine(setting_engine) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?; + Ok(pc) +} + +// ============================================================================ +// A non-generic peer holder +// ============================================================================ + +/// An application-level peer: socket, peer connection, and the counters the tests assert +/// on. +/// +/// Note that this struct has **no type parameter**. With `RTCPeerConnection` it would +/// have needed one (`RtcpPeer`), and that parameter would then infect +/// every function, collection, and struct that touches an `RtcpPeer` — which is exactly +/// the boilerplate erasure removes. +struct RtcpPeer { + name: &'static str, + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, + connected: bool, + rtp_received: u32, + rtcp_received: u32, +} + +impl RtcpPeer { + /// Bind a socket, build the peer connection, and advertise the socket as a host + /// candidate. + async fn new(name: &'static str, forward_rtcp: bool, is_answerer: bool) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + log::info!("{} bound to {}", name, local_addr); + + let mut pc = build_boxed_rtc_peer(forward_rtcp, is_answerer)?; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + name, + pc, + socket: Arc::new(socket), + local_addr, + connected: false, + rtp_received: 0, + rtcp_received: 0, + }) + } + + /// Send everything the peer connection wants to put on the wire. + async fn flush_writes(&mut self) { + while let Some(msg) = self.pc.poll_write() { + // Ignore send errors - some candidate addresses may be unreachable. + let _ = self + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await; + } + } + + /// Drain connection/track events, tracking the connected state. + fn drain_events(&mut self) -> Result<()> { + while let Some(event) = self.pc.poll_event() { + match event { + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { + log::info!("{} ICE state: {}", self.name, state); + if state == RTCIceConnectionState::Failed { + return Err(anyhow::anyhow!("{} ICE connection failed", self.name)); + } + } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { + log::info!("{} connection state: {}", self.name, state); + if state == RTCPeerConnectionState::Connected { + self.connected = true; + } + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + log::info!("{} track opened: {}", self.name, init.track_id); + } + _ => {} + } + } + Ok(()) + } + + /// Drain RTP/RTCP surfaced to the application, counting both. + /// + /// RTCP only ever appears here for peers built with `forward_rtcp: true` — the + /// default chain consumes it. That asymmetry is what test 3 asserts on. + fn drain_reads(&mut self) { + while let Some(message) = self.pc.poll_read() { + match message { + RTCMessage::RtpPacket(_track_id, rtp_packet) => { + self.rtp_received += 1; + if self.rtp_received.is_multiple_of(10) { + log::info!( + "{} received RTP packet #{} (seq: {})", + self.name, + self.rtp_received, + rtp_packet.header.sequence_number + ); + } + } + RTCMessage::RtcpPacket(track_id, rtcp_packets) => { + self.rtcp_received += 1; + log::info!( + "{} received RTCP packet #{} (track: {}, {} sub-packets)", + self.name, + self.rtcp_received, + track_id, + rtcp_packets.len() + ); + for (i, packet) in rtcp_packets.iter().enumerate() { + let header = packet.header(); + log::info!( + " [{}] Type: {:?}, Length: {} words", + i + 1, + header.packet_type, + header.length + ); + } + } + RTCMessage::DataChannelMessage(_, _) => {} + } + } + } + + /// The peer connection's next deadline, or a far-future default. + fn next_timeout(&mut self) -> Instant { + self.pc + .poll_timeout() + .unwrap_or(Instant::now() + DEFAULT_TIMEOUT_DURATION) + } + + /// Feed a datagram read off this peer's socket into the peer connection. + fn handle_datagram(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + self.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?; + Ok(()) + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Create a webrtc peer connection +async fn create_webrtc_peer() -> Result> { + let mut media_engine = WebrtcMediaEngine::default(); + media_engine.register_default_codecs()?; + + let mut registry = WebrtcRegistry::new(); + registry = webrtc_register_default_interceptors(registry, &mut media_engine)?; + + let api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build(); + + let config = WebrtcRTCConfiguration { + ice_servers: vec![WebrtcIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_owned()], + ..Default::default() + }], + ..Default::default() + }; + + Ok(Arc::new(api.new_peer_connection(config).await?)) +} + +/// A VP8 track carrying a single known SSRC. +fn video_track(stream_id: &str, track_id: &str, ssrc: u32) -> MediaStreamTrack { + MediaStreamTrack::new( + stream_id.to_owned(), + track_id.to_owned(), + format!("{track_id}-label"), + RtpCodecKind::Video, + vec![RTCRtpEncodingParameters { + rtp_coding_parameters: RTCRtpCodingParameters { + ssrc: Some(ssrc), + ..Default::default() + }, + codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + ..Default::default() + }], + ) +} + +/// A dummy VP8 RTP packet for `ssrc` with the given sequence number. +fn dummy_rtp(ssrc: u32, seq: u32, payload_type: u8) -> rtc::rtp::packet::Packet { + rtc::rtp::packet::Packet { + header: rtc::rtp::header::Header { + version: 2, + payload_type, + sequence_number: seq as u16, + timestamp: seq.wrapping_mul(3000), + ssrc, + ..Default::default() + }, + payload: bytes::Bytes::from(vec![0xAAu8; 100]), + } +} + +// ============================================================================ +// Test 1: webrtc offerer sends video, boxed-chain RTC answerer receives RTCP +// ============================================================================ + +/// The boxed counterpart of `test_rtcp_processing_webrtc_offerer_rtc_answerer`. +/// +/// Verifies that erasing the chain changes nothing observable: the custom forwarder still +/// surfaces RTCP via `poll_read()`, and the default interceptors still process RTP. +#[tokio::test] +async fn test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + log::info!("Starting boxed RTCP processing test: webrtc (offerer) -> boxed RTC (answerer)"); + + // Create webrtc peer (offerer) with video track + let webrtc_pc = create_webrtc_peer().await?; + + let track = Arc::new(TrackLocalStaticRTP::new( + RTCRtpCodecCapability { + mime_type: "video/VP8".to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + "video".to_owned(), + "boxed-rtcp-test-stream".to_owned(), + )); + webrtc_pc + .add_track(Arc::clone(&track) as Arc) + .await?; + + let offer = webrtc_pc.create_offer(None).await?; + webrtc_pc.set_local_description(offer).await?; + let mut gathering_done = webrtc_pc.gathering_complete_promise().await; + let _ = timeout(Duration::from_secs(5), gathering_done.recv()).await; + let offer_with_candidates = webrtc_pc + .local_description() + .await + .expect("local description should be set"); + + // The RTC answerer: one concrete type, held by an ordinary non-generic struct. + let mut peer = RtcpPeer::new("boxed-answerer", true, true).await?; + + let rtc_offer = + rtc::peer_connection::sdp::RTCSessionDescription::offer(offer_with_candidates.sdp.clone())?; + peer.pc.set_remote_description(rtc_offer)?; + + let answer = peer.pc.create_answer(None)?; + peer.pc.set_local_description(answer.clone())?; + + let webrtc_answer = WebrtcRTCSessionDescription::answer(answer.sdp.clone())?; + webrtc_pc.set_remote_description(webrtc_answer).await?; + + // Event loop + let mut buf = vec![0u8; 2000]; + let mut webrtc_connected = false; + let mut rtp_sending_started = false; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Start sending RTP once webrtc is connected + if webrtc_connected && !rtp_sending_started { + rtp_sending_started = true; + log::info!("WebRTC connected, starting to send RTP packets"); + let track = Arc::clone(&track); + tokio::spawn(async move { + for seq in 0u16..50 { + let rtp = webrtc::rtp::packet::Packet { + header: webrtc::rtp::header::Header { + version: 2, + payload_type: 96, + sequence_number: seq, + timestamp: seq as u32 * 3000, + ssrc: 12345, + ..Default::default() + }, + payload: bytes::Bytes::from(vec![0xAAu8; 100]), + }; + let _ = track.write_rtp(&rtp).await; + tokio::time::sleep(Duration::from_millis(20)).await; + } + }); + } + + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + + if !webrtc_connected + && webrtc_pc.connection_state() == WebrtcRTCPeerConnectionState::Connected + { + webrtc_connected = true; + log::info!("WebRTC peer connected!"); + } + + // Success: the boxed chain surfaced RTCP and passed RTP through. + if peer.rtcp_received >= 2 && peer.rtp_received >= 10 { + log::info!( + "Test passed! RTP received: {}, RTCP received: {}", + peer.rtp_received, + peer.rtcp_received + ); + peer.pc.close()?; + webrtc_pc.close().await?; + return Ok(()); + } + + let delay_from_now = peer + .next_timeout() + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + peer.pc.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket = Arc::clone(&peer.socket); + + tokio::select! { + _ = timer.as_mut() => { + peer.pc.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + if let Ok((n, peer_addr)) = res { + peer.handle_datagram(&buf[..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP: {}, RTCP: {}", + peer.rtp_received, + peer.rtcp_received + )) +} + +// ============================================================================ +// Test 2: boxed-chain RTC sender receives RTCP feedback about its OWN stream +// ============================================================================ + +/// The boxed counterpart of `test_rtcp_processing_rtc_sender_receives_feedback`. +/// +/// The RTC peer *sends* video; webrtc receives it and reports back. The feedback's media +/// SSRC is the RTC peer's sender SSRC, so it surfaces tagged with the sender's track id — +/// exactly what an SFU needs in order to relay PLI/FIR upstream to a publisher. +#[tokio::test] +async fn test_boxed_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + const SENDER_SSRC: u32 = 0x00DE_CAFE; + const SENDER_TRACK_ID: &str = "boxed-rtcp-sender-test-track"; + + log::info!("Starting boxed RTCP processing test: boxed RTC (sender) <- webrtc feedback"); + + let mut peer = RtcpPeer::new("boxed-sender", true, false).await?; + + let sender_id = peer.pc.add_track(video_track( + "boxed-rtcp-sender-test-stream", + SENDER_TRACK_ID, + SENDER_SSRC, + ))?; + + let offer = peer.pc.create_offer(None)?; + peer.pc.set_local_description(offer.clone())?; + + let webrtc_pc = create_webrtc_peer().await?; + let webrtc_offer = WebrtcRTCSessionDescription::offer(offer.sdp.clone())?; + webrtc_pc.set_remote_description(webrtc_offer).await?; + let answer = webrtc_pc.create_answer(None).await?; + webrtc_pc.set_local_description(answer).await?; + let mut gathering_done = webrtc_pc.gathering_complete_promise().await; + let _ = timeout(Duration::from_secs(5), gathering_done.recv()).await; + let answer_with_candidates = webrtc_pc + .local_description() + .await + .expect("local description should be set"); + let rtc_answer = rtc::peer_connection::sdp::RTCSessionDescription::answer( + answer_with_candidates.sdp.clone(), + )?; + peer.pc.set_remote_description(rtc_answer)?; + + let mut buf = vec![0u8; 2000]; + let mut rtp_packets_sent = 0u32; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Keep the webrtc receiver reporting by streaming RTP. + if peer.connected + && rtp_packets_sent < 300 + && let Some(mut sender) = peer.pc.rtp_sender(sender_id) + { + let _ = sender.write_rtp(dummy_rtp(SENDER_SSRC, rtp_packets_sent, 96)); + rtp_packets_sent += 1; + } + + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + + // Success: feedback about our SENT stream surfaced through the boxed chain. + if peer.rtcp_received >= 2 { + log::info!( + "Test passed! RTP sent: {}, RTCP received about sent stream: {}", + rtp_packets_sent, + peer.rtcp_received + ); + peer.pc.close()?; + webrtc_pc.close().await?; + return Ok(()); + } + + let delay_from_now = peer + .next_timeout() + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + peer.pc.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket = Arc::clone(&peer.socket); + + tokio::select! { + _ = timer.as_mut() => { + peer.pc.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + if let Ok((n, peer_addr)) = res { + peer.handle_datagram(&buf[..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP sent: {}, RTCP received about sent stream: {}", + rtp_packets_sent, + peer.rtcp_received + )) +} + +// ============================================================================ +// Test 3: two RTC peers with DIFFERENT chains, driven from one Vec +// ============================================================================ + +/// The test the non-erased form cannot express. +/// +/// Two peer connections are built with genuinely different interceptor chains — the +/// answerer has the RTCP forwarder layered on, the offerer does not — and both are stored +/// in a single `Vec` and driven by the same non-generic code. With +/// `RTCPeerConnection` the two would have incompatible types and could +/// not share a collection. +/// +/// The chains' *behaviour* differs accordingly, and that is the assertion. The offerer +/// streams RTP, so its `SenderReportInterceptor` emits periodic Sender Reports; the +/// answerer receives both. The answerer, having the forwarder, surfaces those SRs to the +/// application through `poll_read()`, while the offerer — same peer connection type, +/// different chain — never surfaces a single RTCP packet, because without the forwarder +/// the default chain consumes RTCP internally. +#[tokio::test] +async fn test_boxed_rtc_to_rtc_heterogeneous_chains() -> Result<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + const SENDER_SSRC: u32 = 0x00B0_0DED; + + log::info!("Starting boxed rtc-to-rtc test with heterogeneous interceptor chains"); + + // Same type, different chains — so they can live in one Vec. + let mut offerer = RtcpPeer::new("without-forwarder", false, false).await?; + let answerer = RtcpPeer::new("with-forwarder", true, true).await?; + + let sender_id = offerer.pc.add_track(video_track( + "boxed-heterogeneous-stream", + "boxed-heterogeneous-track", + SENDER_SSRC, + ))?; + + let mut peers: Vec = vec![offerer, answerer]; + + // Offer/answer between the two. + let offer = peers[0].pc.create_offer(None)?; + peers[0].pc.set_local_description(offer.clone())?; + peers[1].pc.set_remote_description(offer)?; + let answer = peers[1].pc.create_answer(None)?; + peers[1].pc.set_local_description(answer.clone())?; + peers[0].pc.set_remote_description(answer)?; + + let mut bufs = [vec![0u8; 2000], vec![0u8; 2000]]; + let mut rtp_packets_sent = 0u32; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Stream RTP from the offerer once both transports are up. + if peers[0].connected + && peers[1].connected + && rtp_packets_sent < 600 + && let Some(mut sender) = peers[0].pc.rtp_sender(sender_id) + { + let _ = sender.write_rtp(dummy_rtp(SENDER_SSRC, rtp_packets_sent, 96)); + rtp_packets_sent += 1; + } + + // One non-generic loop body drives both peers, whatever chain each was built with. + for peer in &mut peers { + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + } + + // Success: RTP arrived at the answerer, whose forwarder also surfaced the + // offerer's Sender Reports about that stream. + if peers[1].rtp_received >= 10 && peers[1].rtcp_received >= 2 { + log::info!( + "Test passed! RTP sent: {}, RTP received: {}, RTCP surfaced (with-forwarder): {}, \ + RTCP surfaced (without-forwarder): {}", + rtp_packets_sent, + peers[1].rtp_received, + peers[1].rtcp_received, + peers[0].rtcp_received + ); + assert_eq!( + peers[0].rtcp_received, 0, + "a peer built without the forwarder must never surface RTCP to the application" + ); + for peer in &mut peers { + peer.pc.close()?; + } + return Ok(()); + } + + let next_timeout = peers[0].next_timeout().min(peers[1].next_timeout()); + let delay_from_now = next_timeout + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + let now = Instant::now(); + for peer in &mut peers { + peer.pc.handle_timeout(now)?; + } + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket0 = Arc::clone(&peers[0].socket); + let socket1 = Arc::clone(&peers[1].socket); + let (buf0, buf1) = bufs.split_at_mut(1); + + tokio::select! { + _ = timer.as_mut() => { + let now = Instant::now(); + for peer in &mut peers { + peer.pc.handle_timeout(now)?; + } + } + res = socket0.recv_from(&mut buf0[0]) => { + if let Ok((n, peer_addr)) = res { + peers[0].handle_datagram(&buf0[0][..n], peer_addr)?; + } + } + res = socket1.recv_from(&mut buf1[0]) => { + if let Ok((n, peer_addr)) = res { + peers[1].handle_datagram(&buf1[0][..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP sent: {}, RTP received: {}, RTCP surfaced: {}", + rtp_packets_sent, + peers[1].rtp_received, + peers[1].rtcp_received + )) +} From 4acf47b5801ec307c5b041ddbc68328ee78bcacb Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 14:56:02 -0700 Subject: [PATCH 08/40] remove static from trait Interceptor --- rtc-interceptor/src/lib.rs | 70 ++++++++++++++++++++++++++++++++- rtc-interceptor/src/registry.rs | 9 ++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index d42dd42a..fdf90541 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -315,7 +315,6 @@ pub trait Interceptor: Error = shared::error::Error, > + Send + Sync - + 'static { /// Wrap this interceptor with another layer. /// @@ -381,6 +380,33 @@ impl Interceptor for Box

{ } } +/// Blanket implementation for mutable references. +/// +/// This lets a borrowed chain satisfy an `Interceptor` bound, so a function taking +/// `I: Interceptor` by value can be called with `&mut chain` and leave ownership with the +/// caller. It mirrors [`sansio::Protocol`]'s own `&mut P` implementation, and the same idiom +/// in `std` (`impl Read for &mut R`, `impl Iterator for &mut I`). +/// +/// This is only expressible because [`Interceptor`] does not require `'static`: `&'a mut P` +/// outlives only `'a`. See [`Registry::boxed`], which carries that bound locally instead. +impl Interceptor for &mut P { + fn bind_local_stream(&mut self, info: &StreamInfo) { + (**self).bind_local_stream(info) + } + + fn unbind_local_stream(&mut self, info: &StreamInfo) { + (**self).unbind_local_stream(info) + } + + fn bind_remote_stream(&mut self, info: &StreamInfo) { + (**self).bind_remote_stream(info) + } + + fn unbind_remote_stream(&mut self, info: &StreamInfo) { + (**self).unbind_remote_stream(info) + } +} + #[cfg(test)] mod derive_tests { use super::*; @@ -448,4 +474,46 @@ mod derive_tests { chain.bind_remote_stream(&info); chain.unbind_remote_stream(&info); } + + /// Consumes an interceptor by value, as the `Registry`/`with` builders do. + fn takes_by_value(mut interceptor: I, info: &StreamInfo) { + interceptor.bind_local_stream(info); + interceptor.unbind_local_stream(info); + } + + #[test] + fn test_borrowed_chain_satisfies_interceptor_bound() { + let mut chain = SimplePassthrough::new(NoopInterceptor::new()); + let info = StreamInfo { + ssrc: 12345, + ..Default::default() + }; + + // `&mut chain` satisfies a by-value `I: Interceptor` bound thanks to the blanket impl. + takes_by_value(&mut chain, &info); + + // Ownership stayed with us, so the chain is still usable afterwards. + takes_by_value(&mut chain, &info); + chain.bind_remote_stream(&info); + + // The borrow also still drives the Protocol side. + let pkt = TaggedPacket { + now: std::time::Instant::now(), + transport: Default::default(), + message: Packet::Rtp(rtp::Packet::default()), + }; + sansio::Protocol::handle_write(&mut chain, pkt).unwrap(); + assert!(sansio::Protocol::poll_write(&mut chain).is_some()); + } + + #[test] + fn test_boxed_chain_still_satisfies_interceptor_bound() { + // The `Box

` impl coexists with the new `&mut P` impl. + let chain: BoxedInterceptor = Box::new(SimplePassthrough::new(NoopInterceptor::new())); + let info = StreamInfo { + ssrc: 999, + ..Default::default() + }; + takes_by_value(chain, &info); + } } diff --git a/rtc-interceptor/src/registry.rs b/rtc-interceptor/src/registry.rs index 19095119..1a3e7f30 100644 --- a/rtc-interceptor/src/registry.rs +++ b/rtc-interceptor/src/registry.rs @@ -152,7 +152,14 @@ impl Registry

{ /// .with_interceptor_registry(registry.boxed()) /// .build()?; /// ``` - pub fn boxed(self) -> Registry { + /// + /// `P: 'static` is required because [`BoxedInterceptor`] is `Box`, + /// so the chain must not borrow anything shorter-lived. This is the only operation that needs + /// the bound, which is why [`Interceptor`] itself does not require `'static`. + pub fn boxed(self) -> Registry + where + P: 'static, + { Registry { inner: Box::new(self.inner), } From 59e02b7d466d54d34259aaa3edfacf5612bfdac7 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 15:09:54 -0700 Subject: [PATCH 09/40] update inline-docs --- rtc-sctp/src/association/stream.rs | 5 +- src/lib.rs | 43 +++++++++ src/media_stream/track.rs | 90 +++++-------------- .../configuration/interceptor_registry.rs | 73 ++++++++++----- .../configuration/setting_engine.rs | 2 +- src/peer_connection/event/mod.rs | 22 +++-- src/peer_connection/event/track_event.rs | 58 ++++++------ src/peer_connection/mod.rs | 49 +++++++++- src/rtp_transceiver/rtp_receiver/mod.rs | 25 ++++-- src/statistics/accumulator/mod.rs | 5 +- src/statistics/mod.rs | 7 +- src/statistics/report.rs | 7 +- src/statistics/stats/mod.rs | 7 +- 13 files changed, 249 insertions(+), 144 deletions(-) diff --git a/rtc-sctp/src/association/stream.rs b/rtc-sctp/src/association/stream.rs index cbef49dc..f1da78dd 100644 --- a/rtc-sctp/src/association/stream.rs +++ b/rtc-sctp/src/association/stream.rs @@ -159,9 +159,8 @@ impl Stream<'_> { /// /// Unlike [`write_with_ppi`](Self::write_with_ppi), which takes a `&[u8]` and /// must copy it into a freshly allocated buffer, this enqueues the payload - /// zero-copy: each fragment is a refcounted slice of `data` (see - /// [`BytesChunk`]). Prefer this on the hot send path when the caller already - /// owns the payload as `Bytes`. + /// zero-copy: each fragment is a refcounted slice of `data`. Prefer this on the + /// hot send path when the caller already owns the payload as `Bytes`. /// /// Returns the number of bytes successfully written. pub fn write_chunk_with_ppi( diff --git a/src/lib.rs b/src/lib.rs index cebcb4c0..c55fc26c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -456,6 +456,49 @@ //! # } //! ``` //! +//! #### Type-erasing the interceptor chain +//! +//! [`RTCPeerConnection`](peer_connection::RTCPeerConnection) is generic over its +//! interceptor chain, and a chain's type spells out its entire composition +//! (`TwccReceiverInterceptor>`). That type propagates into +//! everything that *holds* a peer connection, which is a problem when the chain is chosen +//! at runtime or when connections have to live in your own structs and collections. +//! +//! [`Registry::boxed`](interceptor::Registry::boxed) erases the chain to +//! [`BoxedInterceptor`](interceptor::BoxedInterceptor), giving every connection the same +//! concrete type no matter how it was built: +//! +//! ```no_run +//! use rtc::interceptor::{BoxedInterceptor, Registry}; +//! use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +//! use rtc::peer_connection::configuration::media_engine::MediaEngine; +//! use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +//! +//! // No type parameter: this struct does not have to know the chain. +//! struct Session { +//! peer_connection: RTCPeerConnection, +//! } +//! +//! # fn example(with_nack: bool) -> Result<(), Box> { +//! let mut media_engine = MediaEngine::default(); +//! let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; +//! +//! let mut sessions: Vec = Vec::new(); +//! sessions.push(Session { +//! peer_connection: RTCPeerConnectionBuilder::new() +//! .with_media_engine(media_engine) +//! .with_interceptor_registry(registry.boxed()) +//! .build()?, +//! }); +//! # Ok(()) +//! # } +//! ``` +//! +//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`, +//! `handle_timeout`, …); the chain's interior remains statically dispatched and inlined. +//! Static dispatch is still the default — keep the generic form when the chain is fixed at +//! compile time. +//! //! ### Creating and Using Data Channels //! //! ```no_run diff --git a/src/media_stream/track.rs b/src/media_stream/track.rs index 17b79a24..76c64bc3 100644 --- a/src/media_stream/track.rs +++ b/src/media_stream/track.rs @@ -1,71 +1,8 @@ -//! MediaStreamTrack API +//! `MediaStreamTrack` implementation. //! -//! This module implements the `MediaStreamTrack` interface as defined in the -//! [W3C Media Capture and Streams specification](https://www.w3.org/TR/mediacapture-streams/#mediastreamtrack). -//! -//! A [`MediaStreamTrack`] represents a single media track within a media stream, such as -//! an audio track from a microphone or a video track from a camera. Each track has properties -//! like enabled/disabled state, mute status, and lifecycle state (live or ended). -//! -//! # Examples -//! -//! ## Creating a video track -//! -//! ``` -//! use rtc::media_stream::MediaStreamTrack; -//! use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind}; -//! use rtc::rtp_transceiver::rtp_sender::{RTCRtpEncodingParameters, RTCRtpCodingParameters}; -//! use rtc::peer_connection::configuration::media_engine::MIME_TYPE_VP8; -//! -//! # fn example() -> Result<(), Box> { -//! let codec = RTCRtpCodec { -//! mime_type: MIME_TYPE_VP8.to_string(), -//! clock_rate: 90000, -//! channels: 0, -//! sdp_fmtp_line: String::new(), -//! rtcp_feedback: vec![], -//! }; -//! -//! let video_track = MediaStreamTrack::new( -//! "stream-123".to_string(), -//! "track-456".to_string(), -//! "Front Camera".to_string(), -//! RtpCodecKind::Video, -//! vec![RTCRtpEncodingParameters { -//! rtp_coding_parameters: RTCRtpCodingParameters { -//! ssrc: Some(789012), -//! ..Default::default() -//! }, -//! codec, -//! ..Default::default() -//! }], -//! ); -//! -//! assert_eq!(video_track.kind(), RtpCodecKind::Video); -//! assert_eq!(video_track.label(), "Front Camera"); -//! assert!(video_track.enabled()); -//! # Ok(()) -//! # } -//! ``` -//! -//! ## Controlling track state -//! -//! ``` -//! # use rtc::media_stream::MediaStreamTrack; -//! # use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind}; -//! # fn example(mut track: MediaStreamTrack) { -//! // Disable the track temporarily -//! track.set_enabled(false); -//! assert!(!track.enabled()); -//! -//! // Re-enable the track -//! track.set_enabled(true); -//! assert!(track.enabled()); -//! -//! // Stop the track permanently -//! track.stop(); -//! # } -//! ``` +//! This module is private; the type is re-exported from +//! [`crate::media_stream`]. All user-facing documentation and examples live on +//! [`MediaStreamTrack`] itself, so that rustdoc renders them and their doctests run. //! //! # Specifications //! @@ -131,6 +68,25 @@ pub type MediaStreamTrackId = String; /// # Ok(()) /// # } /// ``` +/// +/// ## Controlling track state +/// +/// ``` +/// use rtc::media_stream::MediaStreamTrack; +/// +/// # fn example(mut track: MediaStreamTrack) { +/// // Disable the track temporarily +/// track.set_enabled(false); +/// assert!(!track.enabled()); +/// +/// // Re-enable the track +/// track.set_enabled(true); +/// assert!(track.enabled()); +/// +/// // Stop the track permanently — a stopped track cannot be restarted +/// track.stop(); +/// # } +/// ``` #[derive(Default, Debug, Clone)] pub struct MediaStreamTrack { stream_id: MediaStreamId, diff --git a/src/peer_connection/configuration/interceptor_registry.rs b/src/peer_connection/configuration/interceptor_registry.rs index e23823e1..51e2856b 100644 --- a/src/peer_connection/configuration/interceptor_registry.rs +++ b/src/peer_connection/configuration/interceptor_registry.rs @@ -28,33 +28,40 @@ //! For most applications, use [`register_default_interceptors`] to enable //! standard WebRTC functionality: //! -//! ```ignore +//! ```no_run +//! use rtc::interceptor::Registry; +//! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; //! use rtc::peer_connection::configuration::media_engine::MediaEngine; -//! use interceptor::Registry; //! +//! # fn example() -> Result<(), Box> { //! let mut media_engine = MediaEngine::default(); //! let registry = Registry::new(); //! -//! // Register NACK, RTCP reports, simulcast headers, and TWCC receiver +//! // Register NACK, RTCP reports, simulcast headers, and TWCC receiver. +//! // Note this takes `&mut media_engine`: it registers the RTCP feedback types and +//! // header extensions the interceptors need, so pass that same engine to the builder. //! let registry = register_default_interceptors(registry, &mut media_engine)?; //! -//! // Use with RTCConfigurationBuilder -//! let config = RTCConfigurationBuilder::new() +//! let pc = RTCPeerConnectionBuilder::new() //! .with_media_engine(media_engine) //! .with_interceptor_registry(registry) -//! .build(); +//! .build()?; +//! # Ok(()) +//! # } //! ``` //! //! # Custom Configuration //! //! For fine-grained control, configure individual interceptors: //! -//! ```ignore -//! use rtc::peer_connection::configuration::interceptor_registry::*; +//! ```no_run +//! use rtc::interceptor::Registry; +//! use rtc::peer_connection::RTCPeerConnectionBuilder; +//! use rtc::peer_connection::configuration::interceptor_registry::{configure_nack, configure_twcc}; //! use rtc::peer_connection::configuration::media_engine::MediaEngine; -//! use interceptor::Registry; //! +//! # fn example() -> Result<(), Box> { //! let mut media_engine = MediaEngine::default(); //! let registry = Registry::new(); //! @@ -63,6 +70,13 @@ //! //! // Or enable full TWCC for bandwidth estimation //! let registry = configure_twcc(registry, &mut media_engine)?; +//! +//! let pc = RTCPeerConnectionBuilder::new() +//! .with_media_engine(media_engine) +//! .with_interceptor_registry(registry) +//! .build()?; +//! # Ok(()) +//! # } //! ``` //! //! # Available Configurations @@ -117,14 +131,17 @@ use shared::error::Result; /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = register_default_interceptors(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # Customization @@ -169,10 +186,10 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_nack; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); @@ -237,9 +254,9 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_rtcp_reports; -/// use interceptor::Registry; /// /// let registry = Registry::new(); /// let registry = configure_rtcp_reports(registry); @@ -276,12 +293,15 @@ where /// /// # Example /// -/// ```ignore +/// ``` /// use rtc::peer_connection::configuration::interceptor_registry::configure_simulcast_extension_headers; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// configure_simulcast_extension_headers(&mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # References @@ -344,14 +364,17 @@ pub fn configure_simulcast_extension_headers(media_engine: &mut MediaEngine) -> /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # References @@ -423,14 +446,17 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc_sender_only; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc_sender_only(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` pub fn configure_twcc_sender_only

( registry: Registry

, @@ -501,14 +527,17 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc_receiver_only; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc_receiver_only(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` pub fn configure_twcc_receiver_only

( registry: Registry

, diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 0e96fc24..43a707d6 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -1133,7 +1133,7 @@ impl SettingEngine { /// bytes**; smaller values (including `0`) are raised to that floor here, because a /// sub-1500 window makes the peer reject this endpoint's INIT/INIT-ACK and the SCTP /// association never establishes. The window should also be **≥ the largest SCTP - /// message this endpoint will receive** ([`set_sctp_max_message_size`], default + /// message this endpoint will receive** ([`Self::set_sctp_max_message_size`], default /// 64 KiB): a buffer smaller than one message cannot hold it for reassembly, so a /// full-size inbound message would stall that receive direction. `0` here is *not* /// "unbounded" (unlike some other knobs) — to keep the default window, leave this diff --git a/src/peer_connection/event/mod.rs b/src/peer_connection/event/mod.rs index 710d693a..3d6fc6f4 100644 --- a/src/peer_connection/event/mod.rs +++ b/src/peer_connection/event/mod.rs @@ -12,18 +12,25 @@ //! //! # Examples //! -//! ## Basic event loop pattern (conceptual) +//! ## Basic event loop pattern //! -//! ```ignore -//! // Note: poll_event() and related methods are part of the sans-I/O design -//! // but may not be fully exposed in the current public API -//! use rtc::peer_connection::RTCPeerConnection; +//! `poll_event()`, `poll_timeout()` and `handle_timeout()` come from the +//! [`sansio::Protocol`] implementation on [`RTCPeerConnection`](crate::peer_connection::RTCPeerConnection), +//! so that trait must be in +//! scope to call them. +//! +//! ```no_run +//! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::event::RTCPeerConnectionEvent; +//! use rtc::sansio::Protocol; //! use std::time::Instant; //! +//! # fn example() -> Result<(), Box> { //! let config = RTCConfigurationBuilder::new().build(); -//! let mut peer_connection = RTCPeerConnection::new(config)?; +//! let mut peer_connection = RTCPeerConnectionBuilder::new() +//! .with_configuration(config) +//! .build()?; //! //! loop { //! // Poll and handle events @@ -49,7 +56,8 @@ //! //! break; // Exit for example //! } -//! # Ok::<(), Box>(()) +//! # Ok(()) +//! # } //! ``` //! //! ## Handling connection state changes diff --git a/src/peer_connection/event/track_event.rs b/src/peer_connection/event/track_event.rs index 9729a0e4..5eeb7975 100644 --- a/src/peer_connection/event/track_event.rs +++ b/src/peer_connection/event/track_event.rs @@ -22,30 +22,27 @@ use crate::rtp_transceiver::{RTCRtpReceiverId, RtpStreamId}; /// /// ## Accessing track components /// -/// ```ignore -/// // Note: Accessing receiver/transceiver requires &mut RTCPeerConnection +/// ``` +/// // Note: accessing the receiver requires `&mut RTCPeerConnection` /// use rtc::peer_connection::RTCPeerConnection; /// use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; /// -/// fn handle_event(mut peer_connection: RTCPeerConnection, event: RTCPeerConnectionEvent) { -/// match event { -/// RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { -/// // Access the receiver -/// if let Some(receiver) = peer_connection.rtp_receiver(init.receiver_id) { -/// // Get receiver parameters -/// let params = receiver.get_parameters(); -/// println!("Codecs: {:?}", params.codecs); -/// } -/// -/// // Print associated stream IDs -/// println!("Stream IDs: {:?}", init.stream_ids); -/// -/// // Check if this is a simulcast stream -/// if let Some(rid) = &init.rid { -/// println!("Simulcast RID: {}", rid); -/// } +/// fn handle_event(peer_connection: &mut RTCPeerConnection, event: RTCPeerConnectionEvent) { +/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) = event { +/// // Access the receiver +/// if let Some(mut receiver) = peer_connection.rtp_receiver(init.receiver_id) { +/// // Get receiver parameters +/// let params = receiver.get_parameters(); +/// println!("Codecs: {:?}", params.rtp_parameters.codecs); +/// } +/// +/// // Print associated stream IDs +/// println!("Stream IDs: {:?}", init.stream_ids); +/// +/// // Check if this is a simulcast stream +/// if let Some(rid) = &init.rid { +/// println!("Simulcast RID: {}", rid); /// } -/// _ => {} /// } /// } /// ``` @@ -129,30 +126,35 @@ pub struct RTCTrackEventInit { /// # } /// ``` /// -/// ## Reading media from track (conceptual) +/// ## Reading media from track /// -/// ```ignore -/// // Note: poll_read() is part of sans-I/O design +/// `poll_event()` and `poll_read()` come from the [`sansio::Protocol`] implementation on +/// [`RTCPeerConnection`](crate::peer_connection::RTCPeerConnection), so that trait must be +/// in scope. +/// +/// ``` /// use rtc::peer_connection::RTCPeerConnection; /// use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; /// use rtc::peer_connection::message::RTCMessage; +/// use rtc::sansio::Protocol; /// -/// fn handle_events(mut peer_connection: RTCPeerConnection) { +/// fn handle_events(peer_connection: &mut RTCPeerConnection) { /// // Poll events /// while let Some(event) = peer_connection.poll_event() { -/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) = event { +/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(_init)) = event { /// println!("Track opened, ready to receive media"); /// } /// } /// -/// // Poll incoming media +/// // Poll incoming media. Both RTP and RTCP are tagged with the *track* id they +/// // belong to. /// while let Some(message) = peer_connection.poll_read() { /// match message { /// RTCMessage::RtpPacket(track_id, rtp) => { /// println!("RTP packet from track {:?}: {} bytes", track_id, rtp.payload.len()); /// } -/// RTCMessage::RtcpPacket(receiver_id, rtcp) => { -/// println!("RTCP packet: {:?}", rtcp); +/// RTCMessage::RtcpPacket(track_id, rtcp) => { +/// println!("RTCP packet for track {:?}: {:?}", track_id, rtcp); /// } /// _ => {} /// } diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index d15b23d7..f6615fdf 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -550,9 +550,11 @@ where /// - TWCC (Transport-Wide Congestion Control) for bandwidth estimation /// - RTCP Reports for quality statistics /// - /// This method changes the interceptor type from `NoopInterceptor` to the - /// registry's interceptor type. It must be the last builder method called - /// before `build()` when used. + /// This method replaces the builder's interceptor type — `NoopInterceptor` by default — + /// with the registry's, so it returns a `RTCPeerConnectionBuilder

` rather than + /// `Self`. Every other builder setting is carried over, and the remaining setters are + /// available on the returned builder, so this does not have to be the last call before + /// `build()`; it is simply the only one that changes the builder's type. /// /// # Type Parameters /// @@ -578,6 +580,47 @@ where /// # Ok(()) /// # } /// ``` + /// + /// # Type-erasing the chain + /// + /// A chain's type spells out its whole composition + /// (`TwccReceiverInterceptor>`), and it propagates into + /// every type that holds the peer connection. [`Registry::boxed`] erases it to + /// [`BoxedInterceptor`], so the connection has one concrete type — + /// `RTCPeerConnection` — whatever chain it was built from. That is + /// what lets a non-generic struct own one, or two connections with *different* chains + /// share a collection: + /// + /// ``` + /// use rtc::interceptor::{BoxedInterceptor, Registry}; + /// use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; + /// use rtc::peer_connection::configuration::media_engine::MediaEngine; + /// use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; + /// + /// struct Session { + /// peer_connection: RTCPeerConnection, // no type parameter + /// } + /// + /// # fn example() -> Result<(), Box> { + /// let mut media_engine = MediaEngine::default(); + /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + /// + /// let session = Session { + /// peer_connection: RTCPeerConnectionBuilder::new() + /// .with_media_engine(media_engine) + /// .with_interceptor_registry(registry.boxed()) + /// .build()?, + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// The cost is one virtual call per chain entry point; the chain's interior stays + /// statically dispatched. Keep the generic form when the chain is fixed at compile + /// time. + /// + /// [`BoxedInterceptor`]: crate::interceptor::BoxedInterceptor + /// [`Registry::boxed`]: crate::interceptor::Registry::boxed pub fn with_interceptor_registry

( self, interceptor_registry: Registry

, diff --git a/src/rtp_transceiver/rtp_receiver/mod.rs b/src/rtp_transceiver/rtp_receiver/mod.rs index d3fd9b42..97137d98 100644 --- a/src/rtp_transceiver/rtp_receiver/mod.rs +++ b/src/rtp_transceiver/rtp_receiver/mod.rs @@ -317,15 +317,26 @@ where /// /// # Example /// - /// ```ignore + /// ``` /// // Send a Picture Loss Indication to request a keyframe - /// use rtcp::picture_loss_indication::PictureLossIndication; + /// use rtc::peer_connection::RTCPeerConnection; + /// use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; + /// use rtc::rtp_transceiver::RTCRtpReceiverId; /// - /// let pli = PictureLossIndication { - /// sender_ssrc: 0, - /// media_ssrc: remote_ssrc, - /// }; - /// receiver.write_rtcp(vec![Box::new(pli)])?; + /// # fn example( + /// # peer_connection: &mut RTCPeerConnection, + /// # receiver_id: RTCRtpReceiverId, + /// # remote_ssrc: u32, + /// # ) -> Result<(), Box> { + /// if let Some(mut receiver) = peer_connection.rtp_receiver(receiver_id) { + /// let pli = PictureLossIndication { + /// sender_ssrc: 0, + /// media_ssrc: remote_ssrc, + /// }; + /// receiver.write_rtcp(vec![Box::new(pli)])?; + /// } + /// # Ok(()) + /// # } /// ``` pub fn write_rtcp(&mut self, packets: Vec>) -> Result<()> { // peer_connection is mutable borrow, its rtp_transceivers won't be resized and diff --git a/src/statistics/accumulator/mod.rs b/src/statistics/accumulator/mod.rs index 803e4bbb..f1663628 100644 --- a/src/statistics/accumulator/mod.rs +++ b/src/statistics/accumulator/mod.rs @@ -702,8 +702,9 @@ impl RTCStatsAccumulator { /// /// # Arguments /// - /// * `pair_id` - The ID of the candidate pair to sync - /// * `cp_stats.` - CandidatePairStats + /// * `local_id` - The local candidate ID of the pair to sync + /// * `remote_id` - The remote candidate ID of the pair to sync + /// * `cp_stats` - The ice agent's stats for that candidate pair pub(crate) fn update_ice_agent_stats( &mut self, local_id: &str, diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 4cbba968..940c3c44 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -12,14 +12,19 @@ //! //! # Example //! -//! ```ignore +//! ``` +//! use rtc::peer_connection::RTCPeerConnection; +//! use rtc::rtp_transceiver::RTCRtpSenderId; //! use rtc::statistics::StatsSelector; +//! use std::time::Instant; //! +//! # fn example(pc: &mut RTCPeerConnection, sender_id: RTCRtpSenderId) { //! // Get all stats //! let all_stats = pc.get_stats(Instant::now(), StatsSelector::None); //! //! // Get stats for a specific sender //! let sender_stats = pc.get_stats(Instant::now(), StatsSelector::Sender(sender_id)); +//! # } //! ``` use crate::rtp_transceiver::{RTCRtpReceiverId, RTCRtpSenderId}; diff --git a/src/statistics/report.rs b/src/statistics/report.rs index 42f0c530..1be4f5bd 100644 --- a/src/statistics/report.rs +++ b/src/statistics/report.rs @@ -116,9 +116,13 @@ impl RTCStatsReportEntry { /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::peer_connection::RTCPeerConnection; /// use rtc::statistics::StatsSelector; +/// use rtc::statistics::stats::RTCStatsType; +/// use std::time::Instant; /// +/// # fn example(peer_connection: &mut RTCPeerConnection) { /// let report = peer_connection.get_stats(Instant::now(), StatsSelector::None); /// /// // Iterate over all stats @@ -135,6 +139,7 @@ impl RTCStatsReportEntry { /// for inbound in report.iter_by_type(RTCStatsType::InboundRTP) { /// println!("Inbound RTP: {:?}", inbound.id()); /// } +/// # } /// ``` #[derive(Debug, Default)] pub struct RTCStatsReport { diff --git a/src/statistics/stats/mod.rs b/src/statistics/stats/mod.rs index 8692acdc..00dbb116 100644 --- a/src/statistics/stats/mod.rs +++ b/src/statistics/stats/mod.rs @@ -14,10 +14,12 @@ //! //! # Example //! -//! ```ignore -//! use std::time::Instant; +//! ``` +//! use rtc::peer_connection::RTCPeerConnection; //! use rtc::statistics::StatsSelector; +//! use std::time::Instant; //! +//! # fn example(peer_connection: &mut RTCPeerConnection) { //! let report = peer_connection.get_stats(Instant::now(), StatsSelector::None); //! //! // Access transport statistics @@ -30,6 +32,7 @@ //! for dc in report.data_channels() { //! println!("Channel '{}': {} messages sent", dc.label, dc.messages_sent); //! } +//! # } //! ``` use ::serde::{Deserialize, Serialize}; From f4fc5327d9408973d135006bb4938ef68ce73019 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 17:50:52 -0700 Subject: [PATCH 10/40] add #![warn(missing_docs)] for rtc sub-crates --- examples/signal/src/lib.rs | 15 + rtc-datachannel/src/data_channel/mod.rs | 28 + rtc-datachannel/src/lib.rs | 26 + .../src/message/message_channel_open.rs | 48 +- .../src/message/message_channel_threshold.rs | 6 + rtc-datachannel/src/message/message_type.rs | 8 +- rtc-datachannel/src/message/mod.rs | 13 +- rtc-dtls/src/alert/mod.rs | 12 + rtc-dtls/src/application_data.rs | 13 + rtc-dtls/src/change_cipher_spec/mod.rs | 12 + .../cipher_suite/cipher_suite_aes_128_ccm.rs | 2 + .../cipher_suite_aes_128_gcm_sha256.rs | 2 + .../cipher_suite_aes_256_cbc_sha.rs | 2 + .../cipher_suite_chacha20_poly1305_sha256.rs | 2 + ..._suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs | 1 + ...suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs | 1 + .../cipher_suite_tls_psk_with_aes_128_ccm.rs | 1 + .../cipher_suite_tls_psk_with_aes_128_ccm8.rs | 1 + ...r_suite_tls_psk_with_aes_128_gcm_sha256.rs | 1 + rtc-dtls/src/cipher_suite/mod.rs | 53 ++ rtc-dtls/src/client_certificate_type.rs | 4 + rtc-dtls/src/compression_methods.rs | 17 + rtc-dtls/src/config.rs | 15 + rtc-dtls/src/conn/mod.rs | 20 + rtc-dtls/src/content.rs | 22 + rtc-dtls/src/crypto/crypto_cbc.rs | 12 + rtc-dtls/src/crypto/crypto_ccm.rs | 15 + rtc-dtls/src/crypto/crypto_chacha20.rs | 12 + rtc-dtls/src/crypto/crypto_gcm.rs | 12 + rtc-dtls/src/crypto/mod.rs | 15 + rtc-dtls/src/crypto/padding.rs | 3 + rtc-dtls/src/curve/mod.rs | 4 + rtc-dtls/src/curve/named_curve.rs | 11 + rtc-dtls/src/endpoint.rs | 14 + .../src/extension/extension_server_name.rs | 13 + .../extension_supported_elliptic_curves.rs | 13 + .../extension_supported_point_formats.rs | 14 + ...xtension_supported_signature_algorithms.rs | 12 + .../extension_use_extended_master_secret.rs | 12 + rtc-dtls/src/extension/extension_use_srtp.rs | 17 + rtc-dtls/src/extension/mod.rs | 38 ++ rtc-dtls/src/extension/renegotiation_info.rs | 2 + rtc-dtls/src/handshake/handshake_header.rs | 13 + .../handshake_message_certificate.rs | 13 + .../handshake_message_certificate_request.rs | 14 + .../handshake_message_certificate_verify.rs | 13 + .../handshake_message_client_hello.rs | 13 + .../handshake_message_client_key_exchange.rs | 13 + .../handshake/handshake_message_finished.rs | 13 + .../handshake_message_hello_verify_request.rs | 12 + .../handshake_message_server_hello.rs | 13 + .../handshake_message_server_hello_done.rs | 13 + .../handshake_message_server_key_exchange.rs | 13 + rtc-dtls/src/handshake/handshake_random.rs | 16 + rtc-dtls/src/handshake/mod.rs | 57 ++ rtc-dtls/src/lib.rs | 54 ++ rtc-dtls/src/record_layer/mod.rs | 14 + .../src/record_layer/record_layer_header.rs | 27 + rtc-dtls/src/signature_hash_algorithm/mod.rs | 35 +- rtc-dtls/src/state.rs | 24 + rtc-ice/src/agent/agent_config.rs | 2 + rtc-ice/src/agent/agent_stats.rs | 3 +- rtc-ice/src/agent/mod.rs | 20 + rtc-ice/src/attributes/control/mod.rs | 3 + rtc-ice/src/attributes/mod.rs | 3 + rtc-ice/src/attributes/use_candidate/mod.rs | 1 + rtc-ice/src/candidate/candidate_host.rs | 2 + rtc-ice/src/candidate/candidate_pair.rs | 8 + .../src/candidate/candidate_peer_reflexive.rs | 3 + rtc-ice/src/candidate/candidate_relay.rs | 4 + .../candidate/candidate_server_reflexive.rs | 4 + rtc-ice/src/candidate/mod.rs | 47 ++ rtc-ice/src/lib.rs | 41 ++ rtc-ice/src/network_type/mod.rs | 3 + rtc-ice/src/state/mod.rs | 2 + rtc-ice/src/stats/mod.rs | 158 ++--- rtc-ice/src/tcp_type/mod.rs | 3 + rtc-ice/src/url/mod.rs | 8 + rtc-interceptor-derive/src/lib.rs | 1 + rtc-interceptor/src/lib.rs | 1 + rtc-mdns/src/lib.rs | 1 + rtc-mdns/src/socket.rs | 7 + rtc-media/src/audio/buffer.rs | 37 +- rtc-media/src/audio/buffer/info.rs | 3 + rtc-media/src/audio/buffer/layout.rs | 10 + rtc-media/src/audio/mod.rs | 1 + rtc-media/src/audio/sample.rs | 3 + rtc-media/src/io/h26x_reader/mod.rs | 14 + rtc-media/src/io/h26x_reader/sample_reader.rs | 12 + rtc-media/src/io/ivf_reader/mod.rs | 35 +- rtc-media/src/io/ivf_writer/mod.rs | 3 + rtc-media/src/io/mod.rs | 19 + rtc-media/src/io/ogg_reader/mod.rs | 29 + rtc-media/src/io/sample_builder/mod.rs | 5 + rtc-media/src/lib.rs | 25 + rtc-rtcp/src/extended_report/dlrr.rs | 5 + rtc-rtcp/src/extended_report/mod.rs | 32 +- rtc-rtcp/src/extended_report/prt.rs | 6 + rtc-rtcp/src/extended_report/rle.rs | 10 + rtc-rtcp/src/extended_report/rrt.rs | 2 + rtc-rtcp/src/extended_report/ssr.rs | 21 + rtc-rtcp/src/extended_report/unknown.rs | 2 + rtc-rtcp/src/extended_report/vm.rs | 23 + rtc-rtcp/src/header.rs | 35 +- rtc-rtcp/src/lib.rs | 16 + rtc-rtcp/src/packet.rs | 9 + .../full_intra_request/mod.rs | 5 + rtc-rtcp/src/payload_feedbacks/mod.rs | 4 + .../slice_loss_indication/mod.rs | 1 + rtc-rtcp/src/source_description/mod.rs | 28 +- rtc-rtcp/src/transport_feedbacks/mod.rs | 3 + .../transport_layer_cc/mod.rs | 6 + .../transport_layer_nack/mod.rs | 9 + rtc-rtp/src/codec/av1/depacketizer.rs | 1 + rtc-rtp/src/codec/av1/mod.rs | 1 + rtc-rtp/src/codec/g7xx/mod.rs | 1 + rtc-rtp/src/codec/h264/mod.rs | 17 + rtc-rtp/src/codec/h265/mod.rs | 41 ++ rtc-rtp/src/codec/mod.rs | 7 + rtc-rtp/src/codec/opus/mod.rs | 1 + rtc-rtp/src/codec/vp8/mod.rs | 4 + rtc-rtp/src/codec/vp9/mod.rs | 3 + .../extension/abs_send_time_extension/mod.rs | 2 + .../extension/audio_level_extension/mod.rs | 3 + rtc-rtp/src/extension/mod.rs | 14 + .../extension/playout_delay_extension/mod.rs | 6 + .../extension/transport_cc_extension/mod.rs | 4 + .../video_orientation_extension/mod.rs | 12 + rtc-rtp/src/header.rs | 40 ++ rtc-rtp/src/lib.rs | 35 + rtc-rtp/src/packet/mod.rs | 2 + rtc-rtp/src/packetizer/mod.rs | 26 + rtc-rtp/src/sequence.rs | 5 + rtc-sctp/src/association/mod.rs | 2 + rtc-sctp/src/association/stats.rs | 11 + rtc-sctp/src/association/stream.rs | 7 +- rtc-sctp/src/association/timer.rs | 12 + rtc-sctp/src/chunk/chunk_payload_data.rs | 8 + rtc-sctp/src/config.rs | 13 + rtc-sctp/src/lib.rs | 3 + rtc-sctp/src/queue/reassembly_queue.rs | 15 +- rtc-sdp/src/description/common.rs | 203 +++--- rtc-sdp/src/description/media.rs | 636 +++++++++--------- rtc-sdp/src/description/mod.rs | 15 +- rtc-sdp/src/description/session.rs | 37 + rtc-sdp/src/direction/mod.rs | 103 +-- rtc-sdp/src/extmap/mod.rs | 14 + rtc-sdp/src/lib.rs | 30 + rtc-sdp/src/util/mod.rs | 10 + rtc-shared/src/crypto/mod.rs | 9 + rtc-shared/src/error.rs | 602 ++++++++++++++++- rtc-shared/src/ifaces/mod.rs | 18 + rtc-shared/src/lib.rs | 38 ++ rtc-shared/src/marshal/mod.rs | 26 + rtc-shared/src/replay_detector/mod.rs | 39 +- rtc-shared/src/serde.rs | 13 + rtc-shared/src/time.rs | 17 + rtc-shared/src/util.rs | 15 +- rtc-srtp/src/config.rs | 9 + rtc-srtp/src/context/mod.rs | 2 + rtc-srtp/src/context/srtp.rs | 17 + rtc-srtp/src/lib.rs | 27 + rtc-srtp/src/option.rs | 6 + rtc-srtp/src/protection_profile.rs | 17 + rtc-stun/src/addr.rs | 2 + rtc-stun/src/agent.rs | 19 + rtc-stun/src/attributes.rs | 123 ++-- rtc-stun/src/checks.rs | 8 +- rtc-stun/src/client.rs | 10 + rtc-stun/src/error_code.rs | 63 +- rtc-stun/src/fingerprint.rs | 25 +- rtc-stun/src/integrity.rs | 17 +- rtc-stun/src/lib.rs | 47 +- rtc-stun/src/message.rs | 209 +++--- rtc-stun/src/textattrs.rs | 30 +- rtc-stun/src/uattrs.rs | 6 +- rtc-stun/src/uri.rs | 8 +- rtc-stun/src/xoraddr.rs | 2 + rtc-turn/src/client/mod.rs | 39 ++ rtc-turn/src/client/relay.rs | 26 +- rtc-turn/src/lib.rs | 25 + rtc-turn/src/proto/addr.rs | 7 + rtc-turn/src/proto/chandata.rs | 3 + rtc-turn/src/proto/channum.rs | 4 + rtc-turn/src/proto/mod.rs | 12 + rtc-turn/src/proto/peeraddr.rs | 2 + rtc-turn/src/proto/relayaddr.rs | 2 + rtc-turn/src/proto/reqfamily.rs | 2 + rtc-turn/src/proto/reqtrans.rs | 1 + 189 files changed, 3743 insertions(+), 800 deletions(-) diff --git a/examples/signal/src/lib.rs b/examples/signal/src/lib.rs index 0645447b..bdfdc648 100644 --- a/examples/signal/src/lib.rs +++ b/examples/signal/src/lib.rs @@ -1,6 +1,16 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Signaling helpers for the WebRTC.rs examples. +//! +//! The examples in this workspace exchange SDP by hand: one side prints a base64 blob, you +//! paste it into the other. This crate holds the few functions that make that work, so the +//! examples can stay focused on the WebRTC parts. +//! +//! It is a support crate for the examples, not part of the WebRTC API — nothing here is +//! needed to use [`rtc`](https://docs.rs/rtc) or [`webrtc`](https://docs.rs/webrtc). + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; @@ -102,6 +112,11 @@ pub fn decode(s: &str) -> Result { Ok(s) } +/// Best-effort discovery of this host's primary local IPv4 address. +/// +/// Opens a UDP socket toward a public address and reads back the local address the OS +/// picked — no packet is sent. Falls back to `127.0.0.1` if that fails, which is what the +/// examples want when running offline. pub fn get_local_ip() -> IpAddr { if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") && socket.connect("8.8.8.8:80").is_ok() diff --git a/rtc-datachannel/src/data_channel/mod.rs b/rtc-datachannel/src/data_channel/mod.rs index 6e9f6892..2ddbd2be 100644 --- a/rtc-datachannel/src/data_channel/mod.rs +++ b/rtc-datachannel/src/data_channel/mod.rs @@ -17,20 +17,35 @@ const RECEIVE_MTU: usize = 8192; /// DataChannelConfig is used to configure the data channel. #[derive(Eq, PartialEq, Default, Clone, Debug)] pub struct DataChannelConfig { + /// The reliability and ordering guarantees to request. pub channel_type: ChannelType, + /// Whether the channel was negotiated out of band. + /// + /// When `true` no DCEP `DATA_CHANNEL_OPEN` is sent — both sides are assumed to have agreed + /// the stream id and parameters through signalling instead. pub negotiated: bool, + /// The channel's relative priority; see the `CHANNEL_PRIORITY_*` constants. pub priority: u16, + /// The retransmission count or message lifetime, interpreted according to + /// [`Self::channel_type`]. pub reliability_parameter: u32, + /// The channel label, used to distinguish channels on the same association. pub label: String, + /// The subprotocol name, or empty if none. pub protocol: String, } /// DataChannelMessage is used to data sent over SCTP #[derive(Debug, Default, Clone)] pub struct DataChannelMessage { + /// Identifies the SCTP association this message belongs to. pub association_handle: usize, + /// The SCTP stream the message arrived on or should be sent on. pub stream_id: u16, + /// The payload protocol identifier, which distinguishes DCEP control messages from string + /// and binary user data. pub ppi: PayloadProtocolIdentifier, + /// The message bytes. pub payload: BytesMut, /// Marks a `DATA_CHANNEL_OPEN` that belongs to an out-of-band *negotiated* @@ -167,6 +182,7 @@ impl DataChannel { self.stream_id } + /// The configuration this channel was opened with. pub fn config(&self) -> &DataChannelConfig { &self.config } @@ -264,6 +280,10 @@ impl DataChannel { self.stream.on_buffered_amount_low(f) }*/ + /// Decomposes a [`ChannelType`] into the SCTP send parameters it implies. + /// + /// Returns whether delivery is unordered, together with the reliability type and value SCTP + /// needs for partial reliability. pub fn get_reliability_params(channel_type: ChannelType) -> (bool, ReliabilityType) { match channel_type { ChannelType::Reliable => (false, ReliabilityType::Reliable), @@ -275,6 +295,10 @@ impl DataChannel { } } + /// Derives the [`ChannelType`] and reliability parameter from the `maxPacketLifeTime` / + /// `maxRetransmits` pair the W3C API exposes. + /// + /// The two are mutually exclusive; supplying neither yields a fully reliable channel. pub fn get_channel_type_and_reliability_parameter( ordered: bool, max_retransmits: Option, @@ -315,6 +339,10 @@ impl DataChannel { (channel_type, reliability_parameter) } + /// Builds the payload protocol identifier and payload for a user message. + /// + /// Empty messages get their own identifiers (`StringEmpty`/`BinaryEmpty`) because SCTP + /// cannot carry a zero-length payload. pub fn get_data_channel_message(is_string: bool, data: BytesMut) -> DataChannelMessage { // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-12#section-6.6 // SCTP does not support the sending of empty user messages. Therefore, diff --git a/rtc-datachannel/src/lib.rs b/rtc-datachannel/src/lib.rs index f4b2e477..b0a15a09 100644 --- a/rtc-datachannel/src/lib.rs +++ b/rtc-datachannel/src/lib.rs @@ -1,5 +1,31 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! WebRTC data channels over SCTP. +//! +//! The Data Channel Establishment Protocol ([RFC 8832]) and the SCTP-based data channel +//! layer ([RFC 8831]): the `DATA_CHANNEL_OPEN` handshake, the reliability and ordering +//! parameters, and the payload protocol identifiers that distinguish string from binary +//! messages. +//! +//! # Structure +//! +//! * [`data_channel`] — one channel's state and its read/write surface over an SCTP +//! stream, including partial-reliability settings (`maxPacketLifeTime`, +//! `maxRetransmits`). +//! * [`message`] — the DCEP messages themselves: `DataChannelOpen`, `DataChannelAck`, and +//! the channel-type encoding. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate layers it over [`rtc-sctp`] and exposes +//! `RTCDataChannel`. +//! +//! [RFC 8832]: https://datatracker.ietf.org/doc/html/rfc8832 +//! [RFC 8831]: https://datatracker.ietf.org/doc/html/rfc8831 +//! [`rtc-sctp`]: https://docs.rs/rtc-sctp + +/// One data channel's state and its read/write surface over an SCTP stream. pub mod data_channel; +/// The DCEP messages exchanged to open, acknowledge and close a channel. pub mod message; diff --git a/rtc-datachannel/src/message/message_channel_open.rs b/rtc-datachannel/src/message/message_channel_open.rs index 56db1b24..7292913d 100644 --- a/rtc-datachannel/src/message/message_channel_open.rs +++ b/rtc-datachannel/src/message/message_channel_open.rs @@ -11,37 +11,41 @@ const CHANNEL_TYPE_LEN: usize = 1; /// ChannelPriority pub const CHANNEL_PRIORITY_BELOW_NORMAL: u16 = 128; +/// The default channel priority. pub const CHANNEL_PRIORITY_NORMAL: u16 = 256; +/// A higher-than-default channel priority. pub const CHANNEL_PRIORITY_HIGH: u16 = 512; +/// The highest channel priority defined by the spec. pub const CHANNEL_PRIORITY_EXTRA_HIGH: u16 = 1024; #[derive(Default, Eq, PartialEq, Copy, Clone, Debug)] +/// The reliability and ordering guarantees a data channel is opened with. +/// +/// Carried in the `DATA_CHANNEL_OPEN` message ([RFC 8832] §5.1); `reliability_parameter` +/// supplies the retransmission count or lifetime for the partial-reliability variants. +/// +/// [RFC 8832]: https://datatracker.ietf.org/doc/html/rfc8832 pub enum ChannelType { - // `Reliable` determines the Data Channel provides a - // reliable in-order bi-directional communication. + /// Reliable, in-order delivery — the SCTP default, and what `RTCDataChannel` gives you + /// unless you ask otherwise. #[default] Reliable, - // `ReliableUnordered` determines the Data Channel - // provides a reliable unordered bi-directional communication. + /// Reliable delivery, but messages may arrive out of order. ReliableUnordered, - // `PartialReliableRexmit` determines the Data Channel - // provides a partially-reliable in-order bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. + /// Partially reliable and in-order: a message is retransmitted at most + /// `reliability_parameter` times before being abandoned. + /// + /// This is what `maxRetransmits` maps to. PartialReliableRexmit, - // `PartialReliableRexmitUnordered` determines - // the Data Channel provides a partial reliable unordered bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. + /// As [`Self::PartialReliableRexmit`], but messages may arrive out of order. PartialReliableRexmitUnordered, - // `PartialReliableTimed` determines the Data Channel - // provides a partial reliable in-order bi-directional communication. - // User messages might not be transmitted or retransmitted after - // a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. + /// Partially reliable and in-order: a message is abandoned once + /// `reliability_parameter` milliseconds have passed. + /// + /// The lifetime starts when the message is handed to the stack, not when it is first + /// transmitted. This is what `maxPacketLifeTime` maps to. PartialReliableTimed, - // The Data Channel provides a partial reliable unordered bi-directional - // communication. User messages might not be transmitted or retransmitted - // after a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. + /// As [`Self::PartialReliableTimed`], but messages may arrive out of order. PartialReliableTimedUnordered, } @@ -135,10 +139,16 @@ const CHANNEL_OPEN_HEADER_LEN: usize = 11; /// ``` #[derive(Eq, PartialEq, Clone, Debug)] pub struct DataChannelOpen { + /// The reliability and ordering guarantees requested for the channel. pub channel_type: ChannelType, + /// The channel's relative priority, one of the `CHANNEL_PRIORITY_*` constants. pub priority: u16, + /// The retransmission count or message lifetime in milliseconds, interpreted according to + /// [`Self::channel_type`]. pub reliability_parameter: u32, + /// The channel label, as UTF-8 bytes. pub label: Vec, + /// The subprotocol name, as UTF-8 bytes. Empty if none was negotiated. pub protocol: Vec, } diff --git a/rtc-datachannel/src/message/message_channel_threshold.rs b/rtc-datachannel/src/message/message_channel_threshold.rs index 01f24536..24cbfe8c 100644 --- a/rtc-datachannel/src/message/message_channel_threshold.rs +++ b/rtc-datachannel/src/message/message_channel_threshold.rs @@ -13,8 +13,14 @@ use shared::error::Result; ///+-+-+-+-+-+-+-+-+ /// ``` #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// A buffered-amount threshold crossing, reported internally so the channel can raise +/// `OnBufferedAmountLow`/`OnBufferedAmountHigh`. +/// +/// Not a DCEP message — it never appears on the wire. pub enum DataChannelThreshold { + /// The buffered amount fell to or below the low threshold, carrying its value. Low(u32), + /// The buffered amount rose to or above the high threshold, carrying its value. High(u32), } // internal usage only diff --git a/rtc-datachannel/src/message/message_type.rs b/rtc-datachannel/src/message/message_type.rs index 08d0206a..bcb692ec 100644 --- a/rtc-datachannel/src/message/message_type.rs +++ b/rtc-datachannel/src/message/message_type.rs @@ -8,12 +8,16 @@ pub(crate) const MESSAGE_TYPE_ACK: u8 = 0x02; pub(crate) const MESSAGE_TYPE_OPEN: u8 = 0x03; pub(crate) const MESSAGE_TYPE_LEN: usize = 1; -// A parsed DataChannel message +/// The one-byte type that prefixes a DCEP message. #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum MessageType { + /// A buffered-amount threshold crossing. Internal to this crate. DataChannelThreshold, // internal usage only - DataChannelClose, // internal usage only + /// A channel close notification. Internal to this crate. + DataChannelClose, // internal usage only + /// `DATA_CHANNEL_ACK` (`0x02`). DataChannelAck, + /// `DATA_CHANNEL_OPEN` (`0x03`). DataChannelOpen, } diff --git a/rtc-datachannel/src/message/mod.rs b/rtc-datachannel/src/message/mod.rs index b062bf3d..fc465325 100644 --- a/rtc-datachannel/src/message/mod.rs +++ b/rtc-datachannel/src/message/mod.rs @@ -1,10 +1,15 @@ #[cfg(test)] mod message_test; +/// The `DATA_CHANNEL_ACK` message, which confirms a channel was opened. pub mod message_channel_ack; +/// An internal close notification (not a DCEP message). pub mod message_channel_close; +/// The `DATA_CHANNEL_OPEN` message and the channel parameters it carries. pub mod message_channel_open; +/// Internal buffered-amount threshold notifications. pub mod message_channel_threshold; +/// The one-byte message type that prefixes every DCEP message. pub mod message_type; use bytes::{Buf, BufMut}; @@ -19,9 +24,14 @@ use shared::marshal::*; /// A parsed DataChannel message #[derive(Eq, PartialEq, Clone, Debug)] pub enum Message { + /// A buffered-amount threshold crossing. Internal to this crate — not a DCEP message. DataChannelThreshold(DataChannelThreshold), // internal usage only - DataChannelClose(DataChannelClose), // internal usage only + /// A channel close notification. Internal to this crate — not a DCEP message. + DataChannelClose(DataChannelClose), // internal usage only + /// `DATA_CHANNEL_ACK`, sent by the peer to confirm it accepted a `DATA_CHANNEL_OPEN`. DataChannelAck(DataChannelAck), + /// `DATA_CHANNEL_OPEN`, which opens a channel and carries its label, protocol and + /// reliability parameters. DataChannelOpen(DataChannelOpen), } @@ -79,6 +89,7 @@ impl Unmarshal for Message { } impl Message { + /// The type byte that identifies this message on the wire. pub fn message_type(&self) -> MessageType { match self { Self::DataChannelThreshold(_) => MessageType::DataChannelThreshold, // internal usage only diff --git a/rtc-dtls/src/alert/mod.rs b/rtc-dtls/src/alert/mod.rs index 9f4b7973..e818d5e1 100644 --- a/rtc-dtls/src/alert/mod.rs +++ b/rtc-dtls/src/alert/mod.rs @@ -161,14 +161,21 @@ impl fmt::Display for Alert { } impl Alert { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::Alert } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.alert_level as u8)?; writer.write_u8(self.alert_description as u8)?; @@ -176,6 +183,11 @@ impl Alert { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let alert_level = reader.read_u8()?.into(); let alert_description = reader.read_u8()?.into(); diff --git a/rtc-dtls/src/application_data.rs b/rtc-dtls/src/application_data.rs index 0576771c..bc2b4963 100644 --- a/rtc-dtls/src/application_data.rs +++ b/rtc-dtls/src/application_data.rs @@ -15,24 +15,37 @@ use shared::error::Result; /// [RFC 5246 §10]: https://tools.ietf.org/html/rfc5246#section-10 #[derive(Clone, PartialEq, Eq, Debug)] pub struct ApplicationData { + /// The application payload. pub data: BytesMut, } impl ApplicationData { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::ApplicationData } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.data.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_all(&self.data)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { // Read straight into the BytesMut-backed Vec instead of staging in a // temporary Vec and copying the whole payload a second time. diff --git a/rtc-dtls/src/change_cipher_spec/mod.rs b/rtc-dtls/src/change_cipher_spec/mod.rs index ad884061..736e25fa 100644 --- a/rtc-dtls/src/change_cipher_spec/mod.rs +++ b/rtc-dtls/src/change_cipher_spec/mod.rs @@ -20,20 +20,32 @@ use shared::error::*; pub struct ChangeCipherSpec; impl ChangeCipherSpec { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::ChangeCipherSpec } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(0x01)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let data = reader.read_u8()?; if data != 0x01 { diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs index 3ab6fbdc..dd566977 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs @@ -4,6 +4,7 @@ use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen}; use crate::prf::*; #[derive(Clone)] +/// The shared AES-128-CCM implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128Ccm { ccm: Option, client_certificate_type: ClientCertificateType, @@ -17,6 +18,7 @@ impl CipherSuiteAes128Ccm { const PRF_KEY_LEN: usize = 16; const PRF_IV_LEN: usize = 4; + /// Builds an uninitialized AES-128-CCM suite; keys are installed later via `init`. pub fn new( client_certificate_type: ClientCertificateType, id: CipherSuiteId, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs index 0c3d5ffd..87b4fbe0 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs @@ -3,6 +3,7 @@ use crate::crypto::crypto_gcm::*; use crate::prf::*; #[derive(Clone)] +/// The shared AES-128-GCM with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128GcmSha256 { gcm: Option, rsa: bool, @@ -13,6 +14,7 @@ impl CipherSuiteAes128GcmSha256 { const PRF_KEY_LEN: usize = 16; const PRF_IV_LEN: usize = 4; + /// Builds an uninitialized AES-128-GCM with SHA-256 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteAes128GcmSha256 { gcm: None, rsa } } diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs index 3654f93f..52601e2e 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs @@ -3,6 +3,7 @@ use crate::crypto::crypto_cbc::*; use crate::prf::*; #[derive(Clone)] +/// The shared AES-256-CBC with SHA-1 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes256CbcSha { cbc: Option, rsa: bool, @@ -13,6 +14,7 @@ impl CipherSuiteAes256CbcSha { const PRF_KEY_LEN: usize = 32; const PRF_IV_LEN: usize = 16; + /// Builds an uninitialized AES-256-CBC with SHA-1 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteAes256CbcSha { cbc: None, rsa } } diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs index 7066a4bf..2c045ac5 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs @@ -3,6 +3,7 @@ use crate::crypto::crypto_chacha20::*; use crate::prf::*; #[derive(Clone)] +/// The shared ChaCha20-Poly1305 with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteChaCha20Poly1305Sha256 { rsa: bool, cipher: Option, @@ -13,6 +14,7 @@ impl CipherSuiteChaCha20Poly1305Sha256 { const PRF_KEY_LEN: usize = 32; const PRF_IV_LEN: usize = 12; + /// Builds an uninitialized ChaCha20-Poly1305 with SHA-256 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteChaCha20Poly1305Sha256 { rsa, cipher: None } } diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs index c5bf2dfd..86df6793 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` cipher suite. pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::EcdsaSign, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs index aa9a92ce..5e8ed547 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` cipher suite. pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::EcdsaSign, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs index 6f506e0e..cb25399b 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_PSK_WITH_AES_128_CCM` cipher suite. pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::Unsupported, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs index 64b9f1a5..62861cda 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_PSK_WITH_AES_128_CCM_8` cipher suite. pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::Unsupported, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs index cb0f3e51..f0f31840 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs @@ -3,6 +3,7 @@ use crate::crypto::crypto_gcm::*; use crate::prf::*; #[derive(Clone, Default)] +/// The shared `TLS_PSK_WITH_AES_128_GCM_SHA256` implementation, parameterized over the key exchange and signature. pub struct CipherSuiteTlsPskWithAes128GcmSha256 { gcm: Option, } diff --git a/rtc-dtls/src/cipher_suite/mod.rs b/rtc-dtls/src/cipher_suite/mod.rs index 07d6f229..6552ee82 100644 --- a/rtc-dtls/src/cipher_suite/mod.rs +++ b/rtc-dtls/src/cipher_suite/mod.rs @@ -1,11 +1,21 @@ +/// Shared implementation for the AES-128-CCM suites. pub mod cipher_suite_aes_128_ccm; +/// Shared implementation for the AES-128-GCM-SHA256 suites. pub mod cipher_suite_aes_128_gcm_sha256; +/// Shared implementation for the AES-256-CBC-SHA suites. pub mod cipher_suite_aes_256_cbc_sha; +/// Shared implementation for the ChaCha20-Poly1305-SHA256 suites. pub mod cipher_suite_chacha20_poly1305_sha256; +/// An ECDHE-ECDSA suite with AES-128, the pairing WebRTC normally negotiates. +/// An ECDHE-ECDSA suite with AES-128-CCM and a truncated 8-byte tag. pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm; +/// An ECDHE-ECDSA suite with AES-128-CCM and a full 16-byte tag. pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8; +/// `TLS_PSK_WITH_AES_128_CCM`, for pre-shared-key handshakes. pub mod cipher_suite_tls_psk_with_aes_128_ccm; +/// `TLS_PSK_WITH_AES_128_CCM_8`, with a truncated 8-byte tag. pub mod cipher_suite_tls_psk_with_aes_128_ccm8; +/// `TLS_PSK_WITH_AES_128_GCM_SHA256`, for pre-shared-key handshakes. pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256; use std::fmt; @@ -27,27 +37,40 @@ use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*; // Supported Cipher Suites #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The cipher suites this crate can negotiate, by their IANA code points. pub enum CipherSuiteId { // AES-128-CCM + /// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` (`0xc0ac`). Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac, + /// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` (`0xc0ae`). Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae, // AES-128-GCM-SHA256 + /// `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` (`0xc02b`). Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b, + /// `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` (`0xc02f`). Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f, // AES-256-CBC-SHA + /// `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` (`0xc00a`). Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a, + /// `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` (`0xc014`). Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014, + /// `TLS_PSK_WITH_AES_128_CCM` (`0xc0a4`). Tls_Psk_With_Aes_128_Ccm = 0xc0a4, + /// `TLS_PSK_WITH_AES_128_CCM_8` (`0xc0a8`). Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8, + /// `TLS_PSK_WITH_AES_128_GCM_SHA256` (`0x00a8`). Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8, // CHACHA20_POLY1305_SHA256 + /// `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` (`0xcca8`). Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 = 0xcca8, + /// `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` (`0xcca9`). Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 = 0xcca9, + /// A code point this crate does not implement. Unsupported, } @@ -151,7 +174,9 @@ impl From<&str> for CipherSuiteId { } #[derive(Copy, Clone, Debug)] +/// The hash a suite uses in its PRF and `Finished` computation. pub enum CipherSuiteHash { + /// SHA-256. Sha256, } @@ -163,15 +188,28 @@ impl CipherSuiteHash { } } +/// A negotiated cipher suite: its identity, and the record encryption it performs once keys +/// are installed. pub trait CipherSuite: Send + Sync { + /// The suite's IANA name. fn to_string(&self) -> String; + /// The suite's code point. fn id(&self) -> CipherSuiteId; + /// The certificate type this suite requires of a peer. fn certificate_type(&self) -> ClientCertificateType; + /// The hash used for the PRF and `Finished`. fn hash_func(&self) -> CipherSuiteHash; + /// Whether this suite authenticates with a pre-shared key rather than certificates. fn is_psk(&self) -> bool; + /// Whether keys have been installed, so records can be protected. fn is_initialized(&self) -> bool; // Generate the internal encryption state + /// Installs the keying material derived from the handshake. + /// + /// # Errors + /// + /// Fails if the key or salt lengths do not match what this suite expects. fn init( &mut self, master_secret: &[u8], @@ -180,13 +218,28 @@ pub trait CipherSuite: Send + Sync { is_client: bool, ) -> Result<()>; + /// Protects one record, returning the encrypted record including its header. + /// + /// # Errors + /// + /// Fails if keys are not installed, or the cipher rejects the input. fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails, or the record is malformed. fn decrypt(&self, input: &[u8]) -> Result>; } // Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml // A cipher_suite is a specific combination of key agreement, cipher and MAC // function. +/// Builds the [`CipherSuite`] implementation for `id`. +/// +/// # Errors +/// +/// Fails if the id is not one this crate implements. pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result> { match id { CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { diff --git a/rtc-dtls/src/client_certificate_type.rs b/rtc-dtls/src/client_certificate_type.rs index d87aacb6..567d3dcf 100644 --- a/rtc-dtls/src/client_certificate_type.rs +++ b/rtc-dtls/src/client_certificate_type.rs @@ -1,7 +1,11 @@ #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The certificate types a server may request from a client. pub enum ClientCertificateType { + /// `RSA_SIGN` (`1`). RsaSign = 1, + /// `ECDSA_SIGN` (`64`). EcdsaSign = 64, + /// A type this crate does not implement. Unsupported, } diff --git a/rtc-dtls/src/compression_methods.rs b/rtc-dtls/src/compression_methods.rs index b15f6350..7a5dee9f 100644 --- a/rtc-dtls/src/compression_methods.rs +++ b/rtc-dtls/src/compression_methods.rs @@ -4,8 +4,11 @@ use byteorder::{ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Compression methods. DTLS in WebRTC always negotiates `Null`. pub enum CompressionMethodId { + /// `NULL` (`0`). Null = 0, + /// A method this crate does not implement. Unsupported, } @@ -19,15 +22,23 @@ impl From for CompressionMethodId { } #[derive(Clone, Debug, PartialEq, Eq)] +/// The compression-methods list offered or selected in a hello message. pub struct CompressionMethods { + /// The methods, in preference order. pub ids: Vec, } impl CompressionMethods { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + self.ids.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.ids.len() as u8)?; @@ -38,6 +49,11 @@ impl CompressionMethods { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let compression_methods_count = reader.read_u8()? as usize; let mut ids = vec![]; @@ -52,6 +68,7 @@ impl CompressionMethods { } } +/// The default list: null compression only. pub fn default_compression_methods() -> CompressionMethods { CompressionMethods { ids: vec![CompressionMethodId::Null], diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 3a48ea45..9ac4a111 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -236,20 +236,29 @@ pub(crate) type PskCallback = Arc Result>) + Send + Sy #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum ClientAuthType { #[default] + /// `NO_CLIENT_CERT` (`0`). NoClientCert = 0, + /// `REQUEST_CLIENT_CERT` (`1`). RequestClientCert = 1, + /// `REQUIRE_ANY_CLIENT_CERT` (`2`). RequireAnyClientCert = 2, + /// `VERIFY_CLIENT_CERT_IF_GIVEN` (`3`). VerifyClientCertIfGiven = 3, + /// `REQUIRE_AND_VERIFY_CLIENT_CERT` (`4`). RequireAndVerifyClientCert = 4, } // ExtendedMasterSecretType declares the policy the client and server // will follow for the Extended Master Secret extension #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +/// How strictly to require the extended master secret extension ([RFC 7627]). pub enum ExtendedMasterSecretType { #[default] + /// `REQUEST` (`0`). Request = 0, + /// `REQUIRE` (`1`). Require = 1, + /// `DISABLE` (`2`). Disable = 2, } @@ -359,9 +368,14 @@ impl ConfigBuilder { } } +/// A callback that decides whether a peer's certificate chain is acceptable. +/// +/// WebRTC verifies the fingerprint from SDP instead of a CA chain, so this is where that check +/// goes. pub type VerifyPeerCertificateFn = Arc], &[CertificateDer<'static>]) -> Result<()>) + Send + Sync>; +/// Generates a self-signed certificate, as WebRTC endpoints use. pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { let mut certs = rustls::RootCertStore::empty(); certs @@ -377,6 +391,7 @@ pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { } #[derive(Clone)] +/// The resolved configuration a handshake runs with, built from a [`ConfigBuilder`]. pub struct HandshakeConfig { pub(crate) local_psk_callback: Option, pub(crate) local_psk_identity_hint: Option>, diff --git a/rtc-dtls/src/conn/mod.rs b/rtc-dtls/src/conn/mod.rs index ba66d38a..cafa4b42 100644 --- a/rtc-dtls/src/conn/mod.rs +++ b/rtc-dtls/src/conn/mod.rs @@ -45,6 +45,7 @@ pub(crate) static INVALID_KEYING_LABELS: &[&str] = &[ ]; // Conn represents a DTLS connection +/// One DTLS association: the handshake state machine plus the record layer around it. pub struct DTLSConn { is_client: bool, maximum_transmission_unit: usize, @@ -89,6 +90,12 @@ pub struct DTLSConn { } impl DTLSConn { + /// Creates a connection in the given role, ready to handshake. + /// + /// # Errors + /// + /// Fails if the configuration is invalid — no certificate for a server, or an unusable cipher + /// suite list. pub fn new( handshake_config: Arc, is_client: bool, @@ -151,6 +158,7 @@ impl DTLSConn { } // Read reads data from the connection. + /// Takes the next decrypted application payload, if one is ready. pub fn incoming_application_data(&mut self) -> Option { if !self.is_handshake_completed() { None @@ -159,6 +167,7 @@ impl DTLSConn { } } + /// Takes the next datagram the caller should send. pub fn outgoing_raw_packet(&mut self) -> Option { if let Err(err) = self.handle_outgoing_packets() { warn!( @@ -171,6 +180,11 @@ impl DTLSConn { } // Write writes p to the DTLS connection + /// Queues `p` as application data, encrypting it once the handshake has completed. + /// + /// # Errors + /// + /// Fails if the connection is closed or the handshake has not finished. pub fn write(&mut self, p: &[u8]) -> Result<()> { if self.is_connection_closed() { return Err(Error::ErrConnClosed); @@ -198,6 +212,7 @@ impl DTLSConn { } // Close closes the connection. + /// Begins an orderly shutdown by queueing a `close_notify` alert. pub fn close(&mut self) { if !self.closed { self.closed = true; @@ -437,6 +452,11 @@ impl DTLSConn { self.handshake_completed } + /// Feeds one received datagram into the connection. + /// + /// # Errors + /// + /// Fails if the record is malformed or fails authentication. pub fn read(&mut self, buf: &[u8]) -> Result<()> { // Per RFC 6347: buffer future-epoch packets only until Finished is received // (i.e. until handshake completes). After that, discard them. diff --git a/rtc-dtls/src/content.rs b/rtc-dtls/src/content.rs index 3e2b0b28..52194d4a 100644 --- a/rtc-dtls/src/content.rs +++ b/rtc-dtls/src/content.rs @@ -13,11 +13,16 @@ use shared::error::*; /// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] pub enum ContentType { + /// `CHANGE_CIPHER_SPEC` (`20`). ChangeCipherSpec = 20, + /// `ALERT` (`21`). Alert = 21, + /// `HANDSHAKE` (`22`). Handshake = 22, + /// `APPLICATION_DATA` (`23`). ApplicationData = 23, #[default] + /// A content type this crate does not recognise. Invalid, } @@ -34,14 +39,20 @@ impl From for ContentType { } #[derive(PartialEq, Debug, Clone)] +/// The parsed body of a DTLS record. pub enum Content { + /// A ChangeCipherSpec record. ChangeCipherSpec(ChangeCipherSpec), + /// An alert record. Alert(Alert), + /// A handshake record. Handshake(Handshake), + /// An application data record. ApplicationData(ApplicationData), } impl Content { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { match self { Content::ChangeCipherSpec(c) => c.content_type(), @@ -51,6 +62,7 @@ impl Content { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { match self { Content::ChangeCipherSpec(c) => c.size(), @@ -60,6 +72,11 @@ impl Content { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { match self { Content::ChangeCipherSpec(c) => c.marshal(writer), @@ -69,6 +86,11 @@ impl Content { } } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(content_type: ContentType, reader: &mut R) -> Result { match content_type { ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec( diff --git a/rtc-dtls/src/crypto/crypto_cbc.rs b/rtc-dtls/src/crypto/crypto_cbc.rs index 461cdecb..6a958220 100644 --- a/rtc-dtls/src/crypto/crypto_cbc.rs +++ b/rtc-dtls/src/crypto/crypto_cbc.rs @@ -24,6 +24,7 @@ type Aes256CbcDec = cbc::Decryptor; // State needed to handle encrypted input/output #[derive(Clone)] +/// AES-CBC encryption with a separate HMAC for DTLS records, holding the per-direction keys. pub struct CryptoCbc { local_key: Vec, remote_key: Vec, @@ -35,6 +36,7 @@ impl CryptoCbc { const BLOCK_SIZE: usize = 16; const MAC_SIZE: usize = 20; + /// Builds the cipher from the local and remote keys and salts. pub fn new( local_key: &[u8], local_mac: &[u8], @@ -50,6 +52,11 @@ impl CryptoCbc { }) } + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec(); let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -86,6 +93,11 @@ impl CryptoCbc { Ok(r) } + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. pub fn decrypt(&self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; diff --git a/rtc-dtls/src/crypto/crypto_ccm.rs b/rtc-dtls/src/crypto/crypto_ccm.rs index 79ea4c8c..a8437584 100644 --- a/rtc-dtls/src/crypto/crypto_ccm.rs +++ b/rtc-dtls/src/crypto/crypto_ccm.rs @@ -31,8 +31,11 @@ type AesCcm8 = Ccm; type AesCcm = Ccm; #[derive(Clone)] +/// The authentication tag length a CCM suite uses. pub enum CryptoCcmTagLen { + /// An 8-byte tag, as the `_CCM_8` suites use. CryptoCcm8TagLength, + /// The full 16-byte tag. CryptoCcmTagLength, } @@ -42,6 +45,7 @@ enum CryptoCcmType { } // State needed to handle encrypted input/output +/// AES-CCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoCcm { local_ccm: CryptoCcmType, remote_ccm: CryptoCcmType, @@ -74,6 +78,7 @@ impl Clone for CryptoCcm { } impl CryptoCcm { + /// Builds the cipher from the local and remote keys and salts. pub fn new( tag_len: &CryptoCcmTagLen, local_key: &[u8], @@ -103,6 +108,11 @@ impl CryptoCcm { } } + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -142,6 +152,11 @@ impl CryptoCcm { Ok(r) } + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. pub fn decrypt(&self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; diff --git a/rtc-dtls/src/crypto/crypto_chacha20.rs b/rtc-dtls/src/crypto/crypto_chacha20.rs index 6cd5b306..ca02f409 100644 --- a/rtc-dtls/src/crypto/crypto_chacha20.rs +++ b/rtc-dtls/src/crypto/crypto_chacha20.rs @@ -13,6 +13,7 @@ const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12; // State needed to handle encrypted input/output #[derive(Clone)] +/// ChaCha20-Poly1305 authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoChaCha20 { local_cc: ChaCha20Poly1305, remote_cc: ChaCha20Poly1305, @@ -31,6 +32,7 @@ fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) { } impl CryptoChaCha20 { + /// Builds the cipher from the local and remote keys and salts. pub fn new( local_key: &[u8], local_write_iv: &[u8], @@ -53,6 +55,11 @@ impl CryptoChaCha20 { } } + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -87,6 +94,11 @@ impl CryptoChaCha20 { Ok(r) } + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. pub fn decrypt(&self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; diff --git a/rtc-dtls/src/crypto/crypto_gcm.rs b/rtc-dtls/src/crypto/crypto_gcm.rs index 08d79168..20e71d34 100644 --- a/rtc-dtls/src/crypto/crypto_gcm.rs +++ b/rtc-dtls/src/crypto/crypto_gcm.rs @@ -26,6 +26,7 @@ const CRYPTO_GCM_NONCE_LENGTH: usize = 12; // (handshake signatures / key generation). `LessSafeKey` is `Clone`, so // `CryptoGcm` stays cloneable for the cipher-suite state that embeds it. #[derive(Clone)] +/// AES-GCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoGcm { #[cfg(feature = "ring")] local_gcm: LessSafeKey, @@ -42,6 +43,7 @@ pub struct CryptoGcm { } impl CryptoGcm { + /// Builds the cipher from the local and remote keys and salts. pub fn new( local_key: &[u8], local_write_iv: &[u8], @@ -70,6 +72,11 @@ impl CryptoGcm { } } + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -108,6 +115,11 @@ impl CryptoGcm { Ok(r) } + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. pub fn decrypt(&self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index b9cf0154..3e2b9704 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -1,10 +1,15 @@ #[cfg(test)] mod crypto_test; +/// AES-CBC with a separate HMAC, for the older CBC suites. pub mod crypto_cbc; +/// AES-CCM authenticated encryption. pub mod crypto_ccm; +/// ChaCha20-Poly1305 authenticated encryption. pub mod crypto_chacha20; +/// AES-GCM authenticated encryption. pub mod crypto_gcm; +/// Block-cipher padding for the CBC suites. pub mod padding; use std::convert::TryFrom; @@ -154,8 +159,11 @@ pub trait CustomSigner: Send + Sync + std::fmt::Debug { /// Either ED25519, ECDSA, RSA keypair, or a custom external signer. #[derive(Debug)] pub enum CryptoPrivateKeyKind { + /// An Ed25519 key pair. Ed25519(Ed25519KeyPair), + /// An ECDSA key pair over NIST P-256. Ecdsa256(EcdsaKeyPair), + /// An RSA key pair used with SHA-256. Rsa256(ring::rsa::KeyPair), /// Delegate signing to an external provider. The signer receives the raw /// message bytes and must return a signature in the format expected by the @@ -241,6 +249,11 @@ impl TryFrom<&KeyPair> for CryptoPrivateKey { } impl CryptoPrivateKey { + /// Derives the signature scheme that matches `key_pair`. + /// + /// # Errors + /// + /// Fails if the key type has no supported scheme. pub fn from_key_pair(key_pair: &KeyPair) -> Result { let serialized_der = key_pair.serialize_der(); if key_pair.is_compatible(&rcgen::PKCS_ED25519) { @@ -323,7 +336,9 @@ pub(crate) fn generate_key_signature( } // add OID_ED25519 which is not defined in x509_parser +/// The X.509 algorithm OID for Ed25519. pub const OID_ED25519: Oid<'static> = oid!(1.3.101.112); +/// The X.509 algorithm OID for ECDSA with a named curve. pub const OID_ECDSA: Oid<'static> = oid!(1.2.840.10045.2.1); fn verify_signature( diff --git a/rtc-dtls/src/crypto/padding.rs b/rtc-dtls/src/crypto/padding.rs index 05e12dbc..b01d0a80 100644 --- a/rtc-dtls/src/crypto/padding.rs +++ b/rtc-dtls/src/crypto/padding.rs @@ -1,6 +1,9 @@ use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError}; use core::panic; +/// DTLS block-cipher padding, as a marker type for the padding scheme. +/// +/// Has no values — it exists to parameterize the CBC cipher over its padding. pub enum DtlsPadding {} /// Reference: RFC5246, 6.2.3.2 impl RawPadding for DtlsPadding { diff --git a/rtc-dtls/src/curve/mod.rs b/rtc-dtls/src/curve/mod.rs index dd48d5ff..8245bc35 100644 --- a/rtc-dtls/src/curve/mod.rs +++ b/rtc-dtls/src/curve/mod.rs @@ -1,9 +1,13 @@ +/// The named elliptic curves and key generation over them. pub mod named_curve; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10 #[derive(Copy, Clone, PartialEq, Eq, Debug)] +/// How an elliptic curve is identified in a key exchange — by name, or explicitly. pub enum EllipticCurveType { + /// `NAMED_CURVE` (`0x03`). NamedCurve = 0x03, + /// A curve type this crate does not implement. Unsupported, } diff --git a/rtc-dtls/src/curve/named_curve.rs b/rtc-dtls/src/curve/named_curve.rs index 1e2fe0b3..b12cff6e 100644 --- a/rtc-dtls/src/curve/named_curve.rs +++ b/rtc-dtls/src/curve/named_curve.rs @@ -5,10 +5,15 @@ use shared::error::*; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 #[repr(u16)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] +/// The named elliptic curves this crate can perform ECDHE over. pub enum NamedCurve { + /// `UNSUPPORTED` (`0x0000`). Unsupported = 0x0000, + /// `P256` (`0x0017`). P256 = 0x0017, + /// `P384` (`0x0018`). P384 = 0x0018, + /// `X25519` (`0x001d`). X25519 = 0x001d, } @@ -29,6 +34,7 @@ pub(crate) enum NamedCurvePrivateKey { StaticSecretX25519(x25519_dalek::StaticSecret), } +/// An ephemeral ECDHE key pair, with the curve it belongs to. pub struct NamedCurveKeypair { pub(crate) curve: NamedCurve, pub(crate) public_key: Vec, @@ -72,6 +78,11 @@ fn elliptic_curve_keypair(curve: NamedCurve) -> Result { } impl NamedCurve { + /// Generates an ephemeral key pair on this curve. + /// + /// # Errors + /// + /// Fails if the curve is unsupported or key generation fails. pub fn generate_keypair(&self) -> Result { match *self { NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519), diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index e7c82d5a..c9badd21 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -12,8 +12,11 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; +/// What the endpoint reports to its caller. pub enum EndpointEvent { + /// The handshake finished; application data may now be sent, and SRTP keys can be exported. HandshakeComplete, + /// Decrypted application data arrived. ApplicationData(BytesMut), } @@ -205,6 +208,11 @@ impl Endpoint { Ok(messages) } + /// Queues application data for `remote`. + /// + /// # Errors + /// + /// Fails if there is no association with `remote`, or its handshake has not completed. pub fn write(&mut self, remote: SocketAddr, data: &[u8]) -> Result<()> { if let Some(conn) = self.connections.get_mut(&remote) { conn.write(data)?; @@ -226,6 +234,11 @@ impl Endpoint { } } + /// Advances `remote`'s association to `now`, driving handshake retransmissions. + /// + /// # Errors + /// + /// Fails if the handshake has exhausted its retransmissions. pub fn handle_timeout(&mut self, remote: SocketAddr, now: Instant) -> Result<()> { if let Some(conn) = self.connections.get_mut(&remote) { if let Some(current_retransmit_timer) = &conn.current_retransmit_timer @@ -254,6 +267,7 @@ impl Endpoint { } } + /// When `remote`'s association next needs [`Self::handle_timeout`]. pub fn poll_timeout(&self, remote: SocketAddr, eto: &mut Instant) -> Result<()> { if let Some(conn) = self.connections.get(&remote) { if let Some(current_retransmit_timer) = &conn.current_retransmit_timer diff --git a/rtc-dtls/src/extension/extension_server_name.rs b/rtc-dtls/src/extension/extension_server_name.rs index c54a6506..55f9277e 100644 --- a/rtc-dtls/src/extension/extension_server_name.rs +++ b/rtc-dtls/src/extension/extension_server_name.rs @@ -9,20 +9,28 @@ use std::io::{Read, Write}; const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0; #[derive(Clone, Debug, PartialEq, Eq)] +/// The Server Name Indication extension, naming the host the client meant to reach. pub struct ExtensionServerName { pub(crate) server_name: String, } impl ExtensionServerName { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::ServerName } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { //TODO: check how to do cryptobyte? 2 + 2 + 1 + 2 + self.server_name.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { //TODO: check how to do cryptobyte? writer.write_u16::(2 + 1 + 2 + self.server_name.len() as u16)?; @@ -34,6 +42,11 @@ impl ExtensionServerName { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { //TODO: check how to do cryptobyte? let _ = reader.read_u16::()? as usize; diff --git a/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs b/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs index 64fe8084..b7c4d583 100644 --- a/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs +++ b/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs @@ -13,18 +13,26 @@ const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6; /// [RFC 8422 §5.1.1]: https://tools.ietf.org/html/rfc8422#section-5.1.1 #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExtensionSupportedEllipticCurves { + /// The curves the sender accepts, in preference order. pub elliptic_curves: Vec, } impl ExtensionSupportedEllipticCurves { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedEllipticCurves } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.elliptic_curves.len() * 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(2 + 2 * self.elliptic_curves.len() as u16)?; writer.write_u16::(2 * self.elliptic_curves.len() as u16)?; @@ -35,6 +43,11 @@ impl ExtensionSupportedEllipticCurves { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_supported_point_formats.rs b/rtc-dtls/src/extension/extension_supported_point_formats.rs index 60e5df94..7a8dde6d 100644 --- a/rtc-dtls/src/extension/extension_supported_point_formats.rs +++ b/rtc-dtls/src/extension/extension_supported_point_formats.rs @@ -5,8 +5,10 @@ use super::*; const EXTENSION_SUPPORTED_POINT_FORMATS_SIZE: usize = 5; +/// An EC point format code point. pub type EllipticCurvePointFormat = u8; +/// Uncompressed point format, the only one WebRTC uses. pub const ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED: EllipticCurvePointFormat = 0; /// ## Specifications @@ -20,14 +22,21 @@ pub struct ExtensionSupportedPointFormats { } impl ExtensionSupportedPointFormats { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedPointFormats } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 1 + self.point_formats.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(1 + self.point_formats.len() as u16)?; writer.write_u8(self.point_formats.len() as u8)?; @@ -38,6 +47,11 @@ impl ExtensionSupportedPointFormats { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs b/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs index 24ca158d..21f1e43d 100644 --- a/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs +++ b/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs @@ -17,14 +17,21 @@ pub struct ExtensionSupportedSignatureAlgorithms { } impl ExtensionSupportedSignatureAlgorithms { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedSignatureAlgorithms } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.signature_hash_algorithms.len() * 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(2 + 2 * self.signature_hash_algorithms.len() as u16)?; writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; @@ -36,6 +43,11 @@ impl ExtensionSupportedSignatureAlgorithms { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_use_extended_master_secret.rs b/rtc-dtls/src/extension/extension_use_extended_master_secret.rs index d6a1d0a4..d9c8298d 100644 --- a/rtc-dtls/src/extension/extension_use_extended_master_secret.rs +++ b/rtc-dtls/src/extension/extension_use_extended_master_secret.rs @@ -16,14 +16,21 @@ pub struct ExtensionUseExtendedMasterSecret { } impl ExtensionUseExtendedMasterSecret { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::UseExtendedMasterSecret } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { // length writer.write_u16::(0)?; @@ -31,6 +38,11 @@ impl ExtensionUseExtendedMasterSecret { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_use_srtp.rs b/rtc-dtls/src/extension/extension_use_srtp.rs index 5b2dd8b0..af7ca060 100644 --- a/rtc-dtls/src/extension/extension_use_srtp.rs +++ b/rtc-dtls/src/extension/extension_use_srtp.rs @@ -12,10 +12,15 @@ use super::*; #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SrtpProtectionProfile { + /// `SRTP_AES128_CM_HMAC_SHA1_80` (`0x0001`). Srtp_Aes128_Cm_Hmac_Sha1_80 = 0x0001, + /// `SRTP_AES128_CM_HMAC_SHA1_32` (`0x0002`). Srtp_Aes128_Cm_Hmac_Sha1_32 = 0x0002, + /// `SRTP_AEAD_AES_128_GCM` (`0x0007`). Srtp_Aead_Aes_128_Gcm = 0x0007, + /// `SRTP_AEAD_AES_256_GCM` (`0x0008`). Srtp_Aead_Aes_256_Gcm = 0x0008, + /// A protection profile this crate does not implement. Unsupported, } @@ -45,14 +50,21 @@ pub struct ExtensionUseSrtp { } impl ExtensionUseSrtp { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::UseSrtp } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.protection_profiles.len() * 2 + 1 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::( 2 + /* MKI Length */ 1 + 2 * self.protection_profiles.len() as u16, @@ -68,6 +80,11 @@ impl ExtensionUseSrtp { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/mod.rs b/rtc-dtls/src/extension/mod.rs index 8dc73b0f..8a981178 100644 --- a/rtc-dtls/src/extension/mod.rs +++ b/rtc-dtls/src/extension/mod.rs @@ -1,9 +1,18 @@ +/// Server Name Indication (SNI). pub mod extension_server_name; +/// The curves a client will accept for ECDHE. pub mod extension_supported_elliptic_curves; +/// The EC point formats a client will accept; WebRTC uses uncompressed. pub mod extension_supported_point_formats; +/// The signature and hash algorithm pairs a client will accept. pub mod extension_supported_signature_algorithms; +/// The extended master secret extension ([RFC 7627]), which binds the master secret to the +/// whole handshake. pub mod extension_use_extended_master_secret; +/// The `use_srtp` extension, which negotiates SRTP protection profiles during the DTLS +/// handshake ([RFC 5764]). pub mod extension_use_srtp; +/// The renegotiation info extension, sent empty to signal renegotiation is not supported. pub mod renegotiation_info; use extension_server_name::*; @@ -21,14 +30,23 @@ use std::io::{Read, Write}; // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml #[derive(Clone, Debug, PartialEq, Eq)] +/// The extension type code points this crate understands. pub enum ExtensionValue { + /// `SERVER_NAME` (`0`). ServerName = 0, + /// `SUPPORTED_ELLIPTIC_CURVES` (`10`). SupportedEllipticCurves = 10, + /// `SUPPORTED_POINT_FORMATS` (`11`). SupportedPointFormats = 11, + /// `SUPPORTED_SIGNATURE_ALGORITHMS` (`13`). SupportedSignatureAlgorithms = 13, + /// `USE_SRTP` (`14`). UseSrtp = 14, + /// `USE_EXTENDED_MASTER_SECRET` (`23`). UseExtendedMasterSecret = 23, + /// `RENEGOTIATION_INFO` (`65281`). RenegotiationInfo = 65281, + /// An extension this crate does not implement, which is ignored. Unsupported, } @@ -48,17 +66,26 @@ impl From for ExtensionValue { } #[derive(PartialEq, Eq, Debug, Clone)] +/// A parsed hello extension. pub enum Extension { + /// Server Name Indication. ServerName(ExtensionServerName), + /// The curves the sender accepts for ECDHE. SupportedEllipticCurves(ExtensionSupportedEllipticCurves), + /// The EC point formats the sender accepts. SupportedPointFormats(ExtensionSupportedPointFormats), + /// The signature and hash pairs the sender accepts. SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms), + /// The SRTP protection profiles offered or selected. UseSrtp(ExtensionUseSrtp), + /// The extended master secret extension. UseExtendedMasterSecret(ExtensionUseExtendedMasterSecret), + /// The renegotiation info extension. RenegotiationInfo(ExtensionRenegotiationInfo), } impl Extension { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { match self { Extension::ServerName(ext) => ext.extension_value(), @@ -71,6 +98,7 @@ impl Extension { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 2; @@ -87,6 +115,11 @@ impl Extension { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(self.extension_value() as u16)?; match self { @@ -100,6 +133,11 @@ impl Extension { } } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let extension_value: ExtensionValue = reader.read_u16::()?.into(); match extension_value { diff --git a/rtc-dtls/src/extension/renegotiation_info.rs b/rtc-dtls/src/extension/renegotiation_info.rs index 6e3d1c2d..b937a3d9 100644 --- a/rtc-dtls/src/extension/renegotiation_info.rs +++ b/rtc-dtls/src/extension/renegotiation_info.rs @@ -21,10 +21,12 @@ pub struct ExtensionRenegotiationInfo { impl ExtensionRenegotiationInfo { // TypeValue returns the extension TypeValue + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::RenegotiationInfo } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 3 } diff --git a/rtc-dtls/src/handshake/handshake_header.rs b/rtc-dtls/src/handshake/handshake_header.rs index 72cb91fc..19eaab67 100644 --- a/rtc-dtls/src/handshake/handshake_header.rs +++ b/rtc-dtls/src/handshake/handshake_header.rs @@ -8,6 +8,8 @@ use std::io::{Read, Write}; pub(crate) const HANDSHAKE_HEADER_LENGTH: usize = 12; #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +/// The header on every handshake message: type, length, message sequence, and the fragment +/// offset and length that let one message span datagrams. pub struct HandshakeHeader { pub(crate) handshake_type: HandshakeType, pub(crate) length: u32, // uint24 in spec @@ -17,10 +19,16 @@ pub struct HandshakeHeader { } impl HandshakeHeader { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 3 + 2 + 3 + 3 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.handshake_type as u8)?; writer.write_u24::(self.length)?; @@ -31,6 +39,11 @@ impl HandshakeHeader { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let handshake_type = reader.read_u8()?.into(); let length = reader.read_u24::()?; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate.rs b/rtc-dtls/src/handshake/handshake_message_certificate.rs index 8e348d4f..649f69c3 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate.rs @@ -9,15 +9,18 @@ mod handshake_message_certificate_test; const HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE: usize = 3; #[derive(PartialEq, Eq, Debug, Clone)] +/// The sender's certificate chain. pub struct HandshakeMessageCertificate { pub(crate) certificate: Vec>, } impl HandshakeMessageCertificate { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::Certificate } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 3; @@ -28,6 +31,11 @@ impl HandshakeMessageCertificate { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { let mut payload_size = 0; for r in &self.certificate { @@ -48,6 +56,11 @@ impl HandshakeMessageCertificate { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut certificate: Vec> = vec![]; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate_request.rs b/rtc-dtls/src/handshake/handshake_message_certificate_request.rs index 0e882794..1b5cbdd3 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate_request.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate_request.rs @@ -16,6 +16,8 @@ message (if it is sent; otherwise, this message follows the server's Certificate message). */ #[derive(Clone, Debug, PartialEq, Eq)] +/// The server's request that the client authenticate, listing acceptable certificate types +/// and signature algorithms. pub struct HandshakeMessageCertificateRequest { pub(crate) certificate_types: Vec, pub(crate) signature_hash_algorithms: Vec, @@ -24,14 +26,21 @@ pub struct HandshakeMessageCertificateRequest { const HANDSHAKE_MESSAGE_CERTIFICATE_REQUEST_MIN_LENGTH: usize = 5; impl HandshakeMessageCertificateRequest { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::CertificateRequest } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + self.certificate_types.len() + 2 + self.signature_hash_algorithms.len() * 2 + 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.certificate_types.len() as u8)?; for v in &self.certificate_types { @@ -49,6 +58,11 @@ impl HandshakeMessageCertificateRequest { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let certificate_types_length = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs b/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs index 6e0f0953..251318be 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs @@ -8,6 +8,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// A signature over the handshake so far, proving possession of the certificate's private key. pub struct HandshakeMessageCertificateVerify { pub(crate) algorithm: SignatureHashAlgorithm, pub(crate) signature: Vec, @@ -16,14 +17,21 @@ pub struct HandshakeMessageCertificateVerify { const HANDSHAKE_MESSAGE_CERTIFICATE_VERIFY_MIN_LENGTH: usize = 4; impl HandshakeMessageCertificateVerify { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::CertificateVerify } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 1 + 2 + self.signature.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.algorithm.hash as u8)?; writer.write_u8(self.algorithm.signature as u8)?; @@ -33,6 +41,11 @@ impl HandshakeMessageCertificateVerify { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let hash_algorithm = reader.read_u8()?.into(); let signature_algorithm = reader.read_u8()?.into(); diff --git a/rtc-dtls/src/handshake/handshake_message_client_hello.rs b/rtc-dtls/src/handshake/handshake_message_client_hello.rs index 78f91201..eb590b3d 100644 --- a/rtc-dtls/src/handshake/handshake_message_client_hello.rs +++ b/rtc-dtls/src/handshake/handshake_message_client_hello.rs @@ -20,6 +20,7 @@ initiative in order to renegotiate the security parameters in an existing connection. */ #[derive(Clone)] +/// The client's opening message: its random, cookie, offered cipher suites and extensions. pub struct HandshakeMessageClientHello { pub(crate) version: ProtocolVersion, pub(crate) random: HandshakeRandom, @@ -73,10 +74,12 @@ impl fmt::Debug for HandshakeMessageClientHello { const HANDSHAKE_MESSAGE_CLIENT_HELLO_VARIABLE_WIDTH_START: usize = 34; impl HandshakeMessageClientHello { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ClientHello } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 0; @@ -100,6 +103,11 @@ impl HandshakeMessageClientHello { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.cookie.len() > 255 { return Err(Error::ErrCookieTooLong); @@ -136,6 +144,11 @@ impl HandshakeMessageClientHello { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs b/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs index 6c23985d..1d23696f 100644 --- a/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs +++ b/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs @@ -7,16 +7,19 @@ use byteorder::{BigEndian, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// The client's half of the key agreement — its ECDHE public key, or a PSK identity. pub struct HandshakeMessageClientKeyExchange { pub(crate) identity_hint: Vec, pub(crate) public_key: Vec, } impl HandshakeMessageClientKeyExchange { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ClientKeyExchange } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { if !self.public_key.is_empty() { 1 + self.public_key.len() @@ -25,6 +28,11 @@ impl HandshakeMessageClientKeyExchange { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if (!self.identity_hint.is_empty() && !self.public_key.is_empty()) || (self.identity_hint.is_empty() && self.public_key.is_empty()) @@ -43,6 +51,11 @@ impl HandshakeMessageClientKeyExchange { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut data = vec![]; reader.read_to_end(&mut data)?; diff --git a/rtc-dtls/src/handshake/handshake_message_finished.rs b/rtc-dtls/src/handshake/handshake_message_finished.rs index a5bb190e..2c798991 100644 --- a/rtc-dtls/src/handshake/handshake_message_finished.rs +++ b/rtc-dtls/src/handshake/handshake_message_finished.rs @@ -6,25 +6,38 @@ use super::*; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// A hash over every preceding handshake message, which both sides compare to detect tampering. pub struct HandshakeMessageFinished { pub(crate) verify_data: Vec, } impl HandshakeMessageFinished { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::Finished } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.verify_data.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_all(&self.verify_data)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut verify_data: Vec = vec![]; reader.read_to_end(&mut verify_data)?; diff --git a/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs b/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs index 2a3a915b..b6ff126e 100644 --- a/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs +++ b/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs @@ -34,14 +34,21 @@ pub struct HandshakeMessageHelloVerifyRequest { } impl HandshakeMessageHelloVerifyRequest { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::HelloVerifyRequest } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 1 + 1 + self.cookie.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.cookie.len() > 255 { return Err(Error::ErrCookieTooLong); @@ -55,6 +62,11 @@ impl HandshakeMessageHelloVerifyRequest { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_server_hello.rs b/rtc-dtls/src/handshake/handshake_message_server_hello.rs index 3220d7dd..8fcb29e1 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_hello.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_hello.rs @@ -20,6 +20,7 @@ failure alert. https://tools.ietf.org/html/rfc5246#section-7.4.1.3 */ #[derive(Clone)] +/// The server's reply: its random, and the cipher suite and extensions it selected. pub struct HandshakeMessageServerHello { pub(crate) version: ProtocolVersion, pub(crate) random: HandshakeRandom, @@ -52,10 +53,12 @@ impl fmt::Debug for HandshakeMessageServerHello { } impl HandshakeMessageServerHello { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerHello } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 2 + self.random.size(); @@ -74,6 +77,11 @@ impl HandshakeMessageServerHello { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.version.major)?; writer.write_u8(self.version.minor)?; @@ -100,6 +108,11 @@ impl HandshakeMessageServerHello { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs b/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs index ce6445e6..ccf48c91 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs @@ -6,21 +6,34 @@ use super::*; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// Marks the end of the server's first flight. Carries no fields. pub struct HandshakeMessageServerHelloDone; impl HandshakeMessageServerHelloDone { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerHelloDone } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 0 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, _writer: &mut W) -> Result<()> { Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(_reader: &mut R) -> Result { Ok(HandshakeMessageServerHelloDone {}) } diff --git a/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs b/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs index 8062f015..7523599d 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs @@ -11,6 +11,7 @@ use std::io::{Read, Write}; // Structure supports ECDH and PSK #[derive(Clone, Debug, PartialEq, Eq)] +/// The server's half of the key agreement, signed so the client can authenticate it. pub struct HandshakeMessageServerKeyExchange { pub(crate) identity_hint: Vec, @@ -22,10 +23,12 @@ pub struct HandshakeMessageServerKeyExchange { } impl HandshakeMessageServerKeyExchange { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerKeyExchange } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { if !self.identity_hint.is_empty() { 2 + self.identity_hint.len() @@ -34,6 +37,11 @@ impl HandshakeMessageServerKeyExchange { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if !self.identity_hint.is_empty() { writer.write_u16::(self.identity_hint.len() as u16)?; @@ -56,6 +64,11 @@ impl HandshakeMessageServerKeyExchange { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut data = vec![]; reader.read_to_end(&mut data)?; diff --git a/rtc-dtls/src/handshake/handshake_random.rs b/rtc-dtls/src/handshake/handshake_random.rs index a94c8c9e..6ea22848 100644 --- a/rtc-dtls/src/handshake/handshake_random.rs +++ b/rtc-dtls/src/handshake/handshake_random.rs @@ -4,7 +4,9 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{self, Read, Write}; use std::time::{Duration, SystemTime}; +/// Bytes of randomness in a handshake random, excluding the timestamp. pub const RANDOM_BYTES_LENGTH: usize = 28; +/// Total length of a handshake random: 4 bytes of timestamp plus 28 random. pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; /// ## Specifications @@ -14,7 +16,9 @@ pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; /// [RFC 4346 §7.4.1.2]: https://tools.ietf.org/html/rfc4346#section-7.4.1.2 #[derive(Clone, Debug, PartialEq, Eq)] pub struct HandshakeRandom { + /// The sender's clock at the time the random was generated. pub gmt_unix_time: SystemTime, + /// 28 bytes of randomness, which feed key derivation. pub random_bytes: [u8; RANDOM_BYTES_LENGTH], } @@ -28,10 +32,16 @@ impl Default for HandshakeRandom { } impl HandshakeRandom { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 4 + RANDOM_BYTES_LENGTH } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> io::Result<()> { let secs = match self.gmt_unix_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => d.as_secs() as u32, @@ -43,6 +53,11 @@ impl HandshakeRandom { writer.flush() } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> io::Result { let secs = reader.read_u32::()?; let gmt_unix_time = if let Some(unix_time) = @@ -64,6 +79,7 @@ impl HandshakeRandom { // populate fills the HandshakeRandom with random values // may be called multiple times + /// Fills in the current time and fresh random bytes. pub fn populate(&mut self) { self.gmt_unix_time = SystemTime::now(); rand::rng().fill(&mut self.random_bytes); diff --git a/rtc-dtls/src/handshake/mod.rs b/rtc-dtls/src/handshake/mod.rs index 4bb68366..87d7e054 100644 --- a/rtc-dtls/src/handshake/mod.rs +++ b/rtc-dtls/src/handshake/mod.rs @@ -1,15 +1,28 @@ +/// Buffers handshake messages so their hash can be computed for `Finished` verification. pub mod handshake_cache; +/// The header prefixing every handshake message, including fragment offsets. pub mod handshake_header; +/// Certificate: the sender's certificate chain. pub mod handshake_message_certificate; +/// CertificateRequest: the server asks the client to authenticate. pub mod handshake_message_certificate_request; +/// CertificateVerify: proves possession of the certificate's private key. pub mod handshake_message_certificate_verify; +/// ClientHello: opens the handshake with the client's offers. pub mod handshake_message_client_hello; +/// ClientKeyExchange: the client's half of the key agreement. pub mod handshake_message_client_key_exchange; +/// Finished: a hash over the handshake, proving both sides saw the same messages. pub mod handshake_message_finished; +/// HelloVerifyRequest: DTLS's cookie exchange, which resists amplification attacks. pub mod handshake_message_hello_verify_request; +/// ServerHello: the server's chosen parameters. pub mod handshake_message_server_hello; +/// ServerHelloDone: the server has finished its first flight. pub mod handshake_message_server_hello_done; +/// ServerKeyExchange: the server's half of the key agreement. pub mod handshake_message_server_key_exchange; +/// The 32-byte random each side contributes to key derivation. pub mod handshake_random; #[cfg(test)] @@ -40,18 +53,30 @@ use handshake_message_server_key_exchange::*; /// [RFC 5246 §7.4]: https://tools.ietf.org/html/rfc5246#section-7.4 #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum HandshakeType { + /// `HELLO_REQUEST` (`0`). HelloRequest = 0, + /// `CLIENT_HELLO` (`1`). ClientHello = 1, + /// `SERVER_HELLO` (`2`). ServerHello = 2, + /// `HELLO_VERIFY_REQUEST` (`3`). HelloVerifyRequest = 3, + /// `CERTIFICATE` (`11`). Certificate = 11, + /// `SERVER_KEY_EXCHANGE` (`12`). ServerKeyExchange = 12, + /// `CERTIFICATE_REQUEST` (`13`). CertificateRequest = 13, + /// `SERVER_HELLO_DONE` (`14`). ServerHelloDone = 14, + /// `CERTIFICATE_VERIFY` (`15`). CertificateVerify = 15, + /// `CLIENT_KEY_EXCHANGE` (`16`). ClientKeyExchange = 16, + /// `FINISHED` (`20`). Finished = 20, #[default] + /// A handshake type this crate does not recognise. Invalid, } @@ -94,21 +119,33 @@ impl From for HandshakeType { } #[derive(PartialEq, Debug, Clone)] +/// A parsed handshake message. pub enum HandshakeMessage { //HelloRequest(errNotImplemented), + /// ClientHello, which opens the handshake. ClientHello(HandshakeMessageClientHello), + /// ServerHello, carrying the server's chosen parameters. ServerHello(HandshakeMessageServerHello), + /// HelloVerifyRequest, DTLS's cookie challenge. HelloVerifyRequest(HandshakeMessageHelloVerifyRequest), + /// Certificate, carrying a certificate chain. Certificate(HandshakeMessageCertificate), + /// ServerKeyExchange, the server's key-agreement share. ServerKeyExchange(HandshakeMessageServerKeyExchange), + /// CertificateRequest, asking the client to authenticate. CertificateRequest(HandshakeMessageCertificateRequest), + /// ServerHelloDone, ending the server's first flight. ServerHelloDone(HandshakeMessageServerHelloDone), + /// CertificateVerify, proving possession of the certificate key. CertificateVerify(HandshakeMessageCertificateVerify), + /// ClientKeyExchange, the client's key-agreement share. ClientKeyExchange(HandshakeMessageClientKeyExchange), + /// Finished, a hash over the handshake that both sides verify. Finished(HandshakeMessageFinished), } impl HandshakeMessage { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { match self { HandshakeMessage::ClientHello(msg) => msg.handshake_type(), @@ -124,6 +161,7 @@ impl HandshakeMessage { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { match self { HandshakeMessage::ClientHello(msg) => msg.size(), @@ -139,6 +177,11 @@ impl HandshakeMessage { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { match self { HandshakeMessage::ClientHello(msg) => msg.marshal(writer)?, @@ -164,12 +207,14 @@ impl HandshakeMessage { // certificates signed by a trusted certificate authority. // https://tools.ietf.org/html/rfc5246#section-7.3 #[derive(PartialEq, Debug, Clone)] +/// A handshake record: its header plus the message it carries. pub struct Handshake { pub(crate) handshake_header: HandshakeHeader, pub(crate) handshake_message: HandshakeMessage, } impl Handshake { + /// Wraps a message in a handshake record, filling in its header. pub fn new(handshake_message: HandshakeMessage) -> Self { Handshake { handshake_header: HandshakeHeader { @@ -183,20 +228,32 @@ impl Handshake { } } + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::Handshake } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.handshake_header.size() + self.handshake_message.size() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { self.handshake_header.marshal(writer)?; self.handshake_message.marshal(writer)?; Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let handshake_header = HandshakeHeader::unmarshal(reader)?; diff --git a/rtc-dtls/src/lib.rs b/rtc-dtls/src/lib.rs index 41a1020e..277e0f4b 100644 --- a/rtc-dtls/src/lib.rs +++ b/rtc-dtls/src/lib.rs @@ -1,26 +1,80 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! DTLS 1.2 for the Sans-I/O WebRTC stack. +//! +//! An implementation of Datagram Transport Layer Security ([RFC 6347]) with the extensions +//! WebRTC requires: DTLS-SRTP key export ([RFC 5764]), extended master secret +//! ([RFC 7627]), and elliptic-curve cipher suites ([RFC 4492], [RFC 5289]). It secures the +//! media and data-channel path: SRTP keying material comes out of the DTLS handshake, and +//! SCTP data channels run over the DTLS association itself. +//! +//! # Structure +//! +//! * [`endpoint`] — the Sans-I/O entry point: feed it datagrams, poll it for the datagrams +//! it wants to send and the events it produces. No sockets, no timers of its own. +//! * [`config`] — certificates, cipher-suite and curve preferences, the client/server role, +//! and the SRTP protection profiles to negotiate. +//! * [`handshake`], [`flight`], [`state`] — the handshake message types and the flight state +//! machine that drives them, including retransmission. +//! * [`cipher_suite`], [`crypto`], [`curve`], [`signature_hash_algorithm`] — the +//! cryptographic primitives and the negotiated-suite abstraction. +//! * [`extension`] — the ClientHello/ServerHello extensions, including `use_srtp` and SNI. +//! * [`alert`], [`content`], [`record_layer`] — the record layer and its content types. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate drives it as one layer of the peer-connection +//! pipeline. +//! +//! [RFC 6347]: https://datatracker.ietf.org/doc/html/rfc6347 +//! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 +//! [RFC 7627]: https://datatracker.ietf.org/doc/html/rfc7627 +//! [RFC 4492]: https://datatracker.ietf.org/doc/html/rfc4492 +//! [RFC 5289]: https://datatracker.ietf.org/doc/html/rfc5289 + +/// Alert records: fatal errors and the orderly `close_notify`. pub mod alert; +/// Application data records — the payload DTLS carries once the handshake completes. pub mod application_data; +/// The ChangeCipherSpec record, which switches a side over to the negotiated keys. pub mod change_cipher_spec; +/// The negotiable cipher suites and the [`CipherSuite`] trait they +/// implement. pub mod cipher_suite; +/// Certificate types a server may request from a client. pub mod client_certificate_type; +/// The compression-methods field. DTLS in WebRTC always negotiates null compression. pub mod compression_methods; +/// Handshake configuration: certificates, roles, cipher-suite and SRTP profile preferences. pub mod config; +/// Connection state shared across the handshake and record layers. pub mod conn; +/// Record content types: handshake, alert, change-cipher-spec and application data. pub mod content; +/// Cryptographic primitives: the AEAD and CBC ciphers, certificates and signatures. pub mod crypto; +/// Elliptic curves and the key-exchange values exchanged over them. pub mod curve; +/// The Sans-I/O entry point: feed it datagrams, poll it for output and events. pub mod endpoint; +/// ClientHello and ServerHello extensions, including `use_srtp` and SNI. pub mod extension; +/// The flight state machine, which drives the handshake and its retransmissions. pub mod flight; +/// Reassembly of handshake messages fragmented across datagrams. pub mod fragment_buffer; +/// The handshake message types and the cache that hashes them for `Finished`. pub mod handshake; +/// Handshake orchestration: state, roles and the verification callbacks. pub mod handshaker; +/// The pseudo-random function that expands the master secret into keys. pub mod prf; +/// The record layer: framing, sequence numbers and epochs. pub mod record_layer; +/// Signature and hash algorithm pairs, as negotiated for certificate verification. pub mod signature_hash_algorithm; +/// The negotiated connection state: keys, sequence numbers and peer identity. pub mod state; use cipher_suite::*; diff --git a/rtc-dtls/src/record_layer/mod.rs b/rtc-dtls/src/record_layer/mod.rs index 3438a597..b20c81bd 100644 --- a/rtc-dtls/src/record_layer/mod.rs +++ b/rtc-dtls/src/record_layer/mod.rs @@ -1,3 +1,4 @@ +/// The record header: content type, version, epoch and sequence number. pub mod record_layer_header; #[cfg(test)] @@ -35,11 +36,14 @@ use std::io::{Read, Write}; /// [RFC 4347 §4.1]: https://tools.ietf.org/html/rfc4347#section-4.1 #[derive(Debug, Clone, PartialEq)] pub struct RecordLayer { + /// The record's header. pub record_layer_header: RecordLayerHeader, + /// The record's parsed body. pub content: Content, } impl RecordLayer { + /// Builds a record around `content`, filling in its header. pub fn new(protocol_version: ProtocolVersion, epoch: u16, content: Content) -> Self { RecordLayer { record_layer_header: RecordLayerHeader { @@ -53,12 +57,22 @@ impl RecordLayer { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { self.record_layer_header.marshal(writer)?; self.content.marshal(writer)?; Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let record_layer_header = RecordLayerHeader::unmarshal(reader)?; let content = match record_layer_header.content_type { diff --git a/rtc-dtls/src/record_layer/record_layer_header.rs b/rtc-dtls/src/record_layer/record_layer_header.rs index 82413658..77799127 100644 --- a/rtc-dtls/src/record_layer/record_layer_header.rs +++ b/rtc-dtls/src/record_layer/record_layer_header.rs @@ -5,23 +5,32 @@ use shared::error::*; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; +/// Length of the DTLS record header in bytes. pub const RECORD_LAYER_HEADER_SIZE: usize = 13; +/// The largest sequence number the 48-bit field can hold. pub const MAX_SEQUENCE_NUMBER: u64 = 0x0000FFFFFFFFFFFF; +/// Major version byte for DTLS 1.2. pub const DTLS1_2MAJOR: u8 = 0xfe; +/// Minor version byte for DTLS 1.2. pub const DTLS1_2MINOR: u8 = 0xfd; +/// Major version byte for DTLS 1.0. pub const DTLS1_0MAJOR: u8 = 0xfe; +/// Minor version byte for DTLS 1.0. pub const DTLS1_0MINOR: u8 = 0xff; // VERSION_DTLS12 is the DTLS version in the same style as // VersionTLSXX from crypto/tls +/// DTLS 1.2 as a single 16-bit value. pub const VERSION_DTLS12: u16 = 0xfefd; +/// DTLS 1.0 as a [`ProtocolVersion`]. pub const PROTOCOL_VERSION1_0: ProtocolVersion = ProtocolVersion { major: DTLS1_0MAJOR, minor: DTLS1_0MINOR, }; +/// DTLS 1.2 as a [`ProtocolVersion`]. pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { major: DTLS1_2MAJOR, minor: DTLS1_2MINOR, @@ -34,20 +43,33 @@ pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { /// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub struct ProtocolVersion { + /// The major version byte. pub major: u8, + /// The minor version byte. pub minor: u8, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +/// The header on every DTLS record. pub struct RecordLayerHeader { + /// What the record body holds. pub content_type: ContentType, + /// The record's protocol version. pub protocol_version: ProtocolVersion, + /// The key epoch, incremented on each ChangeCipherSpec so old and new keys can coexist. pub epoch: u16, + /// The record sequence number — a 48-bit field on the wire. pub sequence_number: u64, // uint48 in spec + /// The body length in bytes. pub content_len: u16, } impl RecordLayerHeader { + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.sequence_number > MAX_SEQUENCE_NUMBER { return Err(Error::ErrSequenceNumberOverflow); @@ -66,6 +88,11 @@ impl RecordLayerHeader { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let content_type = reader.read_u8()?.into(); let major = reader.read_u8()?; diff --git a/rtc-dtls/src/signature_hash_algorithm/mod.rs b/rtc-dtls/src/signature_hash_algorithm/mod.rs index f16455d5..cf879a71 100644 --- a/rtc-dtls/src/signature_hash_algorithm/mod.rs +++ b/rtc-dtls/src/signature_hash_algorithm/mod.rs @@ -10,15 +10,25 @@ use shared::error::*; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18 // Supported hash hash algorithms #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The hash algorithms that may be paired with a signature algorithm. pub enum HashAlgorithm { - Md2 = 0, // Blacklisted - Md5 = 1, // Blacklisted + /// `MD2` (`0`). + Md2 = 0, // Blacklisted + /// `MD5` (`1`). + Md5 = 1, // Blacklisted + /// `SHA1` (`2`). Sha1 = 2, // Blacklisted + /// `SHA224` (`3`). Sha224 = 3, + /// `SHA256` (`4`). Sha256 = 4, + /// `SHA384` (`5`). Sha384 = 5, + /// `SHA512` (`6`). Sha512 = 6, + /// `ED25519` (`8`). Ed25519 = 8, + /// An algorithm this crate does not implement. Unsupported, } @@ -69,10 +79,15 @@ impl HashAlgorithm { // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-16 #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The signature algorithms this crate can verify and produce. pub enum SignatureAlgorithm { + /// `RSA` (`1`). Rsa = 1, + /// `ECDSA` (`3`). Ecdsa = 3, + /// `ED25519` (`7`). Ed25519 = 7, + /// An algorithm this crate does not implement. Unsupported, } @@ -88,8 +103,11 @@ impl From for SignatureAlgorithm { } #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A signature and hash pair, as negotiated for certificate verification. pub struct SignatureHashAlgorithm { + /// The hash to digest the signed data with. pub hash: HashAlgorithm, + /// The signature algorithm to apply. pub signature: SignatureAlgorithm, } @@ -155,27 +173,40 @@ pub(crate) fn select_signature_scheme( // SignatureScheme identifies a signature algorithm supported by TLS. See // RFC 8446, Section 4.2.3. #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A TLS signature scheme, which names a signature and hash together ([RFC 8446] §4.2.3). pub enum SignatureScheme { // RSASSA-PKCS1-v1_5 algorithms. + /// `PKCS1_WITH_SHA256` (`0x0401`). Pkcs1WithSha256 = 0x0401, + /// `PKCS1_WITH_SHA384` (`0x0501`). Pkcs1WithSha384 = 0x0501, + /// `PKCS1_WITH_SHA512` (`0x0601`). Pkcs1WithSha512 = 0x0601, // RSASSA-PSS algorithms with public key OID rsaEncryption. + /// `PSS_WITH_SHA256` (`0x0804`). PssWithSha256 = 0x0804, + /// `PSS_WITH_SHA384` (`0x0805`). PssWithSha384 = 0x0805, + /// `PSS_WITH_SHA512` (`0x0806`). PssWithSha512 = 0x0806, // ECDSA algorithms. Only constrained to a specific curve in TLS 1.3. + /// `ECDSA_WITH_P256_AND_SHA256` (`0x0403`). EcdsaWithP256AndSha256 = 0x0403, + /// `ECDSA_WITH_P384_AND_SHA384` (`0x0503`). EcdsaWithP384AndSha384 = 0x0503, + /// `ECDSA_WITH_P521_AND_SHA512` (`0x0603`). EcdsaWithP521AndSha512 = 0x0603, // EdDSA algorithms. + /// `ED25519` (`0x0807`). Ed25519 = 0x0807, // Legacy signature and hash algorithms for TLS 1.2. + /// `PKCS1_WITH_SHA1` (`0x0201`). Pkcs1WithSha1 = 0x0201, + /// `ECDSA_WITH_SHA1` (`0x0203`). EcdsaWithSha1 = 0x0203, } diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index 8ecf653c..6924a142 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -10,6 +10,8 @@ use shared::error::*; use std::io::{BufWriter, Cursor}; // State holds the dtls connection state and implements both encoding.BinaryMarshaler and encoding.BinaryUnmarshaler +/// The negotiated connection state: keys, sequence numbers, peer identity and the active +/// cipher suite. pub struct State { pub(crate) local_epoch: u16, pub(crate) remote_epoch: u16, @@ -20,7 +22,11 @@ pub struct State { pub(crate) cipher_suite: Option>, // nil if a cipher_suite hasn't been chosen pub(crate) srtp_protection_profile: SrtpProtectionProfile, // Negotiated srtp_protection_profile + /// The peer's certificate chain, DER-encoded. + /// + /// WebRTC checks its fingerprint against the one signalled in SDP. pub peer_certificates: Vec>, + /// The PSK identity hint, for pre-shared-key handshakes. pub identity_hint: Vec, pub(crate) is_client: bool, @@ -174,6 +180,11 @@ impl State { Ok(()) } + /// Installs the negotiated keys into the cipher suite. + /// + /// # Errors + /// + /// Fails if the master secret or randoms are not yet available. pub fn init_cipher_suite(&mut self) -> Result<()> { if let Some(cipher_suite) = &mut self.cipher_suite { if cipher_suite.is_initialized() { @@ -202,6 +213,11 @@ impl State { } // marshal_binary is a binary.BinaryMarshaler.marshal_binary implementation + /// Serializes the state, so a connection can be resumed or migrated. + /// + /// # Errors + /// + /// Fails if the state is incomplete. pub fn marshal_binary(&self) -> Result> { let serialized = self.serialize()?; @@ -212,6 +228,11 @@ impl State { } // unmarshal_binary is a binary.BinaryUnmarshaler.unmarshal_binary implementation + /// Restores state previously produced by [`Self::marshal_binary`]. + /// + /// # Errors + /// + /// Fails if `data` is truncated or malformed. pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { let serialized: SerializedState = match rkyv::access::(data) @@ -226,14 +247,17 @@ impl State { Ok(()) } + /// The SRTP protection profile negotiated through `use_srtp`. pub fn srtp_protection_profile(&self) -> SrtpProtectionProfile { self.srtp_protection_profile } + /// Whether this endpoint took the client role. pub fn is_client(&self) -> bool { self.is_client } + /// The active cipher suite, once one has been negotiated. pub fn cipher_suite(&self) -> Option<&dyn CipherSuite> { self.cipher_suite.as_deref() } diff --git a/rtc-ice/src/agent/agent_config.rs b/rtc-ice/src/agent/agent_config.rs index 59808394..f6b93c99 100644 --- a/rtc-ice/src/agent/agent_config.rs +++ b/rtc-ice/src/agent/agent_config.rs @@ -51,6 +51,7 @@ pub(crate) fn default_candidate_types() -> Vec { /// future-proofness of the interface. #[derive(Default)] pub struct AgentConfig { + /// The STUN and TURN servers to gather reflexive and relay candidates from. pub urls: Vec, /// It is used to perform connectivity checks. The values MUST be unguessable, with at least @@ -104,6 +105,7 @@ pub struct AgentConfig { /// request or a nomination we set the pair as failed. pub max_binding_requests: Option, + /// Whether this agent takes the controlling role. pub is_controlling: bool, /// lite agents do not perform connectivity check and only provide host candidates. diff --git a/rtc-ice/src/agent/agent_stats.rs b/rtc-ice/src/agent/agent_stats.rs index f68481ac..944767ca 100644 --- a/rtc-ice/src/agent/agent_stats.rs +++ b/rtc-ice/src/agent/agent_stats.rs @@ -7,6 +7,7 @@ use crate::network_type::NetworkType; /// Contains ICE candidate pair statistics. pub struct CandidatePairStats { /// The timestamp associated with this struct. + /// When this snapshot was taken. pub timestamp: Instant, /// The id of the local candidate. @@ -148,7 +149,7 @@ impl Default for CandidatePairStats { /// Contains ICE candidate statistics related to the `ICETransport` objects. #[derive(Debug, Clone)] pub struct CandidateStats { - // The timestamp associated with this struct. + /// The timestamp associated with this struct. pub timestamp: Instant, /// The candidate id. diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index b039824f..edbaaeeb 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -1,9 +1,12 @@ #[cfg(test)] mod agent_test; +/// Configuration for a new [`Agent`]: servers, timeouts and role. pub mod agent_config; mod agent_proto; +/// Pair selection and nomination — which candidate pair becomes the selected one. pub mod agent_selector; +/// Snapshot statistics for an agent's candidates and pairs. pub mod agent_stats; use agent_config::*; @@ -55,8 +58,11 @@ impl Default for BindingRequest { } #[derive(Default, Clone)] +/// The ICE credentials for one side of a session, exchanged in SDP. pub struct Credentials { + /// The username fragment, echoed in every STUN check so a receiver can demultiplex. pub ufrag: String, + /// The password, used as the `MESSAGE-INTEGRITY` key. pub pwd: String, } @@ -87,8 +93,13 @@ fn assert_inbound_message_integrity(m: &mut Message, key: &[u8]) -> Result<()> { message_integrity_attr.check(m) } +/// What the agent reports to its caller. pub enum Event { + /// The agent's connection state changed. ConnectionStateChange(ConnectionState), + /// A new pair was selected, carrying the local and remote candidates. + /// + /// Media should be sent on this pair from now on. SelectedCandidatePairChange(Box, Box), /// Emitted when the ICE role switches due to a role conflict (RFC 8445 §7.3.1.1). /// The bool is `true` if the agent is now controlling, `false` if now controlled. @@ -533,18 +544,26 @@ impl Agent { &self.ufrag_pwd.local_credentials } + /// Whether this agent is the controlling one, which decides nomination. pub fn role(&self) -> bool { self.is_controlling } + /// Sets the controlling role. + /// + /// Determined by which side offered; the two agents must not agree, or nomination stalls. pub fn set_role(&mut self, is_controlling: bool) { self.is_controlling = is_controlling; } + /// The agent's current connection state. pub fn state(&self) -> ConnectionState { self.connection_state } + /// Whether a non-STUN datagram on `transport` should be accepted as media. + /// + /// Guards against accepting media from an address that has not passed a connectivity check. pub fn is_valid_non_stun_traffic(&mut self, transport: TransportContext) -> bool { self.find_local_candidate(transport.local_addr, transport.transport_protocol) .is_some() @@ -595,6 +614,7 @@ impl Agent { } } + /// The highest-priority pair that is still usable, whether or not it has been nominated. pub fn get_best_available_candidate_pair(&self) -> Option<(&Candidate, &Candidate)> { if let Some(pair_index) = self.get_best_available_pair() { let candidate_pair = &self.candidate_pairs[pair_index]; diff --git a/rtc-ice/src/attributes/control/mod.rs b/rtc-ice/src/attributes/control/mod.rs index 34b57201..9438d815 100644 --- a/rtc-ice/src/attributes/control/mod.rs +++ b/rtc-ice/src/attributes/control/mod.rs @@ -112,8 +112,11 @@ impl Getter for AttrControl { #[derive(Default, PartialEq, Eq, Copy, Clone, Debug)] pub enum Role { #[default] + /// This agent is controlling. Controlling, + /// This agent is controlled. Controlled, + /// No role attribute was present. Unspecified, } diff --git a/rtc-ice/src/attributes/mod.rs b/rtc-ice/src/attributes/mod.rs index 2e9d3441..7c39ac50 100644 --- a/rtc-ice/src/attributes/mod.rs +++ b/rtc-ice/src/attributes/mod.rs @@ -1,3 +1,6 @@ +/// `ICE-CONTROLLING` and `ICE-CONTROLLED`, which resolve role conflicts. pub mod control; +/// The `PRIORITY` attribute carried in connectivity checks. pub mod priority; +/// The `USE-CANDIDATE` flag, by which the controlling agent nominates a pair. pub mod use_candidate; diff --git a/rtc-ice/src/attributes/use_candidate/mod.rs b/rtc-ice/src/attributes/use_candidate/mod.rs index 353ff573..9adeec01 100644 --- a/rtc-ice/src/attributes/use_candidate/mod.rs +++ b/rtc-ice/src/attributes/use_candidate/mod.rs @@ -19,6 +19,7 @@ impl Setter for UseCandidateAttr { impl UseCandidateAttr { #[must_use] + /// A `USE-CANDIDATE` attribute, which carries no value. pub const fn new() -> Self { Self } diff --git a/rtc-ice/src/candidate/candidate_host.rs b/rtc-ice/src/candidate/candidate_host.rs index 431ddc57..a642c9b9 100644 --- a/rtc-ice/src/candidate/candidate_host.rs +++ b/rtc-ice/src/candidate/candidate_host.rs @@ -4,8 +4,10 @@ use crate::rand::generate_cand_id; /// The config required to create a new `CandidateHost`. #[derive(Default)] pub struct CandidateHostConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The TCP role, for ICE-TCP host candidates. pub tcp_type: TcpType, } diff --git a/rtc-ice/src/candidate/candidate_pair.rs b/rtc-ice/src/candidate/candidate_pair.rs index 5ff8805b..669d092a 100644 --- a/rtc-ice/src/candidate/candidate_pair.rs +++ b/rtc-ice/src/candidate/candidate_pair.rs @@ -7,6 +7,7 @@ use std::time::Duration; pub enum CandidatePairState { #[default] #[serde(rename = "unspecified")] + /// No state was set. Unspecified = 0, /// Means a check has not been performed for this pair. @@ -56,9 +57,13 @@ impl fmt::Display for CandidatePairState { /// Represents a combination of a local and remote candidate. #[derive(Clone, Copy)] pub struct CandidatePair { + /// Index of the local candidate in the agent's list. pub local_index: usize, + /// Index of the remote candidate in the agent's list. pub remote_index: usize, + /// The local candidate's priority. pub local_priority: u32, + /// The remote candidate's priority. pub remote_priority: u32, pub(crate) ice_role_controlling: bool, pub(crate) binding_request_count: u16, @@ -120,6 +125,9 @@ impl PartialEq for CandidatePair { impl CandidatePair { #[must_use] + /// Forms a pair from a local and a remote candidate. + /// + /// `controlling` selects which priority dominates in the pair's combined priority. pub fn new( local_index: usize, remote_index: usize, diff --git a/rtc-ice/src/candidate/candidate_peer_reflexive.rs b/rtc-ice/src/candidate/candidate_peer_reflexive.rs index 5c017fda..ff3f402b 100644 --- a/rtc-ice/src/candidate/candidate_peer_reflexive.rs +++ b/rtc-ice/src/candidate/candidate_peer_reflexive.rs @@ -6,9 +6,12 @@ use shared::error::*; /// The config required to create a new `CandidatePeerReflexive`. #[derive(Default)] pub struct CandidatePeerReflexiveConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, } diff --git a/rtc-ice/src/candidate/candidate_relay.rs b/rtc-ice/src/candidate/candidate_relay.rs index 7b1de871..256adbab 100644 --- a/rtc-ice/src/candidate/candidate_relay.rs +++ b/rtc-ice/src/candidate/candidate_relay.rs @@ -6,11 +6,15 @@ use shared::error::*; /// The config required to create a new `CandidateRelay`. #[derive(Default)] pub struct CandidateRelayConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, + /// The server this candidate was gathered from. pub url: Option, } diff --git a/rtc-ice/src/candidate/candidate_server_reflexive.rs b/rtc-ice/src/candidate/candidate_server_reflexive.rs index 0da585d6..1df98a60 100644 --- a/rtc-ice/src/candidate/candidate_server_reflexive.rs +++ b/rtc-ice/src/candidate/candidate_server_reflexive.rs @@ -6,11 +6,15 @@ use shared::error::*; /// The config required to create a new `CandidateServerReflexive`. #[derive(Default)] pub struct CandidateServerReflexiveConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, + /// The server this candidate was gathered from. pub url: Option, } diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index f113bded..4564d9bf 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -9,10 +9,15 @@ mod candidate_test; TODO: mod candidate_server_reflexive_test; */ +/// Host candidates: an address on a local interface. pub mod candidate_host; +/// A local/remote candidate pair and its check state. pub mod candidate_pair; +/// Peer-reflexive candidates, learned from an inbound check's source address. pub mod candidate_peer_reflexive; +/// Relay candidates, allocated on a TURN server. pub mod candidate_relay; +/// Server-reflexive candidates, learned from a STUN Binding response. pub mod candidate_server_reflexive; use crate::network_type::NetworkType; @@ -43,14 +48,26 @@ pub(crate) const COMPONENT_RTCP: u16 = 0; pub enum CandidateType { #[default] #[serde(rename = "unspecified")] + /// No candidate type was set. Unspecified, #[serde(rename = "host")] + /// An address on one of this host's own interfaces. + /// + /// Highest priority: reachable without traversal when both peers share a network. Host, #[serde(rename = "srflx")] + /// This host's address as seen by a STUN server — its public mapping through the NAT. ServerReflexive, #[serde(rename = "prflx")] + /// An address learned from a peer's inbound connectivity check. + /// + /// Discovered during checking rather than gathering, when a NAT maps a different port per + /// destination. PeerReflexive, #[serde(rename = "relay")] + /// An address allocated on a TURN server, which forwards on this host's behalf. + /// + /// Lowest priority: it always costs an extra hop. Relay, } @@ -104,7 +121,11 @@ pub(crate) fn contains_candidate_type( /// Convey transport addresses related to the candidate, useful for diagnostics and other purposes. #[derive(PartialEq, Eq, Debug, Clone)] pub struct CandidateRelatedAddress { + /// The address of the related candidate — the base this one was derived from. + /// The candidate's address. pub address: String, + /// The port of the related candidate. + /// The candidate's port. pub port: u16, } @@ -116,17 +137,29 @@ impl fmt::Display for CandidateRelatedAddress { } #[derive(Default)] +/// The fields common to every candidate type, used when constructing one. pub struct CandidateConfig { + /// A unique identifier for this candidate; generated when left empty. pub candidate_id: String, + /// The transport, `udp` or `tcp`. pub network: String, + /// The candidate's address. pub address: String, + /// The candidate's port. pub port: u16, + /// The RTP component id: `1` for RTP, `2` for RTCP when not multiplexed. pub component: u16, + /// The candidate priority; computed from the type and local preference when zero. pub priority: u32, + /// The foundation, which groups candidates that share a base and transport. + /// + /// Pairs with the same foundation are checked together, so redundant checks are avoided. pub foundation: String, } #[derive(Clone, Debug)] +/// One ICE candidate: a transport address this agent can be reached at, or can reach a peer +/// at, together with its type, priority and liveness bookkeeping. pub struct Candidate { pub(crate) id: String, pub(crate) network_type: NetworkType, @@ -205,6 +238,7 @@ impl fmt::Display for Candidate { } impl Candidate { + /// The candidate's foundation, computed from its type, base address and transport. pub fn foundation(&self) -> String { if !self.foundation_override.is_empty() { return self.foundation_override.clone(); @@ -287,10 +321,12 @@ impl Candidate { self.candidate_type } + /// The TCP role for ICE-TCP candidates; `Unspecified` for UDP. pub fn tcp_type(&self) -> TcpType { self.tcp_type } + /// The STUN or TURN server this candidate was gathered from, if any. pub fn url(&self) -> Option<&str> { self.url.as_deref() } @@ -323,6 +359,7 @@ impl Candidate { val } + /// The candidate's socket address. pub fn addr(&self) -> SocketAddr { self.resolved_addr } @@ -350,6 +387,9 @@ impl Candidate { } } + /// Records traffic on this candidate, updating its last-sent or last-received time. + /// + /// Feeds consent freshness — a pair that stops seeing traffic is eventually abandoned. pub fn seen(&mut self, outbound: bool) { let now = Instant::now(); @@ -390,6 +430,11 @@ impl Candidate { } } + /// Sets the resolved IP, deriving the network type from it. + /// + /// # Errors + /// + /// Fails if `ip`'s family does not match this candidate's network type. pub fn set_ip(&mut self, ip: &IpAddr) -> Result<()> { self.network_type = determine_network_type(&self.network, ip)?; self.resolved_addr = SocketAddr::new(*ip, self.port); //TODO: create_addr(network_type, *ip, self.port); @@ -398,10 +443,12 @@ impl Candidate { } impl Candidate { + /// Records that traffic was received on this candidate at `now`. pub fn set_last_received(&mut self, now: Instant) { self.last_received = now; } + /// Records that traffic was sent on this candidate at `now`. pub fn set_last_sent(&mut self, now: Instant) { self.last_sent = now; } diff --git a/rtc-ice/src/lib.rs b/rtc-ice/src/lib.rs index 22bef907..54a1fb6a 100644 --- a/rtc-ice/src/lib.rs +++ b/rtc-ice/src/lib.rs @@ -1,15 +1,56 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! ICE for the Sans-I/O WebRTC stack. +//! +//! Interactive Connectivity Establishment ([RFC 8445], superseding [RFC 5245]) with the +//! extensions WebRTC uses: ICE-TCP candidates ([RFC 6544]), consent freshness +//! ([RFC 7675]), and Trickle ICE. ICE is what finds a path between two peers behind NATs: +//! it gathers candidate addresses, pairs local with remote, and probes each pair with STUN +//! connectivity checks until one succeeds. +//! +//! # Structure +//! +//! * [`agent`] — the Sans-I/O [`Agent`]: give it candidates and inbound +//! datagrams, poll it for checks to send, state transitions, and the selected pair. It +//! owns no sockets and no clock. +//! * [`candidate`] — the candidate types (host, server-reflexive, peer-reflexive, relay), +//! their priorities, and SDP `a=candidate` parsing. +//! * [`state`] — connection and gathering states, and the checklist state machine. +//! * [`url`] — parsing `stun:`/`turn:` server URLs into something the agent can gather from. +//! * [`network_type`], [`tcp_type`] — UDP/TCP and active/passive/simultaneous-open. +//! * [`stats`] — per-candidate and per-pair counters, surfaced through `getStats`. +//! * [`mdns`] — mDNS candidate handling, for hiding private addresses. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate drives the agent as one layer of the peer-connection +//! pipeline. +//! +//! [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 +//! [RFC 5245]: https://datatracker.ietf.org/doc/html/rfc5245 +//! [RFC 6544]: https://datatracker.ietf.org/doc/html/rfc6544 +//! [RFC 7675]: https://datatracker.ietf.org/doc/html/rfc7675 + +/// The Sans-I/O ICE agent: candidate pairing, connectivity checks, and nomination. pub mod agent; +/// The ICE-specific STUN attributes carried in connectivity checks. pub mod attributes; +/// Candidate types, priorities, and SDP `a=candidate` parsing. pub mod candidate; +/// mDNS candidate handling, which hides private addresses behind `.local` names. pub mod mdns; +/// UDP/TCP over IPv4/IPv6, as a candidate's transport. pub mod network_type; +/// Random ICE credentials and identifiers. pub mod rand; +/// Connection and gathering states. pub mod state; +/// Per-candidate and per-pair counters, surfaced through `getStats`. pub mod stats; +/// Active, passive and simultaneous-open, for ICE-TCP candidates. pub mod tcp_type; +/// Parsing `stun:`/`turn:` server URLs. pub mod url; pub use agent::{ diff --git a/rtc-ice/src/network_type/mod.rs b/rtc-ice/src/network_type/mod.rs index df7f3922..a8c37f84 100644 --- a/rtc-ice/src/network_type/mod.rs +++ b/rtc-ice/src/network_type/mod.rs @@ -13,6 +13,7 @@ pub(crate) const UDP: &str = "udp"; pub(crate) const TCP: &str = "tcp"; #[must_use] +/// Every network type this crate can gather candidates for. pub fn supported_network_types() -> Vec { vec![ NetworkType::Udp4, @@ -27,6 +28,7 @@ pub fn supported_network_types() -> Vec { pub enum NetworkType { #[serde(rename = "unspecified")] #[default] + /// No network type was set. Unspecified, /// Indicates UDP over IPv4. @@ -85,6 +87,7 @@ impl NetworkType { } #[must_use] + /// The transport protocol, discarding the address family. pub fn to_protocol(self) -> TransportProtocol { if self.is_tcp() { TransportProtocol::TCP diff --git a/rtc-ice/src/state/mod.rs b/rtc-ice/src/state/mod.rs index d44679ca..aa3c0d95 100644 --- a/rtc-ice/src/state/mod.rs +++ b/rtc-ice/src/state/mod.rs @@ -6,6 +6,7 @@ use std::fmt; /// An enum showing the state of a ICE Connection List of supported States. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum ConnectionState { + /// No state was set. #[default] Unspecified, @@ -65,6 +66,7 @@ impl From for ConnectionState { /// Describes the state of the candidate gathering process. #[derive(Default, PartialEq, Eq, Copy, Clone)] pub enum GatheringState { + /// No state was set. #[default] Unspecified, diff --git a/rtc-ice/src/stats/mod.rs b/rtc-ice/src/stats/mod.rs index 440e910d..192202ea 100644 --- a/rtc-ice/src/stats/mod.rs +++ b/rtc-ice/src/stats/mod.rs @@ -3,157 +3,157 @@ use std::time::Instant; use crate::candidate::candidate_pair::CandidatePairState; use crate::candidate::*; -// CandidatePairStats contains ICE candidate pair statistics +/// CandidatePairStats contains ICE candidate pair statistics. #[derive(Debug, Clone)] pub struct CandidatePairStats { - // timestamp is the timestamp associated with this object. + /// The timestamp associated with this object. pub timestamp: Instant, - // local_candidate_id is the id of the local candidate + /// The id of the local candidate. pub local_candidate_id: String, - // remote_candidate_id is the id of the remote candidate + /// The id of the remote candidate. pub remote_candidate_id: String, - // state represents the state of the checklist for the local and remote - // candidates in a pair. + /// The state of the checklist for the local and remote + /// candidates in a pair. pub state: CandidatePairState, - // nominated is true when this valid pair that should be used for media - // if it is the highest-priority one amongst those whose nominated flag is set + /// True when this valid pair that should be used for media + /// if it is the highest-priority one amongst those whose nominated flag is set. pub nominated: bool, - // packets_sent represents the total number of packets sent on this candidate pair. + /// The total number of packets sent on this candidate pair. pub packets_sent: u32, - // packets_received represents the total number of packets received on this candidate pair. + /// The total number of packets received on this candidate pair. pub packets_received: u32, - // bytes_sent represents the total number of payload bytes sent on this candidate pair - // not including headers or padding. + /// The total number of payload bytes sent on this candidate pair + /// not including headers or padding. pub bytes_sent: u64, - // bytes_received represents the total number of payload bytes received on this candidate pair - // not including headers or padding. + /// The total number of payload bytes received on this candidate pair + /// not including headers or padding. pub bytes_received: u64, - // last_packet_sent_timestamp represents the timestamp at which the last packet was - // sent on this particular candidate pair, excluding STUN packets. + /// The timestamp at which the last packet was + /// sent on this particular candidate pair, excluding STUN packets. pub last_packet_sent_timestamp: Instant, - // last_packet_received_timestamp represents the timestamp at which the last packet - // was received on this particular candidate pair, excluding STUN packets. + /// The timestamp at which the last packet + /// was received on this particular candidate pair, excluding STUN packets. pub last_packet_received_timestamp: Instant, - // first_request_timestamp represents the timestamp at which the first STUN request - // was sent on this particular candidate pair. + /// The timestamp at which the first STUN request + /// was sent on this particular candidate pair. pub first_request_timestamp: Instant, - // last_request_timestamp represents the timestamp at which the last STUN request - // was sent on this particular candidate pair. The average interval between two - // consecutive connectivity checks sent can be calculated with - // (last_request_timestamp - first_request_timestamp) / requests_sent. + /// The timestamp at which the last STUN request + /// was sent on this particular candidate pair. The average interval between two + /// consecutive connectivity checks sent can be calculated with + /// (last_request_timestamp - first_request_timestamp) / requests_sent. pub last_request_timestamp: Instant, - // last_response_timestamp represents the timestamp at which the last STUN response - // was received on this particular candidate pair. + /// The timestamp at which the last STUN response + /// was received on this particular candidate pair. pub last_response_timestamp: Instant, - // total_round_trip_time represents the sum of all round trip time measurements - // in seconds since the beginning of the session, based on STUN connectivity - // check responses (responses_received), including those that reply to requests - // that are sent in order to verify consent. The average round trip time can - // be computed from total_round_trip_time by dividing it by responses_received. + /// The sum of all round trip time measurements + /// in seconds since the beginning of the session, based on STUN connectivity + /// check responses (responses_received), including those that reply to requests + /// that are sent in order to verify consent. The average round trip time can + /// be computed from total_round_trip_time by dividing it by responses_received. pub total_round_trip_time: f64, - // current_round_trip_time represents the latest round trip time measured in seconds, - // computed from both STUN connectivity checks, including those that are sent - // for consent verification. + /// The latest round trip time measured in seconds, + /// computed from both STUN connectivity checks, including those that are sent + /// for consent verification. pub current_round_trip_time: f64, - // available_outgoing_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the outgoing RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated - // over a 1 second window. + /// Calculated by the underlying congestion control + /// by combining the available bitrate for all the outgoing RTP streams using + /// this candidate pair. The bitrate measurement does not count the size of the + /// ip or other transport layers like TCP or UDP. It is similar to the TIAS defined + /// in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated + /// over a 1 second window. pub available_outgoing_bitrate: f64, - // available_incoming_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the incoming RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is - // calculated over a 1 second window. + /// Calculated by the underlying congestion control + /// by combining the available bitrate for all the incoming RTP streams using + /// this candidate pair. The bitrate measurement does not count the size of the + /// ip or other transport layers like TCP or UDP. It is similar to the TIAS defined + /// in RFC 3890, i.e., it is measured in bits per second and the bitrate is + /// calculated over a 1 second window. pub available_incoming_bitrate: f64, - // circuit_breaker_trigger_count represents the number of times the circuit breaker - // is triggered for this particular 5-tuple, ceasing transmission. + /// The number of times the circuit breaker + /// is triggered for this particular 5-tuple, ceasing transmission. pub circuit_breaker_trigger_count: u32, - // requests_received represents the total number of connectivity check requests - // received (including retransmissions). It is impossible for the receiver to - // tell whether the request was sent in order to check connectivity or check - // consent, so all connectivity checks requests are counted here. + /// The total number of connectivity check requests + /// received (including retransmissions). It is impossible for the receiver to + /// tell whether the request was sent in order to check connectivity or check + /// consent, so all connectivity checks requests are counted here. pub requests_received: u64, - // requests_sent represents the total number of connectivity check requests - // sent (not including retransmissions). + /// The total number of connectivity check requests + /// sent (not including retransmissions). pub requests_sent: u64, - // responses_received represents the total number of connectivity check responses received. + /// The total number of connectivity check responses received. pub responses_received: u64, - // responses_sent epresents the total number of connectivity check responses sent. - // Since we cannot distinguish connectivity check requests and consent requests, - // all responses are counted. + /// Responses_sent epresents the total number of connectivity check responses sent. + /// Since we cannot distinguish connectivity check requests and consent requests, + /// all responses are counted. pub responses_sent: u64, - // retransmissions_received represents the total number of connectivity check - // request retransmissions received. + /// The total number of connectivity check + /// request retransmissions received. pub retransmissions_received: u64, - // retransmissions_sent represents the total number of connectivity check - // request retransmissions sent. + /// The total number of connectivity check + /// request retransmissions sent. pub retransmissions_sent: u64, - // consent_requests_sent represents the total number of consent requests sent. + /// The total number of consent requests sent. pub consent_requests_sent: u64, - // consent_expired_timestamp represents the timestamp at which the latest valid - // STUN binding response expired. + /// The timestamp at which the latest valid. + /// STUN binding response expired. pub consent_expired_timestamp: Instant, } -// CandidateStats contains ICE candidate statistics related to the ICETransport objects. +/// CandidateStats contains ICE candidate statistics related to the ICETransport objects. #[derive(Debug, Clone)] pub struct CandidateStats { - // timestamp is the timestamp associated with this object. + /// The timestamp associated with this object. pub timestamp: Instant, - // id is the candidate id + /// The candidate id. pub id: String, - // ip is the ip address of the candidate, allowing for IPv4 addresses and - // IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. + /// The ip address of the candidate, allowing for IPv4 addresses and. + /// IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. pub ip: String, - // port is the port number of the candidate. + /// The port number of the candidate. pub port: u16, - // candidate_type is the "Type" field of the ICECandidate. + /// The "Type" field of the ICECandidate. pub candidate_type: CandidateType, - // priority is the "priority" field of the ICECandidate. + /// The "priority" field of the ICECandidate. pub priority: u32, - // url is the url of the TURN or STUN server indicated in the that translated - // this ip address. It is the url address surfaced in an PeerConnectionICEEvent. + /// The url of the TURN or STUN server indicated in the that translated + /// this ip address. It is the url address surfaced in an PeerConnectionICEEvent. pub url: String, - // relay_protocol is the protocol used by the endpoint to communicate with the - // TURN server. This is only present for local candidates. Valid values for - // the TURN url protocol is one of udp, tcp, or tls. + /// The protocol used by the endpoint to communicate with the. + /// TURN server. This is only present for local candidates. Valid values for + /// the TURN url protocol is one of udp, tcp, or tls. pub relay_protocol: String, // deleted is true if the candidate has been deleted/freed. For host candidates, @@ -161,6 +161,6 @@ pub struct CandidateStats { // candidate have been released. For TURN candidates, this means the TURN allocation // is no longer active. // - // Only defined for local candidates. For remote candidates, this property is not applicable. + /// Only defined for local candidates. For remote candidates, this property is not applicable. pub deleted: bool, } diff --git a/rtc-ice/src/tcp_type/mod.rs b/rtc-ice/src/tcp_type/mod.rs index 7feee6ad..8d074c01 100644 --- a/rtc-ice/src/tcp_type/mod.rs +++ b/rtc-ice/src/tcp_type/mod.rs @@ -6,6 +6,9 @@ use std::fmt; // TCPType is the type of ICE TCP candidate as described in // https://tools.ietf.org/html/rfc6544#section-4.5 #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// The role of an ICE-TCP candidate, per [RFC 6544] §4.5. +/// +/// [RFC 6544]: https://datatracker.ietf.org/doc/html/rfc6544#section-4.5 pub enum TcpType { /// The default value. For example UDP candidates do not need this field. #[default] diff --git a/rtc-ice/src/url/mod.rs b/rtc-ice/src/url/mod.rs index ee5aec22..3d86d90b 100644 --- a/rtc-ice/src/url/mod.rs +++ b/rtc-ice/src/url/mod.rs @@ -24,6 +24,7 @@ pub enum SchemeType { #[default] /// Default public constant to use for "enum" like struct comparisons when no value was defined. + /// A scheme or transport this crate does not recognise. Unknown, } @@ -64,6 +65,7 @@ pub enum ProtoType { /// The URL uses a TCP transport. Tcp, + /// A transport this crate does not recognise. Unknown, } @@ -95,11 +97,17 @@ impl fmt::Display for ProtoType { /// Represents a STUN (rfc7064) or TURN (rfc7065) URL. #[derive(Debug, Clone, Default)] pub struct Url { + /// The URL scheme: `stun`, `stuns`, `turn` or `turns`. pub scheme: SchemeType, + /// The server host name or address. pub host: String, + /// The server port; defaults to 3478, or 5349 for the secure schemes. pub port: u16, + /// The TURN username, for `turn:`/`turns:` URLs. pub username: String, + /// The TURN credential. pub password: String, + /// The transport to reach the server over, from the URL's `?transport=` parameter. pub proto: ProtoType, } diff --git a/rtc-interceptor-derive/src/lib.rs b/rtc-interceptor-derive/src/lib.rs index 876203c0..0f4b4b34 100644 --- a/rtc-interceptor-derive/src/lib.rs +++ b/rtc-interceptor-derive/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Derive macros for RTC Interceptor trait. //! //! This crate provides two macros that work together: diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index fdf90541..ec09c365 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -172,6 +172,7 @@ //! See the [`Interceptor`] trait documentation for more details. #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] use shared::TransportMessage; diff --git a/rtc-mdns/src/lib.rs b/rtc-mdns/src/lib.rs index 093f4383..f2352dd0 100644 --- a/rtc-mdns/src/lib.rs +++ b/rtc-mdns/src/lib.rs @@ -186,6 +186,7 @@ //! - **Compression**: DNS name compression is supported for efficiency #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] pub(crate) mod config; diff --git a/rtc-mdns/src/socket.rs b/rtc-mdns/src/socket.rs index 270cda2f..7de17874 100644 --- a/rtc-mdns/src/socket.rs +++ b/rtc-mdns/src/socket.rs @@ -95,11 +95,18 @@ impl MulticastSocket { } } + /// Sets the local IPv4 address to bind the multicast socket to. + /// + /// Defaults to unspecified (`0.0.0.0`), letting the OS choose. pub fn with_multicast_local_ipv4(mut self, multicast_local_ipv4: Ipv4Addr) -> Self { self.multicast_local_ipv4 = Some(multicast_local_ipv4); self } + /// Sets the local port to bind the multicast socket to. + /// + /// Defaults to the mDNS port (5353), which is required to receive multicast queries + /// from other hosts; a different port is only useful for testing. pub fn with_multicast_local_port(mut self, multicast_local_port: u16) -> Self { self.multicast_local_port = Some(multicast_local_port); self diff --git a/rtc-media/src/audio/buffer.rs b/rtc-media/src/audio/buffer.rs index 93e4092a..1feab110 100644 --- a/rtc-media/src/audio/buffer.rs +++ b/rtc-media/src/audio/buffer.rs @@ -1,4 +1,6 @@ +/// Channel and frame counts for a buffer. pub mod info; +/// The interleaved and deinterleaved buffer layouts. pub mod layout; use std::mem::{ManuallyDrop, MaybeUninit}; @@ -10,16 +12,33 @@ pub use layout::BufferLayout; use layout::{Deinterleaved, Interleaved}; use thiserror::Error; +/// Decodes a buffer from raw little- or big-endian bytes. +/// +/// `L` is the [`BufferLayout`] the decoded samples are arranged in. pub trait FromBytes: Sized { + /// The error type produced by a failed conversion. type Error; + /// Decodes `channels` channels of samples from `bytes`, reading in byte order `B`. + /// + /// # Errors + /// + /// Fails if `bytes` is too short for a whole number of frames. fn from_bytes(bytes: &[u8], channels: usize) -> Result; } +/// Encodes a buffer into raw bytes in a caller-chosen endianness. pub trait ToByteBufferRef: Sized { + /// The error type produced by a failed conversion. type Error; + /// The number of bytes [`Self::to_bytes`] will write. fn bytes_len(&self); + /// Encodes the buffer into `bytes` in byte order `B`, returning the bytes written. + /// + /// # Errors + /// + /// Fails if `bytes` is too short. fn to_bytes( &self, bytes: &mut [u8], @@ -28,18 +47,29 @@ pub trait ToByteBufferRef: Sized { } #[derive(Debug, Error, PartialEq, Eq)] +/// Errors from converting between buffers and raw bytes. pub enum Error { #[error("Unexpected end of buffer: (expected: {expected}, actual: {actual})")] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, + /// The byte slice was too short to hold the expected number of samples. + UnexpectedEndOfBuffer { + /// Bytes required. + expected: usize, + /// Bytes available. + actual: usize, + }, } #[derive(Eq, PartialEq, Clone, Debug)] +/// A borrowed view of multi-channel audio: samples of type `T` in layout `L`. pub struct BufferRef<'a, T, L> { samples: &'a [T], info: BufferInfo, } impl<'a, T, L> BufferRef<'a, T, L> { + /// Wraps `samples` as `channels` interleaved or deinterleaved channels. + /// + /// The frame count is derived from the slice length, which must divide evenly by `channels`. pub fn new(samples: &'a [T], channels: usize) -> Self { debug_assert_eq!(samples.len() % channels, 0); let info = { @@ -58,6 +88,9 @@ pub struct Buffer { } impl Buffer { + /// Takes ownership of `samples` as `channels` channels. + /// + /// The frame count is derived from the length, which must divide evenly by `channels`. pub fn new(samples: Vec, channels: usize) -> Self { debug_assert_eq!(samples.len() % channels, 0); let info = { @@ -67,6 +100,7 @@ impl Buffer { Self { samples, info } } + /// Borrows the whole buffer as a [`BufferRef`]. pub fn as_ref(&'_ self) -> BufferRef<'_, T, L> { BufferRef { samples: &self.samples[..], @@ -74,6 +108,7 @@ impl Buffer { } } + /// Borrows a sample range of the buffer as a [`BufferRef`]. pub fn sub_range(&'_ self, range: Range) -> BufferRef<'_, T, L> { let samples_len = range.len(); let samples = &self.samples[range]; diff --git a/rtc-media/src/audio/buffer/info.rs b/rtc-media/src/audio/buffer/info.rs index bd70e12d..dbd1025c 100644 --- a/rtc-media/src/audio/buffer/info.rs +++ b/rtc-media/src/audio/buffer/info.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use crate::audio::buffer::layout::{Deinterleaved, Interleaved}; #[derive(Eq, PartialEq, Debug)] +/// The channel and frame counts of a buffer, tagged with its layout `L`. pub struct BufferInfo { channels: usize, frames: usize, @@ -10,6 +11,7 @@ pub struct BufferInfo { } impl BufferInfo { + /// Describes a buffer of `channels` channels and `frames` frames per channel. pub fn new(channels: usize, frames: usize) -> Self { Self { channels, @@ -38,6 +40,7 @@ impl BufferInfo { self.frames = frames; } + /// The total sample count: channels times frames. pub fn samples(&self) -> usize { self.channels * self.frames } diff --git a/rtc-media/src/audio/buffer/layout.rs b/rtc-media/src/audio/buffer/layout.rs index d26f1fe7..625634ee 100644 --- a/rtc-media/src/audio/buffer/layout.rs +++ b/rtc-media/src/audio/buffer/layout.rs @@ -1,11 +1,18 @@ use crate::audio::buffer::BufferInfo; use crate::audio::sealed::Sealed; +/// How multi-channel samples are arranged in a flat buffer. +/// +/// Sealed: the only layouts are [`Interleaved`] and [`Deinterleaved`]. pub trait BufferLayout: Sized + Sealed { + /// The flat index of `frame` on `channel`, for a buffer described by `info`. fn index_of(info: &BufferInfo, channel: usize, frame: usize) -> usize; } #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// Channels stored one after another: all of channel 0, then all of channel 1. +/// +/// A marker type — it has no values. pub enum Deinterleaved {} impl Sealed for Deinterleaved {} @@ -18,6 +25,9 @@ impl BufferLayout for Deinterleaved { } #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// Frames stored one after another, each holding one sample per channel. +/// +/// The layout most audio APIs use, and a marker type with no values. pub enum Interleaved {} impl Sealed for Interleaved {} diff --git a/rtc-media/src/audio/mod.rs b/rtc-media/src/audio/mod.rs index e259ae9c..f6d53c68 100644 --- a/rtc-media/src/audio/mod.rs +++ b/rtc-media/src/audio/mod.rs @@ -1,3 +1,4 @@ +/// Multi-channel audio buffers in interleaved or deinterleaved layout. pub mod buffer; mod sample; diff --git a/rtc-media/src/audio/sample.rs b/rtc-media/src/audio/sample.rs index a98910a1..6b65c30d 100644 --- a/rtc-media/src/audio/sample.rs +++ b/rtc-media/src/audio/sample.rs @@ -6,6 +6,9 @@ use nearly_eq::NearlyEq; #[derive(Eq, PartialEq, Copy, Clone, Default, Debug)] #[repr(transparent)] +/// One audio sample of raw type `Raw` (`i16`, `f32`, …). +/// +/// A transparent newtype, so a `[Sample]` can be reinterpreted as `[T]` without copying. pub struct Sample(Raw); impl From for Sample { diff --git a/rtc-media/src/io/h26x_reader/mod.rs b/rtc-media/src/io/h26x_reader/mod.rs index 3f40df99..06869ab7 100644 --- a/rtc-media/src/io/h26x_reader/mod.rs +++ b/rtc-media/src/io/h26x_reader/mod.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod h26x_reader_test; +/// Reads Annex B streams as whole samples rather than individual NAL units. pub mod sample_reader; pub use sample_reader::{H26xSample, H26xSampleReader}; @@ -141,11 +142,14 @@ impl From for H264NalUnitType { /// NAL H.264 Network Abstraction Layer pub struct H264NAL { + /// The picture order count parsed from the slice header, which orders frames for display. pub picture_order_count: u32, /// NAL header pub forbidden_zero_bit: bool, + /// `nal_ref_idc`: how important this unit is as a reference; `0` means it is not referenced. pub ref_idc: u8, + /// The NAL unit type, which says whether this is a slice, SPS, PPS, and so on. pub unit_type: H264NalUnitType, /// header byte + rbsp @@ -153,6 +157,7 @@ pub struct H264NAL { } impl H264NAL { + /// Wraps `data` as a NAL unit, without parsing its header yet. pub fn new(data: BytesMut) -> Self { H264NAL { picture_order_count: 0, @@ -163,6 +168,7 @@ impl H264NAL { } } + /// Parses the NAL header out of the unit's bytes, filling in the fields above. pub fn parse_header(&mut self) { let first_byte = self.data[0]; self.forbidden_zero_bit = ((first_byte & 0x80) >> 7) == 1; // 0x80 = 0b10000000 @@ -302,8 +308,11 @@ impl From for H265NalUnitType { pub struct H265NAL { /// NAL header (2 bytes for H.265) pub forbidden_zero_bit: bool, + /// The NAL unit type. pub unit_type: H265NalUnitType, + /// `nuh_layer_id`: the scalability layer this unit belongs to; `0` for the base layer. pub nuh_layer_id: u8, + /// `nuh_temporal_id_plus1`: the temporal sub-layer, offset by one. pub nuh_temporal_id_plus1: u8, /// NAL unit header (2 bytes) + rbsp @@ -311,6 +320,7 @@ pub struct H265NAL { } impl H265NAL { + /// Wraps `data` as a NAL unit, without parsing its header yet. pub fn new(data: BytesMut) -> Self { H265NAL { forbidden_zero_bit: false, @@ -321,6 +331,7 @@ impl H265NAL { } } + /// Parses the NAL header out of the unit's bytes, filling in the fields above. pub fn parse_header(&mut self) { if self.data.len() < 2 { return; @@ -347,11 +358,14 @@ impl H265NAL { /// H26xNAL represents either an H264 or H265 NAL unit pub enum H26xNAL { + /// An H.264 NAL unit. H264(H264NAL), + /// An H.265 (HEVC) NAL unit. H265(H265NAL), } impl H26xNAL { + /// The unit's bytes, whichever codec it is. pub fn data(&self) -> &BytesMut { match self { H26xNAL::H264(nal) => &nal.data, diff --git a/rtc-media/src/io/h26x_reader/sample_reader.rs b/rtc-media/src/io/h26x_reader/sample_reader.rs index f16d60a5..0e5947a8 100644 --- a/rtc-media/src/io/h26x_reader/sample_reader.rs +++ b/rtc-media/src/io/h26x_reader/sample_reader.rs @@ -7,11 +7,15 @@ use super::{H26xNAL, H26xReader, H264NalUnitType, H265NalUnitType}; const ANNEXB_START_CODE: [u8; 4] = [0x00, 0x00, 0x00, 0x01]; #[derive(Debug, Clone, PartialEq, Eq)] +/// One access unit read from an Annex B stream — the NAL units making up a single frame. pub struct H26xSample { + /// The sample's bytes, start codes included. pub data: Bytes, + /// Whether this sample advances presentation time, i.e. completes a frame. pub timed: bool, } +/// Reads whole samples from an H.264 or H.265 Annex B stream. pub struct H26xSampleReader { reader: H26xReader, is_hevc: bool, @@ -19,6 +23,9 @@ pub struct H26xSampleReader { } impl H26xSampleReader { + /// Wraps `reader`, buffering up to `capacity` bytes. + /// + /// Set `is_hevc` for H.265; the two codecs differ in how NAL headers are parsed. pub fn new(reader: R, capacity: usize, is_hevc: bool) -> Self { Self { reader: H26xReader::new(reader, capacity, is_hevc), @@ -27,6 +34,11 @@ impl H26xSampleReader { } } + /// Reads the next sample. + /// + /// # Errors + /// + /// Fails on an I/O error, or at end of stream. pub fn next_sample(&mut self) -> Result { loop { let nal = match self.reader.next_nal() { diff --git a/rtc-media/src/io/ivf_reader/mod.rs b/rtc-media/src/io/ivf_reader/mod.rs index 18074abc..f4bada88 100644 --- a/rtc-media/src/io/ivf_reader/mod.rs +++ b/rtc-media/src/io/ivf_reader/mod.rs @@ -9,32 +9,47 @@ use bytes::BytesMut; use crate::io::ResetFn; use shared::error::{Error, Result}; +/// The four-byte signature every IVF file starts with. pub const IVF_FILE_HEADER_SIGNATURE: &[u8] = b"DKIF"; +/// The size of the IVF file header in bytes. pub const IVF_FILE_HEADER_SIZE: usize = 32; +/// The size of each IVF frame header in bytes. pub const IVF_FRAME_HEADER_SIZE: usize = 12; /// IVFFileHeader 32-byte header for IVF files /// #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub struct IVFFileHeader { - pub signature: [u8; 4], // 0-3 - pub version: u16, // 4-5 - pub header_size: u16, // 6-7 - pub four_cc: [u8; 4], // 8-11 - pub width: u16, // 12-13 - pub height: u16, // 14-15 + /// Bytes 0–3: always `DKIF`. + pub signature: [u8; 4], // 0-3 + /// Bytes 4–5: the format version, currently 0. + pub version: u16, // 4-5 + /// Bytes 6–7: the header length, normally 32. + pub header_size: u16, // 6-7 + /// Bytes 8–11: the codec FourCC, such as `VP80`, `VP90` or `AV01`. + pub four_cc: [u8; 4], // 8-11 + /// Bytes 12–13: frame width in pixels. + pub width: u16, // 12-13 + /// Bytes 14–15: frame height in pixels. + pub height: u16, // 14-15 + /// Bytes 16–19: the timebase denominator — the frame rate's numerator, confusingly. pub timebase_denominator: u32, // 16-19 - pub timebase_numerator: u32, // 20-23 - pub num_frames: u32, // 24-27 - pub unused: u32, // 28-31 + /// Bytes 20–23: the timebase numerator. + pub timebase_numerator: u32, // 20-23 + /// Bytes 24–27: the frame count, if the writer knew it. + pub num_frames: u32, // 24-27 + /// Bytes 28–31: reserved. + pub unused: u32, // 28-31 } /// IVFFrameHeader 12-byte header for IVF frames /// #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub struct IVFFrameHeader { + /// Bytes 0–3: the frame's payload length in bytes. pub frame_size: u32, // 0-3 - pub timestamp: u64, // 4-11 + /// Bytes 4–11: the frame's presentation timestamp, in timebase units. + pub timestamp: u64, // 4-11 } /// IVFReader is used to read IVF files and return frame payloads diff --git a/rtc-media/src/io/ivf_writer/mod.rs b/rtc-media/src/io/ivf_writer/mod.rs index bef35130..8f35c0be 100644 --- a/rtc-media/src/io/ivf_writer/mod.rs +++ b/rtc-media/src/io/ivf_writer/mod.rs @@ -16,8 +16,11 @@ use shared::error::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum IvfCodec { #[default] + /// VP8, written with the `VP80` FourCC. Vp8, + /// VP9, written with the `VP90` FourCC. Vp9, + /// AV1, written with the `AV01` FourCC. Av1, } diff --git a/rtc-media/src/io/mod.rs b/rtc-media/src/io/mod.rs index a9c7d2af..eb8100a1 100644 --- a/rtc-media/src/io/mod.rs +++ b/rtc-media/src/io/mod.rs @@ -1,22 +1,41 @@ +/// Reads H.264/H.265 Annex B byte streams into NAL units. pub mod h26x_reader; +/// Writes H.264/H.265 Annex B byte streams. pub mod h26x_writer; +/// Reads IVF files, the simple container used for VP8, VP9 and AV1. pub mod ivf_reader; +/// Writes IVF files. pub mod ivf_writer; +/// Reads Ogg files carrying Opus audio. pub mod ogg_reader; +/// Writes Ogg files carrying Opus audio. pub mod ogg_writer; +/// Reassembles inbound RTP packets into complete media samples. pub mod sample_builder; use shared::error::Result; +/// A callback that produces a fresh reader or writer, used to restart a stream. pub type ResetFn = Box R>; // Writer defines an interface to handle // the creation of media files +/// A sink for RTP packets, such as a file in a container format. pub trait Writer { // Add the content of an RTP packet to the media + /// Writes one RTP packet's media to the sink. + /// + /// # Errors + /// + /// Fails on an I/O error, or if the packet cannot be depacketized for this format. fn write_rtp(&mut self, pkt: &rtp::Packet) -> Result<()>; // close the media // Note: close implementation must be idempotent + /// Finalizes the sink, writing any trailing header or index the format needs. + /// + /// # Errors + /// + /// Fails on an I/O error while flushing. fn close(&mut self) -> Result<()>; } diff --git a/rtc-media/src/io/ogg_reader/mod.rs b/rtc-media/src/io/ogg_reader/mod.rs index 5e714bd6..a62c7477 100644 --- a/rtc-media/src/io/ogg_reader/mod.rs +++ b/rtc-media/src/io/ogg_reader/mod.rs @@ -9,14 +9,24 @@ use bytes::BytesMut; use crate::io::ResetFn; use shared::error::{Error, Result}; +/// Page header flag: this page continues a packet from the previous page. pub const PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM: u8 = 0x00; +/// Page header flag: the first page of a logical stream. pub const PAGE_HEADER_TYPE_BEGINNING_OF_STREAM: u8 = 0x02; +/// Page header flag: the last page of a logical stream. pub const PAGE_HEADER_TYPE_END_OF_STREAM: u8 = 0x04; +/// The recommended Opus pre-skip: 3840 samples (80 ms at 48 kHz) of decoder warm-up to +/// discard. pub const DEFAULT_PRE_SKIP: u16 = 3840; // 3840 recommended in the RFC +/// The four-byte signature that begins every Ogg page. pub const PAGE_HEADER_SIGNATURE: &[u8] = b"OggS"; +/// The signature of the Opus identification header. pub const ID_PAGE_SIGNATURE: &[u8] = b"OpusHead"; +/// The signature of the Opus comment header. pub const COMMENT_PAGE_SIGNATURE: &[u8] = b"OpusTags"; +/// The fixed part of an Ogg page header, in bytes, before the segment table. pub const PAGE_HEADER_SIZE: usize = 27; +/// The size of the `OpusHead` payload in bytes. pub const ID_PAGE_PAYLOAD_SIZE: usize = 19; /// Header type classification for Opus pages @@ -41,14 +51,23 @@ pub struct OggReader { /// #[derive(Debug, Clone)] pub struct OggHeader { + /// The channel mapping family, which says how channels map to speakers. pub channel_map: u8, + /// The channel count. pub channels: u8, + /// A gain in Q7.8 dB to apply when decoding. pub output_gain: u16, + /// Samples to discard from the start of the stream — decoder warm-up. pub pre_skip: u16, + /// The original input sample rate. Opus always decodes at 48 kHz regardless. pub sample_rate: u32, + /// The `OpusHead` version, currently 1. pub version: u8, + /// The number of Opus streams, for mapping families above 0. pub stream_count: u8, + /// How many of those streams are coupled stereo pairs. pub coupled_count: u8, + /// Which stream channel feeds each output channel. pub channel_mapping: Vec, } @@ -56,14 +75,18 @@ pub struct OggHeader { /// #[derive(Debug, Clone, Default)] pub struct OpusTags { + /// The encoder that produced the file. pub vendor: String, + /// Metadata tags from the `OpusTags` header. pub user_comments: Vec, } /// A key-value pair from Vorbis comments #[derive(Debug, Clone)] pub struct UserComment { + /// The tag name, such as `TITLE` or `ARTIST`. pub comment: String, + /// The tag value. pub value: String, } @@ -72,6 +95,7 @@ pub struct UserComment { /// #[derive(Debug, Clone)] pub struct OggPageHeader { + /// The page's granule position: total decoded samples at 48 kHz through this page. pub granule_position: u64, /// Serial number of the logical bitstream (track) pub serial: u32, @@ -306,6 +330,11 @@ impl OggReader { // parse_next_page reads from stream and returns Ogg page payload, header, // and an error if there is incomplete page data. + /// Reads the next Ogg page, returning its payload and header. + /// + /// # Errors + /// + /// Fails on an I/O error, at end of stream, or if the page signature or checksum is wrong. pub fn parse_next_page(&mut self) -> Result<(BytesMut, OggPageHeader)> { let mut h = [0u8; PAGE_HEADER_SIZE]; self.reader.read_exact(&mut h)?; diff --git a/rtc-media/src/io/sample_builder/mod.rs b/rtc-media/src/io/sample_builder/mod.rs index 6f7893db..dbbce80f 100644 --- a/rtc-media/src/io/sample_builder/mod.rs +++ b/rtc-media/src/io/sample_builder/mod.rs @@ -3,6 +3,7 @@ mod sample_builder_test; #[cfg(test)] mod sample_sequence_location_test; +/// Tracks where a sample sits within the RTP sequence-number space. pub mod sample_sequence_location; use self::sample_sequence_location::{Comparison, SampleSequenceLocation}; @@ -69,6 +70,10 @@ impl SampleBuilder { } } + /// Sets how long to wait for a missing packet before giving up on the sample it belongs to. + /// + /// Bounds head-of-line blocking: without it a single lost packet would stall reassembly + /// indefinitely. pub fn with_max_time_delay(mut self, max_late_duration: Duration) -> Self { self.max_late_timestamp = (self.sample_rate as u128 * max_late_duration.as_millis() / 1000) as u32; diff --git a/rtc-media/src/lib.rs b/rtc-media/src/lib.rs index be7a610d..f47e6f5d 100644 --- a/rtc-media/src/lib.rs +++ b/rtc-media/src/lib.rs @@ -1,8 +1,33 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Media samples and container I/O. +//! +//! The bridge between encoded media and RTP: a codec-agnostic [`Sample`] type, and readers +//! and writers for the container formats the examples and tests use. +//! +//! # Structure +//! +//! * [`Sample`] — one encoded unit of media (a video frame, an audio frame) with its +//! duration, timestamp and packet metadata. Hand these to a sample-based local track and +//! the RTP packetizer does the rest. +//! * [`io`] — the [`Writer`](io::Writer) trait plus concrete readers and writers for IVF +//! (VP8/VP9), Ogg (Opus) and H.264/H.265 Annex B — [`IVFReader`](io::ivf_reader::IVFReader), +//! [`OggReader`](io::ogg_reader::OggReader) and friends — enough to play media from disk or +//! record it to disk. [`SampleBuilder`](io::sample_builder::SampleBuilder) goes the other +//! way, reassembling inbound RTP into [`Sample`]s. +//! * [`audio`], [`video`] — per-codec helpers, including audio buffering and frame +//! inspection. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::media`. + +/// Audio sample types and multi-channel buffers. pub mod audio; +/// Container readers and writers, plus RTP sample reassembly. pub mod io; +/// Video frame helpers. pub mod video; use bytes::Bytes; diff --git a/rtc-rtcp/src/extended_report/dlrr.rs b/rtc-rtcp/src/extended_report/dlrr.rs index c8701f6c..449bf6ed 100644 --- a/rtc-rtcp/src/extended_report/dlrr.rs +++ b/rtc-rtcp/src/extended_report/dlrr.rs @@ -5,8 +5,11 @@ const DLRR_REPORT_LENGTH: u16 = 12; /// DLRRReport encodes a single report inside a DLRRReportBlock. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct DLRRReport { + /// The SSRC this sub-block reports on. pub ssrc: u32, + /// The middle 32 bits of the NTP timestamp from that receiver's last Receiver Reference Time. pub last_rr: u32, + /// Delay since that report was received, in units of 1/65536 seconds. pub dlrr: u32, } @@ -36,6 +39,7 @@ impl fmt::Display for DLRRReport { /// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct DLRRReportBlock { + /// One sub-block per SSRC reported on. pub reports: Vec, } @@ -46,6 +50,7 @@ impl fmt::Display for DLRRReportBlock { } impl DLRRReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::DLRR, diff --git a/rtc-rtcp/src/extended_report/mod.rs b/rtc-rtcp/src/extended_report/mod.rs index f515c4dd..048aaadf 100644 --- a/rtc-rtcp/src/extended_report/mod.rs +++ b/rtc-rtcp/src/extended_report/mod.rs @@ -1,12 +1,19 @@ #[cfg(test)] mod extended_report_test; +/// Delay Since Last Receiver Report blocks, for round-trip time between receivers. pub mod dlrr; +/// Packet Receipt Times blocks. pub mod prt; +/// Loss and Duplicate RLE blocks, which run-length encode per-packet receipt. pub mod rle; +/// Receiver Reference Time blocks, which anchor DLRR round-trip calculations. pub mod rrt; +/// Statistics Summary blocks: loss, duplicate, jitter and TTL ranges. pub mod ssr; +/// An unparsed block, for types this crate does not model. pub mod unknown; +/// VoIP Metrics blocks, which carry call-quality estimates. pub mod vm; pub use dlrr::{DLRRReport, DLRRReportBlock}; @@ -35,14 +42,22 @@ const XR_HEADER_LENGTH: usize = 4; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum BlockType { #[default] + /// A block type this crate does not model. Unknown = 0, - LossRLE = 1, // RFC 3611, section 4.1 - DuplicateRLE = 2, // RFC 3611, section 4.2 - PacketReceiptTimes = 3, // RFC 3611, section 4.3 + /// Loss RLE report block ([RFC 3611] §4.1). + LossRLE = 1, // RFC 3611, section 4.1 + /// Duplicate RLE report block ([RFC 3611] §4.2). + DuplicateRLE = 2, // RFC 3611, section 4.2 + /// Packet Receipt Times report block ([RFC 3611] §4.3). + PacketReceiptTimes = 3, // RFC 3611, section 4.3 + /// Receiver Reference Time report block ([RFC 3611] §4.4). ReceiverReferenceTime = 4, // RFC 3611, section 4.4 - DLRR = 5, // RFC 3611, section 4.5 - StatisticsSummary = 6, // RFC 3611, section 4.6 - VoIPMetrics = 7, // RFC 3611, section 4.7 + /// Delay Since Last Receiver Report block ([RFC 3611] §4.5). + DLRR = 5, // RFC 3611, section 4.5 + /// Statistics Summary report block ([RFC 3611] §4.6). + StatisticsSummary = 6, // RFC 3611, section 4.6 + /// VoIP Metrics report block ([RFC 3611] §4.7). + VoIPMetrics = 7, // RFC 3611, section 4.7 } impl From for BlockType { @@ -90,8 +105,11 @@ pub type TypeSpecificField = u8; /// packet is marshaled. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct XRHeader { + /// Which kind of report block follows. pub block_type: BlockType, + /// Bits whose meaning depends on the block type. pub type_specific: TypeSpecificField, + /// The block's length in 32-bit words, excluding this header. pub block_length: u16, } @@ -155,7 +173,9 @@ impl Unmarshal for XRHeader { /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, PartialEq, Default, Clone)] pub struct ExtendedReport { + /// The SSRC of the sender of this extended report. pub sender_ssrc: u32, + /// The report blocks this packet carries. pub reports: Vec>, } diff --git a/rtc-rtcp/src/extended_report/prt.rs b/rtc-rtcp/src/extended_report/prt.rs index ebeafa30..ce9ea791 100644 --- a/rtc-rtcp/src/extended_report/prt.rs +++ b/rtc-rtcp/src/extended_report/prt.rs @@ -25,12 +25,17 @@ const PRT_REPORT_BLOCK_MIN_LENGTH: u16 = 8; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct PacketReceiptTimesReportBlock { //not included in marshal/unmarshal + /// The block's `T` field, which scales the receipt-time values. pub t: u8, //marshal/unmarshal + /// The SSRC whose packets are reported on. pub ssrc: u32, + /// The first sequence number covered by this block. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// Receipt time for each packet in the range, in the block's timestamp units. pub receipt_time: Vec, } @@ -41,6 +46,7 @@ impl fmt::Display for PacketReceiptTimesReportBlock { } impl PacketReceiptTimesReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::PacketReceiptTimes, diff --git a/rtc-rtcp/src/extended_report/rle.rs b/rtc-rtcp/src/extended_report/rle.rs index a4c71a2a..cbe246e3 100644 --- a/rtc-rtcp/src/extended_report/rle.rs +++ b/rtc-rtcp/src/extended_report/rle.rs @@ -5,8 +5,11 @@ const RLE_REPORT_BLOCK_MIN_LENGTH: u16 = 8; /// ChunkType enumerates the three kinds of chunks described in RFC 3611 section 4.1. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ChunkType { + /// A run-length chunk: a bit value repeated a stated number of times. RunLength = 0, + /// A bit-vector chunk: 15 explicit per-packet bits. BitVector = 1, + /// The terminating null chunk, which pads the block to a word boundary. TerminatingNull = 2, } @@ -105,13 +108,19 @@ impl Chunk { #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct RLEReportBlock { //not included in marshal/unmarshal + /// `true` for a Loss RLE block, `false` for a Duplicate RLE block. pub is_loss_rle: bool, + /// The block's `T` field. pub t: u8, //marshal/unmarshal + /// The SSRC whose packets are reported on. pub ssrc: u32, + /// The first sequence number covered by this block. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// The run-length and bit-vector chunks encoding per-packet status. pub chunks: Vec, } @@ -126,6 +135,7 @@ pub type LossRLEReportBlock = RLEReportBlock; pub type DuplicateRLEReportBlock = RLEReportBlock; impl RLEReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: if self.is_loss_rle { diff --git a/rtc-rtcp/src/extended_report/rrt.rs b/rtc-rtcp/src/extended_report/rrt.rs index 46d4562c..d95ab0c4 100644 --- a/rtc-rtcp/src/extended_report/rrt.rs +++ b/rtc-rtcp/src/extended_report/rrt.rs @@ -16,6 +16,7 @@ const RRT_REPORT_BLOCK_LENGTH: u16 = 8; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct ReceiverReferenceTimeReportBlock { + /// The receiver's NTP timestamp, which DLRR blocks refer back to. pub ntp_timestamp: u64, } @@ -26,6 +27,7 @@ impl fmt::Display for ReceiverReferenceTimeReportBlock { } impl ReceiverReferenceTimeReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::ReceiverReferenceTime, diff --git a/rtc-rtcp/src/extended_report/ssr.rs b/rtc-rtcp/src/extended_report/ssr.rs index 03916cd4..ae3aceaf 100644 --- a/rtc-rtcp/src/extended_report/ssr.rs +++ b/rtc-rtcp/src/extended_report/ssr.rs @@ -31,24 +31,41 @@ const SSR_REPORT_BLOCK_LENGTH: u16 = 4 + 2 * 2 + 4 * 6 + 4; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct StatisticsSummaryReportBlock { //not included in marshal/unmarshal + /// Whether the loss fields are present. pub loss_reports: bool, + /// Whether the duplicate fields are present. pub duplicate_reports: bool, + /// Whether the jitter fields are present. pub jitter_reports: bool, + /// Whether the TTL fields hold an IPv4 TTL, an IPv6 hop limit, or nothing. pub ttl_or_hop_limit: TTLorHopLimitType, //marshal/unmarshal + /// The SSRC being summarized. pub ssrc: u32, + /// The first sequence number covered. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// Packets lost in the interval. pub lost_packets: u32, + /// Duplicate packets in the interval. pub dup_packets: u32, + /// Minimum observed jitter, in RTP timestamp units. pub min_jitter: u32, + /// Maximum observed jitter. pub max_jitter: u32, + /// Mean observed jitter. pub mean_jitter: u32, + /// Standard deviation of observed jitter. pub dev_jitter: u32, + /// Minimum TTL or hop limit seen. pub min_ttl_or_hl: u8, + /// Maximum TTL or hop limit seen. pub max_ttl_or_hl: u8, + /// Mean TTL or hop limit. pub mean_ttl_or_hl: u8, + /// Standard deviation of TTL or hop limit. pub dev_ttl_or_hl: u8, } @@ -63,8 +80,11 @@ impl fmt::Display for StatisticsSummaryReportBlock { #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum TTLorHopLimitType { #[default] + /// No TTL or hop-limit data is present. Missing = 0, + /// The fields hold an IPv4 TTL. IPv4 = 1, + /// The fields hold an IPv6 hop limit. IPv6 = 2, } @@ -90,6 +110,7 @@ impl fmt::Display for TTLorHopLimitType { } impl StatisticsSummaryReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { let mut type_specific = 0x00; if self.loss_reports { diff --git a/rtc-rtcp/src/extended_report/unknown.rs b/rtc-rtcp/src/extended_report/unknown.rs index d6a4de4b..768d6e06 100644 --- a/rtc-rtcp/src/extended_report/unknown.rs +++ b/rtc-rtcp/src/extended_report/unknown.rs @@ -4,6 +4,7 @@ use super::*; /// that has an unknown Report Block Type. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct UnknownReportBlock { + /// The block's bytes, left unparsed. pub bytes: Bytes, } @@ -14,6 +15,7 @@ impl fmt::Display for UnknownReportBlock { } impl UnknownReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::Unknown, diff --git a/rtc-rtcp/src/extended_report/vm.rs b/rtc-rtcp/src/extended_report/vm.rs index 0bf0d8c8..f7ba16e5 100644 --- a/rtc-rtcp/src/extended_report/vm.rs +++ b/rtc-rtcp/src/extended_report/vm.rs @@ -28,27 +28,49 @@ const VM_REPORT_BLOCK_LENGTH: u16 = 4 + 4 + 2 * 4 + 10 + 2 * 3; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct VoIPMetricsReportBlock { + /// The SSRC these metrics describe. pub ssrc: u32, + /// Fraction of packets lost, as 256ths. pub loss_rate: u8, + /// Fraction of packets discarded by the jitter buffer, as 256ths. pub discard_rate: u8, + /// Fraction of packets lost or discarded during bursts, as 256ths. pub burst_density: u8, + /// Fraction lost or discarded during gaps between bursts, as 256ths. pub gap_density: u8, + /// Mean burst length in milliseconds. pub burst_duration: u16, + /// Mean gap length in milliseconds. pub gap_duration: u16, + /// Round-trip delay in milliseconds. pub round_trip_delay: u16, + /// End-system delay in milliseconds, covering encode, buffer and decode. pub end_system_delay: u16, + /// Signal level in dBm0. pub signal_level: u8, + /// Noise level in dBm0. pub noise_level: u8, + /// Residual echo return loss, in dB. pub rerl: u8, + /// The gap threshold: consecutive received packets needed to end a burst. pub gmin: u8, + /// The R factor, a 0–100 voice-quality estimate. pub rfactor: u8, + /// An external-network R factor, for the segment beyond this one. pub ext_rfactor: u8, + /// Listening-quality mean opinion score, in tenths. pub mos_lq: u8, + /// Conversational-quality mean opinion score, in tenths. pub mos_cq: u8, + /// Receiver configuration: packet-loss concealment and jitter-buffer flags. pub rx_config: u8, + /// Reserved; sent as zero. pub reserved: u8, + /// Nominal jitter-buffer delay in milliseconds. pub jb_nominal: u16, + /// Maximum jitter-buffer delay in milliseconds. pub jb_maximum: u16, + /// The absolute maximum the jitter buffer can grow to, in milliseconds. pub jb_abs_max: u16, } @@ -59,6 +81,7 @@ impl fmt::Display for VoIPMetricsReportBlock { } impl VoIPMetricsReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::VoIPMetrics, diff --git a/rtc-rtcp/src/header.rs b/rtc-rtcp/src/header.rs index 4731f985..5b9e34b1 100644 --- a/rtc-rtcp/src/header.rs +++ b/rtc-rtcp/src/header.rs @@ -11,15 +11,24 @@ use bytes::{Buf, BufMut}; #[repr(u8)] pub enum PacketType { #[default] + /// A packet type this crate does not model. Unsupported = 0, - SenderReport = 200, // RFC 3550, 6.4.1 - ReceiverReport = 201, // RFC 3550, 6.4.2 - SourceDescription = 202, // RFC 3550, 6.5 - Goodbye = 203, // RFC 3550, 6.6 - ApplicationDefined = 204, // RFC 3550, 6.7 (unimplemented) + /// Sender Report ([RFC 3550] §6.4.1): a sender's timing and packet counts. + SenderReport = 200, // RFC 3550, 6.4.1 + /// Receiver Report ([RFC 3550] §6.4.2): reception quality from a receiver. + ReceiverReport = 201, // RFC 3550, 6.4.2 + /// Source Description ([RFC 3550] §6.5): CNAME and other source metadata. + SourceDescription = 202, // RFC 3550, 6.5 + /// BYE ([RFC 3550] §6.6): the source is leaving the session. + Goodbye = 203, // RFC 3550, 6.6 + /// APP ([RFC 3550] §6.7): application-defined data. Not modelled by this crate. + ApplicationDefined = 204, // RFC 3550, 6.7 (unimplemented) + /// Transport-layer feedback ([RFC 4585]): NACK and transport-wide CC. TransportSpecificFeedback = 205, // RFC 4585, 6051 - PayloadSpecificFeedback = 206, // RFC 4585, 6.3 - ExtendedReport = 207, // RFC 3611 + /// Payload-specific feedback ([RFC 4585] §6.3): PLI, FIR, SLI, REMB. + PayloadSpecificFeedback = 206, // RFC 4585, 6.3 + /// Extended Report ([RFC 3611]). + ExtendedReport = 207, // RFC 3611 } /// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here @@ -73,17 +82,28 @@ impl From for PacketType { } } +/// The RTP/RTCP version this crate speaks. pub const RTP_VERSION: u8 = 2; +/// Bit offset of the version field in the first header octet. pub const VERSION_SHIFT: u8 = 6; +/// Bit mask of the version field once shifted. pub const VERSION_MASK: u8 = 0x3; +/// Bit offset of the padding flag. pub const PADDING_SHIFT: u8 = 5; +/// Bit mask of the padding flag once shifted. pub const PADDING_MASK: u8 = 0x1; +/// Bit offset of the report/source count field. pub const COUNT_SHIFT: u8 = 0; +/// Bit mask of the report/source count field. pub const COUNT_MASK: u8 = 0x1f; +/// Length of the RTCP header in bytes. pub const HEADER_LENGTH: usize = 4; +/// The largest report count the 5-bit field can hold. pub const COUNT_MAX: usize = (1 << 5) - 1; +/// Length of an SSRC in bytes. pub const SSRC_LENGTH: usize = 4; +/// The longest SDES item value, bounded by its one-byte length field. pub const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; // https://datatracker.ietf.org/doc/html/rfc5104#section-4.3.1 @@ -94,6 +114,7 @@ pub const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; // // The length of the FIR feedback message MUST be set to // 2+2*N, where N is the number of FCI entries. +/// The smallest valid FIR packet, in bytes. pub const FIR_MIN_OCTET_COUNT: usize = 20; /// A Header is the common header shared by all RTCP packets diff --git a/rtc-rtcp/src/lib.rs b/rtc-rtcp/src/lib.rs index b05ea3cb..6d991fb5 100644 --- a/rtc-rtcp/src/lib.rs +++ b/rtc-rtcp/src/lib.rs @@ -1,4 +1,5 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] //! Package rtcp implements encoding and decoding of RTCP packets according to RFCs 3550 and 5506. @@ -41,17 +42,32 @@ //! // ... //!``` +/// Compound RTCP packets — the several reports that share one datagram. pub mod compound_packet; +/// Extended reports (XR, [RFC 3611]): loss/discard run lengths, receipt times and per-block +/// statistics. +/// +/// [RFC 3611]: https://datatracker.ietf.org/doc/html/rfc3611 pub mod extended_report; +/// The BYE packet, by which a source announces it is leaving. pub mod goodbye; +/// The four-byte header common to every RTCP packet. pub mod header; +/// The [`Packet`] trait every RTCP packet type implements. pub mod packet; +/// Payload-specific feedback (PT 206): PLI, FIR, SLI and REMB. pub mod payload_feedbacks; +/// An unparsed packet, used for types this crate does not model. pub mod raw_packet; +/// The Receiver Report (RR), which reports reception quality back to a sender. pub mod receiver_report; +/// The reception report block carried inside SR and RR packets. pub mod reception_report; +/// The Sender Report (SR), which carries a sender's timing and packet counts. pub mod sender_report; +/// The SDES packet, which carries CNAME and other source metadata. pub mod source_description; +/// Transport-specific feedback (PT 205): NACK and transport-wide congestion control. pub mod transport_feedbacks; mod util; diff --git a/rtc-rtcp/src/packet.rs b/rtc-rtcp/src/packet.rs index 7a650398..95ba43d8 100644 --- a/rtc-rtcp/src/packet.rs +++ b/rtc-rtcp/src/packet.rs @@ -20,11 +20,20 @@ use std::fmt; /// Packet represents an RTCP packet, a protocol used for out-of-band statistics and /// control information for an RTP session pub trait Packet: Send + Sync + Marshal + Unmarshal + fmt::Display + fmt::Debug { + /// This packet's RTCP header. fn header(&self) -> Header; + /// The SSRCs this packet is about. + /// + /// Used to route feedback: a report's destination SSRCs identify the streams it concerns, + /// which is how an SFU decides where to forward it. fn destination_ssrc(&self) -> Vec; + /// The encoded size in bytes, header and padding included. fn raw_size(&self) -> usize; + /// Downcasting hook, so a caller holding `Box` can recover the concrete type. fn as_any(&self) -> &dyn Any; + /// Compares against another packet, since `PartialEq` is not object safe. fn equal(&self, other: &dyn Packet) -> bool; + /// Clones this packet behind a trait object, since `Clone` is not object safe. fn cloned(&self) -> Box; } diff --git a/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs b/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs index 547180d4..1213aebc 100644 --- a/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs @@ -14,7 +14,9 @@ use std::fmt; /// A FIREntry is a (ssrc, seqno) pair, as carried by FullIntraRequest. #[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct FirEntry { + /// The SSRC being asked for an intra frame. pub ssrc: u32, + /// A counter incremented per request, so a sender can ignore retransmitted duplicates. pub sequence_number: u8, } @@ -23,8 +25,11 @@ pub struct FirEntry { /// recovery, which should use PictureLossIndication (PLI) instead. #[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct FullIntraRequest { + /// The SSRC of the requesting receiver. pub sender_ssrc: u32, + /// The media source being addressed. pub media_ssrc: u32, + /// One entry per SSRC an intra frame is requested from. pub fir: Vec, } diff --git a/rtc-rtcp/src/payload_feedbacks/mod.rs b/rtc-rtcp/src/payload_feedbacks/mod.rs index e7d02f17..7e29db55 100644 --- a/rtc-rtcp/src/payload_feedbacks/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/mod.rs @@ -1,4 +1,8 @@ +/// FIR: asks a sender for a full intra frame, used when a receiver joins or resyncs. pub mod full_intra_request; +/// PLI: tells a sender the receiver lost picture and needs a keyframe. pub mod picture_loss_indication; +/// REMB: the receiver's estimate of the bitrate the path can carry. pub mod receiver_estimated_maximum_bitrate; +/// SLI: reports individual lost slices, for finer-grained repair than PLI. pub mod slice_loss_indication; diff --git a/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs b/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs index a82bcb90..c0ae0fef 100644 --- a/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs @@ -34,6 +34,7 @@ pub struct SliceLossIndication { /// SSRC of the media source pub media_ssrc: u32, + /// The lost slices being reported. pub sli_entries: Vec, } diff --git a/rtc-rtcp/src/source_description/mod.rs b/rtc-rtcp/src/source_description/mod.rs index e9d163c3..ec41279b 100644 --- a/rtc-rtcp/src/source_description/mod.rs +++ b/rtc-rtcp/src/source_description/mod.rs @@ -25,15 +25,27 @@ const SDES_TEXT_OFFSET: usize = 2; #[repr(u8)] pub enum SdesType { #[default] + /// End of the SDES item list ([RFC 3550] §6.5). SdesEnd = 0, // end of SDES list RFC 3550, 6.5 - SdesCname = 1, // canonical name RFC 3550, 6.5.1 - SdesName = 2, // user name RFC 3550, 6.5.2 - SdesEmail = 3, // user's electronic mail address RFC 3550, 6.5.3 - SdesPhone = 4, // user's phone number RFC 3550, 6.5.4 + /// CNAME: the canonical end-point identifier, which ties an SSRC to a participant. + /// + /// The one item WebRTC always sends — it is how a receiver associates streams that belong + /// together. + SdesCname = 1, // canonical name RFC 3550, 6.5.1 + /// NAME: the participant's display name. + SdesName = 2, // user name RFC 3550, 6.5.2 + /// EMAIL: the participant's email address. + SdesEmail = 3, // user's electronic mail address RFC 3550, 6.5.3 + /// PHONE: the participant's phone number. + SdesPhone = 4, // user's phone number RFC 3550, 6.5.4 + /// LOC: the participant's geographic location. SdesLocation = 5, // geographic user location RFC 3550, 6.5.5 - SdesTool = 6, // name of application or tool RFC 3550, 6.5.6 - SdesNote = 7, // notice about the source RFC 3550, 6.5.7 - SdesPrivate = 8, // private extensions RFC 3550, 6.5.8 (not implemented) + /// TOOL: the name and version of the sending application. + SdesTool = 6, // name of application or tool RFC 3550, 6.5.6 + /// NOTE: a transient note about the source, such as "on hold". + SdesNote = 7, // notice about the source RFC 3550, 6.5.7 + /// PRIV: a private extension. + SdesPrivate = 8, // private extensions RFC 3550, 6.5.8 (not implemented) } impl fmt::Display for SdesType { @@ -74,6 +86,7 @@ impl From for SdesType { pub struct SourceDescriptionChunk { /// The source (ssrc) or contributing source (csrc) identifier this packet describes pub source: u32, + /// The items describing this source. pub items: Vec, } @@ -272,6 +285,7 @@ impl Unmarshal for SourceDescriptionItem { /// A SourceDescription (SDES) packet describes the sources in an RTP stream. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct SourceDescription { + /// One chunk per source described by this packet. pub chunks: Vec, } diff --git a/rtc-rtcp/src/transport_feedbacks/mod.rs b/rtc-rtcp/src/transport_feedbacks/mod.rs index f59db607..6176fe39 100644 --- a/rtc-rtcp/src/transport_feedbacks/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/mod.rs @@ -1,3 +1,6 @@ +/// RRR: asks a sender to resynchronize as quickly as it can. pub mod rapid_resynchronization_request; +/// Transport-wide congestion control feedback: per-packet arrival status and deltas. pub mod transport_layer_cc; +/// Generic NACK, which lists sequence numbers the receiver did not get. pub mod transport_layer_nack; diff --git a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs index a352a290..9e460e55 100644 --- a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs @@ -49,7 +49,9 @@ use std::fmt; #[repr(u16)] pub enum StatusChunkTypeTcc { #[default] + /// A run-length chunk: one status repeated for a stated number of packets. RunLengthChunk = 0, + /// A status-vector chunk: explicit per-packet status for a small group. StatusVectorChunk = 1, } @@ -76,6 +78,7 @@ pub enum SymbolSizeTypeTcc { /// #[default] OneBit = 0, + /// Two-bit symbols, which can also express "received, large delta". TwoBit = 1, } @@ -112,7 +115,9 @@ impl From for SymbolTypeTcc { /// RunLengthChunk and StatusVectorChunk #[derive(Debug, Clone, PartialEq, Eq)] pub enum PacketStatusChunk { + /// A run-length encoded chunk. RunLengthChunk(RunLengthChunk), + /// A status-vector chunk. StatusVectorChunk(StatusVectorChunk), } @@ -313,6 +318,7 @@ impl Unmarshal for StatusVectorChunk { /// #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct RecvDelta { + /// Which symbol size this chunk uses. pub type_tcc_packet: SymbolTypeTcc, /// us pub delta: i64, diff --git a/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs b/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs index 2639334a..1dfdcf9b 100644 --- a/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs @@ -25,6 +25,7 @@ pub struct NackPair { pub lost_packets: PacketBitmap, } +/// Iterates the individual sequence numbers a [`NackPair`] encodes. pub struct NackIterator { packet_id: u16, bitfield: PacketBitmap, @@ -58,6 +59,7 @@ impl Iterator for NackIterator { } impl NackPair { + /// A NACK pair naming a single lost sequence number, with no additional bitmask bits set. pub fn new(seq: u16) -> Self { Self { packet_id: seq, @@ -70,6 +72,9 @@ impl NackPair { self.into_iter().collect() } + /// Calls `f` with every sequence number this pair reports lost. + /// + /// Stops early if `f` returns `false`. pub fn range(&self, f: F) where F: Fn(u16) -> bool, @@ -119,6 +124,7 @@ pub struct TransportLayerNack { /// SSRC of the media source pub media_ssrc: u32, + /// The lost-packet ranges being reported. pub nacks: Vec, } @@ -252,6 +258,9 @@ impl Unmarshal for TransportLayerNack { } } +/// Packs a list of lost sequence numbers into the smallest set of [`NackPair`]s. +/// +/// Each pair covers a base sequence number plus the next 16, so nearby losses share one pair. pub fn nack_pairs_from_sequence_numbers(seq_nos: &[u16]) -> Vec { if seq_nos.is_empty() { return vec![]; diff --git a/rtc-rtp/src/codec/av1/depacketizer.rs b/rtc-rtp/src/codec/av1/depacketizer.rs index b226e1a1..dc77909a 100644 --- a/rtc-rtp/src/codec/av1/depacketizer.rs +++ b/rtc-rtp/src/codec/av1/depacketizer.rs @@ -34,6 +34,7 @@ pub struct Av1Depacketizer { } impl Av1Depacketizer { + /// An AV1 depacketizer with no buffered fragments. pub fn new() -> Self { Self::default() } diff --git a/rtc-rtp/src/codec/av1/mod.rs b/rtc-rtp/src/codec/av1/mod.rs index 688e23f9..911cddb7 100644 --- a/rtc-rtp/src/codec/av1/mod.rs +++ b/rtc-rtp/src/codec/av1/mod.rs @@ -18,6 +18,7 @@ mod packetizer; pub use depacketizer::Av1Depacketizer; #[derive(Default, Clone, Debug)] +/// Packetizes AV1 temporal units into RTP payloads. pub struct Av1Payloader {} impl Payloader for Av1Payloader { diff --git a/rtc-rtp/src/codec/g7xx/mod.rs b/rtc-rtp/src/codec/g7xx/mod.rs index 5af73ac0..a695e673 100644 --- a/rtc-rtp/src/codec/g7xx/mod.rs +++ b/rtc-rtp/src/codec/g7xx/mod.rs @@ -12,6 +12,7 @@ pub type G711Payloader = G7xxPayloader; pub type G722Payloader = G7xxPayloader; #[derive(Default, Debug, Copy, Clone)] +/// Packetizes G.711/G.722 audio, which needs no fragmentation — one payload per frame. pub struct G7xxPayloader; impl Payloader for G7xxPayloader { diff --git a/rtc-rtp/src/codec/h264/mod.rs b/rtc-rtp/src/codec/h264/mod.rs index a18249ac..04b50944 100644 --- a/rtc-rtp/src/codec/h264/mod.rs +++ b/rtc-rtp/src/codec/h264/mod.rs @@ -13,25 +13,41 @@ pub struct H264Payloader { pps_nalu: Option, } +/// NAL type 24: STAP-A, which aggregates several small NAL units into one payload. pub const STAPA_NALU_TYPE: u8 = 24; +/// NAL type 28: FU-A, which fragments one NAL unit across several payloads. pub const FUA_NALU_TYPE: u8 = 28; +/// NAL type 29: FU-B, FU-A with a decoding-order number. Not used by WebRTC. pub const FUB_NALU_TYPE: u8 = 29; +/// NAL type 7: sequence parameter set. pub const SPS_NALU_TYPE: u8 = 7; +/// NAL type 8: picture parameter set. pub const PPS_NALU_TYPE: u8 = 8; +/// NAL type 9: access unit delimiter. pub const AUD_NALU_TYPE: u8 = 9; +/// NAL type 12: filler data, which is dropped rather than sent. pub const FILLER_NALU_TYPE: u8 = 12; +/// Bytes of FU-A header prefixed to each fragment. pub const FUA_HEADER_SIZE: usize = 2; +/// Bytes of STAP-A header prefixed to an aggregate. pub const STAPA_HEADER_SIZE: usize = 1; +/// Bytes of length prefix before each NAL unit inside a STAP-A. pub const STAPA_NALU_LENGTH_SIZE: usize = 2; +/// Mask selecting the NAL type from a NAL header byte. pub const NALU_TYPE_BITMASK: u8 = 0x1F; +/// Mask selecting `nal_ref_idc` from a NAL header byte. pub const NALU_REF_IDC_BITMASK: u8 = 0x60; +/// FU header start bit: this fragment begins a NAL unit. pub const FU_START_BITMASK: u8 = 0x80; +/// FU header end bit: this fragment completes a NAL unit. pub const FU_END_BITMASK: u8 = 0x40; +/// The STAP-A header byte this payloader emits. pub const OUTPUT_STAP_AHEADER: u8 = 0x78; +/// The Annex B start code (`00 00 00 01`) that delimits NAL units in a byte stream. pub static ANNEXB_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x00, 0x01]); impl H264Payloader { @@ -199,6 +215,7 @@ impl Payloader for H264Payloader { /// H264Packet represents the H264 header that is stored in the payload of an RTP Packet #[derive(PartialEq, Eq, Debug, Default, Clone)] pub struct H264Packet { + /// Whether to emit AVCC length-prefixed output instead of Annex B start codes. pub is_avc: bool, fua_buffer: Option, } diff --git a/rtc-rtp/src/codec/h265/mod.rs b/rtc-rtp/src/codec/h265/mod.rs index 1fd4317a..5eb08ba5 100644 --- a/rtc-rtp/src/codec/h265/mod.rs +++ b/rtc-rtp/src/codec/h265/mod.rs @@ -7,36 +7,68 @@ use shared::error::{Error, Result}; #[cfg(test)] mod h265_test; +/// The three-byte Annex B start code (`00 00 01`). pub static ANNEXB_3_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x01]); +/// Payload header for a single NAL unit packet. pub static SING_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x1C, 0x01]); +/// Payload header for an aggregation packet. pub static AGGR_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x60, 0x01]); +/// Payload header for a fragmentation unit. pub static FRAG_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x62, 0x01]); +/// FU header for the first fragment of an IDR frame. pub static FU_HDR_IDR_S: u8 = 0x93; +/// FU header for a middle fragment of an IDR frame. pub static FU_HDR_IDR_M: u8 = 0x13; +/// FU header for the last fragment of an IDR frame. pub static FU_HDR_IDR_E: u8 = 0x53; +/// FU header for the first fragment of a P frame. pub static FU_HDR_P_S: u8 = 0x81; +/// FU header for a middle fragment of a P frame. pub static FU_HDR_P_M: u8 = 0x01; +/// FU header for the last fragment of a P frame. pub static FU_HDR_P_E: u8 = 0x41; +/// FU header for the first fragment of a B frame. pub static FU_HDR_B_S: u8 = 0x80; +/// FU header for a middle fragment of a B frame. pub static FU_HDR_B_M: u8 = 0x00; +/// FU header for the last fragment of a B frame. pub static FU_HDR_B_E: u8 = 0x40; +/// The payload MTU this payloader targets, chosen to survive typical paths without IP +/// fragmentation. pub const RTP_OUTBOUND_MTU: usize = 1200; +/// Bytes of FU header following the payload header in a fragmentation unit. pub const H265FRAGMENTATION_UNIT_HEADER_SIZE: usize = 1; +/// Bytes in an H.265 NAL header — two, unlike H.264's one. pub const NAL_HEADER_SIZE: usize = 2; #[derive(PartialEq, Hash, Debug, Copy, Clone)] +/// The H.265 NAL unit types this payloader distinguishes. pub enum UnitType { + /// Video parameter set. VPS = 32, + /// Sequence parameter set. SPS = 33, + /// Picture parameter set. PPS = 34, + /// Clean random access picture — a keyframe that allows mid-stream tune-in. CRA = 21, + /// Supplemental enhancement information. SEI = 39, + /// Instantaneous decoder refresh picture — a keyframe. IDR = 19, + /// A predicted (P) frame. PFR = 1, + /// A bidirectionally predicted (B) frame. BFR = 0, + /// A unit type this payloader skips. IGNORE = -1, } impl UnitType { + /// Maps a raw NAL type id to a [`UnitType`]. + /// + /// # Errors + /// + /// Fails if the id is not one this payloader handles. pub fn for_id(id: u8) -> Result { if id > 64 { Err(Error::ErrUnhandledNaluType) @@ -58,6 +90,7 @@ impl UnitType { } #[derive(Default, Debug, Clone)] +/// Packetizes H.265/HEVC NAL units into RTP payloads, fragmenting when needed. pub struct HevcPayloader; impl HevcPayloader { @@ -104,6 +137,9 @@ impl HevcPayloader { header } + /// Locates the NAL unit boundaries in an Annex B buffer. + /// + /// Returns the offset of each start code and the length of the code that was matched. pub fn parse(nalu: &Bytes) -> (Vec, usize) { let finder = memchr::memmem::Finder::new(&ANNEXB_NALUSTART_CODE); let nals = finder.find_iter(nalu).collect::>(); @@ -280,6 +316,7 @@ const H265NALU_PACI_PACKET_TYPE: u8 = 50; pub struct H265NALUHeader(pub u16); impl H265NALUHeader { + /// Parses an H.265 NAL header from its two bytes. pub fn new(high_byte: u8, low_byte: u8) -> Self { H265NALUHeader(((high_byte as u16) << 8) | low_byte as u16) } @@ -975,9 +1012,13 @@ impl H265TSCI { /// #[derive(Debug, Clone, PartialEq, Eq)] pub enum H265Payload { + /// One NAL unit carried whole in a single packet. H265SingleNALUnitPacket(H265SingleNALUnitPacket), + /// One fragment of a NAL unit too large for the MTU. H265FragmentationUnitPacket(H265FragmentationUnitPacket), + /// Several small NAL units aggregated into one packet. H265AggregationPacket(H265AggregationPacket), + /// A PACI packet, which carries payload content information ahead of the NAL unit. H265PACIPacket(H265PACIPacket), } diff --git a/rtc-rtp/src/codec/mod.rs b/rtc-rtp/src/codec/mod.rs index 0296e20c..8488a070 100644 --- a/rtc-rtp/src/codec/mod.rs +++ b/rtc-rtp/src/codec/mod.rs @@ -1,7 +1,14 @@ +/// AV1 payload format ([RFC 9628]). pub mod av1; +/// G.711 and G.722 payload formats, which need no fragmentation. pub mod g7xx; +/// H.264 payload format ([RFC 6184]): STAP-A aggregation and FU-A fragmentation. pub mod h264; +/// H.265/HEVC payload format ([RFC 7798]). pub mod h265; +/// Opus payload format ([RFC 7587]), one packet per frame. pub mod opus; +/// VP8 payload format ([RFC 7741]). pub mod vp8; +/// VP9 payload format (draft-ietf-payload-vp9). pub mod vp9; diff --git a/rtc-rtp/src/codec/opus/mod.rs b/rtc-rtp/src/codec/opus/mod.rs index 7d7768ee..d12e78a0 100644 --- a/rtc-rtp/src/codec/opus/mod.rs +++ b/rtc-rtp/src/codec/opus/mod.rs @@ -7,6 +7,7 @@ use shared::error::{Error, Result}; use bytes::Bytes; #[derive(Default, Debug, Copy, Clone)] +/// Packetizes Opus audio: one RTP payload per Opus frame, never fragmented. pub struct OpusPayloader; impl Payloader for OpusPayloader { diff --git a/rtc-rtp/src/codec/vp8/mod.rs b/rtc-rtp/src/codec/vp8/mod.rs index 8a59604e..1cfc2b4b 100644 --- a/rtc-rtp/src/codec/vp8/mod.rs +++ b/rtc-rtp/src/codec/vp8/mod.rs @@ -6,11 +6,15 @@ use shared::error::{Error, Result}; use bytes::{Buf, BufMut, Bytes, BytesMut}; +/// Bytes of mandatory VP8 payload descriptor prefixed to each payload. pub const VP8_HEADER_SIZE: usize = 1; /// Vp8Payloader payloads VP8 packets #[derive(Default, Debug, Copy, Clone)] pub struct Vp8Payloader { + /// Whether to include the optional picture id in the payload descriptor. + /// + /// Lets a receiver detect loss within a frame, at one or two bytes per packet. pub enable_picture_id: bool, picture_id: u16, } diff --git a/rtc-rtp/src/codec/vp9/mod.rs b/rtc-rtp/src/codec/vp9/mod.rs index 0278c0d7..1cac160c 100644 --- a/rtc-rtp/src/codec/vp9/mod.rs +++ b/rtc-rtp/src/codec/vp9/mod.rs @@ -22,6 +22,7 @@ pub struct Vp9Payloader { picture_id: u16, initialized: bool, + /// Supplies the starting picture id; randomized when absent. pub initial_picture_id_fn: Option, } @@ -188,7 +189,9 @@ pub struct Vp9Packet { pub g: bool, /// N_G indicates the number of pictures in a Picture Group (PG) pub ng: u8, + /// Frame width for each spatial layer. pub width: Vec, + /// Frame height for each spatial layer. pub height: Vec, /// Temporal layer ID of pictures in a Picture Group pub pgtid: Vec, diff --git a/rtc-rtp/src/extension/abs_send_time_extension/mod.rs b/rtc-rtp/src/extension/abs_send_time_extension/mod.rs index 543af52a..dad9c86a 100644 --- a/rtc-rtp/src/extension/abs_send_time_extension/mod.rs +++ b/rtc-rtp/src/extension/abs_send_time_extension/mod.rs @@ -8,12 +8,14 @@ use shared::{ use bytes::{Buf, BufMut}; +/// The extension's encoded size: 3 bytes of 6.18 fixed-point seconds. pub const ABS_SEND_TIME_EXTENSION_SIZE: usize = 3; /// AbsSendTimeExtension is a extension payload format in /// #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub struct AbsSendTimeExtension { + /// The send time in 6.18 fixed-point format — 6 bits of seconds, 18 of fraction. pub timestamp: u64, } diff --git a/rtc-rtp/src/extension/audio_level_extension/mod.rs b/rtc-rtp/src/extension/audio_level_extension/mod.rs index 7ce0f957..142cc7d4 100644 --- a/rtc-rtp/src/extension/audio_level_extension/mod.rs +++ b/rtc-rtp/src/extension/audio_level_extension/mod.rs @@ -10,6 +10,7 @@ use shared::{ use bytes::{Buf, BufMut}; // AUDIO_LEVEL_EXTENSION_SIZE One byte header size +/// The extension's encoded size in bytes. pub const AUDIO_LEVEL_EXTENSION_SIZE: usize = 1; /// AudioLevelExtension is a extension payload format described in @@ -39,7 +40,9 @@ pub const AUDIO_LEVEL_EXTENSION_SIZE: usize = 1; /// [RFC 6464]: https://tools.ietf.org/html/rfc6464 #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct AudioLevelExtension { + /// Loudness in −dBov, from 0 (loudest) to 127 (silence). pub level: u8, + /// Whether the sender detected voice activity in this packet. pub voice: bool, } diff --git a/rtc-rtp/src/extension/mod.rs b/rtc-rtp/src/extension/mod.rs index 276dc0cf..733b54f8 100644 --- a/rtc-rtp/src/extension/mod.rs +++ b/rtc-rtp/src/extension/mod.rs @@ -6,28 +6,41 @@ use shared::{ marshal::{Marshal, MarshalSize}, }; +/// Absolute send time, for one-way-delay based bandwidth estimation. pub mod abs_send_time_extension; +/// Per-packet audio loudness and voice activity ([RFC 6464]). pub mod audio_level_extension; +/// A requested playout-delay range, for latency/smoothness trade-offs. pub mod playout_delay_extension; +/// The transport-wide sequence number that TWCC feedback refers to. pub mod transport_cc_extension; +/// Camera direction and rotation (CVO), so a receiver can display video upright. pub mod video_orientation_extension; /// A generic RTP header extension. pub enum HeaderExtension { + /// The absolute-send-time extension. AbsSendTime(abs_send_time_extension::AbsSendTimeExtension), + /// The audio-level extension. AudioLevel(audio_level_extension::AudioLevelExtension), + /// The playout-delay extension. PlayoutDelay(playout_delay_extension::PlayoutDelayExtension), + /// The transport-wide CC extension. TransportCc(transport_cc_extension::TransportCcExtension), + /// The video-orientation extension. VideoOrientation(video_orientation_extension::VideoOrientationExtension), /// A custom extension Custom { + /// The extension's canonical URI, which is what SDP negotiates ids against. uri: Cow<'static, str>, + /// The extension value, erased so extensions of different types can be held together. extension: Box, }, } impl HeaderExtension { + /// The extension's URI. pub fn uri(&self) -> Cow<'static, str> { use HeaderExtension::*; @@ -43,6 +56,7 @@ impl HeaderExtension { } } + /// Whether both refer to the same extension, comparing URIs rather than values. pub fn is_same(&self, other: &Self) -> bool { use HeaderExtension::*; match (self, other) { diff --git a/rtc-rtp/src/extension/playout_delay_extension/mod.rs b/rtc-rtp/src/extension/playout_delay_extension/mod.rs index 8562b86e..d2097d38 100644 --- a/rtc-rtp/src/extension/playout_delay_extension/mod.rs +++ b/rtc-rtp/src/extension/playout_delay_extension/mod.rs @@ -5,7 +5,9 @@ use bytes::BufMut; use shared::error::{Error, Result}; use shared::marshal::{Marshal, MarshalSize, Unmarshal}; +/// The extension's encoded size: two 12-bit values packed into 3 bytes. pub const PLAYOUT_DELAY_EXTENSION_SIZE: usize = 3; +/// The largest representable delay, in 10 ms units — about 40 seconds. pub const PLAYOUT_DELAY_MAX_VALUE: u16 = (1 << 12) - 1; /// PlayoutDelayExtension is an extension payload format described in @@ -17,7 +19,9 @@ pub const PLAYOUT_DELAY_MAX_VALUE: u16 = (1 << 12) - 1; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub struct PlayoutDelayExtension { + /// The minimum playout delay in 10 ms units. pub min_delay: u16, + /// The maximum playout delay in 10 ms units. pub max_delay: u16, } @@ -72,6 +76,8 @@ impl Marshal for PlayoutDelayExtension { } impl PlayoutDelayExtension { + /// A playout-delay extension requesting a delay between `min_delay` and `max_delay`, both in + /// 10 ms units. pub fn new(min_delay: u16, max_delay: u16) -> Self { PlayoutDelayExtension { min_delay, diff --git a/rtc-rtp/src/extension/transport_cc_extension/mod.rs b/rtc-rtp/src/extension/transport_cc_extension/mod.rs index 581e18c0..00cb7031 100644 --- a/rtc-rtp/src/extension/transport_cc_extension/mod.rs +++ b/rtc-rtp/src/extension/transport_cc_extension/mod.rs @@ -10,6 +10,7 @@ use shared::{ use bytes::{Buf, BufMut}; // transport-wide sequence +/// The extension's encoded size in bytes. pub const TRANSPORT_CC_EXTENSION_SIZE: usize = 2; /// TransportCCExtension is a extension payload format in @@ -23,6 +24,9 @@ pub const TRANSPORT_CC_EXTENSION_SIZE: usize = 2; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct TransportCcExtension { + /// A sequence number counting every packet sent on the transport, across all streams. + /// + /// TWCC feedback reports arrival times against these, which is why it spans SSRCs. pub transport_sequence: u16, } diff --git a/rtc-rtp/src/extension/video_orientation_extension/mod.rs b/rtc-rtp/src/extension/video_orientation_extension/mod.rs index e8868ba8..c1965e0e 100644 --- a/rtc-rtp/src/extension/video_orientation_extension/mod.rs +++ b/rtc-rtp/src/extension/video_orientation_extension/mod.rs @@ -11,6 +11,7 @@ use shared::{ }; // One byte header size +/// The extension's encoded size in bytes. pub const VIDEO_ORIENTATION_EXTENSION_SIZE: usize = 1; /// Coordination of Video Orientation in RTP streams. @@ -38,24 +39,35 @@ pub const VIDEO_ORIENTATION_EXTENSION_SIZE: usize = 1; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct VideoOrientationExtension { + /// Which camera produced the frame. pub direction: CameraDirection, + /// Whether the image is horizontally mirrored, as front cameras usually are. pub flip: bool, + /// How far the receiver must rotate the image to display it upright. pub rotation: VideoRotation, } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +/// Which camera a frame came from. pub enum CameraDirection { #[default] + /// The front-facing (user-facing) camera. Front = 0, + /// The rear-facing camera. Back = 1, } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +/// Clockwise rotation to apply when displaying the frame. pub enum VideoRotation { #[default] + /// No rotation. Degree0 = 0, + /// Rotate 90° clockwise. Degree90 = 1, + /// Rotate 180°. Degree180 = 2, + /// Rotate 270° clockwise. Degree270 = 3, } diff --git a/rtc-rtp/src/header.rs b/rtc-rtp/src/header.rs index 936fcd11..bca25b26 100644 --- a/rtc-rtp/src/header.rs +++ b/rtc-rtp/src/header.rs @@ -5,32 +5,58 @@ use shared::{ use bytes::{Buf, BufMut, Bytes}; +/// The length of the extension-profile and length fields that precede header extensions. pub const HEADER_LENGTH: usize = 4; +/// Bit offset of the version field in the first header octet. pub const VERSION_SHIFT: u8 = 6; +/// Bit mask of the version field once shifted. pub const VERSION_MASK: u8 = 0x3; +/// Bit offset of the padding flag. pub const PADDING_SHIFT: u8 = 5; +/// Bit mask of the padding flag once shifted. pub const PADDING_MASK: u8 = 0x1; +/// Bit offset of the extension flag. pub const EXTENSION_SHIFT: u8 = 4; +/// Bit mask of the extension flag once shifted. pub const EXTENSION_MASK: u8 = 0x1; +/// Extension profile `0xBEDE`, which selects one-byte header extension ids ([RFC 8285]). pub const EXTENSION_PROFILE_ONE_BYTE: u16 = 0xBEDE; +/// Extension profile `0x1000`, which selects two-byte header extension ids, allowing ids +/// above 14. pub const EXTENSION_PROFILE_TWO_BYTE: u16 = 0x1000; +/// Extension id 15, reserved by the RFC and never assigned. pub const EXTENSION_ID_RESERVED: u8 = 0xF; +/// Bit mask of the CSRC count field. pub const CC_MASK: u8 = 0xF; +/// Bit offset of the marker bit. pub const MARKER_SHIFT: u8 = 7; +/// Bit mask of the marker bit once shifted. pub const MARKER_MASK: u8 = 0x1; +/// Bit mask of the payload-type field. pub const PT_MASK: u8 = 0x7F; +/// Byte offset of the sequence number within the header. pub const SEQ_NUM_OFFSET: usize = 2; +/// Length of the sequence number in bytes. pub const SEQ_NUM_LENGTH: usize = 2; +/// Byte offset of the timestamp within the header. pub const TIMESTAMP_OFFSET: usize = 4; +/// Length of the timestamp in bytes. pub const TIMESTAMP_LENGTH: usize = 4; +/// Byte offset of the SSRC within the header. pub const SSRC_OFFSET: usize = 8; +/// Length of the SSRC in bytes. pub const SSRC_LENGTH: usize = 4; +/// Byte offset of the first CSRC within the header. pub const CSRC_OFFSET: usize = 12; +/// Length of each CSRC in bytes. pub const CSRC_LENGTH: usize = 4; #[derive(Debug, Eq, PartialEq, Default, Clone)] +/// One RTP header extension: an id and its payload bytes. pub struct Extension { + /// The extension id, as negotiated by `a=extmap`. pub id: u8, + /// The extension's value. pub payload: Bytes, } @@ -38,17 +64,30 @@ pub struct Extension { /// NOTE: PayloadOffset is populated by Marshal/Unmarshal and should not be modified #[derive(Debug, Eq, PartialEq, Default, Clone)] pub struct Header { + /// The RTP version, always 2. pub version: u8, + /// Whether the payload is followed by padding octets, the last giving the padding length. pub padding: bool, + /// Whether a header-extension block follows the fixed header. pub extension: bool, + /// The marker bit: the last packet of a video frame, or the start of a talk spurt for audio. pub marker: bool, + /// The payload type, which identifies the codec as negotiated in SDP. pub payload_type: u8, + /// Increments by one per packet sent; used to detect loss and restore order. pub sequence_number: u16, + /// The sampling instant of the first octet, in the codec's clock rate. pub timestamp: u32, + /// The synchronization source — the identifier of the stream this packet belongs to. pub ssrc: u32, + /// The contributing sources, listed when a mixer combined several streams. pub csrc: Vec, + /// Which header-extension form is in use: [`EXTENSION_PROFILE_ONE_BYTE`] or + /// [`EXTENSION_PROFILE_TWO_BYTE`]. pub extension_profile: u16, + /// The header extensions present on this packet. pub extensions: Vec, + /// Padding bytes appended to the extension block so it ends on a 32-bit boundary. pub extensions_padding: usize, } @@ -357,6 +396,7 @@ impl Marshal for Header { } impl Header { + /// The total encoded length of the extension payloads, padding excluded. pub fn get_extension_payload_len(&self) -> usize { let payload_len: usize = self .extensions diff --git a/rtc-rtp/src/lib.rs b/rtc-rtp/src/lib.rs index ba7214db..44748016 100644 --- a/rtc-rtp/src/lib.rs +++ b/rtc-rtp/src/lib.rs @@ -1,11 +1,46 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! RTP packets, header extensions and packetization. +//! +//! The Real-time Transport Protocol ([RFC 3550]) wire format, plus the pieces WebRTC needs +//! around it: one-byte and two-byte header extensions ([RFC 8285]) and per-codec +//! packetizers that turn encoded frames into RTP payloads. +//! +//! # Structure +//! +//! * [`Packet`] / [`Header`] — the packet and its header: `unmarshal` one off the wire, +//! `marshal` one back, get and set header extensions by id. +//! * [`packetizer`] — [`Packetizer`](packetizer::Packetizer), which fragments a frame into +//! MTU-sized payloads, and the per-codec [`Payloader`](packetizer::Payloader) +//! implementations in [`codec`] (VP8, VP9, H.264, H.265, AV1, Opus, G.711). +//! * [`sequence`] — [`Sequencer`](sequence::Sequencer), for sequence numbers that start at a +//! random offset as the RFC requires. +//! * [`extension`] — the typed header extensions: audio level ([RFC 6464]), video +//! orientation, transport-wide CC, and the SDES stream ids used for simulcast. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::rtp`, and an application +//! usually meets these types when reading or writing media on a track. +//! +//! [RFC 3550]: https://datatracker.ietf.org/doc/html/rfc3550 +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 +//! [RFC 6464]: https://datatracker.ietf.org/doc/html/rfc6464 + +/// Per-codec payloaders and depacketizers (VP8, VP9, AV1, H.264, H.265, Opus, G.711). pub mod codec; +/// The typed RTP header extensions ([RFC 8285]). +/// +/// [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 pub mod extension; +/// The RTP header, its extensions, and the bit masks that encode it. pub mod header; +/// A whole RTP packet: header plus payload. pub mod packet; +/// Turning encoded frames into RTP packets, and back again. pub mod packetizer; +/// Sequence-number generation, starting from a random offset as the RFC requires. pub mod sequence; pub use header::Header; diff --git a/rtc-rtp/src/packet/mod.rs b/rtc-rtp/src/packet/mod.rs index 5913e732..14f32aa4 100644 --- a/rtc-rtp/src/packet/mod.rs +++ b/rtc-rtp/src/packet/mod.rs @@ -14,7 +14,9 @@ use std::fmt; /// NOTE: Raw is populated by Marshal/Unmarshal and should not be modified #[derive(Debug, Eq, PartialEq, Default, Clone)] pub struct Packet { + /// The packet header. pub header: Header, + /// The media payload, in whatever format the payload type implies. pub payload: Bytes, } diff --git a/rtc-rtp/src/packetizer/mod.rs b/rtc-rtp/src/packetizer/mod.rs index 737baffb..8676ce94 100644 --- a/rtc-rtp/src/packetizer/mod.rs +++ b/rtc-rtp/src/packetizer/mod.rs @@ -15,7 +15,13 @@ use std::time::Instant; /// Payloader payloads a byte array for use as rtp.Packet payloads pub trait Payloader: Send + Sync + fmt::Debug { + /// Splits one encoded frame into payloads no larger than `mtu`. + /// + /// # Errors + /// + /// Fails if the frame is malformed for this codec, or `mtu` is too small to make progress. fn payload(&mut self, mtu: usize, b: &Bytes) -> Result>; + /// Clones this payloader behind a trait object. fn clone_to(&self) -> Box; } @@ -27,9 +33,20 @@ impl Clone for Box { /// Packetizer packetizes a payload pub trait Packetizer: Send + Sync + fmt::Debug { + /// Attaches the absolute-send-time header extension under id `value`. fn enable_abs_send_time(&mut self, value: u8); + /// Packetizes one frame, advancing the timestamp by `samples`. + /// + /// Assigns sequence numbers, sets the marker bit on the final packet, and applies any + /// enabled header extensions. + /// + /// # Errors + /// + /// Propagates payloader failures. fn packetize(&mut self, payload: &Bytes, samples: u32) -> Result>; + /// Advances the timestamp without sending anything, for dropped or silent frames. fn skip_samples(&mut self, skipped_samples: u32); + /// Clones this packetizer behind a trait object. fn clone_to(&self) -> Box; } @@ -41,6 +58,11 @@ impl Clone for Box { /// Depacketizer depacketizes a RTP payload, removing any RTP specific data from the payload pub trait Depacketizer { + /// Reassembles a frame from one RTP payload, buffering fragments as needed. + /// + /// # Errors + /// + /// Fails if the payload is malformed for this codec. fn depacketize(&mut self, b: &Bytes) -> Result; /// Checks if the packet is at the beginning of a partition. This @@ -83,6 +105,10 @@ impl fmt::Debug for PacketizerImpl { } } +/// Builds a packetizer for one outbound stream. +/// +/// Ties together the codec's payloader, a sequencer, and the SSRC, payload type, MTU and clock +/// rate the stream was negotiated with. pub fn new_packetizer( mtu: usize, payload_type: u8, diff --git a/rtc-rtp/src/sequence.rs b/rtc-rtp/src/sequence.rs index 995d0d8b..a17a8192 100644 --- a/rtc-rtp/src/sequence.rs +++ b/rtc-rtp/src/sequence.rs @@ -4,8 +4,13 @@ use std::sync::atomic::{AtomicU16, AtomicU64, Ordering}; /// Sequencer generates sequential sequence numbers for building RTP packets pub trait Sequencer: Send + Sync + fmt::Debug { + /// Returns the next sequence number, wrapping at `u16::MAX` and counting the rollover. fn next_sequence_number(&self) -> u16; + /// How many times the sequence number has wrapped. + /// + /// SRTP needs this: the rollover counter is part of the packet index it encrypts with. fn roll_over_count(&self) -> u64; + /// Clones this sequencer behind a trait object. fn clone_to(&self) -> Box; } diff --git a/rtc-sctp/src/association/mod.rs b/rtc-sctp/src/association/mod.rs index 6c3dc507..6e0ea069 100644 --- a/rtc-sctp/src/association/mod.rs +++ b/rtc-sctp/src/association/mod.rs @@ -100,6 +100,7 @@ pub enum Event { AssociationLost { /// Reason that the association was closed reason: AssociationError, + /// The stream the loss was reported against. id: StreamId, }, /// Stream events @@ -721,6 +722,7 @@ impl Association { } } + /// The identifiers of every stream currently open on this association. pub fn stream_ids(&self) -> Vec { self.streams.keys().cloned().collect() } diff --git a/rtc-sctp/src/association/stats.rs b/rtc-sctp/src/association/stats.rs index 41ea7368..f9819dac 100644 --- a/rtc-sctp/src/association/stats.rs +++ b/rtc-sctp/src/association/stats.rs @@ -9,46 +9,57 @@ pub struct AssociationStats { } impl AssociationStats { + /// Counts one DATA chunk sent. pub fn inc_datas(&mut self) { self.n_datas += 1; } + /// The number of DATA chunks sent. pub fn get_num_datas(&mut self) -> u64 { self.n_datas } + /// Counts one SACK chunk received. pub fn inc_sacks(&mut self) { self.n_sacks += 1; } + /// The number of SACK chunks received. pub fn get_num_sacks(&mut self) -> u64 { self.n_sacks } + /// Counts one T3-rtx retransmission timeout. pub fn inc_t3timeouts(&mut self) { self.n_t3timeouts += 1; } + /// The number of T3-rtx retransmission timeouts, a signal of loss or a stalled path. pub fn get_num_t3timeouts(&mut self) -> u64 { self.n_t3timeouts } + /// Counts one delayed-acknowledgement timeout. pub fn inc_ack_timeouts(&mut self) { self.n_ack_timeouts += 1; } + /// The number of delayed-acknowledgement timeouts. pub fn get_num_ack_timeouts(&mut self) -> u64 { self.n_ack_timeouts } + /// Counts one fast retransmission. pub fn inc_fast_retrans(&mut self) { self.n_fast_retrans += 1; } + /// The number of fast retransmissions, triggered by SACK gap reports rather than a timeout. pub fn get_num_fast_retrans(&mut self) -> u64 { self.n_fast_retrans } + /// Zeroes every counter. pub fn reset(&mut self) { self.n_datas = 0; self.n_sacks = 0; diff --git a/rtc-sctp/src/association/stream.rs b/rtc-sctp/src/association/stream.rs index f1da78dd..ff8f6446 100644 --- a/rtc-sctp/src/association/stream.rs +++ b/rtc-sctp/src/association/stream.rs @@ -18,7 +18,10 @@ pub type StreamId = u16; #[non_exhaustive] pub enum StreamEvent { /// One or more new streams has been opened - Opened { id: StreamId }, + Opened { + /// Which stream was opened. + id: StreamId, + }, /// A currently open stream has data or errors waiting to be read Readable { /// Which stream is now readable @@ -229,6 +232,7 @@ impl Stream<'_> { } } + /// Whether this stream has data or an error waiting to be read. pub fn is_readable(&self) -> bool { if let Some(s) = self.association.streams.get(&self.stream_identifier) { s.state == RecvSendState::Readable || s.state == RecvSendState::ReadWritable @@ -237,6 +241,7 @@ impl Stream<'_> { } } + /// Whether this stream can currently accept more data. pub fn is_writable(&self) -> bool { if let Some(s) = self.association.streams.get(&self.stream_identifier) { s.state == RecvSendState::Writable || s.state == RecvSendState::ReadWritable diff --git a/rtc-sctp/src/association/timer.rs b/rtc-sctp/src/association/timer.rs index 8de4374d..83de856f 100644 --- a/rtc-sctp/src/association/timer.rs +++ b/rtc-sctp/src/association/timer.rs @@ -7,12 +7,24 @@ const NO_MAX_RETRANS: usize = usize::MAX; const TIMER_COUNT: usize = 6; #[derive(Debug, Copy, Clone)] +/// Retransmission limits for the association's timers. +/// +/// Each field caps how many times the corresponding timer may fire before the association is +/// abandoned. `Default` follows the RFC 4960 recommendations. pub struct TimerConfig { + /// How many times INIT may be retransmitted (T1-init) before the association fails. pub max_t1_init_retrans: usize, + /// How many times COOKIE-ECHO may be retransmitted (T1-cookie). pub max_t1_cookie_retrans: usize, + /// How many times SHUTDOWN may be retransmitted (T2-shutdown). pub max_t2_shutdown_retrans: usize, + /// How many times a DATA chunk may be retransmitted on T3-rtx expiry. + /// + /// Defaults to unlimited, leaving reliability to the per-stream partial-reliability settings. pub max_t3_rtx_retrans: usize, + /// How many times a RE-CONFIG chunk (stream reset) may be retransmitted. pub max_reconfig_retrans: usize, + /// How many times a delayed SACK may be retransmitted. pub max_ack_retrans: usize, } diff --git a/rtc-sctp/src/chunk/chunk_payload_data.rs b/rtc-sctp/src/chunk/chunk_payload_data.rs index abf154c6..024c4a53 100644 --- a/rtc-sctp/src/chunk/chunk_payload_data.rs +++ b/rtc-sctp/src/chunk/chunk_payload_data.rs @@ -16,12 +16,20 @@ pub(crate) const PAYLOAD_DATA_HEADER_SIZE: usize = 12; #[derive(Default, Debug, Copy, Clone, PartialEq)] #[repr(C)] pub enum PayloadProtocolIdentifier { + /// `WebRTC DCEP` (50): a Data Channel Establishment Protocol control message. Dcep = 50, + /// `WebRTC String` (51): a non-empty UTF-8 string message. String = 51, + /// `WebRTC Binary` (53): a non-empty binary message. Binary = 53, + /// `WebRTC String Empty` (56): an empty string message. + /// + /// Needed because SCTP cannot carry a zero-length payload. StringEmpty = 56, + /// `WebRTC Binary Empty` (57): an empty binary message. BinaryEmpty = 57, #[default] + /// An identifier this crate does not recognise. Unknown, } diff --git a/rtc-sctp/src/config.rs b/rtc-sctp/src/config.rs index 55fa1819..faeab175 100644 --- a/rtc-sctp/src/config.rs +++ b/rtc-sctp/src/config.rs @@ -39,56 +39,69 @@ impl Default for TransportConfig { } impl TransportConfig { + /// Sets the SCTP port. WebRTC always uses 5000. pub fn with_sctp_port(mut self, value: u16) -> Self { self.sctp_port = value; self } + /// Sets the advertised receive window (a_rwnd), bounding how much unacknowledged data a peer + /// may have in flight toward this endpoint. pub fn with_max_receive_buffer_size(mut self, value: u32) -> Self { self.max_receive_buffer_size = value; self } + /// Sets the largest message this endpoint will accept. pub fn with_max_message_size(mut self, value: u32) -> Self { self.max_message_size = value; self } + /// Sets how many outbound streams to request during the handshake. pub fn with_max_num_outbound_streams(mut self, value: u16) -> Self { self.max_num_outbound_streams = value; self } + /// Sets how many inbound streams this endpoint will accept. pub fn with_max_num_inbound_streams(mut self, value: u16) -> Self { self.max_num_inbound_streams = value; self } + /// Overrides the retransmission limits; see [`TimerConfig`]. pub fn with_timer_config(mut self, value: TimerConfig) -> Self { self.timer_config = value; self } + /// The configured SCTP port. pub fn sctp_port(&self) -> u16 { self.sctp_port } + /// The configured receive window in bytes. pub fn max_receive_buffer_size(&self) -> u32 { self.max_receive_buffer_size } + /// The configured maximum message size in bytes. pub fn max_message_size(&self) -> u32 { self.max_message_size } + /// The configured outbound stream count. pub fn max_num_outbound_streams(&self) -> u16 { self.max_num_outbound_streams } + /// The configured inbound stream count. pub fn max_num_inbound_streams(&self) -> u16 { self.max_num_inbound_streams } + /// The configured retransmission limits. pub fn timer_config(&self) -> TimerConfig { self.timer_config } diff --git a/rtc-sctp/src/lib.rs b/rtc-sctp/src/lib.rs index 390281fb..cdd22076 100644 --- a/rtc-sctp/src/lib.rs +++ b/rtc-sctp/src/lib.rs @@ -13,6 +13,7 @@ //! managing a single association and all the related state (such as streams). #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] #![allow(clippy::bool_to_int_with_if)] @@ -103,6 +104,8 @@ use crate::packet::PartialDecode; /// Payload in Incoming/outgoing Transmit #[derive(Debug)] pub enum Payload { + /// An inbound packet whose header has been decoded but whose chunks have not. PartialDecode(PartialDecode), + /// Outbound packets, already encoded and ready to hand to the transport. RawEncode(Vec), } diff --git a/rtc-sctp/src/queue/reassembly_queue.rs b/rtc-sctp/src/queue/reassembly_queue.rs index 60eb5672..16c0b0f1 100644 --- a/rtc-sctp/src/queue/reassembly_queue.rs +++ b/rtc-sctp/src/queue/reassembly_queue.rs @@ -19,7 +19,9 @@ pub struct Chunk { pub struct Chunks { /// used only with the ordered chunks pub ssn: u16, + /// The payload protocol identifier shared by every fragment of this message. pub ppi: PayloadProtocolIdentifier, + /// The fragments, in order, that make up one complete message. pub chunks: Vec, offset: usize, index: usize, @@ -27,10 +29,12 @@ pub struct Chunks { } impl Chunks { + /// Whether the reassembled message carries no bytes. pub fn is_empty(&self) -> bool { self.len() == 0 } + /// The total length in bytes of the reassembled message. pub fn len(&self) -> usize { let mut l = 0; for c in &self.chunks { @@ -60,7 +64,12 @@ impl Chunks { Ok(buf) } - // Concat all fragments into the buffer + /// Concatenates every fragment into `buf`, returning the number of bytes written. + /// + /// # Errors + /// + /// Returns [`Error::ErrShortBuffer`](shared::error::Error::ErrShortBuffer) if `buf` cannot + /// hold the whole message; the partial copy is left in place. pub fn read(&self, buf: &mut [u8]) -> Result { let mut n_written = 0; for c in &self.chunks { @@ -75,6 +84,10 @@ impl Chunks { Ok(n_written) } + /// Yields the next slice of the reassembled message, up to `max_length` bytes. + /// + /// Advances an internal cursor, so repeated calls walk the message; returns `None` once it is + /// exhausted. pub fn next(&mut self, max_length: usize) -> Option { if self.index >= self.chunks.len() { return None; diff --git a/rtc-sdp/src/description/common.rs b/rtc-sdp/src/description/common.rs index 7df05e09..ea282b19 100644 --- a/rtc-sdp/src/description/common.rs +++ b/rtc-sdp/src/description/common.rs @@ -1,96 +1,107 @@ -use std::fmt; - -use super::session::ATTR_KEY_CANDIDATE; - -/// Information describes the "i=" field which provides textual information -/// about the session. -pub type Information = String; - -/// ConnectionInformation defines the representation for the "c=" field -/// containing connection data. -#[derive(Debug, Default, Clone)] -pub struct ConnectionInformation { - pub network_type: String, - pub address_type: String, - pub address: Option

, -} - -impl fmt::Display for ConnectionInformation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(address) = &self.address { - write!(f, "{} {} {}", self.network_type, self.address_type, address,) - } else { - write!(f, "{} {}", self.network_type, self.address_type,) - } - } -} - -/// Address describes a structured address token from within the "c=" field. -#[derive(Debug, Default, Clone)] -pub struct Address { - pub address: String, - pub ttl: Option, - pub range: Option, -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.address)?; - if let Some(t) = &self.ttl { - write!(f, "/{t}")?; - } - if let Some(r) = &self.range { - write!(f, "/{r}")?; - } - Ok(()) - } -} - -/// Bandwidth describes an optional field which denotes the proposed bandwidth -/// to be used by the session or media. -#[derive(Debug, Default, Clone)] -pub struct Bandwidth { - pub experimental: bool, - pub bandwidth_type: String, - pub bandwidth: u64, -} - -impl fmt::Display for Bandwidth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = if self.experimental { "X-" } else { "" }; - write!(f, "{}{}:{}", output, self.bandwidth_type, self.bandwidth) - } -} - -/// EncryptionKey describes the "k=" which conveys encryption key information. -pub type EncryptionKey = String; - -/// Attribute describes the "a=" field which represents the primary means for -/// extending SDP. -#[derive(Debug, Default, Clone)] -pub struct Attribute { - pub key: String, - pub value: Option, -} - -impl fmt::Display for Attribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(value) = &self.value { - write!(f, "{}:{}", self.key, value) - } else { - write!(f, "{}", self.key) - } - } -} - -impl Attribute { - /// new constructs a new attribute - pub fn new(key: String, value: Option) -> Self { - Attribute { key, value } - } - - /// is_ice_candidate returns true if the attribute key equals "candidate". - pub fn is_ice_candidate(&self) -> bool { - self.key.as_str() == ATTR_KEY_CANDIDATE - } -} +use std::fmt; + +use super::session::ATTR_KEY_CANDIDATE; + +/// Information describes the "i=" field which provides textual information +/// about the session. +pub type Information = String; + +/// ConnectionInformation defines the representation for the "c=" field +/// containing connection data. +#[derive(Debug, Default, Clone)] +pub struct ConnectionInformation { + /// The network type, always `IN` (Internet) in practice. + pub network_type: String, + /// The address type, `IP4` or `IP6`. + pub address_type: String, + /// The connection address, absent for a bare `c=` line. + pub address: Option
, +} + +impl fmt::Display for ConnectionInformation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(address) = &self.address { + write!(f, "{} {} {}", self.network_type, self.address_type, address,) + } else { + write!(f, "{} {}", self.network_type, self.address_type,) + } + } +} + +/// Address describes a structured address token from within the "c=" field. +#[derive(Debug, Default, Clone)] +pub struct Address { + /// The address itself: a host name, IPv4/IPv6 literal, or multicast group. + pub address: String, + /// The multicast TTL, for multicast addresses. + pub ttl: Option, + /// The number of consecutive multicast addresses in the range. + pub range: Option, +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address)?; + if let Some(t) = &self.ttl { + write!(f, "/{t}")?; + } + if let Some(r) = &self.range { + write!(f, "/{r}")?; + } + Ok(()) + } +} + +/// Bandwidth describes an optional field which denotes the proposed bandwidth +/// to be used by the session or media. +#[derive(Debug, Default, Clone)] +pub struct Bandwidth { + /// Whether the bandwidth type is experimental, written with an `X-` prefix. + pub experimental: bool, + /// The bandwidth modifier, such as `AS` (application-specific) or `CT` (conference total). + pub bandwidth_type: String, + /// The proposed bandwidth in kilobits per second. + pub bandwidth: u64, +} + +impl fmt::Display for Bandwidth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = if self.experimental { "X-" } else { "" }; + write!(f, "{}{}:{}", output, self.bandwidth_type, self.bandwidth) + } +} + +/// EncryptionKey describes the "k=" which conveys encryption key information. +pub type EncryptionKey = String; + +/// Attribute describes the "a=" field which represents the primary means for +/// extending SDP. +#[derive(Debug, Default, Clone)] +pub struct Attribute { + /// The attribute name, the part before the `:`. + pub key: String, + /// The attribute value, or `None` for a flag attribute such as `a=rtcp-mux`. + pub value: Option, +} + +impl fmt::Display for Attribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(value) = &self.value { + write!(f, "{}:{}", self.key, value) + } else { + write!(f, "{}", self.key) + } + } +} + +impl Attribute { + /// new constructs a new attribute + pub fn new(key: String, value: Option) -> Self { + Attribute { key, value } + } + + /// is_ice_candidate returns true if the attribute key equals "candidate". + pub fn is_ice_candidate(&self) -> bool { + self.key.as_str() == ATTR_KEY_CANDIDATE + } +} diff --git a/rtc-sdp/src/description/media.rs b/rtc-sdp/src/description/media.rs index 28992599..b2e02125 100644 --- a/rtc-sdp/src/description/media.rs +++ b/rtc-sdp/src/description/media.rs @@ -1,312 +1,324 @@ -use std::collections::HashMap; -use std::fmt; - -use url::Url; - -use crate::description::common::*; -use crate::extmap::*; -use crate::util::{Codec, merge_codecs, parse_fmtp, parse_rtcp_fb, parse_rtpmap}; - -/// Constants for extmap key -pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: u16 = 3; -pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str = - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; - -fn ext_map_uri() -> HashMap { - let mut m = HashMap::new(); - m.insert( - EXT_MAP_VALUE_TRANSPORT_CC_KEY, - EXT_MAP_VALUE_TRANSPORT_CC_URI, - ); - m -} - -/// MediaDescription represents a media type. -/// -/// ## Specifications -/// -/// * [RFC 4566 §5.14] -/// -/// [RFC 4566 §5.14]: https://tools.ietf.org/html/rfc4566#section-5.14 -#[derive(Debug, Default, Clone)] -pub struct MediaDescription { - /// `m= / ...` - /// - /// - pub media_name: MediaName, - - /// `i=` - /// - /// - pub media_title: Option, - - /// `c= ` - /// - /// - pub connection_information: Option, - - /// `b=:` - /// - /// - pub bandwidth: Vec, - - /// `k=` - /// - /// `k=:` - /// - /// - pub encryption_key: Option, - - /// Attributes are the primary means for extending SDP. Attributes may - /// be defined to be used as "session-level" attributes, "media-level" - /// attributes, or both. - /// - /// - pub attributes: Vec, -} - -impl MediaDescription { - /// Returns whether an attribute exists - pub fn has_attribute(&self, key: &str) -> bool { - self.attributes.iter().any(|a| a.key == key) - } - - /// attribute returns the value of an attribute and if it exists - pub fn attribute(&self, key: &str) -> Option> { - for a in &self.attributes { - if a.key == key { - return Some(a.value.as_ref().map(|s| s.as_ref())); - } - } - None - } - - pub fn codecs(&self) -> HashMap { - let mut codecs: HashMap = HashMap::new(); - - for a in &self.attributes { - let attr = a.to_string(); - if attr.starts_with("rtpmap:") { - if let Ok(codec) = parse_rtpmap(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("fmtp:") { - if let Ok(codec) = parse_fmtp(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("rtcp-fb:") - && let Ok(codec) = parse_rtcp_fb(&attr) - { - merge_codecs(codec, &mut codecs); - } - } - - codecs - } - - /// new_jsep_media_description creates a new MediaName with - /// some settings that are required by the JSEP spec. - pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self { - MediaDescription { - media_name: MediaName { - media: codec_type, - port: RangedPort { - value: 9, - range: None, - }, - protos: vec![ - "UDP".to_string(), - "TLS".to_string(), - "RTP".to_string(), - "SAVPF".to_string(), - ], - formats: vec![], - }, - media_title: None, - connection_information: Some(ConnectionInformation { - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - address: Some(Address { - address: "0.0.0.0".to_string(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - } - } - - /// with_property_attribute adds a property attribute 'a=key' to the media description - pub fn with_property_attribute(mut self, key: String) -> Self { - self.attributes.push(Attribute::new(key, None)); - self - } - - /// with_value_attribute adds a value attribute 'a=key:value' to the media description - pub fn with_value_attribute(mut self, key: String, value: String) -> Self { - self.attributes.push(Attribute::new(key, Some(value))); - self - } - - /// with_fingerprint adds a fingerprint to the media description - pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { - self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value) - } - - /// with_ice_credentials adds ICE credentials to the media description - pub fn with_ice_credentials(self, username: String, password: String) -> Self { - self.with_value_attribute("ice-ufrag".to_string(), username) - .with_value_attribute("ice-pwd".to_string(), password) - } - - /// with_codec adds codec information to the media description - pub fn with_codec( - mut self, - payload_type: u8, - name: String, - clockrate: u32, - channels: u16, - fmtp: String, - ) -> Self { - self.media_name.formats.push(payload_type.to_string()); - let rtpmap = if channels > 0 { - format!("{payload_type} {name}/{clockrate}/{channels}") - } else { - format!("{payload_type} {name}/{clockrate}") - }; - - if !fmtp.is_empty() { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}")) - } else { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - } - } - - /// with_media_source adds media source information to the media description - pub fn with_media_source( - self, - ssrc: u32, - cname: String, - stream_id: String, - track_id: String, - ) -> Self { - self. - with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_id} {track_id}")). - with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_id}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{track_id}")) - // Deprecated but not phased out? - } - - /// with_candidate adds an ICE candidate to the media description - /// Deprecated: use WithICECandidate instead - pub fn with_candidate(self, value: String) -> Self { - self.with_value_attribute("candidate".to_string(), value) - } - - pub fn with_extmap(self, e: ExtMap) -> Self { - self.with_property_attribute(e.marshal()) - } - - /// with_transport_cc_extmap adds an extmap to the media description - pub fn with_transport_cc_extmap(self) -> Self { - let uri = { - let m = ext_map_uri(); - if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) { - Url::parse(uri_str).ok() - } else { - None - } - }; - - let e = ExtMap { - value: EXT_MAP_VALUE_TRANSPORT_CC_KEY, - uri, - ..Default::default() - }; - - self.with_extmap(e) - } -} - -/// RangedPort supports special format for the media field "m=" port value. If -/// it may be necessary to specify multiple transport ports, the protocol allows -/// to write it as: `/` where number of ports is a an -/// offsetting range. -#[derive(Debug, Default, Clone)] -pub struct RangedPort { - pub value: isize, - pub range: Option, -} - -impl fmt::Display for RangedPort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(range) = self.range { - write!(f, "{}/{}", self.value, range) - } else { - write!(f, "{}", self.value) - } - } -} - -/// MediaName describes the "m=" field storage structure. -#[derive(Debug, Default, Clone)] -pub struct MediaName { - pub media: String, - pub port: RangedPort, - pub protos: Vec, - pub formats: Vec, -} - -impl fmt::Display for MediaName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.media, self.port)?; - - let mut first = true; - for part in &self.protos { - if first { - first = false; - write!(f, " {part}")?; - } else { - write!(f, "/{part}")?; - } - } - - for part in &self.formats { - write!(f, " {part}")?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::MediaDescription; - - #[test] - fn test_attribute_missing() { - let media_description = MediaDescription::default(); - - assert_eq!(media_description.attribute("recvonly"), None); - } - - #[test] - fn test_attribute_present_with_no_value() { - let media_description = - MediaDescription::default().with_property_attribute("recvonly".to_owned()); - - assert_eq!(media_description.attribute("recvonly"), Some(None)); - } - - #[test] - fn test_attribute_present_with_value() { - let media_description = - MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned()); - - assert_eq!(media_description.attribute("ptime"), Some(Some("1"))); - } -} +use std::collections::HashMap; +use std::fmt; + +use url::Url; + +use crate::description::common::*; +use crate::extmap::*; +use crate::util::{Codec, merge_codecs, parse_fmtp, parse_rtcp_fb, parse_rtpmap}; + +/// Constants for extmap key +pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: u16 = 3; +/// The transport-wide congestion control header-extension URI. +pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str = + "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; + +fn ext_map_uri() -> HashMap { + let mut m = HashMap::new(); + m.insert( + EXT_MAP_VALUE_TRANSPORT_CC_KEY, + EXT_MAP_VALUE_TRANSPORT_CC_URI, + ); + m +} + +/// MediaDescription represents a media type. +/// +/// ## Specifications +/// +/// * [RFC 4566 §5.14] +/// +/// [RFC 4566 §5.14]: https://tools.ietf.org/html/rfc4566#section-5.14 +#[derive(Debug, Default, Clone)] +pub struct MediaDescription { + /// `m= / ...` + /// + /// + pub media_name: MediaName, + + /// `i=` + /// + /// + pub media_title: Option, + + /// `c= ` + /// + /// + pub connection_information: Option, + + /// `b=:` + /// + /// + pub bandwidth: Vec, + + /// `k=` + /// + /// `k=:` + /// + /// + pub encryption_key: Option, + + /// Attributes are the primary means for extending SDP. Attributes may + /// be defined to be used as "session-level" attributes, "media-level" + /// attributes, or both. + /// + /// + pub attributes: Vec, +} + +impl MediaDescription { + /// Returns whether an attribute exists + pub fn has_attribute(&self, key: &str) -> bool { + self.attributes.iter().any(|a| a.key == key) + } + + /// attribute returns the value of an attribute and if it exists + pub fn attribute(&self, key: &str) -> Option> { + for a in &self.attributes { + if a.key == key { + return Some(a.value.as_ref().map(|s| s.as_ref())); + } + } + None + } + + /// The codecs this media section offers, keyed by payload type. + /// + /// Assembled from the `m=` format list plus the `a=rtpmap`, `a=fmtp` and `a=rtcp-fb` + /// attributes that describe them. + pub fn codecs(&self) -> HashMap { + let mut codecs: HashMap = HashMap::new(); + + for a in &self.attributes { + let attr = a.to_string(); + if attr.starts_with("rtpmap:") { + if let Ok(codec) = parse_rtpmap(&attr) { + merge_codecs(codec, &mut codecs); + } + } else if attr.starts_with("fmtp:") { + if let Ok(codec) = parse_fmtp(&attr) { + merge_codecs(codec, &mut codecs); + } + } else if attr.starts_with("rtcp-fb:") + && let Ok(codec) = parse_rtcp_fb(&attr) + { + merge_codecs(codec, &mut codecs); + } + } + + codecs + } + + /// new_jsep_media_description creates a new MediaName with + /// some settings that are required by the JSEP spec. + pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self { + MediaDescription { + media_name: MediaName { + media: codec_type, + port: RangedPort { + value: 9, + range: None, + }, + protos: vec![ + "UDP".to_string(), + "TLS".to_string(), + "RTP".to_string(), + "SAVPF".to_string(), + ], + formats: vec![], + }, + media_title: None, + connection_information: Some(ConnectionInformation { + network_type: "IN".to_string(), + address_type: "IP4".to_string(), + address: Some(Address { + address: "0.0.0.0".to_string(), + ttl: None, + range: None, + }), + }), + bandwidth: vec![], + encryption_key: None, + attributes: vec![], + } + } + + /// with_property_attribute adds a property attribute 'a=key' to the media description + pub fn with_property_attribute(mut self, key: String) -> Self { + self.attributes.push(Attribute::new(key, None)); + self + } + + /// with_value_attribute adds a value attribute 'a=key:value' to the media description + pub fn with_value_attribute(mut self, key: String, value: String) -> Self { + self.attributes.push(Attribute::new(key, Some(value))); + self + } + + /// with_fingerprint adds a fingerprint to the media description + pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { + self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value) + } + + /// with_ice_credentials adds ICE credentials to the media description + pub fn with_ice_credentials(self, username: String, password: String) -> Self { + self.with_value_attribute("ice-ufrag".to_string(), username) + .with_value_attribute("ice-pwd".to_string(), password) + } + + /// with_codec adds codec information to the media description + pub fn with_codec( + mut self, + payload_type: u8, + name: String, + clockrate: u32, + channels: u16, + fmtp: String, + ) -> Self { + self.media_name.formats.push(payload_type.to_string()); + let rtpmap = if channels > 0 { + format!("{payload_type} {name}/{clockrate}/{channels}") + } else { + format!("{payload_type} {name}/{clockrate}") + }; + + if !fmtp.is_empty() { + self.with_value_attribute("rtpmap".to_string(), rtpmap) + .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}")) + } else { + self.with_value_attribute("rtpmap".to_string(), rtpmap) + } + } + + /// with_media_source adds media source information to the media description + pub fn with_media_source( + self, + ssrc: u32, + cname: String, + stream_id: String, + track_id: String, + ) -> Self { + self. + with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out? + with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_id} {track_id}")). + with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_id}")). // Deprecated but not phased out? + with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{track_id}")) + // Deprecated but not phased out? + } + + /// with_candidate adds an ICE candidate to the media description + /// Deprecated: use WithICECandidate instead + pub fn with_candidate(self, value: String) -> Self { + self.with_value_attribute("candidate".to_string(), value) + } + + /// Appends an `a=extmap` header-extension declaration. + pub fn with_extmap(self, e: ExtMap) -> Self { + self.with_property_attribute(e.marshal()) + } + + /// with_transport_cc_extmap adds an extmap to the media description + pub fn with_transport_cc_extmap(self) -> Self { + let uri = { + let m = ext_map_uri(); + if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) { + Url::parse(uri_str).ok() + } else { + None + } + }; + + let e = ExtMap { + value: EXT_MAP_VALUE_TRANSPORT_CC_KEY, + uri, + ..Default::default() + }; + + self.with_extmap(e) + } +} + +/// RangedPort supports special format for the media field "m=" port value. If +/// it may be necessary to specify multiple transport ports, the protocol allows +/// to write it as: `/` where number of ports is a an +/// offsetting range. +#[derive(Debug, Default, Clone)] +pub struct RangedPort { + /// The first port number. + pub value: isize, + /// How many consecutive ports the media uses, for `/` form. + pub range: Option, +} + +impl fmt::Display for RangedPort { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(range) = self.range { + write!(f, "{}/{}", self.value, range) + } else { + write!(f, "{}", self.value) + } + } +} + +/// MediaName describes the "m=" field storage structure. +#[derive(Debug, Default, Clone)] +pub struct MediaName { + /// The media type: `audio`, `video`, or `application` for data channels. + pub media: String, + /// The transport port, possibly a range. + pub port: RangedPort, + /// The transport protocol tokens, such as `UDP`, `TLS`, `RTP`, `SAVPF`. + pub protos: Vec, + /// The payload types (for RTP media) or format tokens this section offers. + pub formats: Vec, +} + +impl fmt::Display for MediaName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.media, self.port)?; + + let mut first = true; + for part in &self.protos { + if first { + first = false; + write!(f, " {part}")?; + } else { + write!(f, "/{part}")?; + } + } + + for part in &self.formats { + write!(f, " {part}")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::MediaDescription; + + #[test] + fn test_attribute_missing() { + let media_description = MediaDescription::default(); + + assert_eq!(media_description.attribute("recvonly"), None); + } + + #[test] + fn test_attribute_present_with_no_value() { + let media_description = + MediaDescription::default().with_property_attribute("recvonly".to_owned()); + + assert_eq!(media_description.attribute("recvonly"), Some(None)); + } + + #[test] + fn test_attribute_present_with_value() { + let media_description = + MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned()); + + assert_eq!(media_description.attribute("ptime"), Some(Some("1"))); + } +} diff --git a/rtc-sdp/src/description/mod.rs b/rtc-sdp/src/description/mod.rs index 0cab15c0..ab4a88d9 100644 --- a/rtc-sdp/src/description/mod.rs +++ b/rtc-sdp/src/description/mod.rs @@ -1,6 +1,9 @@ -#[cfg(test)] -mod description_test; - -pub mod common; -pub mod media; -pub mod session; +#[cfg(test)] +mod description_test; + +/// Field types shared by session and media descriptions (`c=`, `b=`, `a=`). +pub mod common; +/// The `m=` media description and its fields. +pub mod media; +/// The whole session description, plus the well-known attribute keys. +pub mod session; diff --git a/rtc-sdp/src/description/session.rs b/rtc-sdp/src/description/session.rs index 44d4f68d..1c16a7f3 100644 --- a/rtc-sdp/src/description/session.rs +++ b/rtc-sdp/src/description/session.rs @@ -14,32 +14,55 @@ use super::media::*; /// Constants for SDP attributes used in JSEP pub const ATTR_KEY_CANDIDATE: &str = "candidate"; +/// `a=end-of-candidates`: ICE gathering for this section is complete. pub const ATTR_KEY_END_OF_CANDIDATES: &str = "end-of-candidates"; +/// `a=identity`: an identity assertion for the session. pub const ATTR_KEY_IDENTITY: &str = "identity"; +/// `a=group`: groups m-lines, as BUNDLE does. pub const ATTR_KEY_GROUP: &str = "group"; +/// `a=ssrc`: declares an SSRC and its attributes. pub const ATTR_KEY_SSRC: &str = "ssrc"; +/// `a=ssrc-group`: relates SSRCs, such as an RTX stream to its primary. pub const ATTR_KEY_SSRC_GROUP: &str = "ssrc-group"; +/// `a=msid`: associates a track with a media stream. pub const ATTR_KEY_MSID: &str = "msid"; +/// `a=msid-semantic`: declares the semantics used by `a=msid`. pub const ATTR_KEY_MSID_SEMANTIC: &str = "msid-semantic"; +/// `a=setup`: the DTLS role — `active`, `passive` or `actpass`. pub const ATTR_KEY_CONNECTION_SETUP: &str = "setup"; +/// `a=mid`: the media identification tag that names this m-line. pub const ATTR_KEY_MID: &str = "mid"; +/// `a=ice-lite`: the endpoint is an ICE-lite implementation. pub const ATTR_KEY_ICELITE: &str = "ice-lite"; +/// `a=rtcp-mux`: RTP and RTCP share one port. pub const ATTR_KEY_RTCPMUX: &str = "rtcp-mux"; +/// `a=rtcp-rsize`: reduced-size RTCP is supported. pub const ATTR_KEY_RTCPRSIZE: &str = "rtcp-rsize"; +/// `a=inactive`: neither send nor receive. pub const ATTR_KEY_INACTIVE: &str = "inactive"; +/// `a=recvonly`: receive only. pub const ATTR_KEY_RECV_ONLY: &str = "recvonly"; +/// `a=sendonly`: send only. pub const ATTR_KEY_SEND_ONLY: &str = "sendonly"; +/// `a=sendrecv`: send and receive. pub const ATTR_KEY_SEND_RECV: &str = "sendrecv"; +/// `a=extmap`: maps an RTP header-extension URI to an id. pub const ATTR_KEY_EXT_MAP: &str = "extmap"; +/// `a=extmap-allow-mixed`: one- and two-byte header extensions may be mixed. pub const ATTR_KEY_EXTMAP_ALLOW_MIXED: &str = "extmap-allow-mixed"; +/// `a=max-message-size`: the largest SCTP message this endpoint accepts. pub const ATTR_KEY_MAX_MESSAGE_SIZE: &str = "max-message-size"; /// Constants for semantic tokens used in JSEP pub const SEMANTIC_TOKEN_LIP_SYNCHRONIZATION: &str = "LS"; +/// `FID`: flow identification — the SSRCs carry the same content, as RTX does. pub const SEMANTIC_TOKEN_FLOW_IDENTIFICATION: &str = "FID"; +/// `FEC`: one SSRC carries error-correction data for another. pub const SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION: &str = "FEC"; // https://datatracker.ietf.org/doc/html/rfc5956#section-4.1 +/// `FEC-FR`: the FEC framework grouping semantic. pub const SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION_FRAMEWORK: &str = "FEC-FR"; +/// `WMS`: WebRTC media streams, used with `a=msid-semantic`. pub const SEMANTIC_TOKEN_WEBRTC_MEDIA_STREAMS: &str = "WMS"; /// Version describes the value provided by the "v=" field which gives @@ -50,11 +73,17 @@ pub type Version = isize; /// originator of the session plus a session identifier and version number. #[derive(Debug, Default, Clone)] pub struct Origin { + /// The originator's user name, or `-` when withheld. pub username: String, + /// A globally unique session identifier. pub session_id: u64, + /// Incremented on each modification of the session description. pub session_version: u64, + /// The network type, always `IN` in practice. pub network_type: String, + /// The address type, `IP4` or `IP6`. pub address_type: String, + /// The originator's address. WebRTC does not use it and sends a placeholder. pub unicast_address: String, } @@ -74,6 +103,7 @@ impl fmt::Display for Origin { } impl Origin { + /// An empty session description with SDP version 0. pub fn new() -> Self { Origin { username: "".to_owned(), @@ -104,7 +134,9 @@ pub type PhoneNumber = String; /// repeated sessions scheduling. #[derive(Debug, Default, Clone)] pub struct TimeZone { + /// The time at which the offset takes effect. pub adjustment_time: u64, + /// The offset from the session's base time, in seconds. pub offset: i64, } @@ -134,7 +166,9 @@ pub struct TimeDescription { /// stop times. #[derive(Debug, Default, Clone)] pub struct Timing { + /// Session start time in NTP seconds; `0` means unbounded. pub start_time: u64, + /// Session stop time in NTP seconds; `0` means unbounded. pub stop_time: u64, } @@ -148,8 +182,11 @@ impl fmt::Display for Timing { /// represents the intervals and durations for repeated scheduled sessions. #[derive(Debug, Default, Clone)] pub struct RepeatTime { + /// How often the session repeats, in seconds. pub interval: i64, + /// How long each repetition lasts, in seconds. pub duration: i64, + /// Offsets from the start time at which the session repeats. pub offsets: Vec, } diff --git a/rtc-sdp/src/direction/mod.rs b/rtc-sdp/src/direction/mod.rs index 23e5e22e..ca0e604f 100644 --- a/rtc-sdp/src/direction/mod.rs +++ b/rtc-sdp/src/direction/mod.rs @@ -1,51 +1,52 @@ -use std::fmt; - -#[cfg(test)] -mod direction_test; - -/// Direction is a marker for transmission direction of an endpoint -#[derive(Default, Debug, PartialEq, Eq, Clone)] -pub enum Direction { - #[default] - Unspecified = 0, - /// Direction::SendRecv is for bidirectional communication - SendRecv = 1, - /// Direction::SendOnly is for outgoing communication - SendOnly = 2, - /// Direction::RecvOnly is for incoming communication - RecvOnly = 3, - /// Direction::Inactive is for no communication - Inactive = 4, -} - -const DIRECTION_SEND_RECV_STR: &str = "sendrecv"; -const DIRECTION_SEND_ONLY_STR: &str = "sendonly"; -const DIRECTION_RECV_ONLY_STR: &str = "recvonly"; -const DIRECTION_INACTIVE_STR: &str = "inactive"; -const DIRECTION_UNSPECIFIED_STR: &str = "Unspecified"; - -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Direction::SendRecv => DIRECTION_SEND_RECV_STR, - Direction::SendOnly => DIRECTION_SEND_ONLY_STR, - Direction::RecvOnly => DIRECTION_RECV_ONLY_STR, - Direction::Inactive => DIRECTION_INACTIVE_STR, - _ => DIRECTION_UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -impl Direction { - /// new defines a procedure for creating a new direction from a raw string. - pub fn new(raw: &str) -> Self { - match raw { - DIRECTION_SEND_RECV_STR => Direction::SendRecv, - DIRECTION_SEND_ONLY_STR => Direction::SendOnly, - DIRECTION_RECV_ONLY_STR => Direction::RecvOnly, - DIRECTION_INACTIVE_STR => Direction::Inactive, - _ => Direction::Unspecified, - } - } -} +use std::fmt; + +#[cfg(test)] +mod direction_test; + +/// Direction is a marker for transmission direction of an endpoint +#[derive(Default, Debug, PartialEq, Eq, Clone)] +pub enum Direction { + #[default] + /// No direction attribute was present. + Unspecified = 0, + /// Direction::SendRecv is for bidirectional communication + SendRecv = 1, + /// Direction::SendOnly is for outgoing communication + SendOnly = 2, + /// Direction::RecvOnly is for incoming communication + RecvOnly = 3, + /// Direction::Inactive is for no communication + Inactive = 4, +} + +const DIRECTION_SEND_RECV_STR: &str = "sendrecv"; +const DIRECTION_SEND_ONLY_STR: &str = "sendonly"; +const DIRECTION_RECV_ONLY_STR: &str = "recvonly"; +const DIRECTION_INACTIVE_STR: &str = "inactive"; +const DIRECTION_UNSPECIFIED_STR: &str = "Unspecified"; + +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Direction::SendRecv => DIRECTION_SEND_RECV_STR, + Direction::SendOnly => DIRECTION_SEND_ONLY_STR, + Direction::RecvOnly => DIRECTION_RECV_ONLY_STR, + Direction::Inactive => DIRECTION_INACTIVE_STR, + _ => DIRECTION_UNSPECIFIED_STR, + }; + write!(f, "{s}") + } +} + +impl Direction { + /// new defines a procedure for creating a new direction from a raw string. + pub fn new(raw: &str) -> Self { + match raw { + DIRECTION_SEND_RECV_STR => Direction::SendRecv, + DIRECTION_SEND_ONLY_STR => Direction::SendOnly, + DIRECTION_RECV_ONLY_STR => Direction::RecvOnly, + DIRECTION_INACTIVE_STR => Direction::Inactive, + _ => Direction::Unspecified, + } + } +} diff --git a/rtc-sdp/src/extmap/mod.rs b/rtc-sdp/src/extmap/mod.rs index e6afe040..40e1fdad 100644 --- a/rtc-sdp/src/extmap/mod.rs +++ b/rtc-sdp/src/extmap/mod.rs @@ -11,27 +11,41 @@ use url::Url; /// Default ext values pub const DEF_EXT_MAP_VALUE_ABS_SEND_TIME: usize = 1; +/// The default id this crate assigns to the transport-wide CC extension. pub const DEF_EXT_MAP_VALUE_TRANSPORT_CC: usize = 2; +/// The default id assigned to the SDES `mid` extension. pub const DEF_EXT_MAP_VALUE_SDES_MID: usize = 3; +/// The default id assigned to the SDES RTP stream id extension. pub const DEF_EXT_MAP_VALUE_SDES_RTP_STREAM_ID: usize = 4; +/// The absolute-send-time extension URI, used for bandwidth estimation. pub const ABS_SEND_TIME_URI: &str = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; +/// The transport-wide congestion control extension URI. pub const TRANSPORT_CC_URI: &str = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; +/// The SDES `mid` extension URI, which tags each packet with its m-line. pub const SDES_MID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:mid"; +/// The SDES RTP stream id (RID) extension URI, which identifies a simulcast layer. pub const SDES_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; +/// The SDES repaired RTP stream id extension URI, which identifies an RTX layer's target. pub const SDES_REPAIR_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"; +/// The audio-level extension URI, carrying per-packet loudness. pub const AUDIO_LEVEL_URI: &str = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; +/// The video-orientation (CVO) extension URI, carrying rotation flags. pub const VIDEO_ORIENTATION_URI: &str = "urn:3gpp:video-orientation"; /// ExtMap represents the activation of a single RTP header extension #[derive(Debug, Clone, Default)] pub struct ExtMap { + /// The id this extension is negotiated under, as it appears in RTP packets. pub value: u16, + /// The direction the extension applies in, if the attribute restricted it. pub direction: Direction, + /// The extension's canonical URI. pub uri: Option, + /// Extension-specific attributes trailing the URI. pub ext_attr: Option, } diff --git a/rtc-sdp/src/lib.rs b/rtc-sdp/src/lib.rs index 90581d3f..88ee2d11 100644 --- a/rtc-sdp/src/lib.rs +++ b/rtc-sdp/src/lib.rs @@ -1,9 +1,39 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! SDP parsing and serialization. +//! +//! The Session Description Protocol ([RFC 8866], superseding [RFC 4566]) as WebRTC uses +//! it: offers and answers, media sections, and the attributes that carry codecs, ICE +//! candidates, DTLS fingerprints and header-extension mappings ([RFC 8285]). +//! +//! # Structure +//! +//! * [`SessionDescription`] — a whole session description: `unmarshal` one from a string, +//! `marshal` it back, or build one up section by section. +//! * [`MediaDescription`] — one `m=` section, with its attributes, formats and connection +//! data. +//! * [`extmap`] — `a=extmap` header-extension declarations and the well-known extension +//! URIs. +//! * [`direction`] — `sendrecv`/`sendonly`/`recvonly`/`inactive`. +//! +//! This crate is deliberately a *syntax* layer: it parses and prints SDP faithfully and +//! leaves negotiation semantics ([RFC 8829]) to the [`rtc`](https://docs.rs/rtc) crate, +//! which re-exports it as `rtc::sdp`. +//! +//! [RFC 8866]: https://datatracker.ietf.org/doc/html/rfc8866 +//! [RFC 4566]: https://datatracker.ietf.org/doc/html/rfc4566 +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 +//! [RFC 8829]: https://datatracker.ietf.org/doc/html/rfc8829 + +/// Session and media descriptions — the `v=`/`m=` structure of an SDP document. pub mod description; +/// Transmission direction (`sendrecv`, `sendonly`, `recvonly`, `inactive`). pub mod direction; +/// `a=extmap` RTP header-extension declarations and the well-known extension URIs. pub mod extmap; +/// Parsing helpers plus the codec and connection-role types shared across descriptions. pub mod util; pub(crate) mod lexer; diff --git a/rtc-sdp/src/util/mod.rs b/rtc-sdp/src/util/mod.rs index 92846d8f..3f901ae7 100644 --- a/rtc-sdp/src/util/mod.rs +++ b/rtc-sdp/src/util/mod.rs @@ -6,12 +6,14 @@ use shared::error::{Error, Result}; use std::collections::HashMap; use std::fmt; +/// The `a=` line prefix. pub const ATTRIBUTE_KEY: &str = "a="; /// ConnectionRole indicates which of the end points should initiate the connection establishment #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum ConnectionRole { #[default] + /// No `a=setup` attribute was present. Unspecified, /// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection. @@ -80,12 +82,20 @@ pub(crate) fn new_session_id() -> u64 { // Codec represents a codec #[derive(Debug, Clone, Default, PartialEq, Eq)] +/// One codec offered by a media section, assembled from its `a=rtpmap`, `a=fmtp` and +/// `a=rtcp-fb` attributes. pub struct Codec { + /// The RTP payload type that identifies this codec in the stream. pub payload_type: u8, + /// The encoding name, such as `VP8` or `opus`. pub name: String, + /// The RTP clock rate in Hz. pub clock_rate: u32, + /// Codec-specific encoding parameters — the channel count, for audio. pub encoding_parameters: String, + /// The `a=fmtp` format parameters, verbatim. pub fmtp: String, + /// The `a=rtcp-fb` feedback types negotiated for this codec, such as `nack` or `goog-remb`. pub rtcp_feedback: Vec, } diff --git a/rtc-shared/src/crypto/mod.rs b/rtc-shared/src/crypto/mod.rs index 78b28e03..0084e95a 100644 --- a/rtc-shared/src/crypto/mod.rs +++ b/rtc-shared/src/crypto/mod.rs @@ -5,6 +5,15 @@ use crate::error::Result; /// This trait sits here to avoid getting a direct dependency between /// the dtls and srtp crates. pub trait KeyingMaterialExporter { + /// Derives keying material from the established session, per RFC 5705. + /// + /// `label` and `context` bind the derived key to a purpose — DTLS-SRTP uses the + /// `EXTRACTOR-dtls_srtp` label to obtain SRTP master keys and salts from a completed + /// DTLS handshake. + /// + /// # Errors + /// + /// Fails if the session has not completed its handshake, so no secret is available yet. fn export_keying_material(&self, label: &str, context: &[u8], length: usize) -> Result>; } diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index 4465af77..70a21178 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -24,112 +24,165 @@ pub type Result = std::result::Result; #[derive(Error, Debug, PartialEq)] #[non_exhaustive] pub enum Error { + /// Buffer: full. #[error("buffer: full")] ErrBufferFull, + /// Buffer: closed. #[error("buffer: closed")] ErrBufferClosed, + /// Buffer: short. #[error("buffer: short")] ErrBufferShort, + /// Packet too big. #[error("packet too big")] ErrPacketTooBig, + /// I/O timeout. #[error("i/o timeout")] ErrTimeout, + /// UDP: listener closed. #[error("udp: listener closed")] ErrClosedListener, + /// UDP: listen queue exceeded. #[error("udp: listen queue exceeded")] ErrListenQueueExceeded, + /// UDP: listener accept ch closed. #[error("udp: listener accept ch closed")] ErrClosedListenerAcceptCh, + /// Obs cannot be nil. #[error("obs cannot be nil")] ErrObsCannotBeNil, + /// Se of closed network connection. #[error("se of closed network connection")] ErrUseClosedNetworkConn, + /// Addr is not a net.UDPAddr. #[error("addr is not a net.UDPAddr")] ErrAddrNotUdpAddr, + /// Something went wrong with locAddr. #[error("something went wrong with locAddr")] ErrLocAddr, + /// Already closed. #[error("already closed")] ErrAlreadyClosed, + /// No remAddr defined. #[error("no remAddr defined")] ErrNoRemAddr, + /// Address already in use. #[error("address already in use")] ErrAddressAlreadyInUse, + /// No such UDPConn. #[error("no such UDPConn")] ErrNoSuchUdpConn, + /// Cannot remove unspecified IP by the specified IP. #[error("cannot remove unspecified IP by the specified IP")] ErrCannotRemoveUnspecifiedIp, + /// No address assigned. #[error("no address assigned")] ErrNoAddressAssigned, + /// 1:1 NAT requires more than one mapping. #[error("1:1 NAT requires more than one mapping")] ErrNatRequriesMapping, + /// Length mismtach between mappedIPs and localIPs. #[error("length mismtach between mappedIPs and localIPs")] ErrMismatchLengthIp, + /// Non-udp translation is not supported yet. #[error("non-udp translation is not supported yet")] ErrNonUdpTranslationNotSupported, + /// No associated local address. #[error("no associated local address")] ErrNoAssociatedLocalAddress, + /// No NAT binding found. #[error("no NAT binding found")] ErrNoNatBindingFound, + /// Has no permission. #[error("has no permission")] ErrHasNoPermission, + /// Host name must not be empty. #[error("host name must not be empty")] ErrHostnameEmpty, + /// Failed to parse IP address. #[error("failed to parse IP address")] ErrFailedToParseIpaddr, + /// No interface is available. #[error("no interface is available")] ErrNoInterface, + /// Not found. #[error("not found")] ErrNotFound, + /// Unexpected network. #[error("unexpected network")] ErrUnexpectedNetwork, + /// Can't assign requested address. #[error("can't assign requested address")] ErrCantAssignRequestedAddr, + /// Unknown network. #[error("unknown network")] ErrUnknownNetwork, + /// No router linked. #[error("no router linked")] ErrNoRouterLinked, + /// Invalid port number. #[error("invalid port number")] ErrInvalidPortNumber, + /// Unexpected type-switch failure. #[error("unexpected type-switch failure")] ErrUnexpectedTypeSwitchFailure, + /// Bind failed. #[error("bind failed")] ErrBindFailed, + /// End port is less than the start. #[error("end port is less than the start")] ErrEndPortLessThanStart, + /// Port space exhausted. #[error("port space exhausted")] ErrPortSpaceExhausted, + /// Vnet is not enabled. #[error("vnet is not enabled")] ErrVnetDisabled, + /// Invalid local IP in static_ips. #[error("invalid local IP in static_ips")] ErrInvalidLocalIpInStaticIps, + /// Mapped in static_ips is beyond subnet. #[error("mapped in static_ips is beyond subnet")] ErrLocalIpBeyondStaticIpsSubset, + /// All static_ips must have associated local IPs. #[error("all static_ips must have associated local IPs")] ErrLocalIpNoStaticsIpsAssociated, + /// Router already started. #[error("router already started")] ErrRouterAlreadyStarted, + /// Router already stopped. #[error("router already stopped")] ErrRouterAlreadyStopped, + /// Static IP is beyond subnet. #[error("static IP is beyond subnet")] ErrStaticIpIsBeyondSubnet, + /// Address space exhausted. #[error("address space exhausted")] ErrAddressSpaceExhausted, + /// No IP address is assigned for eth0. #[error("no IP address is assigned for eth0")] ErrNoIpaddrEth0, + /// Invalid mask. #[error("Invalid mask")] ErrInvalidMask, //ExportKeyingMaterial errors + /// TLS handshake is in progress. #[error("tls handshake is in progress")] HandshakeInProgress, + /// Context is not supported for export_keying_material. #[error("context is not supported for export_keying_material")] ContextUnsupported, + /// Export_keying_material can not be used with a reserved label. #[error("export_keying_material can not be used with a reserved label")] ReservedExportKeyingMaterial, + /// No cipher suite for export_keying_material. #[error("no cipher suite for export_keying_material")] CipherSuiteUnset, + /// Export_keying_material hash. #[error("export_keying_material hash: {0}")] Hash(String), + /// Mutex poison. #[error("mutex poison: {0}")] PoisonError(String), @@ -217,346 +270,509 @@ pub enum Error { /// Packet status chunk is not 2 bytes. #[error("Packet status chunk must be 2 bytes")] PacketStatusChunkLength, + /// Invalid bitrate. #[error("Invalid bitrate")] InvalidBitrate, + /// Wrong chunk type. #[error("Wrong chunk type")] WrongChunkType, + /// Struct contains unexpected member type. #[error("Struct contains unexpected member type")] BadStructMemberType, + /// Cannot read into non-pointer. #[error("Cannot read into non-pointer")] BadReadParameter, + /// Invalid block size. #[error("Invalid block size")] InvalidBlockSize, //RTP errors + /// RTP header size insufficient. #[error("RTP header size insufficient")] ErrHeaderSizeInsufficient, + /// RTP header size insufficient for extension. #[error("RTP header size insufficient for extension")] ErrHeaderSizeInsufficientForExtension, + /// Buffer too small. #[error("buffer too small")] ErrBufferTooSmall, + /// Extension not enabled. #[error("extension not enabled")] ErrHeaderExtensionsNotEnabled, + /// Extension not found. #[error("extension not found")] ErrHeaderExtensionNotFound, + /// Header extension id must be between 1 and 14 for RFC 5285 extensions. #[error("header extension id must be between 1 and 14 for RFC 5285 extensions")] ErrRfc8285oneByteHeaderIdrange, + /// Header extension payload must be 16bytes or less for RFC 5285 one byte extensions. #[error("header extension payload must be 16bytes or less for RFC 5285 one byte extensions")] ErrRfc8285oneByteHeaderSize, + /// Header extension id must be between 1 and 255 for RFC 5285 extensions. #[error("header extension id must be between 1 and 255 for RFC 5285 extensions")] ErrRfc8285twoByteHeaderIdrange, + /// Header extension payload must be 255bytes or less for RFC 5285 two byte extensions. #[error("header extension payload must be 255bytes or less for RFC 5285 two byte extensions")] ErrRfc8285twoByteHeaderSize, + /// Header extension id must be 0 for none RFC 5285 extensions. #[error("header extension id must be 0 for none RFC 5285 extensions")] ErrRfc3550headerIdrange, + /// Packet is not large enough. #[error("packet is not large enough")] ErrShortPacket, + /// Invalid nil packet. #[error("invalid nil packet")] ErrNilPacket, + /// Too many PDiff. #[error("too many PDiff")] ErrTooManyPDiff, + /// Too many spatial layers. #[error("too many spatial layers")] ErrTooManySpatialLayers, + /// NALU Type is unhandled. #[error("NALU Type is unhandled")] ErrUnhandledNaluType, + /// Corrupted H265 packet. #[error("corrupted h265 packet")] ErrH265CorruptedPacket, + /// Invalid H265 packet type. #[error("invalid h265 packet type")] ErrInvalidH265PacketType, + /// Payload is too small for OBU extension header. #[error("payload is too small for OBU extension header")] ErrPayloadTooSmallForObuExtensionHeader, + /// Payload is too small for OBU payload size. #[error("payload is too small for OBU payload size")] ErrPayloadTooSmallForObuPayloadSize, + /// Extension_payload must be in 32-bit words. #[error("extension_payload must be in 32-bit words")] HeaderExtensionPayloadNot32BitWords, + /// Audio level overflow. #[error("audio level overflow")] AudioLevelOverflow, + /// Playout delay overflow. #[error("playout delay overflow")] PlayoutDelayOverflow, + /// Payload is not large enough. #[error("payload is not large enough")] PayloadIsNotLargeEnough, + /// STAP-A declared size is larger than buffer. #[error("STAP-A declared size({0}) is larger than buffer({1})")] StapASizeLargerThanBuffer(usize, usize), + /// NALU type is currently not handled. #[error("nalu type {0} is currently not handled")] NaluTypeIsNotHandled(u8), //SRTP + /// Duplicated packet. #[error("duplicated packet")] ErrDuplicated, + /// SRTP master key is not long enough. #[error("SRTP master key is not long enough")] ErrShortSrtpMasterKey, + /// SRTP master salt is not long enough. #[error("SRTP master salt is not long enough")] ErrShortSrtpMasterSalt, + /// No such SRTP Profile. #[error("no such SRTP Profile")] ErrNoSuchSrtpProfile, + /// IndexOverKdr > 0 is not supported yet. #[error("indexOverKdr > 0 is not supported yet")] ErrNonZeroKdrNotSupported, + /// Exporter called with wrong label. #[error("exporter called with wrong label")] ErrExporterWrongLabel, + /// No config provided. #[error("no config provided")] ErrNoConfig, + /// No conn provided. #[error("no conn provided")] ErrNoConn, + /// Failed to verify auth tag. #[error("failed to verify auth tag")] ErrFailedToVerifyAuthTag, + /// Packet is too short to be RTP packet. #[error("packet is too short to be RTP packet")] ErrTooShortRtp, + /// Packet is too short to be RTCP packet. #[error("packet is too short to be RTCP packet")] ErrTooShortRtcp, + /// Payload differs. #[error("payload differs")] ErrPayloadDiffers, + /// Started channel used incorrectly, should only be closed. #[error("started channel used incorrectly, should only be closed")] ErrStartedChannelUsedIncorrectly, + /// Stream has not been inited, unable to close. #[error("stream has not been inited, unable to close")] ErrStreamNotInited, + /// Stream is already closed. #[error("stream is already closed")] ErrStreamAlreadyClosed, + /// Stream is already inited. #[error("stream is already inited")] ErrStreamAlreadyInited, + /// Failed to cast child. #[error("failed to cast child")] ErrFailedTypeAssertion, + /// Exceeded the maximum number of packets. #[error("exceeded the maximum number of packets")] ErrExceededMaxPackets, + /// Index_over_kdr > 0 is not supported yet. #[error("index_over_kdr > 0 is not supported yet")] UnsupportedIndexOverKdr, + /// Invalid master key length for AES_256_cm. #[error("invalid master key length for aes_256_cm")] InvalidMasterKeyLength, + /// Invalid master salt length for AES_256_cm. #[error("invalid master salt length for aes_256_cm")] InvalidMasterSaltLength, + /// Out_len > 32 is not supported for AES_256_cm. #[error("out_len > 32 is not supported for aes_256_cm")] UnsupportedOutLength, + /// SRTP Master Key must be len , got. #[error("SRTP Master Key must be len {0}, got {1}")] SrtpMasterKeyLength(usize, usize), + /// SRTP Salt must be len , got. #[error("SRTP Salt must be len {0}, got {1}")] SrtpSaltLength(usize, usize), + /// SyntaxError. #[error("SyntaxError: {0}")] ExtMapParse(String), + /// SSRC not exist in SRTP_SSRC_state. #[error("ssrc {0} not exist in srtp_ssrc_state")] SsrcMissingFromSrtp(u32), + /// SRTP SSRC= index=: duplicated. #[error("srtp ssrc={0} index={1}: duplicated")] SrtpSsrcDuplicated(u32, u16), + /// Srtcp SSRC= index=: duplicated. #[error("srtcp ssrc={0} index={1}: duplicated")] SrtcpSsrcDuplicated(u32, usize), + /// SSRC not exist in srtcp_SSRC_state. #[error("ssrc {0} not exist in srtcp_ssrc_state")] SsrcMissingFromSrtcp(u32), + /// Stream with SSRC exists. #[error("Stream with ssrc {0} exists")] StreamWithSsrcExists(u32), + /// Session RTP/RTCP type must be same as input buffer. #[error("Session RTP/RTCP type must be same as input buffer")] SessionRtpRtcpTypeMismatch, + /// Session EOF. #[error("Session EOF")] SessionEof, + /// Too short SRTP packet: only bytes, expected > bytes. #[error("too short SRTP packet: only {0} bytes, expected > {1} bytes")] SrtpTooSmall(usize, usize), + /// Too short SRTCP packet: only bytes, expected > bytes. #[error("too short SRTCP packet: only {0} bytes, expected > {1} bytes")] SrtcpTooSmall(usize, usize), + /// Failed to verify RTP auth tag. #[error("failed to verify rtp auth tag")] RtpFailedToVerifyAuthTag, + /// Too short auth tag: only bytes, expected > bytes. #[error("too short auth tag: only {0} bytes, expected > {1} bytes")] RtcpInvalidLengthAuthTag(usize, usize), + /// Failed to verify RTCP auth tag. #[error("failed to verify rtcp auth tag")] RtcpFailedToVerifyAuthTag, + /// SessionSRTP has been closed. #[error("SessionSRTP has been closed")] SessionSrtpAlreadyClosed, + /// This stream is not a RTPStream. #[error("this stream is not a RTPStream")] InvalidRtpStream, + /// This stream is not a RTCPStream. #[error("this stream is not a RTCPStream")] InvalidRtcpStream, //STUN errors + /// Attribute not found. #[error("attribute not found")] ErrAttributeNotFound, + /// Transaction is stopped. #[error("transaction is stopped")] ErrTransactionStopped, + /// Transaction not exists. #[error("transaction not exists")] ErrTransactionNotExists, + /// Transaction exists with same id. #[error("transaction exists with same id")] ErrTransactionExists, + /// Agent is closed. #[error("agent is closed")] ErrAgentClosed, + /// Transaction is timed out. #[error("transaction is timed out")] ErrTransactionTimeOut, + /// No default reason for ErrorCode. #[error("no default reason for ErrorCode")] ErrNoDefaultReason, + /// Unexpected EOF. #[error("unexpected EOF")] ErrUnexpectedEof, + /// Attribute size is invalid. #[error("attribute size is invalid")] ErrAttributeSizeInvalid, + /// Attribute size overflow. #[error("attribute size overflow")] ErrAttributeSizeOverflow, + /// Attempt to decode to nil message. #[error("attempt to decode to nil message")] ErrDecodeToNil, + /// Unexpected EOF: not enough bytes to read header. #[error("unexpected EOF: not enough bytes to read header")] ErrUnexpectedHeaderEof, + /// Integrity check failed. #[error("integrity check failed")] ErrIntegrityMismatch, + /// Fingerprint check failed. #[error("fingerprint check failed")] ErrFingerprintMismatch, + /// FINGERPRINT before MESSAGE-INTEGRITY attribute. #[error("FINGERPRINT before MESSAGE-INTEGRITY attribute")] ErrFingerprintBeforeIntegrity, + /// Bad UNKNOWN-ATTRIBUTES size. #[error("bad UNKNOWN-ATTRIBUTES size")] ErrBadUnknownAttrsSize, + /// Invalid length of IP value. #[error("invalid length of IP value")] ErrBadIpLength, + /// No connection provided. #[error("no connection provided")] ErrNoConnection, + /// Client is closed. #[error("client is closed")] ErrClientClosed, + /// No agent is set. #[error("no agent is set")] ErrNoAgent, + /// Collector is closed. #[error("collector is closed")] ErrCollectorClosed, + /// Unsupported network. #[error("unsupported network")] ErrUnsupportedNetwork, + /// Invalid URL. #[error("invalid url")] ErrInvalidUrl, + /// Unknown scheme type. #[error("unknown scheme type")] ErrSchemeType, + /// Invalid hostname. #[error("invalid hostname")] ErrHost, // TURN errors + /// TURN: RelayAddress must be valid IP to use RelayAddressGeneratorStatic. #[error("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic")] ErrRelayAddressInvalid, + /// TURN: PacketConnConfigs and ConnConfigs are empty, unable to proceed. #[error("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed")] ErrNoAvailableConns, + /// TURN: PacketConnConfig must have a non-nil Conn. #[error("turn: PacketConnConfig must have a non-nil Conn")] ErrConnUnset, + /// TURN: ListenerConfig must have a non-nil Listener. #[error("turn: ListenerConfig must have a non-nil Listener")] ErrListenerUnset, + /// TURN: RelayAddressGenerator has invalid ListeningAddress. #[error("turn: RelayAddressGenerator has invalid ListeningAddress")] ErrListeningAddressInvalid, + /// TURN: RelayAddressGenerator in RelayConfig is unset. #[error("turn: RelayAddressGenerator in RelayConfig is unset")] ErrRelayAddressGeneratorUnset, + /// TURN: max retries exceeded. #[error("turn: max retries exceeded")] ErrMaxRetriesExceeded, + /// TURN: MaxPort must be not 0. #[error("turn: MaxPort must be not 0")] ErrMaxPortNotZero, + /// TURN: MaxPort must be not 0. #[error("turn: MaxPort must be not 0")] ErrMinPortNotZero, + /// TURN: MaxPort less than MinPort. #[error("turn: MaxPort less than MinPort")] ErrMaxPortLessThanMinPort, + /// TURN: relay_conn cannot not be nil. #[error("turn: relay_conn cannot not be nil")] ErrNilConn, + /// TURN: TODO. #[error("turn: TODO")] ErrTodo, + /// TURN: already listening. #[error("turn: already listening")] ErrAlreadyListening, + /// TURN: Server failed to close. #[error("turn: Server failed to close")] ErrFailedToClose, + /// TURN: failed to retransmit transaction. #[error("turn: failed to retransmit transaction")] ErrFailedToRetransmitTransaction, + /// All retransmissions failed. #[error("all retransmissions failed")] ErrAllRetransmissionsFailed, + /// No binding found for channel. #[error("no binding found for channel")] ErrChannelBindNotFound, + /// STUN server address is not set for the client. #[error("STUN server address is not set for the client")] ErrStunserverAddressNotSet, + /// Only one Allocate caller is allowed. #[error("only one Allocate() caller is allowed")] ErrOneAllocateOnly, + /// Already allocated. #[error("already allocated")] ErrAlreadyAllocated, + /// Non-STUN message from STUN server. #[error("non-STUN message from STUN server")] ErrNonStunmessage, + /// Failed to decode STUN message. #[error("failed to decode STUN message")] ErrFailedToDecodeStun, + /// Unexpected STUN request message. #[error("unexpected STUN request message")] ErrUnexpectedStunrequestMessage, + /// Channel number not in [0x4000, 0x7FFF]. #[error("channel number not in [0x4000, 0x7FFF]")] ErrInvalidChannelNumber, + /// ChannelData length != len(Data). #[error("channelData length != len(Data)")] ErrBadChannelDataLength, + /// Invalid value for requested family attribute. #[error("invalid value for requested family attribute")] ErrInvalidRequestedFamilyValue, + /// Fake error. #[error("fake error")] ErrFakeErr, + /// Use of closed network connection. #[error("use of closed network connection")] ErrClosed, + /// Addr is not a net.UDPAddr. #[error("addr is not a net.UDPAddr")] ErrUdpaddrCast, + /// Try-lock is already locked. #[error("try-lock is already locked")] ErrDoubleLock, + /// Transaction closed. #[error("transaction closed")] ErrTransactionClosed, + /// Wait_for_result called on non-result transaction. #[error("wait_for_result called on non-result transaction")] ErrWaitForResultOnNonResultTransaction, + /// Failed to build refresh request. #[error("failed to build refresh request")] ErrFailedToBuildRefreshRequest, + /// Failed to refresh allocation. #[error("failed to refresh allocation")] ErrFailedToRefreshAllocation, + /// Failed to get lifetime from refresh response. #[error("failed to get lifetime from refresh response")] ErrFailedToGetLifetime, + /// Too short buffer. #[error("too short buffer")] ErrShortBuffer, + /// Unexpected response type. #[error("unexpected response type")] ErrUnexpectedResponse, + /// AllocatePacketConn must be set. #[error("AllocatePacketConn must be set")] ErrAllocatePacketConnMustBeSet, + /// AllocateConn must be set. #[error("AllocateConn must be set")] ErrAllocateConnMustBeSet, + /// LeveledLogger must be set. #[error("LeveledLogger must be set")] ErrLeveledLoggerMustBeSet, + /// You cannot use the same channel number with different peer. #[error("you cannot use the same channel number with different peer")] ErrSameChannelDifferentPeer, + /// Allocations must not be created with nil FivTuple. #[error("allocations must not be created with nil FivTuple")] ErrNilFiveTuple, + /// Allocations must not be created with nil FiveTuple.src_addr. #[error("allocations must not be created with nil FiveTuple.src_addr")] ErrNilFiveTupleSrcAddr, + /// Allocations must not be created with nil FiveTuple.dst_addr. #[error("allocations must not be created with nil FiveTuple.dst_addr")] ErrNilFiveTupleDstAddr, + /// Allocations must not be created with nil turnSocket. #[error("allocations must not be created with nil turnSocket")] ErrNilTurnSocket, + /// Allocations must not be created with a lifetime of 0. #[error("allocations must not be created with a lifetime of 0")] ErrLifetimeZero, + /// Allocation attempt created with duplicate FiveTuple. #[error("allocation attempt created with duplicate FiveTuple")] ErrDupeFiveTuple, + /// Failed to cast net.Addr to *net.UDPAddr. #[error("failed to cast net.Addr to *net.UDPAddr")] ErrFailedToCastUdpaddr, + /// Failed to generate nonce. #[error("failed to generate nonce")] ErrFailedToGenerateNonce, + /// Failed to send error message. #[error("failed to send error message")] ErrFailedToSendError, + /// Duplicated Nonce generated, discarding request. #[error("duplicated Nonce generated, discarding request")] ErrDuplicatedNonce, + /// No such user exists. #[error("no such user exists")] ErrNoSuchUser, + /// Unexpected class. #[error("unexpected class")] ErrUnexpectedClass, + /// Unexpected method. #[error("unexpected method")] ErrUnexpectedMethod, + /// Failed to handle. #[error("failed to handle")] ErrFailedToHandle, + /// Unhandled STUN packet. #[error("unhandled STUN packet")] ErrUnhandledStunpacket, + /// Unable to handle ChannelData. #[error("unable to handle ChannelData")] ErrUnableToHandleChannelData, + /// Failed to create STUN message from packet. #[error("failed to create stun message from packet")] ErrFailedToCreateStunpacket, + /// Failed to create channel data from packet. #[error("failed to create channel data from packet")] ErrFailedToCreateChannelData, + /// Relay already allocated for 5-TUPLE. #[error("relay already allocated for 5-TUPLE")] ErrRelayAlreadyAllocatedForFiveTuple, + /// RequestedTransport must be UDP. #[error("RequestedTransport must be UDP")] ErrRequestedTransportMustBeUdp, + /// No support for DONT-FRAGMENT. #[error("no support for DONT-FRAGMENT")] ErrNoDontFragmentSupport, + /// Request must not contain RESERVATION-TOKEN and EVEN-PORT. #[error("Request must not contain RESERVATION-TOKEN and EVEN-PORT")] ErrRequestWithReservationTokenAndEvenPort, + /// No allocation found. #[error("no allocation found")] ErrNoAllocationFound, + /// Unable to handle send-indication, no permission added. #[error("unable to handle send-indication, no permission added")] ErrNoPermission, + /// Packet write smaller than packet. #[error("packet write smaller than packet")] ErrShortWrite, + /// No such channel bind. #[error("no such channel bind")] ErrNoSuchChannelBind, + /// Failed writing to socket. #[error("failed writing to socket")] ErrFailedWriteSocket, @@ -694,186 +910,279 @@ pub enum Error { #[error("conn with same remote addr already exists")] ErrTcpRemoteAddrAlreadyExists, + /// Failed to send packet. #[error("failed to send packet")] ErrSendPacket, + /// Attribute not long enough to be ICE candidate. #[error("attribute not long enough to be ICE candidate")] ErrAttributeTooShortIceCandidate, + /// Could not parse component. #[error("could not parse component")] ErrParseComponent, + /// Could not parse priority. #[error("could not parse priority")] ErrParsePriority, + /// Could not parse port. #[error("could not parse port")] ErrParsePort, + /// Could not parse related addresses. #[error("could not parse related addresses")] ErrParseRelatedAddr, + /// Could not parse type. #[error("could not parse type")] ErrParseType, + /// Unknown candidate type. #[error("unknown candidate type")] ErrUnknownCandidateType, + /// Failed to get XOR-MAPPED-ADDRESS response. #[error("failed to get XOR-MAPPED-ADDRESS response")] ErrGetXorMappedAddrResponse, + /// Connection with same remote address already exists. #[error("connection with same remote address already exists")] ErrConnectionAddrAlreadyExist, + /// Error reading streaming packet. #[error("error reading streaming packet")] ErrReadingStreamingPacket, + /// Error writing to. #[error("error writing to")] ErrWriting, + /// Error closing connection. #[error("error closing connection")] ErrClosingConnection, + /// Unable to determine networkType. #[error("unable to determine networkType")] ErrDetermineNetworkType, + /// Missing protocol scheme. #[error("missing protocol scheme")] ErrMissingProtocolScheme, + /// Too many colons in address. #[error("too many colons in address")] ErrTooManyColonsAddr, + /// Unexpected error trying to read. #[error("unexpected error trying to read")] ErrRead, + /// Unknown role. #[error("unknown role")] ErrUnknownRole, + /// Username mismatch. #[error("username mismatch")] ErrMismatchUsername, + /// The ICE conn can't write STUN messages. #[error("the ICE conn can't write STUN messages")] ErrIceWriteStunMessage, + /// URL parse: relative URL without a base. #[error("url parse: relative URL without a base")] ErrUrlParse, + /// Candidate IP could not be found. #[error("Candidate IP could not be found")] ErrCandidateIpNotFound, // DTLS errors + /// Conn is closed. #[error("conn is closed")] ErrConnClosed, + /// Read/write timeout. #[error("read/write timeout")] ErrDeadlineExceeded, + /// Context is not supported for export_keying_material. #[error("context is not supported for export_keying_material")] ErrContextUnsupported, + /// Packet is too short. #[error("packet is too short")] ErrDtlspacketInvalidLength, + /// Handshake is in progress. #[error("handshake is in progress")] ErrHandshakeInProgress, + /// Invalid content type. #[error("invalid content type")] ErrInvalidContentType, + /// Invalid mac. #[error("invalid mac")] ErrInvalidMac, + /// Packet length and declared length do not match. #[error("packet length and declared length do not match")] ErrInvalidPacketLength, + /// Export_keying_material can not be used with a reserved label. #[error("export_keying_material can not be used with a reserved label")] ErrReservedExportKeyingMaterial, + /// Client sent certificate verify but we have no certificate to verify. #[error("client sent certificate verify but we have no certificate to verify")] ErrCertificateVerifyNoCertificate, + /// Client+server do not support any shared cipher suites. #[error("client+server do not support any shared cipher suites")] ErrCipherSuiteNoIntersection, + /// Server hello can not be created without a cipher suite. #[error("server hello can not be created without a cipher suite")] ErrCipherSuiteUnset, + /// Client sent certificate but did not verify it. #[error("client sent certificate but did not verify it")] ErrClientCertificateNotVerified, + /// Server required client verification, but got none. #[error("server required client verification, but got none")] ErrClientCertificateRequired, + /// Server responded with SRTP Profile we do not support. #[error("server responded with SRTP Profile we do not support")] ErrClientNoMatchingSrtpProfile, + /// Client required Extended Master Secret extension, but server does not support it. #[error("client required Extended Master Secret extension, but server does not support it")] ErrClientRequiredButNoServerEms, + /// Server hello can not be created without a compression method. #[error("server hello can not be created without a compression method")] ErrCompressionMethodUnset, + /// Client+server cookie does not match. #[error("client+server cookie does not match")] ErrCookieMismatch, + /// Cookie must not be longer then 255 bytes. #[error("cookie must not be longer then 255 bytes")] ErrCookieTooLong, + /// PSK Identity Hint provided but PSK is nil. #[error("PSK Identity Hint provided but PSK is nil")] ErrIdentityNoPsk, + /// No certificate provided. #[error("no certificate provided")] ErrInvalidCertificate, + /// Cipher spec invalid. #[error("cipher spec invalid")] ErrInvalidCipherSpec, + /// Invalid or unknown cipher suite. #[error("invalid or unknown cipher suite")] ErrInvalidCipherSuite, + /// Unable to determine if ClientKeyExchange is a public key or PSK Identity. #[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")] ErrInvalidClientKeyExchange, + /// Invalid or unknown compression method. #[error("invalid or unknown compression method")] ErrInvalidCompressionMethod, + /// ECDSA signature contained zero or negative values. #[error("ECDSA signature contained zero or negative values")] ErrInvalidEcdsasignature, + /// Invalid or unknown elliptic curve type. #[error("invalid or unknown elliptic curve type")] ErrInvalidEllipticCurveType, + /// Invalid extension type. #[error("invalid extension type")] ErrInvalidExtensionType, + /// Invalid hash algorithm. #[error("invalid hash algorithm")] ErrInvalidHashAlgorithm, + /// Invalid named curve. #[error("invalid named curve")] ErrInvalidNamedCurve, + /// Invalid private key type. #[error("invalid private key type")] ErrInvalidPrivateKey, + /// Named curve and private key type does not match. #[error("named curve and private key type does not match")] ErrNamedCurveAndPrivateKeyMismatch, + /// Invalid server name format. #[error("invalid server name format")] ErrInvalidSniFormat, + /// Invalid signature algorithm. #[error("invalid signature algorithm")] ErrInvalidSignatureAlgorithm, + /// Expected and actual key signature do not match. #[error("expected and actual key signature do not match")] ErrKeySignatureMismatch, + /// Conn can not be created with a nil nextConn. #[error("Conn can not be created with a nil nextConn")] ErrNilNextConn, + /// Connection can not be created, no CipherSuites satisfy this Config. #[error("connection can not be created, no CipherSuites satisfy this Config")] ErrNoAvailableCipherSuites, + /// Connection can not be created, no SignatureScheme satisfy this Config. #[error("connection can not be created, no SignatureScheme satisfy this Config")] ErrNoAvailableSignatureSchemes, + /// No certificates configured. #[error("no certificates configured")] ErrNoCertificates, + /// No config provided. #[error("no config provided")] ErrNoConfigProvided, + /// Client requested zero or more elliptic curves that are not supported by the server. #[error("client requested zero or more elliptic curves that are not supported by the server")] ErrNoSupportedEllipticCurves, + /// Unsupported protocol version. #[error("unsupported protocol version")] ErrUnsupportedProtocolVersion, + /// Certificate and PSK provided. #[error("Certificate and PSK provided")] ErrPskAndCertificate, + /// PSK and PSK Identity Hint must both be set for client. #[error("PSK and PSK Identity Hint must both be set for client")] ErrPskAndIdentityMustBeSetForClient, + /// SRTP support was requested but server did not respond with use_SRTP extension. #[error("SRTP support was requested but server did not respond with use_srtp extension")] ErrRequestedButNoSrtpExtension, + /// Certificate is mandatory for server. #[error("Certificate is mandatory for server")] ErrServerMustHaveCertificate, + /// Client requested SRTP but we have no matching profiles. #[error("client requested SRTP but we have no matching profiles")] ErrServerNoMatchingSrtpProfile, + /// Server requires the Extended Master Secret extension, but the client does not support it. #[error( "server requires the Extended Master Secret extension, but the client does not support it" )] ErrServerRequiredButNoClientEms, + /// Expected and actual verify data does not match. #[error("expected and actual verify data does not match")] ErrVerifyDataMismatch, + /// Handshake message unset, unable to marshal. #[error("handshake message unset, unable to marshal")] ErrHandshakeMessageUnset, + /// Invalid flight number. #[error("invalid flight number")] ErrInvalidFlight, + /// Unable to generate key signature, unimplemented. #[error("unable to generate key signature, unimplemented")] ErrKeySignatureGenerateUnimplemented, + /// Unable to verify key signature, unimplemented. #[error("unable to verify key signature, unimplemented")] ErrKeySignatureVerifyUnimplemented, + /// Data length and declared length do not match. #[error("data length and declared length do not match")] ErrLengthMismatch, + /// Buffer not long enough to contain nonce. #[error("buffer not long enough to contain nonce")] ErrNotEnoughRoomForNonce, + /// Feature has not been implemented yet. #[error("feature has not been implemented yet")] ErrNotImplemented, + /// Sequence number overflow. #[error("sequence number overflow")] ErrSequenceNumberOverflow, + /// Unable to marshal fragmented handshakes. #[error("unable to marshal fragmented handshakes")] ErrUnableToMarshalFragmented, + /// Invalid state machine transition. #[error("invalid state machine transition")] ErrInvalidFsmTransition, + /// ApplicationData with epoch of 0. #[error("ApplicationData with epoch of 0")] ErrApplicationDataEpochZero, + /// Unhandled contentType. #[error("unhandled contentType")] ErrUnhandledContextType, + /// Context canceled. #[error("context canceled")] ErrContextCanceled, + /// Empty fragment. #[error("empty fragment")] ErrEmptyFragment, + /// Alert is Fatal or Close Notify. #[error("Alert is Fatal or Close Notify")] ErrAlertFatalOrClose, + /// Fragment buffer overflow. New size is greater than specified max. #[error( "Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}" )] - ErrFragmentBufferOverflow { new_size: usize, max_size: usize }, + ErrFragmentBufferOverflow { + /// The size the buffer would have grown to. + new_size: usize, + /// The configured maximum. + max_size: usize, + }, + /// Client transport is not set yet. #[error("Client transport is not set yet")] ErrClientTransportNotSet, @@ -903,336 +1212,496 @@ pub enum Error { NoServerConfig, //SCTP errors + /// Raw is too small for a SCTP chunk. #[error("raw is too small for a SCTP chunk")] ErrChunkHeaderTooSmall, + /// Not enough data left in SCTP packet to satisfy requested length. #[error("not enough data left in SCTP packet to satisfy requested length")] ErrChunkHeaderNotEnoughSpace, + /// Chunk PADDING is non-zero at offset. #[error("chunk PADDING is non-zero at offset")] ErrChunkHeaderPaddingNonZero, + /// Chunk has invalid length. #[error("chunk has invalid length")] ErrChunkHeaderInvalidLength, + /// ChunkType is not of type ABORT. #[error("ChunkType is not of type ABORT")] ErrChunkTypeNotAbort, + /// Failed build Abort Chunk. #[error("failed build Abort Chunk")] ErrBuildAbortChunkFailed, + /// ChunkType is not of type COOKIEACK. #[error("ChunkType is not of type COOKIEACK")] ErrChunkTypeNotCookieAck, + /// ChunkType is not of type COOKIEECHO. #[error("ChunkType is not of type COOKIEECHO")] ErrChunkTypeNotCookieEcho, + /// ChunkType is not of type ctError. #[error("ChunkType is not of type ctError")] ErrChunkTypeNotCt, + /// Failed build Error Chunk. #[error("failed build Error Chunk")] ErrBuildErrorChunkFailed, + /// Failed to marshal stream. #[error("failed to marshal stream")] ErrMarshalStreamFailed, + /// Chunk too short. #[error("chunk too short")] ErrChunkTooShort, + /// ChunkType is not of type ForwardTsn. #[error("ChunkType is not of type ForwardTsn")] ErrChunkTypeNotForwardTsn, + /// ChunkType is not of type HEARTBEAT. #[error("ChunkType is not of type HEARTBEAT")] ErrChunkTypeNotHeartbeat, + /// ChunkType is not of type HEARTBEATACK. #[error("ChunkType is not of type HEARTBEATACK")] ErrChunkTypeNotHeartbeatAck, + /// Heartbeat is not long enough to contain Heartbeat Info. #[error("heartbeat is not long enough to contain Heartbeat Info")] ErrHeartbeatNotLongEnoughInfo, + /// Failed to parse param type. #[error("failed to parse param type")] ErrParseParamTypeFailed, + /// Heartbeat should only have HEARTBEAT param. #[error("heartbeat should only have HEARTBEAT param")] ErrHeartbeatParam, + /// Failed unmarshalling param in Heartbeat Chunk. #[error("failed unmarshalling param in Heartbeat Chunk")] ErrHeartbeatChunkUnmarshal, + /// Unimplemented. #[error("unimplemented")] ErrUnimplemented, + /// Heartbeat Ack must have one param. #[error("heartbeat Ack must have one param")] ErrHeartbeatAckParams, + /// Heartbeat Ack must have one param, and it should be a HeartbeatInfo. #[error("heartbeat Ack must have one param, and it should be a HeartbeatInfo")] ErrHeartbeatAckNotHeartbeatInfo, + /// Unable to marshal parameter for Heartbeat Ack. #[error("unable to marshal parameter for Heartbeat Ack")] ErrHeartbeatAckMarshalParam, + /// Raw is too small for error cause. #[error("raw is too small for error cause")] ErrErrorCauseTooSmall, + /// Unhandled ParamType. #[error("unhandled ParamType: {typ}")] - ErrParamTypeUnhandled { typ: u16 }, + ErrParamTypeUnhandled { + /// The raw parameter type that was not recognised. + typ: u16, + }, + /// Unexpected ParamType. #[error("unexpected ParamType")] ErrParamTypeUnexpected, + /// Param header too short. #[error("param header too short")] ErrParamHeaderTooShort, + /// Param self reported length is shorter than header length. #[error("param self reported length is shorter than header length")] ErrParamHeaderSelfReportedLengthShorter, + /// Param self reported length is longer than header length. #[error("param self reported length is longer than header length")] ErrParamHeaderSelfReportedLengthLonger, + /// Failed to parse param type. #[error("failed to parse param type")] ErrParamHeaderParseFailed, + /// Packet to short. #[error("packet to short")] ErrParamPacketTooShort, + /// Outgoing SSN reset request parameter too short. #[error("outgoing SSN reset request parameter too short")] ErrSsnResetRequestParamTooShort, + /// Failed unmarshalling SSN reset request parameter in RE-CONFIG chunk. #[error("failed unmarshalling SSN reset request parameter in RE-CONFIG chunk")] ErrUnmarshalSsnResetRequestParam, + /// Reconfig response parameter too short. #[error("reconfig response parameter too short")] ErrReconfigRespParamTooShort, + /// Failed unmarshalling re-configuration response parameter in RE-CONFIG chunk. #[error("failed unmarshalling re-configuration response parameter in RE-CONFIG chunk")] ErrUnmarshalReconfigRespParam, + /// Invalid algorithm type. #[error("invalid algorithm type")] ErrInvalidAlgorithmType, + /// Failed to parse param type. #[error("failed to parse param type")] ErrInitChunkParseParamTypeFailed, + /// Failed unmarshalling param in Init Chunk. #[error("failed unmarshalling param in Init Chunk")] ErrInitChunkUnmarshalParam, + /// Unable to marshal parameter for INIT/INITACK. #[error("unable to marshal parameter for INIT/INITACK")] ErrInitAckMarshalParam, + /// ChunkType is not of type INIT. #[error("ChunkType is not of type INIT")] ErrChunkTypeNotTypeInit, + /// Chunk Value isn't long enough for mandatory parameters exp. #[error("chunk Value isn't long enough for mandatory parameters exp")] ErrChunkValueNotLongEnough, + /// ChunkType of type INIT flags must be all 0. #[error("ChunkType of type INIT flags must be all 0")] ErrChunkTypeInitFlagZero, + /// Failed to unmarshal INIT body. #[error("failed to unmarshal INIT body")] ErrChunkTypeInitUnmarshalFailed, + /// Failed marshaling INIT common data. #[error("failed marshaling INIT common data")] ErrChunkTypeInitMarshalFailed, + /// ChunkType of type INIT ACK InitiateTag must not be 0. #[error("ChunkType of type INIT ACK InitiateTag must not be 0")] ErrChunkTypeInitInitiateTagZero, + /// INIT ACK inbound stream request must be > 0. #[error("INIT ACK inbound stream request must be > 0")] ErrInitInboundStreamRequestZero, + /// INIT ACK outbound stream request must be > 0. #[error("INIT ACK outbound stream request must be > 0")] ErrInitOutboundStreamRequestZero, + /// INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500. #[error("INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500")] ErrInitAdvertisedReceiver1500, + /// Packet is smaller than the header size. #[error("packet is smaller than the header size")] ErrChunkPayloadSmall, + /// ChunkType is not of type PayloadData. #[error("ChunkType is not of type PayloadData")] ErrChunkTypeNotPayloadData, + /// Failed unmarshalling payload data chunk. #[error("failed unmarshalling payload data chunk")] ErrChunkUnmarshalPayloadData, + /// ChunkType is not of type Reconfig. #[error("ChunkType is not of type Reconfig")] ErrChunkTypeNotReconfig, + /// ChunkReconfig has invalid ParamA. #[error("ChunkReconfig has invalid ParamA")] ErrChunkReconfigInvalidParamA, + /// Failed to parse param type. #[error("failed to parse param type")] ErrChunkParseParamTypeFailed, + /// Unable to marshal parameter A for reconfig. #[error("unable to marshal parameter A for reconfig")] ErrChunkMarshalParamAReconfigFailed, + /// Unable to marshal parameter B for reconfig. #[error("unable to marshal parameter B for reconfig")] ErrChunkMarshalParamBReconfigFailed, + /// ChunkType is not of type SACK. #[error("ChunkType is not of type SACK")] ErrChunkTypeNotSack, + /// SACK Chunk size is not large enough to contain header. #[error("SACK Chunk size is not large enough to contain header")] ErrSackSizeNotLargeEnoughInfo, + /// Failed unmarshalling SACK chunk. #[error("failed unmarshalling SACK chunk")] ErrChunkUnmarshalSack, + /// Invalid chunk size. #[error("invalid chunk size")] ErrInvalidChunkSize, + /// ChunkType is not of type SHUTDOWN. #[error("ChunkType is not of type SHUTDOWN")] ErrChunkTypeNotShutdown, + /// Failed unmarshalling shutdown chunk. #[error("failed unmarshalling shutdown chunk")] ErrChunkUnmarshalShutdown, + /// ChunkType is not of type SHUTDOWN-ACK. #[error("ChunkType is not of type SHUTDOWN-ACK")] ErrChunkTypeNotShutdownAck, + /// ChunkType is not of type SHUTDOWN-COMPLETE. #[error("ChunkType is not of type SHUTDOWN-COMPLETE")] ErrChunkTypeNotShutdownComplete, + /// Raw is smaller than the minimum length for a SCTP packet. #[error("raw is smaller than the minimum length for a SCTP packet")] ErrPacketRawTooSmall, + /// Unable to parse SCTP chunk, not enough data for complete header. #[error("unable to parse SCTP chunk, not enough data for complete header")] ErrParseSctpChunkNotEnoughData, + /// Failed to unmarshal, contains unknown chunk type. #[error("failed to unmarshal, contains unknown chunk type")] ErrUnmarshalUnknownChunkType, + /// Checksum mismatch theirs. #[error("checksum mismatch theirs")] ErrChecksumMismatch, + /// Unexpected chunk popped (unordered). #[error("unexpected chunk popped (unordered)")] ErrUnexpectedChuckPoppedUnordered, + /// Unexpected chunk popped (ordered). #[error("unexpected chunk popped (ordered)")] ErrUnexpectedChuckPoppedOrdered, + /// Unexpected q state (should've been selected). #[error("unexpected q state (should've been selected)")] ErrUnexpectedQState, + /// Try again. #[error("try again")] ErrTryAgain, + /// Abort chunk, with following errors. #[error("abort chunk, with following errors: {0}")] ErrAbortChunk(String), + /// Shutdown called in non-Established state. #[error("shutdown called in non-Established state")] ErrShutdownNonEstablished, + /// Association closed before connecting. #[error("association closed before connecting")] ErrAssociationClosedBeforeConn, + /// Association init failed. #[error("association init failed")] ErrAssociationInitFailed, + /// Association handshake closed. #[error("association handshake closed")] ErrAssociationHandshakeClosed, + /// Silently discard. #[error("silently discard")] ErrSilentlyDiscard, + /// The init not stored to send. #[error("the init not stored to send")] ErrInitNotStoredToSend, + /// CookieEcho not stored to send. #[error("cookieEcho not stored to send")] ErrCookieEchoNotStoredToSend, + /// SCTP packet must not have a source port of 0. #[error("sctp packet must not have a source port of 0")] ErrSctpPacketSourcePortZero, + /// SCTP packet must not have a destination port of 0. #[error("sctp packet must not have a destination port of 0")] ErrSctpPacketDestinationPortZero, + /// Init chunk must not be bundled with any other chunk. #[error("init chunk must not be bundled with any other chunk")] ErrInitChunkBundled, + /// Init chunk expects a verification tag of 0 on the packet when out-of-the-blue. #[error("init chunk expects a verification tag of 0 on the packet when out-of-the-blue")] ErrInitChunkVerifyTagNotZero, + /// Todo: handle Init when in state. #[error("todo: handle Init when in state")] ErrHandleInitState, + /// No cookie in InitAck. #[error("no cookie in InitAck")] ErrInitAckNoCookie, + /// There already exists a stream with identifier. #[error("there already exists a stream with identifier")] ErrStreamAlreadyExist, + /// Failed to create a stream with identifier. #[error("Failed to create a stream with identifier")] ErrStreamCreateFailed, + /// Unable to be popped from inflight queue TSN. #[error("unable to be popped from inflight queue TSN")] ErrInflightQueueTsnPop, + /// Requested non-existent TSN. #[error("requested non-existent TSN")] ErrTsnRequestNotExist, + /// Sending reset packet in non-Established state. #[error("sending reset packet in non-Established state")] ErrResetPacketInStateNotExist, + /// Unexpected parameter type. #[error("unexpected parameter type")] ErrParameterType, + /// Sending payload data in non-Established state. #[error("sending payload data in non-Established state")] ErrPayloadDataStateNotExist, + /// Unhandled chunk type. #[error("unhandled chunk type")] ErrChunkTypeUnhandled, + /// Handshake failed (INIT ACK). #[error("handshake failed (INIT ACK)")] ErrHandshakeInitAck, + /// Handshake failed (COOKIE ECHO). #[error("handshake failed (COOKIE ECHO)")] ErrHandshakeCookieEcho, + /// Outbound packet larger than maximum message size. #[error("outbound packet larger than maximum message size")] ErrOutboundPacketTooLarge, + /// Stream closed. #[error("Stream closed")] ErrStreamClosed, + /// Stream not existed. #[error("Stream not existed")] ErrStreamNotExisted, + /// Association not existed. #[error("Association not existed")] ErrAssociationNotExisted, + /// Transport not existed. #[error("Transport not existed")] ErrTransportNoExisted, + /// Io EOF. #[error("Io EOF")] ErrEof, + /// Invalid SystemTime. #[error("Invalid SystemTime")] ErrInvalidSystemTime, + /// Net Conn read error. #[error("Net Conn read error")] ErrNetConnRead, + /// Max Data Channel ID. #[error("Max Data Channel ID")] ErrMaxDataChannelID, //Data Channel + /// DataChannel message is not long enough to determine type: (expected: , actual: ). #[error( "DataChannel message is not long enough to determine type: (expected: {expected}, actual: {actual})" )] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, + UnexpectedEndOfBuffer { + /// The number of bytes the parser required. + expected: usize, + /// The number of bytes actually available. + actual: usize, + }, + /// Unknown MessageType. #[error("Unknown MessageType {0}")] InvalidMessageType(u8), + /// Unknown ChannelType. #[error("Unknown ChannelType {0}")] InvalidChannelType(u8), + /// Unknown PayloadProtocolIdentifier. #[error("Unknown PayloadProtocolIdentifier {0}")] InvalidPayloadProtocolIdentifier(u8), + /// Unknow Protocol. #[error("Unknow Protocol")] UnknownProtocol, //Media + /// Stream is nil. #[error("stream is nil")] ErrNilStream, + /// Incomplete frame header. #[error("incomplete frame header")] ErrIncompleteFrameHeader, + /// Incomplete frame data. #[error("incomplete frame data")] ErrIncompleteFrameData, + /// Incomplete file header. #[error("incomplete file header")] ErrIncompleteFileHeader, + /// IVF signature mismatch. #[error("IVF signature mismatch")] ErrSignatureMismatch, + /// IVF version unknown, parser may not parse correctly. #[error("IVF version unknown, parser may not parse correctly")] ErrUnknownIVFVersion, + /// File not opened. #[error("file not opened")] ErrFileNotOpened, + /// Invalid nil packet. #[error("invalid nil packet")] ErrInvalidNilPacket, + /// Bad header signature. #[error("bad header signature")] ErrBadIDPageSignature, + /// Wrong header, expected beginning of stream. #[error("wrong header, expected beginning of stream")] ErrBadIDPageType, + /// Payload for id page must be 19 bytes. #[error("payload for id page must be 19 bytes")] ErrBadIDPageLength, + /// Bad payload signature. #[error("bad payload signature")] ErrBadIDPagePayloadSignature, + /// Not enough data for payload header. #[error("not enough data for payload header")] ErrShortPageHeader, + /// Bad OpusTags signature. #[error("bad OpusTags signature")] ErrBadOpusTagsSignature, + /// Unsupported channel mapping family. #[error("unsupported channel mapping family")] ErrUnsupportedChannelMappingFamily, + /// Data is not a H264 bitstream. #[error("data is not a H264 bitstream")] ErrDataIsNotH264Stream, + /// Data is not a H265 bitstream. #[error("data is not a H265 bitstream")] ErrDataIsNotH265Stream, + /// Io EOF. #[error("Io EOF")] ErrIoEOF, // mDNS + /// MDNS: port not support, only 5353 is supported. #[error("mDNS: port not support, only 5353 is supported")] ErrMDNSPortNotSupported, + /// MDNS: connection is closed. #[error("mDNS: connection is closed")] ErrMDNSConnectionClosed, + /// MDNS: query not found. #[error("mDNS: query not found")] ErrMDNSQueryNotFound, + /// MDNS: parsing/packing of this type isn't available yet. #[error("mDNS: parsing/packing of this type isn't available yet")] ErrNotStarted, + /// MDNS: parsing/packing of this section has completed. #[error("mDNS: parsing/packing of this section has completed")] ErrSectionDone, + /// MDNS: parsing/packing of this section is header. #[error("mDNS: parsing/packing of this section is header")] ErrSectionHeader, + /// MDNS: insufficient data for base length type. #[error("mDNS: insufficient data for base length type")] ErrBaseLen, + /// MDNS: insufficient data for calculated length type. #[error("mDNS: insufficient data for calculated length type")] ErrCalcLen, + /// MDNS: segment prefix is reserved. #[error("mDNS: segment prefix is reserved")] ErrReserved, + /// MDNS: too many pointers (>10). #[error("mDNS: too many pointers (>10)")] ErrTooManyPtr, + /// MDNS: invalid pointer. #[error("mDNS: invalid pointer")] ErrInvalidPtr, + /// MDNS: nil resource body. #[error("mDNS: nil resource body")] ErrNilResourceBody, + /// MDNS: insufficient data for resource body length. #[error("mDNS: insufficient data for resource body length")] ErrResourceLen, + /// MDNS: segment length too long. #[error("mDNS: segment length too long")] ErrSegTooLong, + /// MDNS: zero length segment. #[error("mDNS: zero length segment")] ErrZeroSegLen, + /// MDNS: resource length too long. #[error("mDNS: resource length too long")] ErrResTooLong, + /// MDNS: too many Questions to pack (>65535). #[error("mDNS: too many Questions to pack (>65535)")] ErrTooManyQuestions, + /// MDNS: too many Answers to pack (>65535). #[error("mDNS: too many Answers to pack (>65535)")] ErrTooManyAnswers, + /// MDNS: too many Authorities to pack (>65535). #[error("mDNS: too many Authorities to pack (>65535)")] ErrTooManyAuthorities, + /// MDNS: too many Additionals to pack (>65535). #[error("mDNS: too many Additionals to pack (>65535)")] ErrTooManyAdditionals, + /// MDNS: name is not in canonical format (it must end with a .). #[error("mDNS: name is not in canonical format (it must end with a .)")] ErrNonCanonicalName, + /// MDNS: character string exceeds maximum length (255). #[error("mDNS: character string exceeds maximum length (255)")] ErrStringTooLong, + /// MDNS: compressed name in SRV resource data. #[error("mDNS: compressed name in SRV resource data")] ErrCompressedSrv, + /// MDNS: empty builder msg. #[error("mDNS: empty builder msg")] ErrEmptyBuilderMsg, @@ -1422,12 +1891,15 @@ pub enum Error { #[error("unable to start track, codec is not supported by remote")] ErrUnsupportedCodec, + /// Invalid state error. #[error("Invalid state error")] InvalidStateError, + /// Invalid modification error. #[error("Invalid modification error")] InvalidModificationError, + /// Range error. #[error("Range error {0}")] RangeError(String), @@ -1445,6 +1917,7 @@ pub enum Error { #[error("new track must be of the same kind as previous")] ErrRTPSenderNewTrackHasIncorrectKind, + /// New track has incorrect envelope. #[error("new track has incorrect envelope")] ErrRTPSenderNewTrackHasIncorrectEnvelope, @@ -1469,6 +1942,7 @@ pub enum Error { #[error("a header extension must be registered with the same direction each time")] ErrRegisterHeaderExtensionInvalidDirection, + /// Invalid direction. #[error("invalid direction")] ErrInvalidDirection, @@ -1483,164 +1957,243 @@ pub enum Error { #[error("simulcast probe limit has been reached, new SSRC has been discarded")] ErrSimulcastProbeOverflow, + /// Enable detaching by calling webrtc.DetachDataChannels. #[error("enable detaching by calling webrtc.DetachDataChannels()")] ErrDetachNotEnabled, + /// Datachannel not opened yet, try calling Detach from OnOpen. #[error("datachannel not opened yet, try calling Detach from OnOpen")] ErrDetachBeforeOpened, + /// The DTLS transport has not started yet. #[error("the DTLS transport has not started yet")] ErrDtlsTransportNotStarted, + /// Failed extracting keys from DTLS for SRTP. #[error("failed extracting keys from DTLS for SRTP")] ErrDtlsKeyExtractionFailed, + /// Failed to start SRTP. #[error("failed to start SRTP")] ErrFailedToStartSRTP, + /// Failed to start SRTCP. #[error("failed to start SRTCP")] ErrFailedToStartSRTCP, + /// Attempted to start DTLSTransport that is not in new state. #[error("attempted to start DTLSTransport that is not in new state")] ErrInvalidDTLSStart, + /// Peer didn't provide certificate via DTLS. #[error("peer didn't provide certificate via DTLS")] ErrNoRemoteCertificate, + /// Identity provider is not implemented. #[error("identity provider is not implemented")] ErrIdentityProviderNotImplemented, + /// Remote certificate does not match any fingerprint. #[error("remote certificate does not match any fingerprint")] ErrNoMatchingCertificateFingerprint, + /// Unsupported fingerprint algorithm. #[error("unsupported fingerprint algorithm")] ErrUnsupportedFingerprintAlgorithm, + /// ICE connection not started. #[error("ICE connection not started")] ErrICEConnectionNotStarted, + /// Unknown candidate type. #[error("unknown candidate type")] ErrICECandidateTypeUnknown, + /// Cannot convert ICE.CandidateType into webrtc.ICECandidateType, invalid type. #[error("cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type")] ErrICEInvalidConvertCandidateType, + /// ICEAgent does not exist. #[error("ICEAgent does not exist")] ErrICEAgentNotExist, + /// Unable to convert ICE candidates to ICECandidates. #[error("unable to convert ICE candidates to ICECandidates")] ErrICECandidatesConversionFailed, + /// Unknown ICE Role. #[error("unknown ICE Role")] ErrICERoleUnknown, + /// Unknown protocol. #[error("unknown protocol")] ErrICEProtocolUnknown, + /// Gatherer not started. #[error("gatherer not started")] ErrICEGathererNotStarted, + /// Unknown network type. #[error("unknown network type")] ErrNetworkTypeUnknown, + /// New SDP does not match previous offer. #[error("new sdp does not match previous offer")] ErrSDPDoesNotMatchOffer, + /// New SDP does not match previous answer. #[error("new sdp does not match previous answer")] ErrSDPDoesNotMatchAnswer, + /// Provided value is not a valid enum value of type SDPType. #[error("provided value is not a valid enum value of type SDPType")] ErrPeerConnSDPTypeInvalidValue, + /// Invalid state change op. #[error("invalid state change op")] ErrPeerConnStateChangeInvalid, + /// Unhandled state change op. #[error("unhandled state change op")] ErrPeerConnStateChangeUnhandled, + /// Invalid SDP type supplied to SetLocalDescription. #[error("invalid SDP type supplied to SetLocalDescription()")] ErrPeerConnSDPTypeInvalidValueSetLocalDescription, + /// RemoteDescription contained media section without mid value. #[error("remoteDescription contained media section without mid value")] ErrPeerConnRemoteDescriptionWithoutMidValue, + /// LocalDescription contained media section without mid value. #[error("localDescription contained media section without mid value")] ErrPeerConnLocalDescriptionWithoutMidValue, + /// RemoteDescription has not been set yet. #[error("remoteDescription has not been set yet")] ErrPeerConnRemoteDescriptionNil, + /// LocalDescription has not been set yet. #[error("localDescription has not been set yet")] ErrPeerConnLocalDescriptionNil, + /// Single media section has an explicit SSRC. #[error("single media section has an explicit SSRC")] ErrPeerConnSingleMediaSectionHasExplicitSSRC, + /// Could not add transceiver for remote SSRC. #[error("could not add transceiver for remote SSRC")] ErrPeerConnRemoteSSRCAddTransceiver, + /// Mid RTP Extensions required for Simulcast. #[error("mid RTP Extensions required for Simulcast")] ErrPeerConnSimulcastMidRTPExtensionRequired, + /// Stream id RTP Extensions required for Simulcast. #[error("stream id RTP Extensions required for Simulcast")] ErrPeerConnSimulcastStreamIDRTPExtensionRequired, + /// Incoming SSRC failed Simulcast probing. #[error("incoming SSRC failed Simulcast probing")] ErrPeerConnSimulcastIncomingSSRCFailed, + /// Failed collecting stats. #[error("failed collecting stats")] ErrPeerConnStatsCollectionFailed, + /// Add_transceiver_from_kind only accepts one RTPTransceiverInit. #[error("add_transceiver_from_kind only accepts one RTPTransceiverInit")] ErrPeerConnAddTransceiverFromKindOnlyAcceptsOne, + /// Add_transceiver_from_track only accepts one RTPTransceiverInit. #[error("add_transceiver_from_track only accepts one RTPTransceiverInit")] ErrPeerConnAddTransceiverFromTrackOnlyAcceptsOne, + /// Add_transceiver_from_kind currently only supports recvonly. #[error("add_transceiver_from_kind currently only supports recvonly")] ErrPeerConnAddTransceiverFromKindSupport, + /// Add_transceiver_from_track currently only supports sendonly and sendrecv. #[error("add_transceiver_from_track currently only supports sendonly and sendrecv")] ErrPeerConnAddTransceiverFromTrackSupport, + /// TODO set_identity_provider. #[error("TODO set_identity_provider")] ErrPeerConnSetIdentityProviderNotImplemented, + /// Write_RTCP failed to open write_stream. #[error("write_rtcp failed to open write_stream")] ErrPeerConnWriteRTCPOpenWriteStream, + /// Cannot find transceiver with mid. #[error("cannot find transceiver with mid")] ErrPeerConnTransceiverMidNil, + /// DTLSTransport must not be nil. #[error("DTLSTransport must not be nil")] ErrRTPReceiverDTLSTransportNil, + /// Receive has already been called. #[error("Receive has already been called")] ErrRTPReceiverReceiveAlreadyCalled, + /// Unable to find stream for Track with SSRC. #[error("unable to find stream for Track with SSRC")] ErrRTPReceiverWithSSRCTrackStreamNotFound, + /// No trackStreams found for SSRC. #[error("no trackStreams found for SSRC")] ErrRTPReceiverForSSRCTrackStreamNotFound, + /// No trackStreams found for RID. #[error("no trackStreams found for RID")] ErrRTPReceiverForRIDTrackStreamNotFound, + /// Invalid RTP Receiver transition. #[error("invalid RTP Receiver transition")] ErrRTPReceiverStateChangeInvalid, + /// Track must not be nil. #[error("Track must not be nil")] ErrRTPSenderTrackNil, + /// RTPSender not existed. #[error("RTPSender not existed")] ErrRTPSenderNotExisted, + /// Sender Track has been removed or replaced to nil. #[error("Sender Track has been removed or replaced to nil")] ErrRTPSenderTrackRemoved, + /// Sender cannot add encoding as rid is empty. #[error("Sender cannot add encoding as rid is empty")] ErrRTPSenderRidNil, + /// Sender cannot add encoding as there is no base track. #[error("Sender cannot add encoding as there is no base track")] ErrRTPSenderNoBaseEncoding, + /// Sender cannot add encoding as provided track does not match base track. #[error("Sender cannot add encoding as provided track does not match base track")] ErrRTPSenderBaseEncodingMismatch, + /// Sender cannot encoding due to RID collision. #[error("Sender cannot encoding due to RID collision")] ErrRTPSenderRIDCollision, + /// Sender does not have track for RID. #[error("Sender does not have track for RID")] ErrRTPSenderNoTrackForRID, + /// RTPReceiver not existed. #[error("RTPReceiver not existed")] ErrRTPReceiverNotExisted, + /// DTLSTransport must not be nil. #[error("DTLSTransport must not be nil")] ErrRTPSenderDTLSTransportNil, + /// Send has already been called. #[error("Send has already been called")] ErrRTPSenderSendAlreadyCalled, + /// RTPTransceiver not existed. #[error("RTPTransceiver not existed")] ErrRTPTransceiverNotExisted, + /// ErrRTPSenderTrackNil. #[error("errRTPSenderTrackNil")] ErrRTPTransceiverCannotChangeMid, + /// Invalid state change in RTPTransceiver.setSending. #[error("invalid state change in RTPTransceiver.setSending")] ErrRTPTransceiverSetSendingInvalidState, + /// Unsupported codec type by this transceiver. #[error("unsupported codec type by this transceiver")] ErrRTPTransceiverCodecUnsupported, + /// DTLS not established. #[error("DTLS not established")] ErrSCTPTransportDTLS, + /// Add_transceiver_SDP called with 0 transceivers. #[error("add_transceiver_sdp() called with 0 transceivers")] ErrSDPZeroTransceivers, + /// Invalid Media Section. Media + DataChannel both enabled. #[error("invalid Media Section. Media + DataChannel both enabled")] ErrSDPMediaSectionMediaDataChanInvalid, + /// Invalid Media Section Track Index. #[error("invalid Media Section Track Index")] ErrSDPMediaSectionTrackInvalid, + /// Set_answering_dtlsrole must DTLSRoleClient or DTLSRoleServer. #[error("set_answering_dtlsrole must DTLSRoleClient or DTLSRoleServer")] ErrSettingEngineSetAnsweringDTLSRole, + /// Can't rollback from stable state. #[error("can't rollback from stable state")] ErrSignalingStateCannotRollback, + /// Invalid proposed signaling state transition. #[error("invalid proposed signaling state transition: {0}")] ErrSignalingStateProposedTransitionInvalid(String), + /// Cannot convert to StatsICECandidatePairStateSucceeded invalid ICE candidate state. #[error("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state")] ErrStatsICECandidateStateInvalid, + /// ICETransport can only be called in ICETransportStateNew. #[error("ICETransport can only be called in ICETransportStateNew")] ErrICETransportNotInNew, + /// Bad Certificate PEM format. #[error("bad Certificate PEM format")] ErrCertificatePEMFormatError, + /// SCTP is not established. #[error("SCTP is not established")] ErrSCTPNotEstablished, + /// DataChannel is not opened. #[error("DataChannel is not opened")] ErrClosedPipe, + /// Interceptor is not bind. #[error("Interceptor is not bind")] ErrInterceptorNotBind, + /// Excessive retries in CreateOffer. #[error("excessive retries in CreateOffer")] ErrExcessiveRetries, + /// Not long enough to be a RTP Packet. #[error("not long enough to be a RTP Packet")] ErrRTPTooShort, @@ -1652,82 +2205,123 @@ pub enum Error { SimulcastRidParseErrorUnknownDirection, //SDP + /// Codec not found. #[error("codec not found")] CodecNotFound, + /// Missing whitespace. #[error("missing whitespace")] MissingWhitespace, + /// Missing colon. #[error("missing colon")] MissingColon, + /// Payload type not found. #[error("payload type not found")] PayloadTypeNotFound, + /// SdpInvalidSyntax. #[error("SdpInvalidSyntax: {0}")] SdpInvalidSyntax(String), + /// SdpInvalidValue. #[error("SdpInvalidValue: {0}")] SdpInvalidValue(String), + /// SDP: empty time_descriptions. #[error("sdp: empty time_descriptions")] SdpEmptyTimeDescription, + /// Parse extmap. #[error("parse extmap: {0}")] ParseExtMap(String), + /// A syntax error at a known offset in the input, rendered with the offending character marked. #[error("{} --> {} <-- {}", .s.substring(0,*.p), .s.substring(*.p, *.p+1), .s.substring(*.p+1, .s.len()) )] - SyntaxError { s: String, p: usize }, + SyntaxError { + /// The input being parsed. + s: String, + /// The byte offset of the offending character in `s`. + p: usize, + }, //Third Party Error + /// An error from the `sec1` crate while handling EC key encodings. #[error("{0}")] Sec1(#[source] sec1::Error), + /// An error from the `p256` crate during NIST P-256 elliptic-curve operations. #[error("{0}")] P256(#[source] P256Error), + /// An error from the `rcgen` crate while generating a self-signed certificate. #[error("{0}")] RcGen(#[from] rcgen::Error), + /// Invalid PEM. #[error("invalid PEM: {0}")] InvalidPEM(String), + /// AES GCM. #[error("aes gcm: {0}")] AesGcm(#[from] aes_gcm::Error), + /// Parse ip. #[error("parse ip: {0}")] ParseIp(#[from] net::AddrParseError), + /// Parse int. #[error("parse int: {0}")] ParseInt(#[from] ParseIntError), + /// An underlying I/O error. #[error("{0}")] Io(#[source] IoError), + /// URL parse. #[error("url parse: {0}")] Url(#[from] url::ParseError), + /// UTF-8. #[error("utf8: {0}")] Utf8(#[from] FromUtf8Error), + /// An error from the standard library or another boxed source. #[error("{0}")] Std(#[source] StdError), + /// An error from the `aes` crate during block-cipher setup. #[error("{0}")] Aes(#[from] aes::cipher::InvalidLength), //Other Errors + /// Other RTCP Err. #[error("Other RTCP Err: {0}")] OtherRtcpErr(String), + /// Other RTP Err. #[error("Other RTP Err: {0}")] OtherRtpErr(String), + /// Other SRTP Err. #[error("Other SRTP Err: {0}")] OtherSrtpErr(String), + /// Other STUN Err. #[error("Other STUN Err: {0}")] OtherStunErr(String), + /// Other TURN Err. #[error("Other TURN Err: {0}")] OtherTurnErr(String), + /// Other ICE Err. #[error("Other ICE Err: {0}")] OtherIceErr(String), + /// Other DTLS Err. #[error("Other DTLS Err: {0}")] OtherDtlsErr(String), + /// Other SCTP Err. #[error("Other SCTP Err: {0}")] OtherSctpErr(String), + /// Other DataChannel Err. #[error("Other DataChannel Err: {0}")] OtherDataChannelErr(String), + /// Other Interceptor Err. #[error("Other Interceptor Err: {0}")] OtherInterceptorErr(String), + /// Other Media Err. #[error("Other Media Err: {0}")] OtherMediaErr(String), + /// Other mDNS Err. #[error("Other mDNS Err: {0}")] OtherMdnsErr(String), + /// Other SDP Err. #[error("Other SDP Err: {0}")] OtherSdpErr(String), + /// Other PeerConnection Err. #[error("Other PeerConnection Err: {0}")] OtherPeerConnectionErr(String), #[error("{0}")] + /// An error that does not fit any other variant, carrying a description. Other(String), } diff --git a/rtc-shared/src/ifaces/mod.rs b/rtc-shared/src/ifaces/mod.rs index b786659e..de10fcfd 100644 --- a/rtc-shared/src/ifaces/mod.rs +++ b/rtc-shared/src/ifaces/mod.rs @@ -1,26 +1,44 @@ +/// Platform-specific interface enumeration (FFI into the OS). pub mod ffi; pub use ffi::ifaces; #[derive(PartialEq, Eq, Debug, Clone)] +/// The next hop configured for an interface address. pub enum NextHop { + /// The broadcast address of the attached network. Broadcast(::std::net::SocketAddr), + /// The destination address, for point-to-point links. Destination(::std::net::SocketAddr), } #[derive(PartialEq, Eq, Debug, Clone)] +/// The address family or link type an [`Interface`] entry describes. pub enum Kind { + /// A raw packet-level (link layer) address. Packet, + /// A link-layer address, such as a MAC address. Link, + /// An IPv4 address. Ipv4, + /// An IPv6 address. Ipv6, + /// An address family this crate does not recognise, carrying the raw OS value. Unknow(i32), } #[derive(Debug, Clone)] +/// One address on one local network interface. +/// +/// ICE gathering walks these to produce host candidates. pub struct Interface { + /// The OS name of the interface, such as `en0` or `eth0`. pub name: String, + /// Which address family or link type this entry describes. pub kind: Kind, + /// The address itself, if the OS reported one. pub addr: Option<::std::net::SocketAddr>, + /// The netmask for [`Self::addr`], if the OS reported one. pub mask: Option<::std::net::SocketAddr>, + /// The broadcast or point-to-point destination address, if any. pub hop: Option, } diff --git a/rtc-shared/src/lib.rs b/rtc-shared/src/lib.rs index cb841d94..ace56d6b 100644 --- a/rtc-shared/src/lib.rs +++ b/rtc-shared/src/lib.rs @@ -1,27 +1,65 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Shared types and utilities for the Sans-I/O WebRTC stack. +//! +//! This crate holds what every other crate in the [`rtc`](https://docs.rs/rtc) stack needs: +//! the common [`Error`](error::Error) type, the [`Marshal`](marshal::Marshal)/[`Unmarshal`](marshal::Unmarshal) +//! traits that every protocol codec implements, and the transport plumbing that carries +//! bytes between the network and a protocol state machine. +//! +//! # Key types +//! +//! * [`TransportContext`] / [`TransportMessage`] — a datagram plus the 4-tuple and protocol +//! it arrived on or should be sent on. Every layer in the stack passes these around +//! instead of touching sockets. +//! * [`marshal`] — [`Marshal`](marshal::Marshal), [`Unmarshal`](marshal::Unmarshal) and +//! [`MarshalSize`](marshal::MarshalSize), the wire-format traits shared by STUN, RTP, +//! RTCP, SDP, DTLS and SCTP. +//! * [`error`] — the crate-wide [`Error`](error::Error) enum and `Result` alias, re-exported +//! by the higher-level crates so callers import from one place. +//! * [`crypto`], [`replay_detector`] — primitives shared by DTLS and SRTP. +//! * [`tcp_framing`] — RFC 4571 length-prefixed framing, for ICE-TCP candidates. +//! * [`ifaces`] — local interface enumeration used during ICE candidate gathering. +//! +//! # Feature flags +//! +//! `crypto`, `ifaces`, `marshal` and `replay` are all enabled by default; each gates the +//! correspondingly named module so that dependents can compile only what they use. +//! +//! Most applications do not depend on this crate directly — the [`rtc`](https://docs.rs/rtc) +//! crate re-exports what it needs as `rtc::shared`. + #[cfg(target_family = "windows")] #[macro_use] extern crate bitflags; #[cfg(feature = "crypto")] +/// Cryptographic primitives shared by DTLS and SRTP, including DTLS-SRTP keying-material export. pub mod crypto; #[cfg(feature = "ifaces")] +/// Local network interface enumeration, used to gather ICE host candidates. pub mod ifaces; #[cfg(feature = "marshal")] +/// The wire-format traits every protocol codec in the stack implements. pub mod marshal; #[cfg(feature = "replay")] +/// Replay protection for sequence-numbered packets, as DTLS and SRTP require. pub mod replay_detector; +/// The crate-wide error type shared by every protocol in the stack. pub mod error; +/// `serde` helpers for types that have no natural serialized form, such as [`std::time::Instant`]. pub mod serde; pub mod tcp_framing; +/// Conversions between monotonic, Unix and NTP time. pub mod time; pub(crate) mod transport; +/// Small shared helpers: packet demultiplexing predicates and random-string generation. pub mod util; pub use transport::{ diff --git a/rtc-shared/src/marshal/mod.rs b/rtc-shared/src/marshal/mod.rs index 10915115..0a7f3e6a 100644 --- a/rtc-shared/src/marshal/mod.rs +++ b/rtc-shared/src/marshal/mod.rs @@ -2,13 +2,33 @@ use bytes::{Buf, BytesMut}; use crate::error::{Error, Result}; +/// The encoded size of a value, in bytes. +/// +/// Implemented alongside [`Marshal`]/[`Unmarshal`] so a caller can size a buffer before +/// encoding, and so nested codecs can compute offsets without encoding twice. pub trait MarshalSize: Send + Sync { + /// The number of bytes [`Marshal::marshal_to`] will write for this value. fn marshal_size(&self) -> usize; } +/// Encodes a value into its wire format. +/// +/// Every protocol codec in the stack — STUN, RTP, RTCP, SDP, DTLS, SCTP — implements this +/// so that higher layers can serialize uniformly. pub trait Marshal: MarshalSize { + /// Encodes into `buf`, returning the number of bytes written. + /// + /// # Errors + /// + /// Fails if `buf` is shorter than [`MarshalSize::marshal_size`], or if the value itself is + /// not encodable (an out-of-range field, for instance). fn marshal_to(&self, buf: &mut [u8]) -> Result; + /// Encodes into a freshly allocated buffer sized by [`MarshalSize::marshal_size`]. + /// + /// # Errors + /// + /// Propagates any failure from [`Self::marshal_to`]. fn marshal(&self) -> Result { let l = self.marshal_size(); let mut buf = BytesMut::with_capacity(l); @@ -24,7 +44,13 @@ pub trait Marshal: MarshalSize { } } +/// Decodes a value from its wire format. pub trait Unmarshal: MarshalSize { + /// Decodes one value from `buf`, advancing it past the bytes consumed. + /// + /// # Errors + /// + /// Fails if `buf` is truncated, or if its contents are not a valid encoding of `Self`. fn unmarshal(buf: &mut B) -> Result where Self: Sized, diff --git a/rtc-shared/src/replay_detector/mod.rs b/rtc-shared/src/replay_detector/mod.rs index a8c4d40c..72adf020 100644 --- a/rtc-shared/src/replay_detector/mod.rs +++ b/rtc-shared/src/replay_detector/mod.rs @@ -5,13 +5,26 @@ mod replay_detector_test; use fixed_big_int::*; // ReplayDetector is the interface of sequence replay detector. +/// Tracks which sequence numbers have already been seen, so replayed packets can be dropped. +/// +/// Both DTLS and SRTP require this: an attacker who captures a packet must not be able to +/// have it accepted a second time. pub trait ReplayDetector: Send + Sync { - // Check returns true if given sequence number is not replayed. - // Call accept() to mark the packet is received properly. + /// Returns `true` if `seq` has not been seen before and is inside the window. + /// + /// This only tests; call [`Self::accept`] afterwards to record the packet as received. fn check(&mut self, seq: u64) -> bool; + /// Commits the sequence number from the preceding [`Self::check`] call as received. + /// + /// Split from `check` so a caller can validate a packet's authenticity first and only then + /// record it — a forged packet must not advance the window. fn accept(&mut self); } +/// A replay detector over a monotonically increasing sequence number that never wraps. +/// +/// Handles the full 64-bit range, which is what DTLS needs. See +/// [`WrappedSlidingWindowDetector`] for sequence numbers that do wrap. pub struct SlidingWindowDetector { accepted: bool, seq: u64, @@ -22,10 +35,11 @@ pub struct SlidingWindowDetector { } impl SlidingWindowDetector { - // New creates ReplayDetector. - // Created ReplayDetector doesn't allow wrapping. - // It can handle monotonically increasing sequence number up to - // full 64bit number. It is suitable for DTLS replay protection. + /// Creates a detector with a `window_size`-wide window over sequence numbers up to + /// `max_seq`. + /// + /// Does not allow wrapping: it handles monotonically increasing sequence numbers across + /// the full 64-bit range, which is what DTLS replay protection needs. pub fn new(window_size: usize, max_seq: u64) -> Self { SlidingWindowDetector { accepted: false, @@ -77,6 +91,10 @@ impl ReplayDetector for SlidingWindowDetector { } } +/// A replay detector for a sequence number that wraps at a known maximum. +/// +/// SRTP's 16-bit sequence numbers wrap, so the window has to interpret a large backwards +/// jump as a rollover rather than a replay. pub struct WrappedSlidingWindowDetector { accepted: bool, seq: u64, @@ -88,8 +106,10 @@ pub struct WrappedSlidingWindowDetector { } impl WrappedSlidingWindowDetector { - // WithWrap creates ReplayDetector allowing sequence wrapping. - // This is suitable for short bitwidth counter like SRTP and SRTCP. + /// Creates a detector with a `window_size`-wide window that allows the sequence number to + /// wrap at `max_seq`. + /// + /// Suitable for the short counters used by SRTP and SRTCP. pub fn new(window_size: usize, max_seq: u64) -> Self { WrappedSlidingWindowDetector { accepted: false, @@ -169,6 +189,9 @@ impl ReplayDetector for WrappedSlidingWindowDetector { } #[derive(Default)] +/// A detector that accepts everything. +/// +/// For contexts where replay protection is disabled or handled elsewhere. pub struct NoOpReplayDetector; impl ReplayDetector for NoOpReplayDetector { diff --git a/rtc-shared/src/serde.rs b/rtc-shared/src/serde.rs index 54a864dc..27d628ab 100644 --- a/rtc-shared/src/serde.rs +++ b/rtc-shared/src/serde.rs @@ -8,6 +8,14 @@ pub mod instant_to_epoch { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + /// Serializes an [`Instant`] as a duration relative to the process's reference instant. + /// + /// [`Instant`] has no absolute representation, so this encodes the offset instead. Use + /// with `#[serde(with = "...")]`. + /// + /// # Errors + /// + /// Propagates any failure from the underlying serializer. pub fn serialize(instant: &Instant, serializer: S) -> Result where S: Serializer, @@ -24,6 +32,11 @@ pub mod instant_to_epoch { epoch_s.serialize(serializer) } + /// Deserializes an [`Instant`] from the relative offset written by [`serialize`]. + /// + /// # Errors + /// + /// Propagates any failure from the underlying deserializer. pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/rtc-shared/src/time.rs b/rtc-shared/src/time.rs index da7f3981..d6988d46 100644 --- a/rtc-shared/src/time.rs +++ b/rtc-shared/src/time.rs @@ -2,12 +2,18 @@ use std::ops::Add; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// A monotonic [`Instant`] paired with the wall-clock time it was taken at. +/// +/// Sans-I/O protocol code measures time with a monotonic [`Instant`], but RTCP timestamps +/// and NTP fields need wall-clock time. Capturing both once lets either be derived from the +/// other later without re-reading the (non-monotonic) system clock. pub struct SystemInstant { instant: Instant, duration_since_unix_epoch: Duration, } impl SystemInstant { + /// Captures the current monotonic instant together with the current wall-clock time. pub fn now() -> Self { Self { instant: Instant::now(), @@ -17,23 +23,31 @@ impl SystemInstant { } } + /// Converts a Unix-epoch duration back into the monotonic [`Instant`] it corresponds to. pub fn instant(&self, duration_since_unix_epoch: Duration) -> Instant { self.instant + duration_since_unix_epoch - self.duration_since_unix_epoch } + /// The wall-clock time, as a duration since the Unix epoch, captured at construction. pub fn duration_since_unix_epoch(&self) -> Duration { self.duration_since_unix_epoch } + /// Converts the monotonic `now` into wall-clock time as a duration since the Unix epoch. pub fn unix(&self, now: Instant) -> Duration { now.duration_since(self.instant) .add(self.duration_since_unix_epoch) } + /// Converts the monotonic `now` into a 64-bit NTP timestamp, as RTCP Sender Reports carry. pub fn ntp(&self, now: Instant) -> u64 { SystemInstant::unix2ntp(self.unix(now)) } + /// Converts a Unix-epoch duration into a 64-bit NTP timestamp. + /// + /// The result is seconds since the NTP epoch (1900-01-01) in the high 32 bits and a binary + /// fraction of a second in the low 32. pub fn unix2ntp(duration_since_unix_epoch: Duration) -> u64 { let u = duration_since_unix_epoch.as_nanos() as u64; @@ -47,6 +61,9 @@ impl SystemInstant { s | f } + /// Converts a 64-bit NTP timestamp into a duration since the Unix epoch. + /// + /// The inverse of [`Self::unix2ntp`]. pub fn ntp2unix(ntp: u64) -> Duration { let mut s = ntp >> 32; let mut f = ntp & 0xFFFFFFFF; diff --git a/rtc-shared/src/util.rs b/rtc-shared/src/util.rs index 4f7cef6f..2a00d2c7 100644 --- a/rtc-shared/src/util.rs +++ b/rtc-shared/src/util.rs @@ -32,12 +32,20 @@ pub fn match_dtls(b: &[u8]) -> bool { match_range(20, 63)(b) } -// match_srtp_or_srtcp is a MatchFunc that accepts packets with the first byte in [128..191] -// as defied in RFC7983 +/// Returns `true` if `b` looks like SRTP or SRTCP: its first byte is in `[128, 191]`. +/// +/// One of the demultiplexing predicates from [RFC 7983], which is how a single port can carry +/// STUN, DTLS and SRTP at once. +/// +/// [RFC 7983]: https://datatracker.ietf.org/doc/html/rfc7983 pub fn match_srtp_or_srtcp(b: &[u8]) -> bool { match_range(128, 191)(b) } +/// Returns `true` if `buf` is RTCP rather than RTP. +/// +/// Distinguished by the payload-type byte: RTCP packet types occupy `[192, 223]`, which RTP +/// cannot use. Returns `false` for buffers too short to tell. pub fn is_rtcp(buf: &[u8]) -> bool { // Not long enough to determine RTP/RTCP if buf.len() < 4 { @@ -86,6 +94,9 @@ pub fn math_rand_alpha_number(n: usize) -> String { } //TODO: generates a random string for cryptographic usage. +/// Generates a random `n`-character string drawn from `runes`. +/// +/// Used for values that must be unguessable, such as ICE credentials and SDP identifiers. pub fn generate_crypto_random_string(n: usize, runes: &[u8]) -> String { let mut rng = rng(); diff --git a/rtc-srtp/src/config.rs b/rtc-srtp/src/config.rs index 2084c6ed..fb058ea6 100644 --- a/rtc-srtp/src/config.rs +++ b/rtc-srtp/src/config.rs @@ -6,9 +6,13 @@ const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; /// SessionKeys bundles the keys required to setup an SRTP session #[derive(Default, Debug, Clone)] pub struct SessionKeys { + /// The master key used to protect outbound packets. pub local_master_key: Vec, + /// The master salt used to protect outbound packets. pub local_master_salt: Vec, + /// The master key used to unprotect inbound packets. pub remote_master_key: Vec, + /// The master salt used to unprotect inbound packets. pub remote_master_salt: Vec, } @@ -18,16 +22,21 @@ pub struct SessionKeys { /// After a Config is passed to a session it must not be modified. #[derive(Default)] pub struct Config { + /// The master keys and salts for both directions. pub keys: SessionKeys, + /// The negotiated protection profile, which fixes the cipher and tag lengths. pub profile: ProtectionProfile, //LoggerFactory: logging.LoggerFactory /// List of local/remote context options. /// ReplayProtection is enabled on remote context by default. /// Default replay protection window size is 64. pub local_rtp_options: Option, + /// Replay-protection options for the inbound RTP context. Enabled by default. pub remote_rtp_options: Option, + /// Replay-protection options for the outbound RTCP context. pub local_rtcp_options: Option, + /// Replay-protection options for the inbound RTCP context. Enabled by default. pub remote_rtcp_options: Option, } diff --git a/rtc-srtp/src/context/mod.rs b/rtc-srtp/src/context/mod.rs index 78036fbf..ec8b5b20 100644 --- a/rtc-srtp/src/context/mod.rs +++ b/rtc-srtp/src/context/mod.rs @@ -16,7 +16,9 @@ use crate::option::*; use crate::protection_profile::*; use shared::error::{Error, Result}; +/// SRTCP protection and unprotection. pub mod srtcp; +/// SRTP protection and unprotection. pub mod srtp; const MAX_ROC: u32 = u32::MAX; diff --git a/rtc-srtp/src/context/srtp.rs b/rtc-srtp/src/context/srtp.rs index efe1b208..80f4c391 100644 --- a/rtc-srtp/src/context/srtp.rs +++ b/rtc-srtp/src/context/srtp.rs @@ -7,6 +7,15 @@ use shared::{ use bytes::BytesMut; impl Context { + /// Decrypts an SRTP packet whose header has already been parsed. + /// + /// Saves re-parsing when the caller needed the header to route the packet. The header must + /// be the one belonging to `encrypted`. + /// + /// # Errors + /// + /// Fails if authentication fails, if the packet is a replay, or if it is too short to hold + /// the profile's auth tag. pub fn decrypt_rtp_with_header( &mut self, encrypted: &[u8], @@ -47,6 +56,14 @@ impl Context { self.decrypt_rtp_with_header(encrypted, &header) } + /// Encrypts an RTP payload, using an already-parsed header. + /// + /// Saves re-parsing when the caller has just built the header. Returns the full protected + /// packet, header included. + /// + /// # Errors + /// + /// Fails if the SRTP context has no key for this SSRC or the cipher rejects the input. pub fn encrypt_rtp_with_header( &mut self, plaintext: &[u8], diff --git a/rtc-srtp/src/lib.rs b/rtc-srtp/src/lib.rs index 2b1ee642..0012bfa3 100644 --- a/rtc-srtp/src/lib.rs +++ b/rtc-srtp/src/lib.rs @@ -1,11 +1,38 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! SRTP and SRTCP for the Sans-I/O WebRTC stack. +//! +//! The Secure Real-time Transport Protocol ([RFC 3711]) as WebRTC keys it: protection +//! profiles negotiated through DTLS-SRTP ([RFC 5764]), with keying material exported from +//! the DTLS handshake rather than signalled. +//! +//! # Structure +//! +//! * [`context`] — [`Context`](context::Context), the encrypt/decrypt state for one +//! direction: `encrypt_rtp`/`decrypt_rtp` and the RTCP equivalents, plus the replay +//! protection and rollover-counter tracking the RFC requires. +//! * [`protection_profile`] — the negotiable profiles (AES-128-CM-SHA1-80, +//! AEAD-AES-128-GCM, and friends) and their key/salt lengths. +//! * [`config`], [`option`] — how a context is built, including replay-window sizing. +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate creates the contexts from the DTLS handshake and +//! applies them to media as one layer of the peer-connection pipeline. +//! +//! [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 +//! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 + mod cipher; +/// Session configuration: keys, protection profile, and replay-protection options. pub mod config; +/// The encrypt/decrypt state for one SRTP/SRTCP session. pub mod context; mod key_derivation; +/// Per-context options, currently the replay-detector factory. pub mod option; +/// The DTLS-SRTP protection profiles and their key, salt and tag lengths. pub mod protection_profile; #[cfg(all(feature = "aws-lc-rs", feature = "ring"))] diff --git a/rtc-srtp/src/option.rs b/rtc-srtp/src/option.rs index 6b5a79a1..3e2b6889 100644 --- a/rtc-srtp/src/option.rs +++ b/rtc-srtp/src/option.rs @@ -1,5 +1,11 @@ use shared::replay_detector::*; +/// A factory for the [`ReplayDetector`] a context +/// should use. +/// +/// A factory rather than a value because each SSRC in a session needs its own detector +/// state. Remote contexts default to a 64-packet sliding window; pass a different factory to +/// widen it or to disable replay protection. pub type ContextOption = Box Box + Send + Sync>; pub(crate) const MAX_SEQUENCE_NUMBER: u16 = 65535; diff --git a/rtc-srtp/src/protection_profile.rs b/rtc-srtp/src/protection_profile.rs index 268237c7..01c1266a 100644 --- a/rtc-srtp/src/protection_profile.rs +++ b/rtc-srtp/src/protection_profile.rs @@ -3,15 +3,26 @@ #[repr(u8)] pub enum ProtectionProfile { #[default] + /// `SRTP_AES128_CM_HMAC_SHA1_80`: AES-128 counter mode with an 80-bit HMAC-SHA1 tag. + /// + /// The profile every WebRTC implementation supports. Aes128CmHmacSha1_80 = 0x0001, + /// `SRTP_AES128_CM_HMAC_SHA1_32`: as above with a truncated 32-bit tag, trading + /// authentication strength for 6 bytes per packet. Aes128CmHmacSha1_32 = 0x0002, + /// `SRTP_AES256_CM_HMAC_SHA1_80`: AES-256 counter mode with an 80-bit HMAC-SHA1 tag. Aes256CmHmacSha1_80 = 0x0003, + /// `SRTP_AES256_CM_HMAC_SHA1_32`: AES-256 counter mode with a 32-bit HMAC-SHA1 tag. Aes256CmHmacSha1_32 = 0x0004, + /// `SRTP_AEAD_AES_128_GCM`: AES-128 in GCM, which authenticates as part of encryption + /// rather than with a separate HMAC. AeadAes128Gcm = 0x0007, + /// `SRTP_AEAD_AES_256_GCM`: AES-256 in GCM. AeadAes256Gcm = 0x0008, } impl ProtectionProfile { + /// The master key length in bytes for this profile. pub fn key_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_32 @@ -22,6 +33,7 @@ impl ProtectionProfile { } } + /// The master salt length in bytes for this profile. pub fn salt_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_32 @@ -32,6 +44,7 @@ impl ProtectionProfile { } } + /// The authentication tag length appended to each SRTP packet, in bytes. pub fn rtp_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_80 => 10, @@ -40,6 +53,7 @@ impl ProtectionProfile { } } + /// The authentication tag length appended to each SRTCP packet, in bytes. pub fn rtcp_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 @@ -50,6 +64,7 @@ impl ProtectionProfile { } } + /// The AEAD tag length in bytes, for the GCM profiles; `0` for the HMAC-SHA1 ones. pub fn aead_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 @@ -60,6 +75,8 @@ impl ProtectionProfile { } } + /// The HMAC authentication key length in bytes; `0` for the AEAD profiles, which derive + /// authentication from the cipher itself. pub fn auth_key_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 diff --git a/rtc-stun/src/addr.rs b/rtc-stun/src/addr.rs index 23b25f20..fe225f66 100644 --- a/rtc-stun/src/addr.rs +++ b/rtc-stun/src/addr.rs @@ -20,7 +20,9 @@ pub(crate) const IPV6LEN: usize = 16; /// /// RFC 5389 Section 15.1 pub struct MappedAddress { + /// The IP address. pub ip: IpAddr, + /// The port. pub port: u16, } diff --git a/rtc-stun/src/agent.rs b/rtc-stun/src/agent.rs index c6d0f0e6..68c34fcb 100644 --- a/rtc-stun/src/agent.rs +++ b/rtc-stun/src/agent.rs @@ -27,15 +27,22 @@ pub struct Agent { /// Do not reuse outside Handler. #[derive(Debug)] //Clone pub struct Event { + /// The transaction this event belongs to. pub id: TransactionId, + /// What happened. pub evt: StunEvent, } #[derive(Debug)] //Clone +/// What became of a STUN transaction. pub enum StunEvent { + /// The agent was closed, abandoning this transaction. AgentClosed, + /// The transaction was stopped by the caller. TransactionStopped, + /// The transaction timed out with no response. TransactionTimeOut, + /// A response arrived for this transaction. Message(Message), } @@ -54,10 +61,15 @@ const AGENT_COLLECT_CAP: usize = 100; /// process transactions. #[derive(Debug)] pub enum ClientAgent { + /// Hand an inbound message to the agent for matching against a transaction. Process(Message), + /// Advance time so the agent can expire transactions. Collect(Instant), + /// Register a new transaction with its deadline. Start(TransactionId, Instant), + /// Abandon a transaction without waiting for its deadline. Stop(TransactionId), + /// Close the agent, abandoning every outstanding transaction. Close, } @@ -71,6 +83,11 @@ impl Agent { } } + /// Applies an agent command: start, stop, process a message, collect timeouts, or close. + /// + /// # Errors + /// + /// Fails if the agent is closed, or a transaction id is already in use. pub fn handle_event(&mut self, client_agent: ClientAgent) -> Result<()> { match client_agent { ClientAgent::Process(message) => self.process(message), @@ -81,6 +98,7 @@ impl Agent { } } + /// When the agent next needs [`ClientAgent::Collect`], or `None` with nothing outstanding. pub fn poll_timeout(&mut self) -> Option { let mut deadline = None; for transaction in self.transactions.values() { @@ -91,6 +109,7 @@ impl Agent { deadline } + /// The next transaction event, or `None` when there is nothing to report. pub fn poll_event(&mut self) -> Option { self.events_queue.pop_front() } diff --git a/rtc-stun/src/attributes.rs b/rtc-stun/src/attributes.rs index 843782a8..612f2822 100644 --- a/rtc-stun/src/attributes.rs +++ b/rtc-stun/src/attributes.rs @@ -91,66 +91,106 @@ impl AttrType { } /// Attributes from comprehension-required range (0x0000-0x7FFF). -pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); // MAPPED-ADDRESS -pub const ATTR_USERNAME: AttrType = AttrType(0x0006); // USERNAME -pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); // MESSAGE-INTEGRITY -pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); // ERROR-CODE -pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); // UNKNOWN-ATTRIBUTES -pub const ATTR_REALM: AttrType = AttrType(0x0014); // REALM -pub const ATTR_NONCE: AttrType = AttrType(0x0015); // NONCE -pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); // XOR-MAPPED-ADDRESS +/// MAPPED-ADDRESS. +pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); +/// USERNAME. +pub const ATTR_USERNAME: AttrType = AttrType(0x0006); +/// MESSAGE-INTEGRITY. +pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); +/// ERROR-CODE. +pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); +/// UNKNOWN-ATTRIBUTES. +pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); +/// REALM. +pub const ATTR_REALM: AttrType = AttrType(0x0014); +/// NONCE. +pub const ATTR_NONCE: AttrType = AttrType(0x0015); +/// XOR-MAPPED-ADDRESS. +pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); /// Attributes from comprehension-optional range (0x8000-0xFFFF). -pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); // SOFTWARE -pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); // ALTERNATE-SERVER -pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); // FINGERPRINT +/// SOFTWARE. +pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); +/// ALTERNATE-SERVER. +pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); +/// FINGERPRINT. +pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); /// Attributes from RFC 5245 ICE. -pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); // PRIORITY -pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); // USE-CANDIDATE -pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); // ICE-CONTROLLED -pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); // ICE-CONTROLLING -pub const ATTR_NETWORK_COST: AttrType = AttrType(0xC057); // NETWORK-COST +/// PRIORITY. +pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); +/// USE-CANDIDATE. +pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); +/// ICE-CONTROLLED. +pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); +/// ICE-CONTROLLING. +pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); +/// NETWORK-COST. +pub const ATTR_NETWORK_COST: AttrType = AttrType(0xC057); /// Attributes from RFC 5766 TURN. -pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); // CHANNEL-NUMBER -pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); // LIFETIME -pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); // XOR-PEER-ADDRESS -pub const ATTR_DATA: AttrType = AttrType(0x0013); // DATA -pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); // XOR-RELAYED-ADDRESS -pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); // EVEN-PORT -pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); // REQUESTED-TRANSPORT -pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); // DONT-FRAGMENT -pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); // RESERVATION-TOKEN +/// CHANNEL-NUMBER. +pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); +/// LIFETIME. +pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); +/// XOR-PEER-ADDRESS. +pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); +/// DATA. +pub const ATTR_DATA: AttrType = AttrType(0x0013); +/// XOR-RELAYED-ADDRESS. +pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); +/// EVEN-PORT. +pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); +/// REQUESTED-TRANSPORT. +pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); +/// DONT-FRAGMENT. +pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); +/// RESERVATION-TOKEN. +pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); /// Attributes from RFC 5780 NAT Behavior Discovery -pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); // CHANGE-REQUEST -pub const ATTR_PADDING: AttrType = AttrType(0x0026); // PADDING -pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); // RESPONSE-PORT -pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); // CACHE-TIMEOUT -pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); // RESPONSE-ORIGIN -pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); // OTHER-ADDRESS +/// CHANGE-REQUEST. +pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); +/// PADDING. +pub const ATTR_PADDING: AttrType = AttrType(0x0026); +/// RESPONSE-PORT. +pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); +/// CACHE-TIMEOUT. +pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); +/// RESPONSE-ORIGIN. +pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); +/// OTHER-ADDRESS. +pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); /// Attributes from RFC 3489, removed by RFC 5389, /// but still used by RFC5389-implementing software like Vovida.org, reTURNServer, etc. -pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); // SOURCE-ADDRESS -pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); // CHANGED-ADDRESS +/// SOURCE-ADDRESS. +pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); +/// CHANGED-ADDRESS. +pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); /// Attributes from RFC 6062 TURN Extensions for TCP Allocations. -pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); // CONNECTION-ID +/// CONNECTION-ID. +pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); /// Attributes from RFC 6156 TURN IPv6. -pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); // REQUESTED-ADDRESS-FAMILY +/// REQUESTED-ADDRESS-FAMILY. +pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); /// Attributes from An Origin Attribute for the STUN Protocol. pub const ATTR_ORIGIN: AttrType = AttrType(0x802F); /// Attributes from RFC 8489 STUN. -pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); // MESSAGE-INTEGRITY-SHA256 -pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); // PASSWORD-ALGORITHM -pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); // USER-HASH -pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); // PASSWORD-ALGORITHMS -pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAIN +/// MESSAGE-INTEGRITY-SHA256. +pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); +/// PASSWORD-ALGORITHM. +pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); +/// USER-HASH. +pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); +/// PASSWORD-ALGORITHMS. +pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); +/// ALTERNATE-DOMAIN. +pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); /// RawAttribute is a Type-Length-Value (TLV) object that /// can be added to a STUN message. Attributes are divided into two @@ -161,8 +201,11 @@ pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAI /// understood. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct RawAttribute { + /// Which attribute this is. pub typ: AttrType, + /// The value length in bytes; recomputed when encoding, so it is ignored there. pub length: u16, // ignored while encoding + /// The attribute's raw value. pub value: Vec, } diff --git a/rtc-stun/src/checks.rs b/rtc-stun/src/checks.rs index 1fc4dd9d..07312017 100644 --- a/rtc-stun/src/checks.rs +++ b/rtc-stun/src/checks.rs @@ -3,7 +3,7 @@ use shared::error::*; use subtle::ConstantTimeEq; -// check_size returns ErrAttrSizeInvalid if got is not equal to expected. +/// Check_size returns ErrAttrSizeInvalid if got is not equal to expected. pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { if got == expected { Ok(()) @@ -12,7 +12,7 @@ pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { } } -// is_attr_size_invalid returns true if error means that attribute size is invalid. +/// Is_attr_size_invalid returns true if error means that attribute size is invalid. pub fn is_attr_size_invalid(err: &Error) -> bool { Error::ErrAttributeSizeInvalid == *err } @@ -33,7 +33,7 @@ pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { } } -// check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. +/// Check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { if got <= max { Ok(()) @@ -42,7 +42,7 @@ pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { } } -// is_attr_size_overflow returns true if error means that attribute size is too big. +/// Is_attr_size_overflow returns true if error means that attribute size is too big. pub fn is_attr_size_overflow(err: &Error) -> bool { Error::ErrAttributeSizeOverflow == *err } diff --git a/rtc-stun/src/client.rs b/rtc-stun/src/client.rs index e19f5017..5bdab81b 100644 --- a/rtc-stun/src/client.rs +++ b/rtc-stun/src/client.rs @@ -55,6 +55,8 @@ impl Default for ClientSettings { } #[derive(Default)] +/// Builds a [`Client`] with a chosen transaction timeout, retransmission schedule and +/// handler. pub struct ClientBuilder { settings: ClientSettings, } @@ -90,12 +92,18 @@ impl ClientBuilder { self } + /// A builder with the RFC's default timings. pub fn new() -> Self { ClientBuilder { settings: ClientSettings::default(), } } + /// Builds the client for the given local and remote addresses. + /// + /// # Errors + /// + /// Fails if the configured timings are inconsistent. pub fn build( self, local: SocketAddr, @@ -135,10 +143,12 @@ impl Client { } } + /// The address this client sends from. pub fn local_addr(&self) -> SocketAddr { self.local } + /// The STUN server this client talks to. pub fn peer_addr(&self) -> SocketAddr { self.remote } diff --git a/rtc-stun/src/error_code.rs b/rtc-stun/src/error_code.rs index f847a430..c4615d26 100644 --- a/rtc-stun/src/error_code.rs +++ b/rtc-stun/src/error_code.rs @@ -13,8 +13,11 @@ use std::fmt; // // RFC 5389 Section 15.6 #[derive(Default)] +/// The `ERROR-CODE` attribute: a numeric code and a human-readable reason. pub struct ErrorCodeAttribute { + /// The numeric error code. pub code: ErrorCode, + /// The reason phrase, as UTF-8 bytes. pub reason: Vec, } @@ -81,6 +84,7 @@ impl Getter for ErrorCodeAttribute { // ErrorCode is code for ERROR-CODE attribute. #[derive(PartialEq, Eq, Hash, Copy, Clone, Default)] +/// A STUN error code, as carried in `ERROR-CODE`. pub struct ErrorCode(pub u16); impl Setter for ErrorCode { @@ -99,42 +103,59 @@ impl Setter for ErrorCode { } } -// Possible error codes. +/// Possible error codes. pub const CODE_TRY_ALTERNATE: ErrorCode = ErrorCode(300); +/// 400 Bad Request: the request was malformed. pub const CODE_BAD_REQUEST: ErrorCode = ErrorCode(400); +/// 401 Unauthorized: authentication is required, or the credentials were wrong. pub const CODE_UNAUTHORIZED: ErrorCode = ErrorCode(401); +/// 420 Unknown Attribute: the request carried a comprehension-required attribute the server +/// does not understand. pub const CODE_UNKNOWN_ATTRIBUTE: ErrorCode = ErrorCode(420); +/// 438 Stale Nonce: the nonce expired; retry with the one in this response. pub const CODE_STALE_NONCE: ErrorCode = ErrorCode(438); +/// 487 Role Conflict: both ICE agents claimed the same role. pub const CODE_ROLE_CONFLICT: ErrorCode = ErrorCode(487); +/// 500 Server Error: a temporary failure on the server. pub const CODE_SERVER_ERROR: ErrorCode = ErrorCode(500); -// DEPRECATED constants. -// DEPRECATED, use CODE_UNAUTHORIZED. +/// DEPRECATED constants. +/// DEPRECATED, use CODE_UNAUTHORIZED. pub const CODE_UNAUTHORISED: ErrorCode = CODE_UNAUTHORIZED; -// Error codes from RFC 5766. -// -// RFC 5766 Section 15 -pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); // Forbidden -pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); // Allocation Mismatch -pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); // Wrong Credentials -pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); // Unsupported Transport Protocol -pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); // Allocation Quota Reached -pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); // Insufficient Capacity - -// Error codes from RFC 6062. -// -// RFC 6062 Section 6.3 +/// Error codes from RFC 5766. +/// +/// RFC 5766 Section 15. +/// Forbidden. +pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); +/// Allocation Mismatch. +pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); +/// Wrong Credentials. +pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); +/// Unsupported Transport Protocol. +pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); +/// Allocation Quota Reached. +pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); +/// Insufficient Capacity. +pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); + +/// Error codes from RFC 6062. +/// +/// RFC 6062 Section 6.3. pub const CODE_CONN_ALREADY_EXISTS: ErrorCode = ErrorCode(446); +/// 447 Connection Timeout or Failure: the TURN TCP connection to the peer failed. pub const CODE_CONN_TIMEOUT_OR_FAILURE: ErrorCode = ErrorCode(447); -// Error codes from RFC 6156. -// -// RFC 6156 Section 10.2 -pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); // Address Family not Supported -pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); // Peer Address Family Mismatch +/// Error codes from RFC 6156. +/// +/// RFC 6156 Section 10.2. +/// Address Family not Supported. +pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); +/// Peer Address Family Mismatch. +pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); lazy_static! { + /// The reason phrase each known [`ErrorCode`] is sent with. pub static ref ERROR_REASONS:HashMap> = [ (CODE_TRY_ALTERNATE, b"Try Alternate".to_vec()), diff --git a/rtc-stun/src/fingerprint.rs b/rtc-stun/src/fingerprint.rs index 0ec73bb5..26e70fb6 100644 --- a/rtc-stun/src/fingerprint.rs +++ b/rtc-stun/src/fingerprint.rs @@ -8,20 +8,22 @@ use shared::error::*; use crc::{CRC_32_ISO_HDLC, Crc, Table}; -// FingerprintAttr represents FINGERPRINT attribute. -// -// RFC 5389 Section 15.5 +/// FINGERPRINT attribute. +/// +/// RFC 5389 Section 15.5. pub struct FingerprintAttr; -// FINGERPRINT is shorthand for FingerprintAttr. -// -// Example: -// -// m := New() -// FINGERPRINT.add_to(m) +/// Shorthand for FingerprintAttr. +/// +/// Example: +/// +/// m := New() +/// FINGERPRINT.add_to(m). pub const FINGERPRINT: FingerprintAttr = FingerprintAttr {}; +/// The value the CRC-32 is XORed with, `0x5354554e` — ASCII `STUN`. pub const FINGERPRINT_XOR_VALUE: u32 = 0x5354554e; +/// The attribute's value length in bytes. pub const FINGERPRINT_SIZE: usize = 4; // 32 bit // FingerprintValue returns CRC-32 of b XOR-ed by 0x5354554e. @@ -37,6 +39,7 @@ pub const FINGERPRINT_SIZE: usize = 4; // 32 bit /// messages (one fingerprint per ICE connectivity check / consent probe). static CRC_32: Crc> = Crc::>::new(&CRC_32_ISO_HDLC); +/// Computes the `FINGERPRINT` value over `b`: CRC-32 XORed with [`FINGERPRINT_XOR_VALUE`]. pub fn fingerprint_value(b: &[u8]) -> u32 { let checksum = CRC_32.checksum(b); checksum ^ FINGERPRINT_XOR_VALUE // XOR @@ -58,8 +61,8 @@ impl Setter for FingerprintAttr { } impl FingerprintAttr { - // Check reads fingerprint value from m and checks it, returning error if any. - // Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. + /// Check reads fingerprint value from m and checks it, returning error if any. + /// Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. pub fn check(&self, m: &Message) -> Result<()> { let b = m.get(ATTR_FINGERPRINT)?; check_size(ATTR_FINGERPRINT, b.len(), FINGERPRINT_SIZE)?; diff --git a/rtc-stun/src/integrity.rs b/rtc-stun/src/integrity.rs index 8ac340fd..38d918a3 100644 --- a/rtc-stun/src/integrity.rs +++ b/rtc-stun/src/integrity.rs @@ -20,6 +20,9 @@ pub(crate) const CREDENTIALS_SEP: &str = ":"; // // RFC 5389 Section 15.4 #[derive(Default, Clone)] +/// The `MESSAGE-INTEGRITY` key: an HMAC-SHA1 is computed over the message with it. +/// +/// Built from a short-term password, or from a long-term username/realm/password triple. pub struct MessageIntegrity(pub Vec); fn new_hmac(key: &[u8], message: &[u8]) -> Vec { @@ -64,8 +67,8 @@ impl Setter for MessageIntegrity { pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; impl MessageIntegrity { - // new_long_term_integrity returns new MessageIntegrity with key for long-term - // credentials. Password, username, and realm must be SASL-prepared. + /// New_long_term_integrity returns new MessageIntegrity with key for long-term + /// credentials. Password, username, and realm must be SASL-prepared. pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { let s = [username, realm, password].join(CREDENTIALS_SEP); @@ -75,15 +78,15 @@ impl MessageIntegrity { MessageIntegrity(h.finalize().as_slice().to_vec()) } - // new_short_term_integrity returns new MessageIntegrity with key for short-term - // credentials. Password must be SASL-prepared. + /// New_short_term_integrity returns new MessageIntegrity with key for short-term + /// credentials. Password must be SASL-prepared. pub fn new_short_term_integrity(password: String) -> Self { MessageIntegrity(password.as_bytes().to_vec()) } - // Check checks MESSAGE-INTEGRITY attribute. - // - // CPU costly, see BenchmarkMessageIntegrity_Check. + /// Check checks MESSAGE-INTEGRITY attribute. + /// + /// CPU costly, see BenchmarkMessageIntegrity_Check. pub fn check(&self, m: &mut Message) -> Result<()> { let v = m.get(ATTR_MESSAGE_INTEGRITY)?; diff --git a/rtc-stun/src/lib.rs b/rtc-stun/src/lib.rs index ccaf4b32..621469a5 100644 --- a/rtc-stun/src/lib.rs +++ b/rtc-stun/src/lib.rs @@ -1,25 +1,70 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! STUN for the Sans-I/O WebRTC stack. +//! +//! Session Traversal Utilities for NAT ([RFC 5389], superseding [RFC 3489]), plus the +//! attributes ICE ([RFC 8445]) and TURN ([RFC 5766]) layer on top. In WebRTC, STUN does +//! double duty: it discovers a peer's server-reflexive address, and its binding +//! request/response exchange *is* the ICE connectivity check. +//! +//! # Structure +//! +//! * [`message`] — [`Message`](message::Message), the STUN message itself: build one from +//! attributes, marshal it, unmarshal one off the wire. +//! * [`attributes`], [`textattrs`], [`uattrs`], [`xoraddr`], [`error_code`] — the attribute +//! types, including `XOR-MAPPED-ADDRESS`, `USERNAME`, `REALM` and `ERROR-CODE`. +//! * [`integrity`], [`fingerprint`] — `MESSAGE-INTEGRITY` (HMAC-SHA1) and `FINGERPRINT` +//! (CRC-32), the two attributes whose values depend on the encoded message. +//! * [`agent`], [`client`] — transaction tracking and a Sans-I/O client for talking to a +//! STUN server. +//! * [`uri`] — parsing `stun:`/`stuns:` URLs. +//! * [`checks`] — validation helpers for received messages. +//! +//! Most applications do not depend on this crate directly — [`rtc-ice`] and +//! [`rtc-turn`] build on it, and the [`rtc`](https://docs.rs/rtc) crate drives those. +//! +//! [RFC 5389]: https://datatracker.ietf.org/doc/html/rfc5389 +//! [RFC 3489]: https://datatracker.ietf.org/doc/html/rfc3489 +//! [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 +//! [RFC 5766]: https://datatracker.ietf.org/doc/html/rfc5766 +//! [`rtc-ice`]: https://docs.rs/rtc-ice +//! [`rtc-turn`]: https://docs.rs/rtc-turn + #[macro_use] extern crate lazy_static; +/// Socket-address helpers shared by the address attributes. pub mod addr; +/// Transaction tracking: which requests are outstanding and when they time out. pub mod agent; +/// The STUN attribute types and the raw attribute representation. pub mod attributes; +/// Validation helpers for received messages and attributes. pub mod checks; +/// A Sans-I/O STUN client for talking to a STUN server. pub mod client; +/// The `ERROR-CODE` attribute and the codes defined by STUN, TURN and ICE. pub mod error_code; +/// The `FINGERPRINT` attribute, a CRC-32 over the message. pub mod fingerprint; +/// The `MESSAGE-INTEGRITY` attribute, an HMAC-SHA1 over the message. pub mod integrity; +/// The STUN message itself: header, attributes, and encoding. pub mod message; +/// Text-valued attributes such as `USERNAME`, `REALM` and `SOFTWARE`. pub mod textattrs; +/// The `UNKNOWN-ATTRIBUTES` attribute, listing attributes a server could not process. pub mod uattrs; +/// Parsing `stun:` and `stuns:` URIs. pub mod uri; +/// The `XOR-MAPPED-ADDRESS` attribute, whose value is masked with the magic cookie. pub mod xoraddr; -// IANA assigned ports for "stun" protocol. +/// IANA assigned ports for "stun" protocol. pub const DEFAULT_PORT: u16 = 3478; +/// The default port for `stuns:` (STUN over TLS/DTLS). pub const DEFAULT_TLS_PORT: u16 = 5349; #[cfg(all(feature = "aws-lc-rs", feature = "ring"))] diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index bf15477e..44f6c5b3 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -9,22 +9,26 @@ use rand::RngExt; use std::fmt; use std::io::{Read, Write}; -// MAGIC_COOKIE is fixed value that aids in distinguishing STUN packets -// from packets of other protocols when STUN is multiplexed with those -// other protocols on the same Port. -// -// The magic cookie field MUST contain the fixed value 0x2112A442 in -// network byte order. -// -// Defined in "STUN Message Structure", section 6. +/// Fixed value that aids in distinguishing STUN packets +/// from packets of other protocols when STUN is multiplexed with those +/// other protocols on the same Port. +/// +/// The magic cookie field MUST contain the fixed value 0x2112A442 in +/// network byte order. +/// +/// Defined in "STUN Message Structure", section 6. pub const MAGIC_COOKIE: u32 = 0x2112A442; +/// Bytes of type and length preceding each attribute value. pub const ATTRIBUTE_HEADER_SIZE: usize = 4; +/// Bytes in the STUN message header. pub const MESSAGE_HEADER_SIZE: usize = 20; -// TRANSACTION_ID_SIZE is length of transaction id array (in bytes). -pub const TRANSACTION_ID_SIZE: usize = 12; // 96 bit +/// Length of transaction id array (in bytes). +/// 96 bit. +pub const TRANSACTION_ID_SIZE: usize = 12; #[derive(PartialEq, Eq, Hash, Copy, Clone, Default, Debug)] +/// A 96-bit transaction id, which pairs a response with its request. pub struct TransactionId(pub [u8; TRANSACTION_ID_SIZE]); impl TransactionId { @@ -45,26 +49,44 @@ impl Setter for TransactionId { } } -// Interfaces that are implemented by message attributes, shorthands for them, -// or helpers for message fields as type or transaction id. +/// Interfaces that are implemented by message attributes, shorthands for them, +/// or helpers for message fields as type or transaction id. pub trait Setter { // Setter sets *Message attribute. + /// Encodes this value into `m` as an attribute. + /// + /// # Errors + /// + /// Fails if the value cannot be encoded, or the message has no room for it. fn add_to(&self, m: &mut Message) -> Result<()>; } -// Getter parses attribute from *Message. +/// Getter parses attribute from *Message. pub trait Getter { + /// Decodes this value from the corresponding attribute of `m`. + /// + /// # Errors + /// + /// Fails if the attribute is absent or malformed. fn get_from(&mut self, m: &Message) -> Result<()>; } -// Checker checks *Message attribute. +/// Checker checks *Message attribute. pub trait Checker { + /// Validates this value against `m`. + /// + /// Used by attributes whose value depends on the encoded message, such as + /// `MESSAGE-INTEGRITY` and `FINGERPRINT`. + /// + /// # Errors + /// + /// Fails if the check does not hold. fn check(&self, m: &Message) -> Result<()>; } -// is_stun_message returns true if b looks like STUN message. -// Useful for multiplexing. is_stun_message does not guarantee -// that decoding will be successful. +/// Is_stun_message returns true if b looks like STUN message. +/// Useful for multiplexing. is_stun_message does not guarantee +/// that decoding will be successful. pub fn is_stun_message(b: &[u8]) -> bool { b.len() >= MESSAGE_HEADER_SIZE && u32::from_be_bytes([b[4], b[5], b[6], b[7]]) == MAGIC_COOKIE } @@ -75,11 +97,17 @@ pub fn is_stun_message(b: &[u8]) -> bool { // Message, its fields, results of m.Get or any attribute a.GetFrom // are valid only until Message.Raw is not modified. #[derive(Default, Debug, Clone)] +/// A STUN message: type, transaction id, and attributes, plus the encoded bytes. pub struct Message { + /// The message class and method — request, response or indication, and which method. pub typ: MessageType, + /// The attribute section's length in bytes, header excluded. pub length: u32, // len(Raw) not including header + /// The transaction id, echoed by the responder. pub transaction_id: TransactionId, + /// The message's attributes. pub attributes: Attributes, + /// The encoded message. Attributes whose value covers the message are computed over this. pub raw: Vec, } @@ -131,7 +159,7 @@ impl Setter for Message { } impl Message { - // New returns *Message with pre-allocated Raw. + /// New returns *Message with pre-allocated Raw. pub fn new() -> Self { Message { raw: { @@ -143,14 +171,14 @@ impl Message { } } - // marshal_binary implements the encoding.BinaryMarshaler interface. + /// Marshal_binary implements the encoding.BinaryMarshaler interface. pub fn marshal_binary(&self) -> Result> { // We can't return m.Raw, allocation is expected by implicit interface // contract induced by other implementations. Ok(self.raw.clone()) } - // unmarshal_binary implements the encoding.BinaryUnmarshaler interface. + /// Unmarshal_binary implements the encoding.BinaryUnmarshaler interface. pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { // We can't retain data, copy is expected by interface contract. self.raw.clear(); @@ -158,15 +186,15 @@ impl Message { self.decode() } - // NewTransactionID sets m.TransactionID to random value from crypto/rand - // and returns error if any. + /// NewTransactionID sets m.TransactionID to random value from crypto/rand + /// and returns error if any. pub fn new_transaction_id(&mut self) -> Result<()> { rand::rng().fill(&mut self.transaction_id.0); self.write_transaction_id(); Ok(()) } - // Reset resets Message, attributes and underlying buffer length. + /// Reset resets Message, attributes and underlying buffer length. pub fn reset(&mut self) { self.raw.clear(); self.length = 0; @@ -184,10 +212,10 @@ impl Message { self.raw.extend_from_slice(&vec![0; n - self.raw.len()]); } - // Add appends new attribute to message. Not goroutine-safe. - // - // Value of attribute is copied to internal buffer so - // it is safe to reuse v. + /// Add appends new attribute to message. Not goroutine-safe. + /// + /// Value of attribute is copied to internal buffer so + /// it is safe to reuse v. pub fn add(&mut self, t: AttrType, v: &[u8]) { // Allocating buffer for TLV (type-length-value). // T = t, L = len(v), V = v. @@ -237,13 +265,13 @@ impl Message { self.write_length(); } - // WriteLength writes m.Length to m.Raw. + /// WriteLength writes m.Length to m.Raw. pub fn write_length(&mut self) { self.grow(4, false); self.raw[2..4].copy_from_slice(&(self.length as u16).to_be_bytes()); } - // WriteHeader writes header to underlying buffer. Not goroutine-safe. + /// WriteHeader writes header to underlying buffer. Not goroutine-safe. pub fn write_header(&mut self) { self.grow(MESSAGE_HEADER_SIZE, false); @@ -254,13 +282,13 @@ impl Message { // transaction ID } - // WriteTransactionID writes m.TransactionID to m.Raw. + /// WriteTransactionID writes m.TransactionID to m.Raw. pub fn write_transaction_id(&mut self) { self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); // transaction ID } - // WriteAttributes encodes all m.Attributes to m. + /// WriteAttributes encodes all m.Attributes to m. pub fn write_attributes(&mut self) { let attributes: Vec = self.attributes.0.drain(..).collect(); for a in &attributes { @@ -269,19 +297,19 @@ impl Message { self.attributes = Attributes(attributes); } - // WriteType writes m.Type to m.Raw. + /// WriteType writes m.Type to m.Raw. pub fn write_type(&mut self) { self.grow(2, false); self.raw[..2].copy_from_slice(&self.typ.value().to_be_bytes()); // message type } - // SetType sets m.Type and writes it to m.Raw. + /// SetType sets m.Type and writes it to m.Raw. pub fn set_type(&mut self, t: MessageType) { self.typ = t; self.write_type(); } - // Encode re-encodes message into m.Raw. + /// Encode re-encodes message into m.Raw. pub fn encode(&mut self) { self.raw.clear(); self.write_header(); @@ -289,7 +317,7 @@ impl Message { self.write_attributes(); } - // Decode decodes m.Raw into m. + /// Decode decodes m.Raw into m. pub fn decode(&mut self) -> Result<()> { // decoding message header let buf = &self.raw; @@ -365,18 +393,18 @@ impl Message { Ok(()) } - // WriteTo implements WriterTo via calling Write(m.Raw) on w and returning - // call result. + /// WriteTo implements WriterTo via calling Write(m.Raw) on w and returning + /// call result. pub fn write_to(&self, writer: &mut W) -> Result { let n = writer.write(&self.raw)?; Ok(n) } - // ReadFrom implements ReaderFrom. Reads message from r into m.Raw, - // Decodes it and return error if any. If m.Raw is too small, will return - // ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. - // - // Can return *DecodeErr while decoding too. + /// ReadFrom implements ReaderFrom. Reads message from r into m.Raw, + /// Decodes it and return error if any. If m.Raw is too small, will return + /// ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. + /// + /// Can return *DecodeErr while decoding too. pub fn read_from(&mut self, reader: &mut R) -> Result { let mut t_buf = vec![0; DEFAULT_RAW_CAPACITY]; let n = reader.read(&mut t_buf)?; @@ -385,9 +413,9 @@ impl Message { Ok(n) } - // Write decodes message and return error if any. - // - // Any error is unrecoverable, but message could be partially decoded. + /// Write decodes message and return error if any. + /// + /// Any error is unrecoverable, but message could be partially decoded. pub fn write(&mut self, t_buf: &[u8]) -> Result { self.raw.clear(); self.raw.extend_from_slice(t_buf); @@ -395,14 +423,14 @@ impl Message { Ok(t_buf.len()) } - // CloneTo clones m to b securing any further m mutations. + /// CloneTo clones m to b securing any further m mutations. pub fn clone_to(&self, b: &mut Message) -> Result<()> { b.raw.clear(); b.raw.extend_from_slice(&self.raw); b.decode() } - // Contains return true if message contain t attribute. + /// Contains return true if message contain t attribute. pub fn contains(&self, t: AttrType) -> bool { for a in &self.attributes.0 { if a.typ == t { @@ -412,9 +440,9 @@ impl Message { false } - // get returns byte slice that represents attribute value, - // if there is no attribute with such type, - // ErrAttributeNotFound is returned. + /// Get returns byte slice that represents attribute value, + /// if there is no attribute with such type, + /// ErrAttributeNotFound is returned. pub fn get(&self, t: AttrType) -> Result> { let (v, ok) = self.attributes.get(t); if ok { @@ -424,21 +452,21 @@ impl Message { } } - // Build resets message and applies setters to it in batch, returning on - // first error. To prevent allocations, pass pointers to values. - // - // Example: - // var ( - // t = BindingRequest - // username = NewUsername("username") - // nonce = NewNonce("nonce") - // realm = NewRealm("example.org") - // ) - // m := new(Message) - // m.Build(t, username, nonce, realm) // 4 allocations - // m.Build(&t, &username, &nonce, &realm) // 0 allocations - // - // See BenchmarkBuildOverhead. + /// Build resets message and applies setters to it in batch, returning on + /// first error. To prevent allocations, pass pointers to values. + /// + /// Example: + /// var ( + /// t = BindingRequest + /// username = NewUsername("username") + /// nonce = NewNonce("nonce") + /// realm = NewRealm("example.org") + /// ) + /// m := new(Message) + /// m.Build(t, username, nonce, realm) // 4 allocations + /// m.Build(&t, &username, &nonce, &realm) // 0 allocations + /// + /// See BenchmarkBuildOverhead. pub fn build(&mut self, setters: &[Box]) -> Result<()> { self.reset(); self.write_header(); @@ -448,7 +476,7 @@ impl Message { Ok(()) } - // Check applies checkers to message in batch, returning on first error. + /// Check applies checkers to message in batch, returning on first error. pub fn check(&self, checkers: &[C]) -> Result<()> { for c in checkers { c.check(self)?; @@ -456,7 +484,7 @@ impl Message { Ok(()) } - // Parse applies getters to message in batch, returning on first error. + /// Parse applies getters to message in batch, returning on first error. pub fn parse(&self, getters: &mut [G]) -> Result<()> { for c in getters { c.get_from(self)?; @@ -467,13 +495,18 @@ impl Message { // MessageClass is 8-bit representation of 2-bit class of STUN Message Class. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// A STUN message class: request, indication, success response or error response. pub struct MessageClass(u8); -// Possible values for message class in STUN Message Type. -pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); // 0b00 -pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); // 0b01 -pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); // 0b10 -pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); // 0b11 +/// Possible values for message class in STUN Message Type. +/// 0b00. +pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); +/// 0b01. +pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); +/// 0b10. +pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); +/// 0b11. +pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); impl fmt::Display for MessageClass { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -491,20 +524,29 @@ impl fmt::Display for MessageClass { // Method is uint16 representation of 12-bit STUN method. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// A STUN method, such as Binding or one of TURN's. pub struct Method(u16); -// Possible methods for STUN Message. +/// Possible methods for STUN Message. pub const METHOD_BINDING: Method = Method(0x001); +/// TURN Allocate: asks a relay for a public address. pub const METHOD_ALLOCATE: Method = Method(0x003); +/// TURN Refresh: extends or releases an allocation. pub const METHOD_REFRESH: Method = Method(0x004); +/// TURN Send: an indication carrying data to a peer through the relay. pub const METHOD_SEND: Method = Method(0x006); +/// TURN Data: an indication carrying data from a peer through the relay. pub const METHOD_DATA: Method = Method(0x007); +/// TURN CreatePermission: authorizes traffic to and from a peer address. pub const METHOD_CREATE_PERMISSION: Method = Method(0x008); +/// TURN ChannelBind: binds a channel number to a peer for compact framing. pub const METHOD_CHANNEL_BIND: Method = Method(0x009); -// Methods from RFC 6062. +/// Methods from RFC 6062. pub const METHOD_CONNECT: Method = Method(0x000a); +/// TURN-TCP ConnectionBind: associates a new TCP connection with an allocation. pub const METHOD_CONNECTION_BIND: Method = Method(0x000b); +/// TURN-TCP ConnectionAttempt: notifies the client of an inbound TCP connection. pub const METHOD_CONNECTION_ATTEMPT: Method = Method(0x000c); impl fmt::Display for Method { @@ -533,23 +575,26 @@ impl fmt::Display for Method { // MessageType is STUN Message Type Field. #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +/// A message's class and method together, as encoded in the first two header bytes. pub struct MessageType { - pub method: Method, // e.g. binding + /// The method, such as Binding or Allocate. + pub method: Method, // e.g. binding + /// The class: request, indication, success response, or error response. pub class: MessageClass, // e.g. request } -// Common STUN message types. -// Binding request message type. +/// Common STUN message types. +/// Binding request message type. pub const BINDING_REQUEST: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_REQUEST, }; -// Binding success response message type +/// Binding success response message type. pub const BINDING_SUCCESS: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_SUCCESS_RESPONSE, }; -// Binding error response message type. +/// Binding error response message type. pub const BINDING_ERROR: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_ERROR_RESPONSE, @@ -586,12 +631,12 @@ impl Setter for MessageType { } impl MessageType { - // NewType returns new message type with provided method and class. + /// NewType returns new message type with provided method and class. pub fn new(method: Method, class: MessageClass) -> Self { MessageType { method, class } } - // Value returns bit representation of messageType. + /// Value returns bit representation of messageType. pub fn value(&self) -> u16 { // 0 1 // 2 3 4 5 6 7 8 9 0 1 2 3 4 5 @@ -625,7 +670,7 @@ impl MessageType { method + class } - // ReadValue decodes uint16 into MessageType. + /// ReadValue decodes uint16 into MessageType. pub fn read_value(&mut self, value: u16) { // Decoding class. // We are taking first bit from v >> 4 and second from v >> 7. diff --git a/rtc-stun/src/textattrs.rs b/rtc-stun/src/textattrs.rs index 887e325e..3016a2ab 100644 --- a/rtc-stun/src/textattrs.rs +++ b/rtc-stun/src/textattrs.rs @@ -13,30 +13,33 @@ const MAX_REALM_B: usize = 763; const MAX_SOFTWARE_B: usize = 763; const MAX_NONCE_B: usize = 763; -// Username represents USERNAME attribute. -// -// RFC 5389 Section 15.3 +/// USERNAME attribute. +/// +/// RFC 5389 Section 15.3. pub type Username = TextAttribute; -// Realm represents REALM attribute. -// -// RFC 5389 Section 15.7 +/// REALM attribute. +/// +/// RFC 5389 Section 15.7. pub type Realm = TextAttribute; -// Nonce represents NONCE attribute. -// -// RFC 5389 Section 15.8 +/// NONCE attribute. +/// +/// RFC 5389 Section 15.8. pub type Nonce = TextAttribute; -// Software is SOFTWARE attribute. -// -// RFC 5389 Section 15.10 +/// SOFTWARE attribute. +/// +/// RFC 5389 Section 15.10. pub type Software = TextAttribute; // TextAttribute is helper for adding and getting text attributes. #[derive(Clone, Default)] +/// A text-valued attribute such as `USERNAME`, `REALM`, `NONCE` or `SOFTWARE`. pub struct TextAttribute { + /// Which attribute this text belongs to. pub attr: AttrType, + /// The text value. pub text: String, } @@ -74,11 +77,12 @@ impl Getter for TextAttribute { } impl TextAttribute { + /// A text attribute of type `attr` holding `text`. pub fn new(attr: AttrType, text: String) -> Self { TextAttribute { attr, text } } - // get_from_as gets t attribute from m and appends its value to reset v. + /// Get_from_as gets t attribute from m and appends its value to reset v. pub fn get_from_as(m: &Message, attr: AttrType) -> Result { match attr { ATTR_USERNAME => {} diff --git a/rtc-stun/src/uattrs.rs b/rtc-stun/src/uattrs.rs index 4148297a..2fca17e2 100644 --- a/rtc-stun/src/uattrs.rs +++ b/rtc-stun/src/uattrs.rs @@ -7,9 +7,9 @@ use shared::error::*; use std::fmt; -// UnknownAttributes represents UNKNOWN-ATTRIBUTES attribute. -// -// RFC 5389 Section 15.9 +/// UNKNOWN-ATTRIBUTES attribute. +/// +/// RFC 5389 Section 15.9. pub struct UnknownAttributes(pub Vec); impl fmt::Display for UnknownAttributes { diff --git a/rtc-stun/src/uri.rs b/rtc-stun/src/uri.rs index b829c523..e347d05c 100644 --- a/rtc-stun/src/uri.rs +++ b/rtc-stun/src/uri.rs @@ -7,14 +7,20 @@ use std::fmt; // SCHEME definitions from RFC 7064 Section 3.2. +/// The `stun:` URI scheme. pub const SCHEME: &str = "stun"; +/// The `stuns:` URI scheme, for STUN over TLS or DTLS. pub const SCHEME_SECURE: &str = "stuns"; // URI as defined in RFC 7064. #[derive(PartialEq, Eq, Debug)] +/// A parsed `stun:` or `stuns:` URI. pub struct Uri { + /// The scheme, `stun` or `stuns`. pub scheme: String, + /// The server host name or address. pub host: String, + /// The port, if the URI specified one. pub port: Option, } @@ -35,7 +41,7 @@ impl fmt::Display for Uri { } impl Uri { - // parse_uri parses URI from string. + /// Parse_uri parses URI from string. pub fn parse_uri(raw: &str) -> Result { // work around for url crate if raw.contains("//") { diff --git a/rtc-stun/src/xoraddr.rs b/rtc-stun/src/xoraddr.rs index f212a150..b328e0f4 100644 --- a/rtc-stun/src/xoraddr.rs +++ b/rtc-stun/src/xoraddr.rs @@ -67,7 +67,9 @@ pub fn xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { /// /// RFC 5389 Section 15.2 pub struct XorMappedAddress { + /// The IP address. pub ip: IpAddr, + /// The port. pub port: u16, } diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 5655f322..503039ec 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -1,10 +1,14 @@ #[cfg(test)] mod client_test; +/// Channel bindings, which replace the 36-byte Data indication header with a 4-byte one. pub mod binding; +/// Per-peer send permissions, which a relay requires before it will forward to an address. pub mod permission; mod proto; +/// A live allocation on the server, and sending or receiving through it. pub mod relay; +/// Outstanding request tracking, with the RFC's retransmission schedule. pub mod transaction; use bytes::BytesMut; @@ -41,23 +45,43 @@ const DEFAULT_RTO_IN_MS: u64 = 200; const MAX_DATA_BUFFER_SIZE: usize = u16::MAX as usize; // message size limit for Chromium const MAX_READ_QUEUE_SIZE: usize = 1024; +/// The public address the TURN server allocated on this client's behalf. +/// +/// Peers send here; the server forwards to the client. pub type RelayedAddr = SocketAddr; +/// The client's own address as seen by the server — its server-reflexive address. pub type ReflexiveAddr = SocketAddr; +/// The address of a remote peer the client exchanges data with through the relay. pub type PeerAddr = SocketAddr; #[derive(Debug)] +/// What the client produces in response to inbound datagrams and elapsed time. +/// +/// Every variant carries the [`TransactionId`] of the request it answers, so a caller can +/// match responses to the requests it issued. pub enum Event { + /// A request exhausted its retransmissions without a response. TransactionTimeout(TransactionId), + /// A STUN Binding succeeded, reporting this client's server-reflexive address. BindingResponse(TransactionId, ReflexiveAddr), + /// A STUN Binding failed. BindingError(TransactionId, Error), + /// An Allocate succeeded; the relayed address is now usable. AllocateResponse(TransactionId, RelayedAddr), + /// An Allocate failed — commonly authentication, or the server being out of ports. AllocateError(TransactionId, Error), + /// A CreatePermission succeeded; the relay will now forward to and from this peer. CreatePermissionResponse(TransactionId, PeerAddr), + /// A CreatePermission failed. CreatePermissionError(TransactionId, Error), + /// Data arrived from a peer through the relay. + /// + /// The channel number is `Some` when it came as ChannelData and `None` when it came as a + /// Data indication. DataIndicationOrChannelData(Option, PeerAddr, BytesMut), } @@ -78,14 +102,23 @@ enum AllocateState { /// ClientConfig is a bag of config parameters for Client. pub struct ClientConfig { + /// The STUN server to use for Binding requests, as `host:port`. May be empty. pub stun_serv_addr: String, // STUN server address (e.g. "stun.abc.com:3478") + /// The TURN server to allocate from, as `host:port`. pub turn_serv_addr: String, // TURN server address (e.g. "turn.abc.com:3478") + /// The local address the client sends from. pub local_addr: SocketAddr, + /// Whether to reach the server over UDP or TCP. pub transport_protocol: TransportProtocol, + /// The long-term credential username for the TURN server. pub username: String, + /// The long-term credential password. pub password: String, + /// The authentication realm, used in the `MESSAGE-INTEGRITY` computation. pub realm: String, + /// An optional `SOFTWARE` attribute value, sent for diagnostics. pub software: String, + /// The initial retransmission timeout in milliseconds; each retry doubles it. pub rto_in_ms: u64, } @@ -333,6 +366,12 @@ impl Client { Ok(()) } + /// Borrows the allocation for `relayed_addr` so data can be sent or permissions created. + /// + /// # Errors + /// + /// Fails if this client has no allocation for that address — it was never allocated, or has + /// already been closed. pub fn relay(&mut self, relayed_addr: SocketAddr) -> Result> { if !self.relays.contains_key(&relayed_addr) { Err(Error::ErrStreamNotExisted) diff --git a/rtc-turn/src/client/relay.rs b/rtc-turn/src/client/relay.rs index f9ef8174..aadc6c18 100644 --- a/rtc-turn/src/client/relay.rs +++ b/rtc-turn/src/client/relay.rs @@ -68,13 +68,25 @@ impl RelayState { } } -// Relay is the implementation of the Conn interfaces for UDP Relayed network connections. +/// A borrowed handle to one live allocation on the TURN server. +/// +/// Obtained from [`Client::relay`](crate::client::Client::relay). Sending to a peer requires +/// a permission for it first — see [`Self::create_permission`]. pub struct Relay<'a> { pub(crate) relayed_addr: RelayedAddr, pub(crate) client: &'a mut Client, } impl Relay<'_> { + /// Asks the server to permit traffic to and from `peer_addr`. + /// + /// The relay silently drops data for peers with no permission, and permissions expire after + /// five minutes unless refreshed. Returns the transaction id to match against the resulting + /// [`Event`], or `None` if a permission is already in place. + /// + /// # Errors + /// + /// Fails if the allocation no longer exists or the request cannot be encoded. pub fn create_permission(&mut self, peer_addr: SocketAddr) -> Result> { if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { relay @@ -137,6 +149,13 @@ impl Relay<'_> { } } + /// Sends `p` to `peer_addr` through the relay. + /// + /// Uses ChannelData framing if a channel is bound for that peer, otherwise a Data indication. + /// + /// # Errors + /// + /// Fails if the allocation is gone, or if no permission exists for `peer_addr`. pub fn send_to(&mut self, p: &[u8], peer_addr: SocketAddr) -> Result<()> { // check if we have a permission for the destination IP addr let result = if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { @@ -234,6 +253,11 @@ impl Relay<'_> { // Close closes the connection. // Any blocked ReadFrom or write_to operations will be unblocked and return errors. + /// Releases the allocation by refreshing it with a zero lifetime. + /// + /// # Errors + /// + /// Fails if the refresh request cannot be sent. pub fn close(&mut self) -> Result<()> { self.refresh_allocation(Duration::from_secs(0)) } diff --git a/rtc-turn/src/lib.rs b/rtc-turn/src/lib.rs index f8e44019..aaa5cd18 100644 --- a/rtc-turn/src/lib.rs +++ b/rtc-turn/src/lib.rs @@ -1,5 +1,30 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! TURN for the Sans-I/O WebRTC stack. +//! +//! Traversal Using Relays around NAT ([RFC 5766]) with IPv6 support ([RFC 6156]). TURN is +//! ICE's fallback: when no direct path between two peers can be found, each relays its +//! media through a server, which allocates a public address on their behalf. +//! +//! # Structure +//! +//! * [`client`] — the Sans-I/O client: allocate a relayed address, create permissions, +//! bind channels, and send or receive through the allocation. It owns no sockets. +//! * [`proto`] — the TURN-specific STUN attributes and methods (`ALLOCATE`, +//! `CREATE-PERMISSION`, `CHANNEL-BIND`, `XOR-RELAYED-ADDRESS`, ChannelData framing), +//! built on [`rtc-stun`]. +//! +//! Most applications do not depend on this crate directly — [`rtc-ice`] gathers relay +//! candidates through it, and the [`rtc`](https://docs.rs/rtc) crate drives that. +//! +//! [RFC 5766]: https://datatracker.ietf.org/doc/html/rfc5766 +//! [RFC 6156]: https://datatracker.ietf.org/doc/html/rfc6156 +//! [`rtc-stun`]: https://docs.rs/rtc-stun +//! [`rtc-ice`]: https://docs.rs/rtc-ice + +/// The Sans-I/O TURN client: allocate a relayed address and send through it. pub mod client; +/// The TURN-specific STUN attributes, methods and ChannelData framing. pub mod proto; diff --git a/rtc-turn/src/proto/addr.rs b/rtc-turn/src/proto/addr.rs index 70cc4f45..7fdb8fcf 100644 --- a/rtc-turn/src/proto/addr.rs +++ b/rtc-turn/src/proto/addr.rs @@ -49,9 +49,16 @@ impl Addr { // FiveTuple represents 5-TUPLE value. #[derive(PartialEq, Eq, Default)] +/// The five-tuple that uniquely identifies a TURN allocation. +/// +/// Client address, server address and transport together — the server keys allocations by +/// this, so the same client may hold several over different transports. pub struct FiveTuple { + /// The client's transport address. pub client: Addr, + /// The server's transport address. pub server: Addr, + /// The transport carrying the allocation. pub proto: Protocol, } diff --git a/rtc-turn/src/proto/chandata.rs b/rtc-turn/src/proto/chandata.rs index cb24bbf9..cf5f1fad 100644 --- a/rtc-turn/src/proto/chandata.rs +++ b/rtc-turn/src/proto/chandata.rs @@ -22,8 +22,11 @@ const CHANNEL_DATA_HEADER_SIZE: usize = CHANNEL_DATA_LENGTH_SIZE + CHANNEL_DATA_ /// [RFC 5766 Section 11.4](https://www.rfc-editor.org/rfc/rfc5766#section-11.4). #[derive(Default, Debug)] pub struct ChannelData { + /// The relayed payload. May be a subslice of [`Self::raw`]. pub data: Vec, // can be subslice of Raw + /// The channel this data belongs to, which identifies the peer. pub number: ChannelNumber, + /// The full encoded message, header included. pub raw: Vec, } diff --git a/rtc-turn/src/proto/channum.rs b/rtc-turn/src/proto/channum.rs index 0b9f6f0f..ab753f90 100644 --- a/rtc-turn/src/proto/channum.rs +++ b/rtc-turn/src/proto/channum.rs @@ -16,7 +16,11 @@ const CHANNEL_NUMBER_SIZE: usize = 4; // // 0x4000 through 0x7FFF: These values are the allowed channel // numbers (16,383 possible values). +/// The lowest channel number a client may bind. +/// +/// The range is chosen so ChannelData can be told apart from STUN messages on the same port. pub const MIN_CHANNEL_NUMBER: u16 = 0x4000; +/// The highest channel number a client may bind. pub const MAX_CHANNEL_NUMBER: u16 = 0x7FFF; /// `ChannelNumber` represents `CHANNEL-NUMBER` attribute. Encoded as `u16`. diff --git a/rtc-turn/src/proto/mod.rs b/rtc-turn/src/proto/mod.rs index ada03ffd..0961d54b 100644 --- a/rtc-turn/src/proto/mod.rs +++ b/rtc-turn/src/proto/mod.rs @@ -1,17 +1,29 @@ #[cfg(test)] mod proto_test; +/// Address helpers and the five-tuple that identifies an allocation. pub mod addr; +/// ChannelData messages — the compact 4-byte framing for relayed data. pub mod chandata; +/// The `CHANNEL-NUMBER` attribute. pub mod channum; +/// The `DATA` attribute, which carries relayed payloads in Send/Data indications. pub mod data; +/// The `DONT-FRAGMENT` attribute, asking the server to set DF on relayed packets. pub mod dontfrag; +/// The `EVEN-PORT` attribute, requesting an even relayed port (for RTP/RTCP pairs). pub mod evenport; +/// The `LIFETIME` attribute, which sets and reports allocation expiry. pub mod lifetime; +/// The `XOR-PEER-ADDRESS` attribute, naming the peer in permission and data messages. pub mod peeraddr; +/// The `XOR-RELAYED-ADDRESS` attribute, which reports the allocated public address. pub mod relayaddr; +/// The `REQUESTED-ADDRESS-FAMILY` attribute, for asking for an IPv4 or IPv6 allocation. pub mod reqfamily; +/// The `REQUESTED-TRANSPORT` attribute, which selects the relay's transport to peers. pub mod reqtrans; +/// The `RESERVATION-TOKEN` attribute, used to claim a previously reserved port. pub mod rsrvtoken; use std::fmt; diff --git a/rtc-turn/src/proto/peeraddr.rs b/rtc-turn/src/proto/peeraddr.rs index f56c738a..7cfcc95d 100644 --- a/rtc-turn/src/proto/peeraddr.rs +++ b/rtc-turn/src/proto/peeraddr.rs @@ -18,7 +18,9 @@ use stun::xoraddr::*; /// [RFC 5766 Section 14.3](https://www.rfc-editor.org/rfc/rfc5766#section-14.3). #[derive(PartialEq, Eq, Debug)] pub struct PeerAddress { + /// The peer IP address. pub ip: IpAddr, + /// The peer port. pub port: u16, } diff --git a/rtc-turn/src/proto/relayaddr.rs b/rtc-turn/src/proto/relayaddr.rs index 36a50dbf..dbc23f42 100644 --- a/rtc-turn/src/proto/relayaddr.rs +++ b/rtc-turn/src/proto/relayaddr.rs @@ -17,7 +17,9 @@ use stun::xoraddr::*; /// [RFC 5766 Section 14.5](https://www.rfc-editor.org/rfc/rfc5766#section-14.5). #[derive(PartialEq, Eq, Debug)] pub struct RelayedAddress { + /// The relayed IP address. pub ip: IpAddr, + /// The relayed port. pub port: u16, } diff --git a/rtc-turn/src/proto/reqfamily.rs b/rtc-turn/src/proto/reqfamily.rs index ea007286..40b99d2a 100644 --- a/rtc-turn/src/proto/reqfamily.rs +++ b/rtc-turn/src/proto/reqfamily.rs @@ -10,7 +10,9 @@ use stun::message::*; use shared::error::{Error, Result}; // Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1. +/// Requests an IPv4 relayed address. pub const REQUESTED_FAMILY_IPV4: RequestedAddressFamily = RequestedAddressFamily(0x01); +/// Requests an IPv6 relayed address. pub const REQUESTED_FAMILY_IPV6: RequestedAddressFamily = RequestedAddressFamily(0x02); /// `RequestedAddressFamily` represents the `REQUESTED-ADDRESS-FAMILY` Attribute as diff --git a/rtc-turn/src/proto/reqtrans.rs b/rtc-turn/src/proto/reqtrans.rs index 94c80be9..5aa7ecaf 100644 --- a/rtc-turn/src/proto/reqtrans.rs +++ b/rtc-turn/src/proto/reqtrans.rs @@ -19,6 +19,7 @@ use shared::error::Result; /// [RFC 5766 Section 14.7](https://www.rfc-editor.org/rfc/rfc5766#section-14.7). #[derive(Default, Debug, PartialEq, Eq)] pub struct RequestedTransport { + /// The transport the relay should use toward peers. WebRTC always requests UDP. pub protocol: Protocol, } From 30673740d89785d719419df5d08feca8b168d44e Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 20:23:23 -0700 Subject: [PATCH 11/40] fix module level inline doc issue --- examples/signal/src/lib.rs | 13 +++ rtc-datachannel/src/data_channel/mod.rs | 12 ++ rtc-datachannel/src/lib.rs | 25 +++++ rtc-dtls/src/config.rs | 17 ++- rtc-dtls/src/conn/mod.rs | 9 ++ rtc-dtls/src/crypto/mod.rs | 9 ++ rtc-dtls/src/endpoint.rs | 9 ++ rtc-dtls/src/handshake/mod.rs | 13 +++ rtc-dtls/src/lib.rs | 19 ++++ .../src/record_layer/record_layer_header.rs | 9 ++ rtc-ice/src/agent/mod.rs | 10 ++ rtc-ice/src/candidate/candidate_pair.rs | 9 ++ rtc-ice/src/candidate/mod.rs | 14 +++ rtc-ice/src/lib.rs | 18 +++ rtc-ice/src/network_type/mod.rs | 5 + rtc-interceptor-derive/src/lib.rs | 48 +++++++- rtc-interceptor/src/lib.rs | 71 +++++++++--- rtc-interceptor/src/nack/generator.rs | 2 +- rtc-interceptor/src/nack/mod.rs | 57 +--------- rtc-interceptor/src/nack/responder.rs | 2 +- rtc-interceptor/src/noop.rs | 12 +- rtc-interceptor/src/registry.rs | 50 ++++++--- rtc-interceptor/src/report/mod.rs | 53 +-------- rtc-interceptor/src/report/receiver.rs | 18 +-- rtc-interceptor/src/report/sender.rs | 26 +++-- rtc-interceptor/src/stream_info.rs | 2 +- rtc-interceptor/src/twcc/mod.rs | 67 +----------- rtc-interceptor/src/twcc/receiver.rs | 2 +- rtc-interceptor/src/twcc/sender.rs | 2 +- rtc-mdns/src/config.rs | 103 +++++++++--------- rtc-mdns/src/proto/mod.rs | 87 ++++----------- rtc-media/src/audio/buffer.rs | 10 ++ rtc-media/src/io/h26x_reader/mod.rs | 8 ++ rtc-media/src/io/ivf_reader/mod.rs | 5 + rtc-media/src/io/ogg_reader/mod.rs | 9 ++ rtc-media/src/lib.rs | 18 +++ rtc-rtcp/src/extended_report/rle.rs | 7 ++ rtc-rtcp/src/header.rs | 8 ++ rtc-rtcp/src/lib.rs | 60 ++++++---- .../transport_layer_cc/mod.rs | 8 ++ rtc-rtp/src/codec/h264/mod.rs | 8 ++ rtc-rtp/src/codec/h265/mod.rs | 12 ++ rtc-rtp/src/header.rs | 16 +++ rtc-rtp/src/lib.rs | 28 +++++ rtc-sctp/src/fuzzing.rs | 6 + rtc-sctp/src/lib.rs | 60 ++++++++-- rtc-sdp/src/description/common.rs | 8 ++ rtc-sdp/src/description/media.rs | 9 ++ rtc-sdp/src/description/session.rs | 11 ++ rtc-sdp/src/extmap/mod.rs | 9 ++ rtc-sdp/src/lib.rs | 27 +++++ rtc-shared/src/error.rs | 6 + rtc-shared/src/ifaces/ffi/mod.rs | 10 ++ rtc-shared/src/lib.rs | 22 ++++ rtc-shared/src/time.rs | 6 + rtc-shared/src/util.rs | 7 ++ rtc-srtp/src/key_derivation.rs | 6 +- rtc-srtp/src/lib.rs | 17 +++ rtc-stun/src/agent.rs | 8 ++ rtc-stun/src/attributes.rs | 15 +++ rtc-stun/src/client.rs | 5 + rtc-stun/src/error_code.rs | 8 ++ rtc-stun/src/lib.rs | 26 +++++ rtc-stun/src/message.rs | 11 ++ rtc-turn/src/client/mod.rs | 9 ++ rtc-turn/src/lib.rs | 24 ++++ rtc-turn/src/proto/mod.rs | 10 ++ 67 files changed, 934 insertions(+), 376 deletions(-) diff --git a/examples/signal/src/lib.rs b/examples/signal/src/lib.rs index bdfdc648..a0bb84a2 100644 --- a/examples/signal/src/lib.rs +++ b/examples/signal/src/lib.rs @@ -8,6 +8,19 @@ //! paste it into the other. This crate holds the few functions that make that work, so the //! examples can stay focused on the WebRTC parts. //! +//! # Example +//! +//! ``` +//! # fn main() -> Result<(), Box> { +//! let json = r#"{"type":"offer","sdp":"v=0\r\n"}"#; +//! +//! // The examples print this blob for you to paste into the other peer. +//! let blob = rtc_signal::encode(json); +//! assert_eq!(rtc_signal::decode(&blob)?, json); +//! # Ok(()) +//! # } +//! ``` +//! //! It is a support crate for the examples, not part of the WebRTC API — nothing here is //! needed to use [`rtc`](https://docs.rs/rtc) or [`webrtc`](https://docs.rs/webrtc). diff --git a/rtc-datachannel/src/data_channel/mod.rs b/rtc-datachannel/src/data_channel/mod.rs index 2ddbd2be..445d74d1 100644 --- a/rtc-datachannel/src/data_channel/mod.rs +++ b/rtc-datachannel/src/data_channel/mod.rs @@ -1,3 +1,15 @@ +//! One data channel over an SCTP stream. +//! +//! A [`DataChannel`](crate::data_channel::DataChannel) is opened either by the DCEP handshake (`DATA_CHANNEL_OPEN`, then +//! `DATA_CHANNEL_ACK`) or out of band when [`DataChannelConfig::negotiated`](crate::data_channel::DataChannelConfig::negotiated) is set and both +//! sides already agreed the stream id through signalling. +//! +//! The reliability the channel is opened with maps onto SCTP send parameters: +//! [`get_reliability_params`](crate::data_channel::DataChannel::get_reliability_params) turns a +//! [`ChannelType`](crate::message::message_channel_open::ChannelType) into the ordered flag and +//! partial-reliability setting SCTP needs, and +//! [`get_channel_type_and_reliability_parameter`](crate::data_channel::DataChannel::get_channel_type_and_reliability_parameter) +//! goes the other way from the W3C `maxPacketLifeTime`/`maxRetransmits` pair. #[cfg(test)] mod data_channel_test; diff --git a/rtc-datachannel/src/lib.rs b/rtc-datachannel/src/lib.rs index b0a15a09..a46d58ec 100644 --- a/rtc-datachannel/src/lib.rs +++ b/rtc-datachannel/src/lib.rs @@ -17,6 +17,31 @@ //! * [`message`] — the DCEP messages themselves: `DataChannelOpen`, `DataChannelAck`, and //! the channel-type encoding. //! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_datachannel::message::message_channel_open::{ChannelType, DataChannelOpen}; +//! use rtc_datachannel::message::{Message, message_type::MessageType}; +//! use shared::marshal::{Marshal, Unmarshal}; +//! +//! # fn example() -> Result<(), Box> { +//! let open = Message::DataChannelOpen(DataChannelOpen { +//! channel_type: ChannelType::PartialReliableRexmit, +//! priority: 256, +//! reliability_parameter: 3, // give up after 3 retransmissions +//! label: b"chat".to_vec(), +//! protocol: Vec::new(), +//! }); +//! assert_eq!(open.message_type(), MessageType::DataChannelOpen); +//! +//! let encoded = open.marshal()?; +//! let mut buf = Bytes::from(encoded.to_vec()); +//! assert_eq!(Message::unmarshal(&mut buf)?, open); +//! # Ok(()) +//! # } +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate layers it over [`rtc-sctp`] and exposes //! `RTCDataChannel`. diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 9ac4a111..25735853 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -1,3 +1,18 @@ +//! Handshake configuration. +//! +//! [`ConfigBuilder`](crate::config::ConfigBuilder) is what a caller supplies: certificates, the client/server role, which cipher +//! suites and curves to offer, the SRTP protection profiles to negotiate through `use_srtp`, +//! and how strictly to require the extended master secret +//! ([`ExtendedMasterSecretType`](crate::config::ExtendedMasterSecretType)). +//! +//! WebRTC authenticates peers by comparing the certificate fingerprint against the one +//! signalled in SDP, not against a CA chain — so certificates here are normally self-signed +//! (see [`gen_self_signed_root_cert`](crate::config::gen_self_signed_root_cert)) and the check is implemented by supplying a +//! [`VerifyPeerCertificateFn`](crate::config::VerifyPeerCertificateFn). +//! +//! [`HandshakeConfig`](crate::config::HandshakeConfig) is the resolved form the handshake +//! actually runs with, produced by [`ConfigBuilder::build`](crate::config::ConfigBuilder::build). + #[cfg(test)] mod config_test; @@ -391,7 +406,7 @@ pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { } #[derive(Clone)] -/// The resolved configuration a handshake runs with, built from a [`ConfigBuilder`]. +/// The resolved configuration a handshake runs with, produced by [`ConfigBuilder::build`]. pub struct HandshakeConfig { pub(crate) local_psk_callback: Option, pub(crate) local_psk_identity_hint: Option>, diff --git a/rtc-dtls/src/conn/mod.rs b/rtc-dtls/src/conn/mod.rs index cafa4b42..695591a4 100644 --- a/rtc-dtls/src/conn/mod.rs +++ b/rtc-dtls/src/conn/mod.rs @@ -1,3 +1,12 @@ +//! The DTLS association. +//! +//! [`DTLSConn`](crate::conn::DTLSConn) joins the handshake state machine to the record layer for one peer: inbound +//! datagrams go in through [`read`](crate::conn::DTLSConn::read), application data comes out through +//! [`incoming_application_data`](crate::conn::DTLSConn::incoming_application_data), and whatever should go on +//! the wire is collected from [`outgoing_raw_packet`](crate::conn::DTLSConn::outgoing_raw_packet). +//! +//! Normally an application drives [`Endpoint`](crate::endpoint::Endpoint) instead, which owns one +//! of these per remote address. #[cfg(test)] mod conn_test; diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index 3e2b9704..02474fa0 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -1,3 +1,12 @@ +//! Cryptographic primitives for DTLS. +//! +//! The record ciphers ([`crypto_gcm`](crate::crypto::crypto_gcm), [`crypto_ccm`](crate::crypto::crypto_ccm), [`crypto_chacha20`](crate::crypto::crypto_chacha20), [`crypto_cbc`](crate::crypto::crypto_cbc)), plus +//! the certificate and signature handling the handshake needs. Which cipher is used is decided by +//! the negotiated cipher suite, so a caller normally reaches these only through +//! [`CipherSuite`](crate::cipher_suite::CipherSuite). +//! +//! Certificates here are usually self-signed: WebRTC authenticates a peer by comparing the +//! certificate fingerprint against the one signalled in SDP, not by validating a CA chain. #[cfg(test)] mod crypto_test; diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index c9badd21..0c03de9f 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -1,3 +1,12 @@ +//! The Sans-I/O DTLS endpoint. +//! +//! An [`Endpoint`](crate::endpoint::Endpoint) multiplexes several DTLS associations by remote address. Feed it inbound +//! datagrams, poll it for the datagrams to send and for [`EndpointEvent`](crate::endpoint::EndpointEvent)s, and drive its timers +//! with `handle_timeout`/`poll_timeout`. It owns no sockets and reads no clock. +//! +//! [`EndpointEvent::HandshakeComplete`](crate::endpoint::EndpointEvent::HandshakeComplete) is the signal an application waits for: from that point +//! application data can be written, and the SRTP keying material can be exported from the +//! completed handshake. use crate::conn::DTLSConn; use shared::error::{Error, Result}; use shared::{EcnCodepoint, TransportContext}; diff --git a/rtc-dtls/src/handshake/mod.rs b/rtc-dtls/src/handshake/mod.rs index 87d7e054..2aa52995 100644 --- a/rtc-dtls/src/handshake/mod.rs +++ b/rtc-dtls/src/handshake/mod.rs @@ -1,3 +1,16 @@ +//! DTLS handshake messages. +//! +//! Each message type has its own module; [`HandshakeMessage`](crate::handshake::HandshakeMessage) is the parsed union of them and +//! [`Handshake`](crate::handshake::Handshake) pairs one with its [`HandshakeHeader`](crate::handshake::handshake_header::HandshakeHeader). +//! +//! Two things distinguish this from a TLS handshake. Messages carry a sequence number and +//! fragment offsets, because a handshake message may be larger than a datagram and must be +//! reassembled. And the server may answer a ClientHello with a +//! [`HelloVerifyRequest`](crate::handshake::handshake_message_hello_verify_request) carrying a cookie, which the +//! client echoes — a cheap defence against using the handshake for amplification. +//! +//! [`handshake_cache`](crate::handshake::handshake_cache) retains the messages so the hash in `Finished` can be computed over +//! exactly what both sides saw. /// Buffers handshake messages so their hash can be computed for `Finished` verification. pub mod handshake_cache; /// The header prefixing every handshake message, including fragment offsets. diff --git a/rtc-dtls/src/lib.rs b/rtc-dtls/src/lib.rs index 277e0f4b..da906ace 100644 --- a/rtc-dtls/src/lib.rs +++ b/rtc-dtls/src/lib.rs @@ -23,6 +23,25 @@ //! * [`extension`] — the ClientHello/ServerHello extensions, including `use_srtp` and SNI. //! * [`alert`], [`content`], [`record_layer`] — the record layer and its content types. //! +//! # Example +//! +//! A WebRTC handshake is configured with a self-signed certificate and the SRTP profiles to +//! negotiate through `use_srtp`; the keys for those profiles are then exported from the +//! completed handshake rather than signalled: +//! +//! ``` +//! use rtc_dtls::config::{ConfigBuilder, ExtendedMasterSecretType}; +//! use rtc_dtls::extension::extension_use_srtp::SrtpProtectionProfile; +//! +//! let builder = ConfigBuilder::default() +//! .with_srtp_protection_profiles(vec![ +//! SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, +//! SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, +//! ]) +//! .with_extended_master_secret(ExtendedMasterSecretType::Require); +//! # let _ = builder; +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate drives it as one layer of the peer-connection //! pipeline. diff --git a/rtc-dtls/src/record_layer/record_layer_header.rs b/rtc-dtls/src/record_layer/record_layer_header.rs index 77799127..ceb7db01 100644 --- a/rtc-dtls/src/record_layer/record_layer_header.rs +++ b/rtc-dtls/src/record_layer/record_layer_header.rs @@ -1,3 +1,12 @@ +//! The DTLS record header. +//! +//! Thirteen bytes: content type, protocol version, a 16-bit epoch, a 48-bit sequence number, and +//! the body length. The epoch is what DTLS adds over TLS here — it increments on every +//! ChangeCipherSpec, so records protected with the old and new keys can be told apart while a +//! rekey is in flight. +//! +//! The sequence number is 48 bits on the wire but held as a `u64`; [`MAX_SEQUENCE_NUMBER`](crate::record_layer::record_layer_header::MAX_SEQUENCE_NUMBER) is the +//! largest value that fits. use crate::content::*; use shared::error::*; diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index edbaaeeb..24a973e4 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -1,3 +1,13 @@ +//! The Sans-I/O ICE agent. +//! +//! An [`Agent`](crate::agent::Agent) is given local and remote candidates and inbound datagrams; it produces the +//! connectivity checks to send, the state transitions to report, and eventually a selected +//! candidate pair. It owns no sockets and no clock — the caller drives time with +//! `handle_timeout`. +//! +//! Two roles exist: the controlling agent nominates the pair that will carry media, the +//! controlled agent accepts that choice. Which side controls is decided by which offered, and the +//! two must not agree — see [`Credentials`](crate::agent::Credentials) and [`Agent::set_role`](crate::agent::Agent::set_role). #[cfg(test)] mod agent_test; diff --git a/rtc-ice/src/candidate/candidate_pair.rs b/rtc-ice/src/candidate/candidate_pair.rs index 669d092a..306a98db 100644 --- a/rtc-ice/src/candidate/candidate_pair.rs +++ b/rtc-ice/src/candidate/candidate_pair.rs @@ -1,3 +1,12 @@ +//! Candidate pairs and their check state. +//! +//! ICE forms a pair from each compatible local/remote candidate combination and works through +//! them in priority order. A pair's combined priority is computed from both sides' priorities with +//! the controlling agent's dominating, so both agents derive the same ordering. +//! +//! [`CandidatePairState`](crate::candidate::candidate_pair::CandidatePairState) tracks how far a pair has got: waiting, in progress, succeeded or +//! failed. The controlling agent nominates one of the succeeded pairs, and that pair carries the +//! media. use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index 4564d9bf..b5a8c585 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -1,3 +1,17 @@ +//! ICE candidates: the addresses an agent can be reached at. +//! +//! A [`Candidate`](crate::candidate::Candidate) pairs a transport address with a [`CandidateType`](crate::candidate::CandidateType) — host, server-reflexive, +//! peer-reflexive or relay — and the bookkeeping ICE needs: a priority, a foundation, and +//! last-sent/last-received times that feed consent freshness. +//! +//! Type drives priority, and priority drives check order: host candidates are tried first +//! because they need no traversal, relay candidates last because they always cost an extra hop. +//! The [`foundation`](crate::candidate::Candidate::foundation) groups candidates that share a base and transport, +//! so redundant checks can be skipped. +//! +//! Each type has its own constructor module (`candidate_host`, `candidate_relay`, …), all +//! built on the shared [`CandidateConfig`](crate::candidate::CandidateConfig). + #[cfg(test)] mod candidate_pair_test; #[cfg(test)] diff --git a/rtc-ice/src/lib.rs b/rtc-ice/src/lib.rs index 54a1fb6a..70f3cefd 100644 --- a/rtc-ice/src/lib.rs +++ b/rtc-ice/src/lib.rs @@ -23,6 +23,24 @@ //! * [`stats`] — per-candidate and per-pair counters, surfaced through `getStats`. //! * [`mdns`] — mDNS candidate handling, for hiding private addresses. //! +//! # Example +//! +//! ``` +//! use rtc_ice::url::{ProtoType, SchemeType, Url}; +//! +//! # fn example() -> Result<(), Box> { +//! let stun = Url::parse_url("stun:stun.l.google.com:19302")?; +//! assert_eq!(stun.scheme, SchemeType::Stun); +//! assert_eq!(stun.port, 19302); +//! +//! // TURN URLs may pin the transport used to reach the server. +//! let turn = Url::parse_url("turn:turn.example.com:3478?transport=tcp")?; +//! assert_eq!(turn.scheme, SchemeType::Turn); +//! assert_eq!(turn.proto, ProtoType::Tcp); +//! # Ok(()) +//! # } +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate drives the agent as one layer of the peer-connection //! pipeline. diff --git a/rtc-ice/src/network_type/mod.rs b/rtc-ice/src/network_type/mod.rs index a8c37f84..5f3069a2 100644 --- a/rtc-ice/src/network_type/mod.rs +++ b/rtc-ice/src/network_type/mod.rs @@ -1,3 +1,8 @@ +//! Candidate transports: UDP or TCP, over IPv4 or IPv6. +//! +//! A [`NetworkType`](crate::network_type::NetworkType) pairs the transport protocol with the address family, because ICE treats +//! them together: a candidate's type constrains which remote candidates it can be paired with, +//! and pairs across families are never formed. #[cfg(test)] mod network_type_test; diff --git a/rtc-interceptor-derive/src/lib.rs b/rtc-interceptor-derive/src/lib.rs index 0f4b4b34..ccbcce73 100644 --- a/rtc-interceptor-derive/src/lib.rs +++ b/rtc-interceptor-derive/src/lib.rs @@ -8,10 +8,18 @@ //! //! # Design Pattern //! +//! The examples below are illustrative rather than compiled: the macros only expand to something +//! meaningful in the presence of the `Interceptor` trait from +//! [`rtc-interceptor`](https://docs.rs/rtc-interceptor), which depends on *this* crate — so a +//! doctest here cannot import it. They are exercised for real by `rtc-interceptor`'s own +//! documentation and tests. +//! //! The design follows Rust's derive pattern (similar to `#[derive(Default)]` with `#[default]`): //! //! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc_shared::error::Error; +//! use sansio::Protocol; //! use std::collections::VecDeque; //! //! #[derive(Interceptor)] @@ -36,6 +44,10 @@ //! For interceptors that just pass through without modification: //! //! ```ignore +//! # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! # use rtc_shared::error::Error; +//! # use sansio::Protocol; +//! # use std::collections::VecDeque; //! #[derive(Interceptor)] //! pub struct PassthroughInterceptor { //! #[next] @@ -51,12 +63,22 @@ //! //! The macros require certain types to be in scope: //! +//! The generated code names `sansio::Protocol`, `Error`, `StreamInfo` and +//! `TaggedPacket`, so all four must be in scope at the use site — not only the macros +//! themselves: +//! +//! ```ignore +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc_shared::error::Error; +//! use sansio::Protocol; +//! ``` +//! +//! Through the `rtc` umbrella crate the same imports are: +//! //! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; -//! // Or through rtc umbrella crate: -//! use rtc::interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +//! use rtc::interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc::sansio::Protocol; //! use rtc::shared::error::Error; -//! use rtc::sansio; // Required for macro-generated code //! ``` use proc_macro::TokenStream; @@ -77,6 +99,10 @@ use syn::{Data, DeriveInput, Fields, Ident, ImplItem, ItemImpl, Type, parse_macr /// /// Pure delegation (no custom logic): /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct PassthroughInterceptor { /// #[next] @@ -89,6 +115,10 @@ use syn::{Data, DeriveInput, Fields, Ident, ImplItem, ItemImpl, Type, parse_macr /// /// With custom logic: /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct MyInterceptor { /// #[next] @@ -150,6 +180,10 @@ pub fn derive_interceptor(input: TokenStream) -> TokenStream { /// /// With custom logic: /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct MyInterceptor { /// #[next] @@ -169,6 +203,10 @@ pub fn derive_interceptor(input: TokenStream) -> TokenStream { /// /// Pure delegation (no custom logic): /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct PassthroughInterceptor { /// #[next] diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index ec09c365..b10d4b73 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -76,7 +76,7 @@ //! //! # Quick Start //! -//! ```ignore +//! ``` //! use rtc_interceptor::{ //! Registry, SenderReportBuilder, ReceiverReportBuilder, //! NackGeneratorBuilder, NackResponderBuilder, @@ -109,12 +109,44 @@ //! .build(); //! ``` //! +//! # Type-Erasing a Chain +//! +//! A chain's type spells out its whole composition +//! (`TwccReceiverInterceptor>`), and it propagates into every type +//! that holds the peer connection built from it. That is fine when the chain is fixed at compile +//! time, and a problem when it is chosen at runtime or has to live in your own structs. +//! +//! [`Interceptor`] is object safe, so [`Registry::boxed`] can erase the chain to +//! [`BoxedInterceptor`] — one concrete type, whatever it was built from: +//! +//! ``` +//! use rtc_interceptor::{BoxedInterceptor, NackGeneratorBuilder, Registry, SenderReportBuilder}; +//! +//! // Two different chain types, unified by `.boxed()`. +//! let chain: BoxedInterceptor = if cfg!(feature = "unstable") { +//! Registry::new() +//! .with(SenderReportBuilder::new().build()) +//! .with(NackGeneratorBuilder::new().build()) +//! .boxed() +//! .build() +//! } else { +//! Registry::new().with(SenderReportBuilder::new().build()).boxed().build() +//! }; +//! ``` +//! +//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`, +//! `handle_timeout`, …); the layers inside still call each other through static dispatch and +//! inline as before. `Box

` and `&mut P` both implement [`Interceptor`], so a boxed or borrowed +//! chain satisfies an `I: Interceptor` bound like any other. +//! //! # Stream Binding //! //! Before interceptors can process packets for a stream, the stream must be bound: //! -//! ```ignore -//! use rtc_interceptor::{StreamInfo, RTCPFeedback, RTPHeaderExtension}; +//! ``` +//! use rtc_interceptor::{Interceptor, RTCPFeedback, RTPHeaderExtension, Registry, StreamInfo}; +//! +//! let mut chain = Registry::new().build(); //! //! // Create stream info with NACK and TWCC support //! let stream_info = StreamInfo { @@ -144,8 +176,10 @@ //! //! Use the derive macros to easily create custom interceptors: //! -//! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, StreamInfo}; +//! ``` +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use sansio::Protocol; +//! use shared::error::Error; // the generated `Protocol` impl names it //! use std::collections::VecDeque; //! //! #[derive(Interceptor)] @@ -244,8 +278,10 @@ pub type TaggedPacket = TransportMessage; /// /// The easiest way to create a custom interceptor is using the derive macros: /// -/// ```ignore -/// use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +/// ``` +/// use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// use sansio::Protocol; +/// use shared::error::Error; // the generated `Protocol` impl names it /// use std::collections::VecDeque; /// /// #[derive(Interceptor)] @@ -274,7 +310,9 @@ pub type TaggedPacket = TransportMessage; /// /// ## Manual Implementation /// -/// For more control, you can implement the traits manually: +/// For more control, you can implement the traits manually. The sketch below omits the +/// `Protocol` method bodies, so it is not compiled — see [`NoopInterceptor`] for a complete +/// hand-written implementation: /// /// ```ignore /// pub struct MyInterceptor

{ @@ -300,9 +338,14 @@ pub type TaggedPacket = TransportMessage; /// /// # Using with Registry /// -/// ```ignore -/// let chain = Registry::new() -/// .with(|inner| MyInterceptor { next: inner, buffer: VecDeque::new() }); +/// A builder is just a closure from the next layer to the wrapping one, so a custom +/// interceptor can be added the same way as a built-in: +/// +/// ``` +/// use rtc_interceptor::{Registry, SenderReportBuilder}; +/// +/// let registry = Registry::new().with(SenderReportBuilder::new().build()); +/// // ...or with a closure: `.with(|inner| MyInterceptor { next: inner, .. })` /// ``` pub trait Interceptor: sansio::Protocol< @@ -324,11 +367,11 @@ pub trait Interceptor: /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{Interceptor, NoopInterceptor, SenderReportBuilder}; /// use std::time::Duration; - /// use rtc_interceptor::{NoopInterceptor, SenderReportBuilder}; /// - /// // Using the builder pattern (recommended) + /// // `Interceptor` must be in scope for `with` to resolve. /// let chain = NoopInterceptor::new() /// .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build()); /// ``` diff --git a/rtc-interceptor/src/nack/generator.rs b/rtc-interceptor/src/nack/generator.rs index ec4b2180..7d858e9e 100644 --- a/rtc-interceptor/src/nack/generator.rs +++ b/rtc-interceptor/src/nack/generator.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, NackGeneratorBuilder}; /// use std::time::Duration; /// diff --git a/rtc-interceptor/src/nack/mod.rs b/rtc-interceptor/src/nack/mod.rs index 7f8a73f7..03b818f9 100644 --- a/rtc-interceptor/src/nack/mod.rs +++ b/rtc-interceptor/src/nack/mod.rs @@ -1,59 +1,12 @@ -//! NACK (Negative Acknowledgement) Interceptors. +//! NACK interceptors (internal module). //! -//! This module provides interceptors for handling RTCP NACK-based packet loss recovery -//! as specified in RFC 4585 (Extended RTP Profile for RTCP-Based Feedback). -//! -//! # Interceptors -//! -//! - [`NackGeneratorInterceptor`]: Monitors incoming RTP packets and generates -//! NACK requests for missing packets. -//! - [`NackResponderInterceptor`]: Buffers outgoing RTP packets and retransmits -//! them when NACK requests are received. -//! -//! # How NACK Works -//! -//! 1. **Detection**: The receiver detects missing packets by tracking sequence numbers -//! 2. **Request**: The receiver sends an RTCP NACK packet listing missing sequence numbers -//! 3. **Retransmission**: The sender retransmits the requested packets -//! -//! # RTX Support (RFC 4588) -//! -//! The responder supports RFC 4588 RTX (Retransmission) format, which uses a separate -//! SSRC and payload type for retransmissions. This allows the receiver to distinguish -//! between original and retransmitted packets. RTX is enabled by setting `ssrc_rtx` -//! and `payload_type_rtx` in [`StreamInfo`](crate::StreamInfo). -//! -//! # NACK Support Detection -//! -//! Both interceptors check if a stream supports NACK by looking for an [`RTCPFeedback`](crate::RTCPFeedback) -//! entry with `type: "nack"` and empty `parameter`. Streams without NACK support -//! are passed through without modification. +//! `NackGeneratorInterceptor` and `NackResponderInterceptor` are re-exported from the crate +//! root, where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [RFC 4585](https://datatracker.ietf.org/doc/html/rfc4585) - Extended RTP Profile for RTCP-Based Feedback (RTP/AVPF) -//! - [RFC 4588](https://datatracker.ietf.org/doc/html/rfc4588) - RTP Retransmission Payload Format -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, NackGeneratorBuilder, NackResponderBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Generator for incoming streams (detects loss, sends NACKs) -//! .with(NackGeneratorBuilder::new() -//! .with_size(512) // Buffer size for tracking -//! .with_interval(Duration::from_millis(100)) // NACK generation interval -//! .with_skip_last_n(2) // Skip recent packets (may just be delayed) -//! .build()) -//! // Responder for outgoing streams (buffers packets, handles NACKs) -//! .with(NackResponderBuilder::new() -//! .with_size(1024) // Buffer size for retransmission -//! .build()) -//! .build(); -//! ``` - +//! - [RFC 4585](https://datatracker.ietf.org/doc/html/rfc4585) - RTP/AVPF (NACK feedback) +//! - [RFC 4588](https://datatracker.ietf.org/doc/html/rfc4588) - RTP Retransmission (RTX) pub(crate) mod generator; pub(crate) mod receive_log; pub(crate) mod responder; diff --git a/rtc-interceptor/src/nack/responder.rs b/rtc-interceptor/src/nack/responder.rs index 392ccf20..9bc0663f 100644 --- a/rtc-interceptor/src/nack/responder.rs +++ b/rtc-interceptor/src/nack/responder.rs @@ -14,7 +14,7 @@ use std::time::Instant; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, NackResponderBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/noop.rs b/rtc-interceptor/src/noop.rs index a6321532..09a9a205 100644 --- a/rtc-interceptor/src/noop.rs +++ b/rtc-interceptor/src/noop.rs @@ -14,12 +14,18 @@ use std::time::Instant; /// /// # Example /// -/// ```ignore -/// use rtc_interceptor::NoopInterceptor; +/// ``` +/// use rtc_interceptor::{NoopInterceptor, Packet, TaggedPacket}; /// use sansio::Protocol; +/// use std::time::Instant; /// /// let mut noop = NoopInterceptor::new(); -/// noop.handle_read(TaggedPacket::Rtp(...)).unwrap(); +/// noop.handle_read(TaggedPacket { +/// now: Instant::now(), +/// transport: Default::default(), +/// message: Packet::Rtp(rtp::Packet::default()), +/// }) +/// .unwrap(); /// assert!(noop.poll_read().is_some()); /// ``` pub struct NoopInterceptor { diff --git a/rtc-interceptor/src/registry.rs b/rtc-interceptor/src/registry.rs index 1a3e7f30..d3afa272 100644 --- a/rtc-interceptor/src/registry.rs +++ b/rtc-interceptor/src/registry.rs @@ -29,14 +29,11 @@ use crate::{BoxedInterceptor, Interceptor}; /// /// # Example /// -/// ```ignore -/// use rtc_interceptor::Registry; -/// -/// // Create a new registry -/// let mut registry = Registry::new(); +/// ``` +/// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder}; /// -/// // Add interceptors (can be done in helper functions) -/// registry = registry +/// // Each `with` changes the registry's type, so rebind rather than reassign. +/// let registry = Registry::new() /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()); /// @@ -46,10 +43,12 @@ use crate::{BoxedInterceptor, Interceptor}; /// /// # Helper Function Pattern /// -/// ```ignore +/// ``` +/// use rtc_interceptor::{Interceptor, ReceiverReportBuilder, Registry, SenderReportBuilder}; +/// /// fn register_default_interceptors( /// registry: Registry

, -/// ) -> Registry { +/// ) -> Registry> { /// registry /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()) @@ -71,7 +70,7 @@ impl Registry { /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::Registry; /// /// let registry = Registry::new(); @@ -94,8 +93,10 @@ impl Registry

{ /// /// # Example /// - /// ```ignore - /// let custom = MyCustomInterceptor::new(); + /// ``` + /// use rtc_interceptor::{NoopInterceptor, Registry}; + /// + /// let custom = NoopInterceptor::new(); /// let registry = Registry::from(custom); /// ``` pub fn from(inner: P) -> Self { @@ -108,7 +109,9 @@ impl Registry

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder}; + /// /// let registry = Registry::new() /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()); @@ -129,8 +132,10 @@ impl Registry

{ /// /// # Example /// - /// ```ignore - /// let registry = Registry::new().with(MyInterceptor::new); + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; + /// + /// let registry = Registry::new().with(SenderReportBuilder::new().build()); /// let chain = registry.build(); /// ``` pub fn build(self) -> P { @@ -146,9 +151,22 @@ impl Registry

{ /// /// # Example /// + /// ``` + /// use rtc_interceptor::{BoxedInterceptor, Registry, SenderReportBuilder}; + /// + /// // Whatever the chain was composed of, the result has one concrete type. + /// let chain: BoxedInterceptor = Registry::new() + /// .with(SenderReportBuilder::new().build()) + /// .boxed() + /// .build(); + /// ``` + /// + /// The `rtc` crate accepts the erased registry directly, so a peer connection can be stored + /// as `RTCPeerConnection`: + /// /// ```ignore /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; - /// let pc: RTCPeerConnection = RTCPeerConnectionBuilder::new() + /// let pc = RTCPeerConnectionBuilder::new() /// .with_interceptor_registry(registry.boxed()) /// .build()?; /// ``` diff --git a/rtc-interceptor/src/report/mod.rs b/rtc-interceptor/src/report/mod.rs index 52c850e9..0c0cf27f 100644 --- a/rtc-interceptor/src/report/mod.rs +++ b/rtc-interceptor/src/report/mod.rs @@ -1,55 +1,12 @@ -//! RTCP Report Interceptors. +//! RTCP report interceptors (internal module). //! -//! This module provides interceptors for generating and handling RTCP reports -//! as specified in RFC 3550 (RTP: A Transport Protocol for Real-Time Applications). -//! -//! # Interceptors -//! -//! - [`SenderReportInterceptor`]: Generates RTCP Sender Reports (SR) for local streams -//! and filters hop-by-hop RTCP feedback that shouldn't be forwarded end-to-end. -//! - [`ReceiverReportInterceptor`]: Generates RTCP Receiver Reports (RR) based on -//! incoming RTP packet statistics (loss, jitter, etc.). -//! -//! # Sender Reports (SR) -//! -//! Sender Reports contain: -//! - NTP timestamp (wall-clock time) -//! - RTP timestamp (media time) -//! - Sender's packet count and octet count -//! - Optional report blocks for streams the sender is also receiving -//! -//! # Receiver Reports (RR) -//! -//! Receiver Reports contain report blocks with: -//! - Fraction of packets lost since last report -//! - Cumulative packets lost -//! - Extended highest sequence number received -//! - Interarrival jitter estimate -//! - Last SR timestamp (LSR) and delay since last SR (DLSR) +//! `SenderReportInterceptor` and `ReceiverReportInterceptor` are re-exported from the crate +//! root, where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [RFC 3550](https://datatracker.ietf.org/doc/html/rfc3550) - RTP: A Transport Protocol for Real-Time Applications -//! - [RFC 3611](https://datatracker.ietf.org/doc/html/rfc3611) - RTP Control Protocol Extended Reports (RTCP XR) -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, SenderReportBuilder, ReceiverReportBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Sender Report for outgoing streams -//! .with(SenderReportBuilder::new() -//! .with_interval(Duration::from_secs(1)) -//! .build()) -//! // Receiver Report for incoming streams -//! .with(ReceiverReportBuilder::new() -//! .with_interval(Duration::from_secs(1)) -//! .build()) -//! .build(); -//! ``` - +//! - [RFC 3550](https://datatracker.ietf.org/doc/html/rfc3550) - RTP (Sender/Receiver Reports) +//! - [RFC 3611](https://datatracker.ietf.org/doc/html/rfc3611) - RTCP Extended Reports (XR) pub(crate) mod receiver; pub(crate) mod receiver_stream; pub(crate) mod sender; diff --git a/rtc-interceptor/src/report/receiver.rs b/rtc-interceptor/src/report/receiver.rs index 6387fbe2..2d6308e7 100644 --- a/rtc-interceptor/src/report/receiver.rs +++ b/rtc-interceptor/src/report/receiver.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// use std::time::Duration; /// @@ -54,12 +54,16 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{ReceiverReportBuilder, Registry}; /// use std::time::Duration; - /// use rtc_interceptor::ReceiverReportBuilder; /// - /// let builder = ReceiverReportBuilder::new() - /// .with_interval(Duration::from_millis(500)); + /// // The builder is generic over the next layer, so its type is pinned by `with`. + /// let registry = Registry::new().with( + /// ReceiverReportBuilder::new() + /// .with_interval(Duration::from_millis(500)) + /// .build(), + /// ); /// ``` pub fn with_interval(mut self, interval: Duration) -> Self { self.interval = interval; @@ -72,7 +76,7 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// /// let registry = Registry::new() @@ -94,7 +98,7 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/report/sender.rs b/rtc-interceptor/src/report/sender.rs index b9c33df0..60875409 100644 --- a/rtc-interceptor/src/report/sender.rs +++ b/rtc-interceptor/src/report/sender.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// use std::time::Duration; /// @@ -63,12 +63,16 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// use std::time::Duration; - /// use rtc_interceptor::SenderReportBuilder; /// - /// let builder = SenderReportBuilder::new() - /// .with_interval(Duration::from_millis(500)); + /// // The builder is generic over the next layer, so its type is pinned by `with`. + /// let registry = Registry::new().with( + /// SenderReportBuilder::new() + /// .with_interval(Duration::from_millis(500)) + /// .build(), + /// ); /// ``` pub fn with_interval(mut self, interval: Duration) -> Self { self.interval = interval; @@ -89,11 +93,11 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore - /// use rtc_interceptor::SenderReportBuilder; + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// - /// let builder = SenderReportBuilder::new() - /// .with_use_latest_packet(); + /// let registry = + /// Registry::new().with(SenderReportBuilder::new().with_use_latest_packet().build()); /// ``` pub fn with_use_latest_packet(mut self) -> Self { self.use_latest_packet = true; @@ -106,7 +110,7 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// /// let registry = Registry::new() @@ -129,7 +133,7 @@ impl

SenderReportBuilder

{ /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/stream_info.rs b/rtc-interceptor/src/stream_info.rs index 5d385a9c..5e279321 100644 --- a/rtc-interceptor/src/stream_info.rs +++ b/rtc-interceptor/src/stream_info.rs @@ -86,7 +86,7 @@ pub struct RTCPFeedback { /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{StreamInfo, RTCPFeedback, RTPHeaderExtension}; /// /// let info = StreamInfo { diff --git a/rtc-interceptor/src/twcc/mod.rs b/rtc-interceptor/src/twcc/mod.rs index 2a994622..278942fa 100644 --- a/rtc-interceptor/src/twcc/mod.rs +++ b/rtc-interceptor/src/twcc/mod.rs @@ -1,69 +1,12 @@ -//! TWCC (Transport Wide Congestion Control) Interceptors. +//! TWCC interceptors (internal module). //! -//! This module provides interceptors for Transport Wide Congestion Control, -//! a bandwidth estimation mechanism that provides detailed per-packet feedback. -//! -//! # Interceptors -//! -//! - [`TwccSenderInterceptor`]: Adds transport-wide sequence numbers to outgoing RTP packets. -//! - [`TwccReceiverInterceptor`]: Tracks incoming RTP packets and generates TransportLayerCC feedback. -//! -//! # How TWCC Works -//! -//! 1. **Sender**: Adds a transport-wide sequence number to each RTP packet via header extension -//! 2. **Receiver**: Records arrival time of each packet by sequence number -//! 3. **Feedback**: Receiver periodically sends TransportLayerCC RTCP packets with arrival info -//! 4. **Estimation**: Sender uses feedback to estimate available bandwidth -//! -//! # Sequence Number Sharing -//! -//! Unlike per-stream RTP sequence numbers, TWCC sequence numbers are shared across -//! all streams in a session. This allows the sender to correlate feedback across -//! multiple media tracks for more accurate bandwidth estimation. -//! -//! # TWCC Support Detection -//! -//! Interceptors detect TWCC support by checking [`StreamInfo::rtp_header_extensions`](crate::StreamInfo::rtp_header_extensions) -//! for the TWCC header extension URI. Streams without the extension are passed through -//! without modification. +//! `TwccSenderInterceptor` and `TwccReceiverInterceptor` are re-exported from the crate root, +//! where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [draft-holmer-rmcat-transport-wide-cc-extensions-01](https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01) - RTP Extensions for Transport-wide Congestion Control -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, TwccSenderBuilder, TwccReceiverBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Sender: adds TWCC sequence numbers to outgoing packets -//! .with(TwccSenderBuilder::new().build()) -//! // Receiver: generates TWCC feedback for incoming packets -//! .with(TwccReceiverBuilder::new() -//! .with_interval(Duration::from_millis(100)) // Feedback interval -//! .build()) -//! .build(); -//! ``` -//! -//! # Stream Configuration -//! -//! To enable TWCC for a stream, include the header extension in [`StreamInfo`](crate::StreamInfo): -//! -//! ```ignore -//! use rtc_interceptor::{StreamInfo, RTPHeaderExtension}; -//! -//! let stream_info = StreamInfo { -//! ssrc: 0x12345678, -//! rtp_header_extensions: vec![RTPHeaderExtension { -//! uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(), -//! id: 5, // Extension ID negotiated via SDP -//! }], -//! ..Default::default() -//! }; -//! ``` - +//! - [draft-holmer-rmcat-transport-wide-cc-extensions-01](https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01) +//! - RTP Extensions for Transport-wide Congestion Control pub(crate) mod arrival_time_map; pub(crate) mod receiver; pub(crate) mod recorder; diff --git a/rtc-interceptor/src/twcc/receiver.rs b/rtc-interceptor/src/twcc/receiver.rs index cbb6509b..fd24325e 100644 --- a/rtc-interceptor/src/twcc/receiver.rs +++ b/rtc-interceptor/src/twcc/receiver.rs @@ -18,7 +18,7 @@ const DEFAULT_INTERVAL: Duration = Duration::from_millis(100); /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, TwccReceiverBuilder}; /// use std::time::Duration; /// diff --git a/rtc-interceptor/src/twcc/sender.rs b/rtc-interceptor/src/twcc/sender.rs index 5349d9e7..45da3339 100644 --- a/rtc-interceptor/src/twcc/sender.rs +++ b/rtc-interceptor/src/twcc/sender.rs @@ -12,7 +12,7 @@ use std::marker::PhantomData; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, TwccSenderBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-mdns/src/config.rs b/rtc-mdns/src/config.rs index 9cd3a98f..1e5cd69c 100644 --- a/rtc-mdns/src/config.rs +++ b/rtc-mdns/src/config.rs @@ -1,55 +1,7 @@ -//! MdnsConfiguration for mDNS connections. +//! `MdnsConfig`, the configuration for an mDNS connection. //! -//! This module provides the [`MdnsConfig`] struct for configuring mDNS client and server behavior. -//! -//! # Examples -//! -//! ## Client MdnsConfiguration -//! -//! For a client that only sends queries: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::time::Duration; -//! -//! let config = MdnsConfig::default() -//! .with_query_interval(Duration::from_millis(500)); // Retry every 500ms -//! ``` -//! -//! ## Server MdnsConfiguration -//! -//! For a server that responds to queries: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::net::{IpAddr, Ipv4Addr}; -//! -//! let config = MdnsConfig::default() -//! .with_local_names(vec![ -//! "mydevice.local".to_string(), -//! "mydevice._http._tcp.local".to_string(), -//! ]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), -//! ); -//! ``` -//! -//! ## Combined Client/Server -//! -//! For a connection that both queries and responds: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::net::{IpAddr, Ipv4Addr}; -//! use std::time::Duration; -//! -//! let config = MdnsConfig::default() -//! .with_query_interval(Duration::from_secs(1)) -//! .with_local_names(vec!["myhost.local".to_string()]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), -//! ); -//! ``` +//! This module is private; the type is re-exported as [`crate::MdnsConfig`], where its +//! documentation and examples live so that rustdoc renders them and their doctests run. use std::net::IpAddr; use std::time::Duration; @@ -90,6 +42,55 @@ pub(crate) const RESPONSE_TTL: u32 = 120; /// - `query_timeout`: Maximum time to wait for a query answer (default: None - no timeout) /// - `local_names`: Names this connection will respond to (empty by default) /// - `local_addr`: IP address to advertise in responses (required for server mode) +/// +/// # Configuration scenarios +/// +/// ## Client MdnsConfiguration +/// +/// For a client that only sends queries: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::time::Duration; +/// +/// let config = MdnsConfig::default() +/// .with_query_interval(Duration::from_millis(500)); // Retry every 500ms +/// ``` +/// +/// ## Server MdnsConfiguration +/// +/// For a server that responds to queries: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::net::{IpAddr, Ipv4Addr}; +/// +/// let config = MdnsConfig::default() +/// .with_local_names(vec![ +/// "mydevice.local".to_string(), +/// "mydevice._http._tcp.local".to_string(), +/// ]) +/// .with_local_ip( +/// IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), +/// ); +/// ``` +/// +/// ## Combined Client/Server +/// +/// For a connection that both queries and responds: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::net::{IpAddr, Ipv4Addr}; +/// use std::time::Duration; +/// +/// let config = MdnsConfig::default() +/// .with_query_interval(Duration::from_secs(1)) +/// .with_local_names(vec!["myhost.local".to_string()]) +/// .with_local_ip( +/// IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), +/// ); +/// ``` #[derive(Clone, Debug)] pub struct MdnsConfig { /// How often to retry unanswered queries. diff --git a/rtc-mdns/src/proto/mod.rs b/rtc-mdns/src/proto/mod.rs index 6180de14..74a569e9 100644 --- a/rtc-mdns/src/proto/mod.rs +++ b/rtc-mdns/src/proto/mod.rs @@ -1,70 +1,7 @@ -//! Sans-I/O mDNS Connection implementation. +//! The Sans-I/O mDNS connection. //! -//! This module provides [`Mdns`], a sans-I/O implementation of an mDNS client/server -//! that implements the [`sansio::Protocol`] trait for integration with any I/O framework. -//! -//! # Overview -//! -//! The [`Mdns`] struct handles the mDNS protocol logic without performing any I/O. -//! The caller is responsible for: -//! -//! 1. **Network I/O**: Reading/writing UDP packets to/from 224.0.0.251:5353 -//! 2. **Timing**: Calling `handle_timeout()` when `poll_timeout()` expires -//! 3. **Event Processing**: Handling events from `poll_event()` -//! -//! # Client Usage -//! -//! To query for a hostname: -//! -//! ```rust -//! use rtc_mdns::{MdnsConfig, Mdns, MdnsEvent}; -//! use sansio::Protocol; -//! use std::time::Instant; -//! -//! let mut mdns_client = Mdns::new(MdnsConfig::default()); -//! -//! // Start a query - this queues a packet to send -//! let query_id = mdns_client.query("printer.local"); -//! -//! // Get the packet to send over the network -//! if let Some(packet) = mdns_client.poll_write() { -//! // Send packet.message to packet.transport.peer_addr via UDP -//! println!("Send {} bytes to {}", packet.message.len(), packet.transport.peer_addr); -//! } -//! -//! // When a response packet arrives, call handle_read() -//! // Then check poll_event() for QueryAnswered events -//! ``` -//! -//! # Server Usage -//! -//! To respond to queries: -//! -//! ```rust -//! use rtc_mdns::{MdnsConfig, Mdns}; -//! use std::net::{IpAddr, Ipv4Addr}; -//! -//! let config = MdnsConfig::default() -//! .with_local_names(vec!["myserver.local".to_string()]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), -//! ); -//! -//! let mut mdns_client = Mdns::new(config); -//! -//! // When a query packet arrives, call handle_read() -//! // The connection automatically queues responses for configured local_names -//! // Retrieve them with poll_write() -//! ``` -//! -//! # Query Lifecycle -//! -//! 1. Call [`Mdns::query()`] with the hostname to resolve -//! 2. Retrieve the query packet from [`poll_write()`](sansio::Protocol::poll_write) -//! 3. Send the packet to the mDNS multicast address -//! 4. When responses arrive, pass them to [`handle_read()`](sansio::Protocol::handle_read) -//! 5. Check [`poll_event()`](sansio::Protocol::poll_event) for [`MdnsEvent::QueryAnswered`] -//! 6. If no answer, call [`handle_timeout()`](sansio::Protocol::handle_timeout) to trigger retries +//! This module is private; [`Mdns`] is re-exported as [`crate::Mdns`], where its +//! documentation and examples live so that rustdoc renders them and their doctests run. use std::collections::{HashMap, VecDeque}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -253,6 +190,24 @@ pub enum MdnsEvent { /// assert_eq!(mdns.pending_query_count(), 2); /// assert!(!mdns.is_query_pending(id2)); /// ``` +/// +/// # Overview +/// +/// The [`Mdns`] struct handles the mDNS protocol logic without performing any I/O. +/// The caller is responsible for: +/// +/// 1. **Network I/O**: Reading/writing UDP packets to/from 224.0.0.251:5353 +/// 2. **Timing**: Calling `handle_timeout()` when `poll_timeout()` expires +/// 3. **Event Processing**: Handling events from `poll_event()` +/// +/// # Query Lifecycle +/// +/// 1. Call [`Mdns::query()`] with the hostname to resolve +/// 2. Retrieve the query packet from [`poll_write()`](sansio::Protocol::poll_write) +/// 3. Send the packet to the mDNS multicast address +/// 4. When responses arrive, pass them to [`handle_read()`](sansio::Protocol::handle_read) +/// 5. Check [`poll_event()`](sansio::Protocol::poll_event) for [`MdnsEvent::QueryAnswered`] +/// 6. If no answer, call [`handle_timeout()`](sansio::Protocol::handle_timeout) to trigger retries pub struct Mdns { /// MdnsConfiguration config: MdnsConfig, diff --git a/rtc-media/src/audio/buffer.rs b/rtc-media/src/audio/buffer.rs index 1feab110..e37383b3 100644 --- a/rtc-media/src/audio/buffer.rs +++ b/rtc-media/src/audio/buffer.rs @@ -1,3 +1,13 @@ +//! Multi-channel audio buffers. +//! +//! A buffer is a flat slice of samples plus a [`BufferInfo`](crate::audio::buffer::BufferInfo) recording how many channels and +//! frames it holds. The layout is a type parameter: [`Interleaved`](crate::audio::buffer::layout::Interleaved) stores +//! one sample per channel per frame (what most audio APIs use), while +//! [`Deinterleaved`](crate::audio::buffer::layout::Deinterleaved) stores each channel contiguously. +//! +//! Encoding a layout in the type rather than a runtime flag means indexing is resolved at compile +//! time and the two cannot be mixed up. [`FromBytes`](crate::audio::buffer::FromBytes) and [`ToByteBufferRef`](crate::audio::buffer::ToByteBufferRef) convert to and from +//! raw bytes in a caller-chosen endianness. /// Channel and frame counts for a buffer. pub mod info; /// The interleaved and deinterleaved buffer layouts. diff --git a/rtc-media/src/io/h26x_reader/mod.rs b/rtc-media/src/io/h26x_reader/mod.rs index 06869ab7..da528ddd 100644 --- a/rtc-media/src/io/h26x_reader/mod.rs +++ b/rtc-media/src/io/h26x_reader/mod.rs @@ -1,3 +1,11 @@ +//! Reading H.264/H.265 Annex B byte streams. +//! +//! Annex B delimits NAL units with start codes (`00 00 01` or `00 00 00 01`). This module walks +//! those boundaries and parses each unit's header, which differs between the codecs: H.264 uses +//! one byte ([`H264NAL`](crate::io::h26x_reader::H264NAL)), H.265 two ([`H265NAL`](crate::io::h26x_reader::H265NAL)). +//! +//! Use [`sample_reader`](crate::io::h26x_reader::sample_reader) instead when you want whole access units — the NAL units making up one +//! frame — rather than individual units. #[cfg(test)] mod h26x_reader_test; /// Reads Annex B streams as whole samples rather than individual NAL units. diff --git a/rtc-media/src/io/ivf_reader/mod.rs b/rtc-media/src/io/ivf_reader/mod.rs index f4bada88..2b0b51eb 100644 --- a/rtc-media/src/io/ivf_reader/mod.rs +++ b/rtc-media/src/io/ivf_reader/mod.rs @@ -1,3 +1,8 @@ +//! Reading IVF files. +//! +//! IVF is the minimal container the VPx and AV1 tools use: a 32-byte file header naming the codec +//! and frame size, then a 12-byte header before each frame giving its length and timestamp. +//! [`IVFReader`](crate::io::ivf_reader::IVFReader) yields one frame at a time, which is the unit an RTP payloader wants. #[cfg(test)] mod ivf_reader_test; diff --git a/rtc-media/src/io/ogg_reader/mod.rs b/rtc-media/src/io/ogg_reader/mod.rs index a62c7477..49238af2 100644 --- a/rtc-media/src/io/ogg_reader/mod.rs +++ b/rtc-media/src/io/ogg_reader/mod.rs @@ -1,3 +1,12 @@ +//! Reading Opus audio from an Ogg container. +//! +//! An Ogg stream is a sequence of pages; the first two carry the Opus headers (`OpusHead`, then +//! `OpusTags`), and the rest carry audio packets. [`OggReader`](crate::io::ogg_reader::OggReader) walks the pages and hands back +//! one Opus packet at a time, which is exactly the unit RTP carries. +//! +//! The `OpusHead` fields worth noting are [`pre_skip`](crate::io::ogg_reader::OggHeader::pre_skip) — decoder warm-up +//! samples to discard — and [`sample_rate`](crate::io::ogg_reader::OggHeader::sample_rate), which records the *original* +//! input rate; Opus itself always decodes at 48 kHz. #[cfg(test)] mod ogg_reader_test; diff --git a/rtc-media/src/lib.rs b/rtc-media/src/lib.rs index f47e6f5d..23e8efd7 100644 --- a/rtc-media/src/lib.rs +++ b/rtc-media/src/lib.rs @@ -20,6 +20,24 @@ //! * [`audio`], [`video`] — per-codec helpers, including audio buffering and frame //! inspection. //! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_media::Sample; +//! use shared::time::SystemInstant; +//! use std::time::Duration; +//! +//! // One encoded frame, ready to hand to a sample-based local track. +//! let sample = Sample { +//! data: Bytes::from_static(&[0u8; 128]), +//! timestamp: SystemInstant::now(), +//! duration: Duration::from_millis(33), // ~30 fps +//! ..Default::default() +//! }; +//! assert_eq!(sample.data.len(), 128); +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::media`. diff --git a/rtc-rtcp/src/extended_report/rle.rs b/rtc-rtcp/src/extended_report/rle.rs index cbe246e3..7cebbc44 100644 --- a/rtc-rtcp/src/extended_report/rle.rs +++ b/rtc-rtcp/src/extended_report/rle.rs @@ -1,3 +1,10 @@ +//! Loss and Duplicate RLE report blocks. +//! +//! Both blocks report per-packet status over a sequence-number range, run-length encoded: a +//! [`Chunk`](crate::extended_report::rle::Chunk) is either a run of identical values or an explicit 15-bit vector. That keeps a report +//! covering a long range compact while still describing individual packets. +//! +//! The same structure serves both blocks; [`RLEReportBlock::is_loss_rle`](crate::extended_report::rle::RLEReportBlock::is_loss_rle) selects which. use super::*; const RLE_REPORT_BLOCK_MIN_LENGTH: u16 = 8; diff --git a/rtc-rtcp/src/header.rs b/rtc-rtcp/src/header.rs index 5b9e34b1..ed96210a 100644 --- a/rtc-rtcp/src/header.rs +++ b/rtc-rtcp/src/header.rs @@ -1,3 +1,11 @@ +//! The RTCP header. +//! +//! Four bytes on every packet: version and padding flags, a 5-bit count whose meaning depends on +//! the packet type, the [`PacketType`](crate::header::PacketType) itself, and a length in 32-bit words. The `*_SHIFT` and +//! `*_MASK` constants describe how the flags pack into the first octet. +//! +//! The length field is why RTCP is compound: several packets can be concatenated in one +//! datagram and walked by stepping over each header's length. use shared::{ error::{Error, Result}, marshal::{Marshal, MarshalSize, Unmarshal}, diff --git a/rtc-rtcp/src/lib.rs b/rtc-rtcp/src/lib.rs index 6d991fb5..6fddf8c7 100644 --- a/rtc-rtcp/src/lib.rs +++ b/rtc-rtcp/src/lib.rs @@ -15,32 +15,48 @@ //! in a streaming multimedia session. An application may use this information to control quality of //! service parameters, perhaps by limiting flow, or using a different codec. //! -//! Decoding RTCP packets: -//!```nobuild -//! let pkt = rtcp::unmarshal(&rtcp_data).unwrap(); +//! # Decoding RTCP packets //! -//! if let Some(e) = pkt -//! .as_any() -//! .downcast_ref::() -//! { +//! One datagram may hold several packets — RTCP is compound — so [`packet::unmarshal`] +//! consumes the buffer and returns them all. Recover each concrete type by downcasting: //! -//! } -//! else if let Some(e) = packet -//! .as_any() -//! .downcast_ref::(){} -//! .... -//!``` +//! ``` +//! use bytes::Bytes; +//! use rtc_rtcp::goodbye::Goodbye; +//! use rtc_rtcp::packet::unmarshal; +//! use rtc_rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; //! -//! Encoding RTCP packets: -//!```nobuild -//! let pkt = PictureLossIndication{ -//! sender_ssrc: sender_ssrc, -//! media_ssrc: media_ssrc -//! }; +//! # fn example(rtcp_data: Bytes) -> Result<(), Box> { +//! let mut buf = rtcp_data; +//! for packet in unmarshal(&mut buf)? { +//! if let Some(pli) = packet.as_any().downcast_ref::() { +//! println!("keyframe requested for ssrc {}", pli.media_ssrc); +//! } else if let Some(bye) = packet.as_any().downcast_ref::() { +//! println!("{:?} left the session", bye.sources); +//! } +//! } +//! # Ok(()) +//! # } +//! ``` //! -//! let pli_data = pkt.marshal().unwrap(); -//! // ... -//!``` +//! # Encoding RTCP packets +//! +//! Every packet type implements [`Marshal`](shared::marshal::Marshal), so that trait must be +//! in scope: +//! +//! ``` +//! use rtc_rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; +//! use shared::marshal::Marshal; +//! +//! # fn example(sender_ssrc: u32, media_ssrc: u32) -> Result<(), Box> { +//! let pli = PictureLossIndication { +//! sender_ssrc, +//! media_ssrc, +//! }; +//! let pli_data = pli.marshal()?; +//! # Ok(()) +//! # } +//! ``` /// Compound RTCP packets — the several reports that share one datagram. pub mod compound_packet; diff --git a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs index 9e460e55..bc057388 100644 --- a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs @@ -1,3 +1,11 @@ +//! Transport-wide congestion control feedback. +//! +//! Reports the arrival status and time of packets by their *transport-wide* sequence number — the +//! one the `TransportCcExtension` RTP header extension stamps on every packet +//! regardless of stream, which is why one report can cover audio and video together. +//! +//! Status is run-length or bit-vector encoded ([`StatusChunkTypeTcc`](crate::transport_feedbacks::transport_layer_cc::StatusChunkTypeTcc)) so a report covering +//! hundreds of packets stays small. #[cfg(test)] mod transport_layer_cc_test; diff --git a/rtc-rtp/src/codec/h264/mod.rs b/rtc-rtp/src/codec/h264/mod.rs index 04b50944..8f551eb7 100644 --- a/rtc-rtp/src/codec/h264/mod.rs +++ b/rtc-rtp/src/codec/h264/mod.rs @@ -1,3 +1,11 @@ +//! H.264 RTP payload format ([RFC 6184]). +//! +//! A NAL unit that fits the MTU is sent as-is. Larger ones are split into FU-A fragments, and +//! several small ones (typically SPS and PPS) may be combined into one STAP-A aggregate. The +//! `*_NALU_TYPE` constants name the types this payloader recognises, and the `*_BITMASK` +//! constants describe how the NAL header and FU header pack their fields. +//! +//! [RFC 6184]: https://datatracker.ietf.org/doc/html/rfc6184 #[cfg(test)] mod h264_test; diff --git a/rtc-rtp/src/codec/h265/mod.rs b/rtc-rtp/src/codec/h265/mod.rs index 5eb08ba5..b766571d 100644 --- a/rtc-rtp/src/codec/h265/mod.rs +++ b/rtc-rtp/src/codec/h265/mod.rs @@ -1,3 +1,15 @@ +//! H.265/HEVC RTP payload format ([RFC 7798]). +//! +//! HEVC differs from H.264 in ways that matter to packetization: NAL headers are two bytes +//! rather than one, and there is an extra aggregation form (PACI) that can carry payload +//! content information ahead of the NAL unit. +//! +//! This module provides the three packet shapes the RFC defines — single NAL unit, +//! aggregation, and fragmentation unit — plus [`HevcPayloader`](crate::codec::h265::HevcPayloader), which picks between them +//! based on the MTU, and the depacketizer that reverses the choice. +//! +//! [RFC 7798]: https://datatracker.ietf.org/doc/html/rfc7798 + use bytes::{BufMut, Bytes, BytesMut}; use super::h264::ANNEXB_NALUSTART_CODE; diff --git a/rtc-rtp/src/header.rs b/rtc-rtp/src/header.rs index bca25b26..fbd873c4 100644 --- a/rtc-rtp/src/header.rs +++ b/rtc-rtp/src/header.rs @@ -1,3 +1,19 @@ +//! The RTP header and its extensions. +//! +//! [`Header`](crate::header::Header) is the fixed 12-byte header plus the optional CSRC list and header-extension +//! block. The `*_SHIFT`/`*_MASK` constants describe how the flag fields pack into the first two +//! octets, and the `*_OFFSET`/`*_LENGTH` constants give the byte positions a caller can patch in +//! place without re-encoding. +//! +//! Header extensions ([RFC 8285]) come in two forms, selected by +//! [`Header::extension_profile`](crate::header::Header::extension_profile): one-byte ids ([`EXTENSION_PROFILE_ONE_BYTE`](crate::header::EXTENSION_PROFILE_ONE_BYTE)) or two-byte ids +//! ([`EXTENSION_PROFILE_TWO_BYTE`](crate::header::EXTENSION_PROFILE_TWO_BYTE)) when an id above 14 is needed. Use +//! [`Header::set_extension`](crate::header::Header::set_extension) and [`Header::get_extension`](crate::header::Header::get_extension) rather than touching +//! [`Header::extensions`](crate::header::Header::extensions) directly — they keep [`Header::extensions_padding`](crate::header::Header::extensions_padding) and the extension +//! flag consistent, which marshalling depends on. +//! +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 + use shared::{ error::{Error, Result}, marshal::{Marshal, MarshalSize, Unmarshal}, diff --git a/rtc-rtp/src/lib.rs b/rtc-rtp/src/lib.rs index 44748016..c7938114 100644 --- a/rtc-rtp/src/lib.rs +++ b/rtc-rtp/src/lib.rs @@ -20,6 +20,34 @@ //! * [`extension`] — the typed header extensions: audio level ([RFC 6464]), video //! orientation, transport-wide CC, and the SDES stream ids used for simulcast. //! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_rtp::Packet; +//! use shared::marshal::{Marshal, Unmarshal}; +//! +//! # fn example() -> Result<(), Box> { +//! // A minimal RTP packet: version 2, payload type 96, one byte of payload. +//! let raw = Bytes::from_static(&[ +//! 0x80, 0x60, 0x00, 0x01, // V=2, PT=96, seq=1 +//! 0x00, 0x00, 0x00, 0x20, // timestamp +//! 0xDE, 0xAD, 0xBE, 0xEF, // ssrc +//! 0xAA, // payload +//! ]); +//! +//! let mut buf = raw.clone(); +//! let packet = Packet::unmarshal(&mut buf)?; +//! assert_eq!(packet.header.payload_type, 96); +//! assert_eq!(packet.header.sequence_number, 1); +//! assert_eq!(packet.header.ssrc, 0xDEAD_BEEF); +//! +//! // Re-encoding reproduces the original bytes. +//! assert_eq!(packet.marshal()?, raw); +//! # Ok(()) +//! # } +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::rtp`, and an application //! usually meets these types when reading or writing media on a track. diff --git a/rtc-sctp/src/fuzzing.rs b/rtc-sctp/src/fuzzing.rs index fcf40523..03c51f3c 100644 --- a/rtc-sctp/src/fuzzing.rs +++ b/rtc-sctp/src/fuzzing.rs @@ -1,3 +1,9 @@ +//! Entry points for fuzz targets and benchmarks. +//! +//! Each function drives one encode or decode step over a raw byte slice, so a fuzzer or a +//! benchmark can reach the packet codec without establishing an association first. Gated behind +//! `cfg(fuzzing)` or the `bench` feature; not part of the supported API and not subject to the +//! crate's stability guarantees. use bytes::Bytes; use shared::error::Result; diff --git a/rtc-sctp/src/lib.rs b/rtc-sctp/src/lib.rs index cdd22076..b0e95bbd 100644 --- a/rtc-sctp/src/lib.rs +++ b/rtc-sctp/src/lib.rs @@ -1,16 +1,51 @@ -//! Low-level protocol logic for the SCTP protocol +//! SCTP for the Sans-I/O WebRTC stack. //! -//! sctp-proto contains a fully deterministic implementation of SCTP protocol logic. It contains -//! no networking code and does not get any relevant timestamps from the operating system. Most -//! users may want to use the futures-based sctp-async API instead. +//! The Stream Control Transmission Protocol ([RFC 4960]) with the extensions WebRTC data +//! channels need: partial reliability ([RFC 3758]) and stream reset / reconfiguration +//! ([RFC 6525]). In WebRTC, SCTP runs *over* DTLS rather than over IP, and carries the data +//! channels described by [`rtc-datachannel`]. //! -//! The sctp-proto API might be of interest if you want to use it from a C or C++ project -//! through C bindings or if you want to use a different event loop than the one tokio provides. +//! This is a fully deterministic implementation of the protocol logic. It contains no +//! networking code and reads no clock of its own: you feed it datagrams and time, and poll it +//! for the datagrams and events it produces. That is what makes it testable without a network +//! and reusable under any executor. //! -//! The most important types are `Endpoint`, which conceptually represents the protocol state for -//! a single socket and mostly manages configuration and dispatches incoming datagrams to the -//! related `Association`. `Association` types contain the bulk of the protocol logic related to -//! managing a single association and all the related state (such as streams). +//! # Structure +//! +//! * [`Endpoint`] — the protocol state for one socket. It holds configuration and dispatches +//! inbound datagrams to the right association. +//! * [`Association`] — the bulk of the logic for a single +//! association: handshake, congestion control, retransmission, and its streams. +//! * [`Stream`] — one stream's reads, writes and reliability +//! settings. +//! * [`Chunks`] — a reassembled inbound message, delivered once every fragment has arrived. +//! +//! # Example +//! +//! Configuration is plain data, and the association is driven entirely by the caller — feed it +//! datagrams and time, poll it for output: +//! +//! ``` +//! use rtc_sctp::{EndpointConfig, TransportConfig}; +//! use std::sync::Arc; +//! +//! let transport = TransportConfig::default() +//! .with_max_message_size(65_536) +//! .with_max_num_outbound_streams(1024); +//! +//! let endpoint_config = Arc::new(EndpointConfig::new()); +//! assert_eq!(transport.max_message_size(), 65_536); +//! # let _ = endpoint_config; +//! ``` +//! +//! Most applications do not depend on this crate directly — the [`rtc`](https://docs.rs/rtc) +//! crate drives it as one layer of the peer-connection pipeline and exposes data channels, +//! and [`webrtc`](https://docs.rs/webrtc) wraps that in an async API. +//! +//! [RFC 4960]: https://datatracker.ietf.org/doc/html/rfc4960 +//! [RFC 3758]: https://datatracker.ietf.org/doc/html/rfc3758 +//! [RFC 6525]: https://datatracker.ietf.org/doc/html/rfc6525 +//! [`rtc-datachannel`]: https://docs.rs/rtc-datachannel #![warn(rust_2018_idioms)] #![warn(missing_docs)] @@ -52,6 +87,11 @@ pub use crate::queue::reassembly_queue::{Chunk, Chunks}; pub(crate) mod util; +/// Entry points for fuzz targets and benchmarks. +/// +/// Thin wrappers that drive one encode or decode step over a raw byte slice, so a fuzzer or +/// a benchmark can reach the packet codec without setting up an association. Gated behind +/// `cfg(fuzzing)` or the `bench` feature; not part of the supported API. #[cfg(any(fuzzing, feature = "bench"))] pub mod fuzzing; diff --git a/rtc-sdp/src/description/common.rs b/rtc-sdp/src/description/common.rs index ea282b19..e6288ac3 100644 --- a/rtc-sdp/src/description/common.rs +++ b/rtc-sdp/src/description/common.rs @@ -1,3 +1,11 @@ +//! Fields shared by session and media descriptions. +//! +//! `c=` connection data ([`ConnectionInformation`](crate::description::common::ConnectionInformation), [`Address`](crate::description::common::Address)), `b=` bandwidth +//! ([`Bandwidth`](crate::description::common::Bandwidth)) and `a=` attributes ([`Attribute`](crate::description::common::Attribute)) may appear at either level in SDP, with +//! the media-level value overriding the session-level one — so they are modelled once here. +//! +//! An [`Attribute`](crate::description::common::Attribute) with no value is a flag, which is how `a=rtcp-mux` and the direction +//! attributes are expressed. use std::fmt; use super::session::ATTR_KEY_CANDIDATE; diff --git a/rtc-sdp/src/description/media.rs b/rtc-sdp/src/description/media.rs index b2e02125..2f22a6e8 100644 --- a/rtc-sdp/src/description/media.rs +++ b/rtc-sdp/src/description/media.rs @@ -1,3 +1,12 @@ +//! The `m=` media description. +//! +//! One [`MediaDescription`](crate::description::media::MediaDescription) is a media type, a transport port and protocol, a list of formats, +//! and the attributes that describe them. For RTP media the formats are payload types, and +//! [`MediaDescription::codecs`](crate::description::media::MediaDescription::codecs) assembles them into [`Codec`](crate::util::Codec)s by joining the +//! `a=rtpmap`, `a=fmtp` and `a=rtcp-fb` attributes that belong to each. +//! +//! [`RangedPort`](crate::description::media::RangedPort) exists because a media section may claim consecutive ports (`/`), +//! which RTP/RTCP without multiplexing needs. use std::collections::HashMap; use std::fmt; diff --git a/rtc-sdp/src/description/session.rs b/rtc-sdp/src/description/session.rs index 1c16a7f3..b533e32a 100644 --- a/rtc-sdp/src/description/session.rs +++ b/rtc-sdp/src/description/session.rs @@ -1,3 +1,14 @@ +//! The session description — a whole SDP document. +//! +//! A [`SessionDescription`](crate::description::session::SessionDescription) is the session-level fields (`v=`, `o=`, `s=`, `t=`, …) followed by +//! any number of [`MediaDescription`](crate::description::media::MediaDescription)s. `unmarshal` parses one +//! from a string and `marshal` prints it back; the round trip is faithful, including attributes +//! this crate does not interpret. +//! +//! The `ATTR_KEY_*` constants name the `a=` attributes WebRTC relies on — `mid`, `msid`, +//! `setup`, `rtcp-mux`, `extmap` and the direction flags — and the `SEMANTIC_TOKEN_*` constants +//! name the grouping semantics used with `a=group` and `a=msid-semantic`. + use std::collections::HashMap; use std::convert::TryFrom; use std::time::{Duration, SystemTime, UNIX_EPOCH}; diff --git a/rtc-sdp/src/extmap/mod.rs b/rtc-sdp/src/extmap/mod.rs index 40e1fdad..6ebf25c1 100644 --- a/rtc-sdp/src/extmap/mod.rs +++ b/rtc-sdp/src/extmap/mod.rs @@ -1,3 +1,12 @@ +//! `a=extmap` RTP header-extension declarations. +//! +//! An [`ExtMap`](crate::extmap::ExtMap) binds a header-extension URI to the small integer id that will appear in RTP +//! packets. Both sides must agree, which is the whole point of negotiating it in SDP: the id is +//! per-session, while the URI is the stable name. +//! +//! The `*_URI` constants are the extensions this stack uses — audio level, video orientation, +//! absolute send time, transport-wide CC, and the SDES ids that make simulcast demultiplexing +//! possible. #[cfg(test)] mod extmap_test; diff --git a/rtc-sdp/src/lib.rs b/rtc-sdp/src/lib.rs index 88ee2d11..06fbadcf 100644 --- a/rtc-sdp/src/lib.rs +++ b/rtc-sdp/src/lib.rs @@ -18,6 +18,33 @@ //! URIs. //! * [`direction`] — `sendrecv`/`sendonly`/`recvonly`/`inactive`. //! +//! # Example +//! +//! ``` +//! use rtc_sdp::SessionDescription; +//! use std::io::Cursor; +//! +//! # fn example() -> Result<(), Box> { +//! let sdp = "v=0\r\n\ +//! o=- 0 0 IN IP4 127.0.0.1\r\n\ +//! s=-\r\n\ +//! t=0 0\r\n\ +//! m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\ +//! a=mid:0\r\n\ +//! a=sendrecv\r\n"; +//! +//! let desc = SessionDescription::unmarshal(&mut Cursor::new(sdp))?; +//! for media in &desc.media_descriptions { +//! assert_eq!(media.media_name.media, "audio"); +//! assert_eq!(media.attribute("mid").flatten(), Some("0")); +//! } +//! +//! // Printing it back yields valid SDP. +//! assert!(desc.marshal().starts_with("v=0")); +//! # Ok(()) +//! # } +//! ``` +//! //! This crate is deliberately a *syntax* layer: it parses and prints SDP faithfully and //! leaves negotiation semantics ([RFC 8829]) to the [`rtc`](https://docs.rs/rtc) crate, //! which re-exports it as `rtc::sdp`. diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index 70a21178..5d0a3c39 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -1,3 +1,9 @@ +//! The error type shared across the stack. +//! +//! One [`Error`](crate::error::Error) enum spans every layer — buffers, UDP, ICE, DTLS, SCTP, SRTP, RTP/RTCP, SDP and +//! the data channel — so a value can propagate from the innermost codec to the application +//! without conversion at each boundary. The higher-level crates re-export it, which is why an +//! application only ever imports one error type. #![allow(dead_code)] use std::io; diff --git a/rtc-shared/src/ifaces/ffi/mod.rs b/rtc-shared/src/ifaces/ffi/mod.rs index 499bd7d7..abcabebc 100644 --- a/rtc-shared/src/ifaces/ffi/mod.rs +++ b/rtc-shared/src/ifaces/ffi/mod.rs @@ -8,6 +8,16 @@ mod unix; #[cfg(target_family = "unix")] pub use self::unix::ifaces; +/// Enumerates local network interfaces. +/// +/// This is the fallback for platforms that are neither Windows nor Unix: it always returns +/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported). ICE gathering treats that as +/// "no host candidates from interface enumeration" and falls back to the addresses the caller +/// supplied explicitly. +/// +/// # Errors +/// +/// Always fails on such platforms. #[cfg(not(any(target_family = "windows", target_family = "unix")))] pub fn ifaces() -> Result, std::io::Error> { Err(std::io::Error::new( diff --git a/rtc-shared/src/lib.rs b/rtc-shared/src/lib.rs index ace56d6b..d1f9526a 100644 --- a/rtc-shared/src/lib.rs +++ b/rtc-shared/src/lib.rs @@ -28,6 +28,28 @@ //! `crypto`, `ifaces`, `marshal` and `replay` are all enabled by default; each gates the //! correspondingly named module so that dependents can compile only what they use. //! +//! # Example +//! +//! Every protocol codec in the stack implements the same three traits, so encoding and decoding +//! look the same whichever layer you are at: +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_shared::marshal::{Marshal, MarshalSize, Unmarshal}; +//! +//! # fn round_trip(value: T) +//! # -> Result<(), Box> { +//! // Size the buffer, encode into it, then decode the result back. +//! let n = value.marshal_size(); +//! let encoded = value.marshal()?; +//! assert_eq!(encoded.len(), n); +//! +//! let mut buf = Bytes::from(encoded.to_vec()); +//! assert_eq!(T::unmarshal(&mut buf)?, value); +//! # Ok(()) +//! # } +//! ``` +//! //! Most applications do not depend on this crate directly — the [`rtc`](https://docs.rs/rtc) //! crate re-exports what it needs as `rtc::shared`. diff --git a/rtc-shared/src/time.rs b/rtc-shared/src/time.rs index d6988d46..04a49b31 100644 --- a/rtc-shared/src/time.rs +++ b/rtc-shared/src/time.rs @@ -1,3 +1,9 @@ +//! Monotonic, Unix and NTP time. +//! +//! Protocol logic measures time with a monotonic [`Instant`](std::time::Instant), which cannot go +//! backwards but has no absolute meaning. RTCP timestamps need the opposite: wall-clock time in +//! NTP format. [`SystemInstant`](crate::time::SystemInstant) captures both once, so either can be derived from the other later +//! without re-reading a clock that may have been adjusted in between. use std::ops::Add; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; diff --git a/rtc-shared/src/util.rs b/rtc-shared/src/util.rs index 2a00d2c7..16dcd948 100644 --- a/rtc-shared/src/util.rs +++ b/rtc-shared/src/util.rs @@ -1,3 +1,10 @@ +//! Shared helpers: packet demultiplexing and random strings. +//! +//! WebRTC multiplexes STUN, DTLS and SRTP onto one port, so the first byte of a datagram decides +//! which layer receives it ([RFC 7983]). The `match_*` predicates implement those ranges, and +//! [`is_rtcp`](crate::util::is_rtcp) separates RTCP from RTP once a packet is known to be one of the two. +//! +//! [RFC 7983]: https://datatracker.ietf.org/doc/html/rfc7983 use crate::error::{Error, Result}; use rand::{RngExt, rng}; use std::net::{SocketAddr, ToSocketAddrs}; diff --git a/rtc-srtp/src/key_derivation.rs b/rtc-srtp/src/key_derivation.rs index dbe36280..fff4e2c3 100644 --- a/rtc-srtp/src/key_derivation.rs +++ b/rtc-srtp/src/key_derivation.rs @@ -114,9 +114,9 @@ pub(crate) fn aes_256_cm_key_derivation( /// ROC = a 32-bit unsigned rollover counter (roc), which records how many /// times the 16-bit RTP sequence number has been reset to zero after /// passing through 65,535 -/// ```nobuild -/// i = 2^16 * roc + SEQ -/// IV = (salt*2 ^ 16) | (ssrc*2 ^ 64) | (i*2 ^ 16) +/// ```text +/// i = 2^16 * roc + SEQ +/// IV = (salt * 2^16) | (ssrc * 2^64) | (i * 2^16) /// ``` pub(crate) fn generate_counter( sequence_number: u16, diff --git a/rtc-srtp/src/lib.rs b/rtc-srtp/src/lib.rs index 0012bfa3..97444871 100644 --- a/rtc-srtp/src/lib.rs +++ b/rtc-srtp/src/lib.rs @@ -17,6 +17,23 @@ //! AEAD-AES-128-GCM, and friends) and their key/salt lengths. //! * [`config`], [`option`] — how a context is built, including replay-window sizing. //! +//! # Example +//! +//! A profile is negotiated through DTLS-SRTP, and it fixes the key, salt and tag sizes the +//! context will use: +//! +//! ``` +//! use rtc_srtp::protection_profile::ProtectionProfile; +//! +//! let profile = ProtectionProfile::Aes128CmHmacSha1_80; +//! assert_eq!(profile.key_len(), 16); // AES-128 +//! assert_eq!(profile.salt_len(), 14); +//! assert_eq!(profile.rtp_auth_tag_len(), 10); // 80-bit tag +//! +//! // The AEAD profiles authenticate inside the cipher, so they carry no HMAC key. +//! assert_eq!(ProtectionProfile::AeadAes128Gcm.auth_key_len(), 0); +//! ``` +//! //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate creates the contexts from the DTLS handshake and //! applies them to media as one layer of the peer-connection pipeline. diff --git a/rtc-stun/src/agent.rs b/rtc-stun/src/agent.rs index 68c34fcb..cb932492 100644 --- a/rtc-stun/src/agent.rs +++ b/rtc-stun/src/agent.rs @@ -1,3 +1,11 @@ +//! STUN transaction tracking. +//! +//! The agent remembers which requests are outstanding and when each should be considered lost. It +//! performs no I/O: the caller submits [`ClientAgent`](crate::agent::ClientAgent) commands — start a transaction, hand over +//! an inbound message, advance time, stop or close — and polls for the resulting [`Event`](crate::agent::Event)s. +//! +//! This split is what lets the retransmission schedule be tested without a network, and lets ICE +//! reuse the same transaction bookkeeping for its connectivity checks. #[cfg(test)] mod agent_test; diff --git a/rtc-stun/src/attributes.rs b/rtc-stun/src/attributes.rs index 612f2822..e4aaa430 100644 --- a/rtc-stun/src/attributes.rs +++ b/rtc-stun/src/attributes.rs @@ -1,3 +1,18 @@ +//! STUN attribute types. +//! +//! Every attribute is a type code, a length and a value ([`RawAttribute`](crate::attributes::RawAttribute)). This module defines +//! the code points — those from STUN itself ([RFC 5389]) plus the ones TURN, ICE and the NAT +//! behaviour discovery extensions add — while the typed accessors live in +//! [`textattrs`](crate::textattrs), [`xoraddr`](crate::xoraddr), +//! [`error_code`](crate::error_code) and friends. +//! +//! Codes below `0x8000` are *comprehension-required*: a receiver that does not understand one +//! must reject the message. Codes at or above `0x8000` are comprehension-optional and may be +//! ignored, which is how `FINGERPRINT` and the ICE attributes can be added without breaking +//! older peers. +//! +//! [RFC 5389]: https://datatracker.ietf.org/doc/html/rfc5389 + #[cfg(test)] mod attributes_test; diff --git a/rtc-stun/src/client.rs b/rtc-stun/src/client.rs index 5bdab81b..aec31a29 100644 --- a/rtc-stun/src/client.rs +++ b/rtc-stun/src/client.rs @@ -1,3 +1,8 @@ +//! A Sans-I/O STUN client. +//! +//! Sends Binding requests and matches the responses, applying the retransmission schedule the RFC +//! specifies (an initial RTO, doubling per retry) so a lost request on a UDP path is retried +//! rather than lost. Build one with [`ClientBuilder`](crate::client::ClientBuilder); drive it with datagrams and time. use bytes::BytesMut; use shared::error::*; use std::collections::{HashMap, VecDeque}; diff --git a/rtc-stun/src/error_code.rs b/rtc-stun/src/error_code.rs index c4615d26..6a79ecd1 100644 --- a/rtc-stun/src/error_code.rs +++ b/rtc-stun/src/error_code.rs @@ -1,3 +1,11 @@ +//! The `ERROR-CODE` attribute. +//! +//! An [`ErrorCodeAttribute`](crate::error_code::ErrorCodeAttribute) is a numeric [`ErrorCode`](crate::error_code::ErrorCode) plus a reason phrase. The codes here span +//! three specs — STUN's own (400, 401, 420, 500), ICE's role conflict (487), and TURN's +//! allocation failures (437, 441, 486, 508) — because all three share this attribute. +//! +//! [`ERROR_REASONS`](crate::error_code::ERROR_REASONS) maps each known code to the phrase it is normally sent with, so a responder +//! does not have to invent one. #[cfg(test)] mod error_code_test; diff --git a/rtc-stun/src/lib.rs b/rtc-stun/src/lib.rs index 621469a5..f853a8a6 100644 --- a/rtc-stun/src/lib.rs +++ b/rtc-stun/src/lib.rs @@ -22,6 +22,32 @@ //! * [`uri`] — parsing `stun:`/`stuns:` URLs. //! * [`checks`] — validation helpers for received messages. //! +//! # Example +//! +//! ``` +//! use rtc_stun::attributes::ATTR_SOFTWARE; +//! use rtc_stun::message::{BINDING_REQUEST, Message, TransactionId}; +//! use rtc_stun::textattrs::TextAttribute; +//! +//! # fn example() -> Result<(), Box> { +//! let mut msg = Message::new(); +//! msg.build(&[ +//! Box::new(TransactionId::new()), +//! Box::new(BINDING_REQUEST), +//! Box::new(TextAttribute::new(ATTR_SOFTWARE, "webrtc-rs".to_owned())), +//! ])?; +//! +//! // `build` encodes as it goes, so `raw` is ready to send. +//! assert!(!msg.raw.is_empty()); +//! +//! let mut decoded = Message::new(); +//! decoded.raw = msg.raw.clone(); +//! decoded.decode()?; +//! assert_eq!(decoded.typ, BINDING_REQUEST); +//! # Ok(()) +//! # } +//! ``` +//! //! Most applications do not depend on this crate directly — [`rtc-ice`] and //! [`rtc-turn`] build on it, and the [`rtc`](https://docs.rs/rtc) crate drives those. //! diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index 44f6c5b3..c599951e 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -1,3 +1,14 @@ +//! The STUN message: header, attributes, and encoding. +//! +//! A [`Message`](crate::message::Message) is a class and method ([`MessageType`](crate::message::MessageType)), a 96-bit [`TransactionId`](crate::message::TransactionId), and a +//! list of attributes. Build one by applying [`Setter`](crate::message::Setter)s, read one back with [`Getter`](crate::message::Getter)s, and +//! move it across the wire with `marshal`/`unmarshal`. +//! +//! Two attributes are special, because their value covers the *encoded* message: `FINGERPRINT` +//! and `MESSAGE-INTEGRITY` must be appended last and are validated through [`Checker`](crate::message::Checker). That is +//! why [`Message::raw`](crate::message::Message::raw) is kept alongside the parsed attributes — those checks are computed over +//! it rather than over a re-encoding. + #[cfg(test)] mod message_test; diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 503039ec..ea1ee47f 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -1,3 +1,12 @@ +//! The Sans-I/O TURN client. +//! +//! Using a relay takes three steps: Allocate to obtain a public address, CreatePermission for +//! each peer you intend to exchange data with, and then Send/Data indications (or a bound channel) +//! to move bytes. Each step is a STUN transaction, so every [`Event`](crate::client::Event) carries the +//! transaction id of the request it answers. +//! +//! The three address kinds are easy to confuse: [`RelayedAddr`](crate::client::RelayedAddr) is what peers send to, +//! [`ReflexiveAddr`](crate::client::ReflexiveAddr) is how the server sees this client, and [`PeerAddr`](crate::client::PeerAddr) is the far end. #[cfg(test)] mod client_test; diff --git a/rtc-turn/src/lib.rs b/rtc-turn/src/lib.rs index aaa5cd18..e37f694b 100644 --- a/rtc-turn/src/lib.rs +++ b/rtc-turn/src/lib.rs @@ -16,6 +16,30 @@ //! `CREATE-PERMISSION`, `CHANNEL-BIND`, `XOR-RELAYED-ADDRESS`, ChannelData framing), //! built on [`rtc-stun`]. //! +//! # Example +//! +//! A client is configured with the server to allocate from and the long-term credentials to +//! authenticate with; driving it is then a matter of feeding it datagrams and polling for +//! [`Event`](client::Event)s: +//! +//! ``` +//! use rtc_turn::client::ClientConfig; +//! use shared::TransportProtocol; +//! +//! let config = ClientConfig { +//! turn_serv_addr: "turn.example.com:3478".to_owned(), +//! local_addr: "0.0.0.0:0".parse().unwrap(), +//! transport_protocol: TransportProtocol::UDP, +//! username: "user".to_owned(), +//! password: "pass".to_owned(), +//! realm: "example.com".to_owned(), +//! stun_serv_addr: String::new(), // optional: only for Binding requests +//! software: String::new(), +//! rto_in_ms: 0, // 0 selects the default retransmission timeout +//! }; +//! assert_eq!(config.turn_serv_addr, "turn.example.com:3478"); +//! ``` +//! //! Most applications do not depend on this crate directly — [`rtc-ice`] gathers relay //! candidates through it, and the [`rtc`](https://docs.rs/rtc) crate drives that. //! diff --git a/rtc-turn/src/proto/mod.rs b/rtc-turn/src/proto/mod.rs index 0961d54b..74ada82e 100644 --- a/rtc-turn/src/proto/mod.rs +++ b/rtc-turn/src/proto/mod.rs @@ -1,3 +1,13 @@ +//! TURN's STUN attributes and ChannelData framing. +//! +//! TURN is defined as a set of STUN methods and attributes, so these build on +//! [`rtc-stun`](https://docs.rs/rtc-stun). The attributes name the relay's parts: +//! [`relayaddr`](crate::proto::relayaddr) the allocated public address, [`peeraddr`](crate::proto::peeraddr) the far end, [`lifetime`](crate::proto::lifetime) the +//! allocation's expiry, [`data`](crate::proto::data) the relayed payload. +//! +//! [`chandata`](crate::proto::chandata) is the exception — a ChannelData message is not STUN at all, but a compact +//! four-byte framing that replaces the 36-byte Send/Data indication header once a channel is +//! bound. Its [`channum`](crate::proto::channum) range is chosen so the two can be told apart on a shared port. #[cfg(test)] mod proto_test; From 840bcdfb7b2945d3642e7aee015db95a86396639 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 20:59:17 -0700 Subject: [PATCH 12/40] Fix "cargo test --workspace --no-default-features --features aws-lc-rs" failure #134 --- Cargo.toml | 1 - rtc-dtls/src/config.rs | 60 +++++++++++++++---- rtc-dtls/src/crypto/padding.rs | 2 +- src/peer_connection/transport/dtls/mod.rs | 7 --- tests/common/mod.rs | 21 +++++++ tests/data_channels_close_by_rtc_interop.rs | 3 + .../data_channels_close_by_webrtc_interop.rs | 3 + tests/data_channels_create_interop.rs | 3 + tests/data_channels_interop.rs | 3 + tests/ice_restart_by_rtc_interop.rs | 3 + tests/ice_restart_by_webrtc_interop.rs | 3 + tests/interceptor_rtcp_reports_interop.rs | 5 ++ tests/mdns_query_and_gather_interop.rs | 6 ++ tests/media_rejection_interop.rs | 4 ++ ...rtc_set_remote_before_add_track_interop.rs | 3 + tests/play_from_disk_vpx_interop.rs | 3 + tests/reflect_rtc_to_webrtc_interop.rs | 3 + tests/reflect_webrtc_to_rtc_interop.rs | 3 + tests/rtcp_processing_boxed_interop.rs | 5 ++ tests/rtcp_processing_interop.rs | 5 ++ tests/save_to_disk_vpx_interop.rs | 3 + tests/simulcast_rtc_to_webrtc_interop.rs | 3 + tests/simulcast_webrtc_to_rtc_interop.rs | 3 + tests/trickle_ice_interop.rs | 4 ++ 24 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 tests/common/mod.rs diff --git a/Cargo.toml b/Cargo.toml index f3d23074..5705e806 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,6 @@ openssl = ["srtp/openssl"] vendored-openssl = ["srtp/vendored-openssl"] ring = ["dep:ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] aws-lc-rs = ["dep:aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] -__testing = [] [dependencies] shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 25735853..0c7692db 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -35,6 +35,53 @@ use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::CertificateDer; use rustls::server::danger::ClientCertVerifier; +/// The rustls [`CryptoProvider`](rustls::crypto::CryptoProvider) this crate was built with. +/// +/// rustls can infer a process-wide default from its own crate features, but only when exactly +/// one of `ring`/`aws-lc-rs` is enabled — and it panics otherwise. Feature unification makes +/// that easy to violate: any other crate in the graph that asks rustls for a different provider +/// enables both, which is what happens as soon as `rtc`'s `webrtc` interop dev-dependency joins +/// the build. Since our own `ring`/`aws-lc-rs` features already decide the answer, pass it +/// explicitly and never consult the global default. +/// +/// If neither feature is enabled there is no provider to name, so fall back to whatever the +/// application installed. +fn crypto_provider() -> Option> { + #[cfg(feature = "aws-lc-rs")] + { + return Some(std::sync::Arc::new( + rustls::crypto::aws_lc_rs::default_provider(), + )); + } + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + { + return Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())); + } + #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + { + None + } +} + +/// Builds the default server-certificate verifier, with an explicit provider where we have one. +/// +/// # Errors +/// +/// Fails if the root store holds no usable trust anchors. +fn server_cert_verifier( + roots: std::sync::Arc, +) -> Result> { + let builder = match crypto_provider() { + Some(provider) => { + rustls::client::WebPkiServerVerifier::builder_with_provider(roots, provider) + } + None => rustls::client::WebPkiServerVerifier::builder(roots), + }; + builder + .build() + .map_err(|err| Error::Other(format!("rustls server cert verifier: {err}"))) +} + /// Config is used to configure a DTLS client or server. /// After a Config is passed to a DTLS function it must not be modified. #[derive(Clone)] @@ -368,11 +415,7 @@ impl ConfigBuilder { 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() - .unwrap(), + server_cert_verifier: server_cert_verifier(Arc::new(gen_self_signed_root_cert()))?, client_cert_verifier: None, retransmit_interval, initial_epoch: 0, @@ -475,11 +518,8 @@ impl Default for HandshakeConfig { 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(), + server_cert_verifier: server_cert_verifier(Arc::new(gen_self_signed_root_cert())) + .expect("the built-in self-signed root is always a valid trust anchor"), client_cert_verifier: None, retransmit_interval: std::time::Duration::from_secs(0), initial_epoch: 0, diff --git a/rtc-dtls/src/crypto/padding.rs b/rtc-dtls/src/crypto/padding.rs index b01d0a80..0f1337ea 100644 --- a/rtc-dtls/src/crypto/padding.rs +++ b/rtc-dtls/src/crypto/padding.rs @@ -53,7 +53,7 @@ fn set(dst: &mut [u8], value: u8) { } #[cfg(test)] -pub mod tests { +mod tests { use rand::RngExt; use super::*; diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 3ca524f5..a2d8b87b 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -122,13 +122,6 @@ impl RTCDtlsTransport { return Err(Error::ErrInvalidDTLSStart); } - // The `webrtc` interop tests uses `ring` for its crypto provider, - // so we need to avoid conflicts with `aws-lc-rs`. - // We need a feature gate because - // `#[cfg(test)]` is not propagated to this crate in integration tests. - #[cfg(all(any(feature = "__testing", test), feature = "aws-lc-rs"))] - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - self.dtls_role = self.derive_role(ice_role, remote_dtls_parameters.role); let remote_fingerprints = remote_dtls_parameters.fingerprints; diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..c5abc899 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,21 @@ +//! Shared helpers for the interop tests. + +/// Installs a process-wide rustls `CryptoProvider` if one is not already installed. +/// +/// The interop tests drive the published `webrtc` crate alongside this one, and its `dtls 0.13` +/// dependency asks rustls to infer the provider from crate features. That inference only works +/// when exactly one of rustls' `ring`/`aws-lc-rs` features is enabled — but in a test build both +/// are: `dtls 0.13` turns on `rustls/ring` while `rtc` turns on whichever its own feature selects. +/// Cargo unifies the two, rustls finds an ambiguity, and the old crate panics. +/// +/// Naming a provider here resolves it for that crate. `rtc` itself no longer consults this +/// default — it passes its provider explicitly (see `rtc_dtls::config`) — so which one we install +/// only has to satisfy the old code. `ring` is always available in a test build: either `rtc` +/// selected it, or `dtls 0.13` pulled it in. +/// +/// Idempotent, and safe to call from every test: `install_default` returns `Err` once a provider +/// is set, which is ignored here, so the first caller wins and an application that installed its +/// own is unaffected. +pub fn install_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} diff --git a/tests/data_channels_close_by_rtc_interop.rs b/tests/data_channels_close_by_rtc_interop.rs index 5f5b4e5c..561b2824 100644 --- a/tests/data_channels_close_by_rtc_interop.rs +++ b/tests/data_channels_close_by_rtc_interop.rs @@ -39,11 +39,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel close behavior with RTC sending periodic messages and closing #[tokio::test] async fn test_data_channel_close_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/data_channels_close_by_webrtc_interop.rs b/tests/data_channels_close_by_webrtc_interop.rs index 5d4ad6f8..ffcdb220 100644 --- a/tests/data_channels_close_by_webrtc_interop.rs +++ b/tests/data_channels_close_by_webrtc_interop.rs @@ -40,11 +40,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel close behavior with WebRTC sending periodic messages and closing #[tokio::test] async fn test_data_channel_close_by_webrtc_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/data_channels_create_interop.rs b/tests/data_channels_create_interop.rs index 825e6cd4..b279adc4 100644 --- a/tests/data_channels_create_interop.rs +++ b/tests/data_channels_create_interop.rs @@ -41,11 +41,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel creation where RTC creates the channel, sends messages, and receives echoes #[tokio::test] async fn test_data_channel_create_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/data_channels_interop.rs b/tests/data_channels_interop.rs index d663d460..8c4c858b 100644 --- a/tests/data_channels_interop.rs +++ b/tests/data_channels_interop.rs @@ -35,11 +35,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel communication between rtc (sansio) and webrtc (async) implementations #[tokio::test] async fn test_data_channel_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/ice_restart_by_rtc_interop.rs b/tests/ice_restart_by_rtc_interop.rs index 851bf9a2..d6502734 100644 --- a/tests/ice_restart_by_rtc_interop.rs +++ b/tests/ice_restart_by_rtc_interop.rs @@ -33,12 +33,15 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const TEST_MESSAGE: &str = "Hello before restart!"; const TEST_MESSAGE_AFTER_RESTART: &str = "Hello after restart!"; #[tokio::test] async fn test_ice_restart_by_rtc_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/ice_restart_by_webrtc_interop.rs b/tests/ice_restart_by_webrtc_interop.rs index 102cd9f5..52e45275 100644 --- a/tests/ice_restart_by_webrtc_interop.rs +++ b/tests/ice_restart_by_webrtc_interop.rs @@ -35,6 +35,8 @@ use webrtc::peer_connection::offer_answer_options::RTCOfferOptions; use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const TEST_MESSAGE_1: &str = "Hello before restart!"; const TEST_MESSAGE_2: &str = "Hello after restart!"; @@ -69,6 +71,7 @@ async fn create_webrtc_peer() -> Result> { /// Test ICE restart between webrtc (offerer) and rtc (answerer) #[tokio::test] async fn test_ice_restart_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/interceptor_rtcp_reports_interop.rs b/tests/interceptor_rtcp_reports_interop.rs index 4d6810d6..25c39c86 100644 --- a/tests/interceptor_rtcp_reports_interop.rs +++ b/tests/interceptor_rtcp_reports_interop.rs @@ -49,12 +49,15 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test that custom interceptor registry with SenderReportBuilder and ReceiverReportBuilder /// can be used with RTCConfigurationBuilder. #[tokio::test] async fn test_custom_interceptor_registry_with_rtcp_reports() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -386,6 +389,7 @@ async fn test_custom_interceptor_registry_with_rtcp_reports() -> Result<()> { /// This test monitors the outgoing packets to verify SR generation. #[tokio::test] async fn test_sender_report_generation_on_rtp_send() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Debug) .is_test(true) @@ -672,6 +676,7 @@ async fn test_sender_report_generation_on_rtp_send() -> Result<()> { /// Test that using register_default_interceptors helper function works correctly. #[tokio::test] async fn test_register_default_interceptors_helper() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/mdns_query_and_gather_interop.rs b/tests/mdns_query_and_gather_interop.rs index 6eec2377..03403393 100644 --- a/tests/mdns_query_and_gather_interop.rs +++ b/tests/mdns_query_and_gather_interop.rs @@ -46,6 +46,8 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const MDNS_LOCAL_NAME: &str = "webrtc-rs-test-mdns.local"; const WEBRTC_MDNS_LOCAL_NAME: &str = "webrtc-peer-mdns.local"; @@ -287,6 +289,7 @@ async fn run_rtc_event_loop( /// advertise its own IP via mDNS. #[tokio::test] async fn test_mdns_query_only_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -452,6 +455,7 @@ async fn test_mdns_query_only_webrtc_offerer_rtc_answerer() -> Result<()> { /// advertises its own IP via an mDNS name. #[tokio::test] async fn test_mdns_query_and_gather_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -605,6 +609,7 @@ async fn test_mdns_query_and_gather_webrtc_offerer_rtc_answerer() -> Result<()> /// Test mDNS QueryOnly mode: sansio RTC as offerer, webrtc as answerer #[tokio::test] async fn test_mdns_query_only_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -779,6 +784,7 @@ async fn test_mdns_query_only_rtc_offerer_webrtc_answerer() -> Result<()> { /// Test mDNS QueryAndGather mode: sansio RTC as offerer, webrtc as answerer #[tokio::test] async fn test_mdns_query_and_gather_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/media_rejection_interop.rs b/tests/media_rejection_interop.rs index 3b5d3715..c59a9d72 100644 --- a/tests/media_rejection_interop.rs +++ b/tests/media_rejection_interop.rs @@ -51,6 +51,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // ============================================================================ @@ -128,6 +130,7 @@ fn create_rtc_peer_config_video_only() /// - Video RTP packets are received successfully #[tokio::test] async fn test_video_only_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -415,6 +418,7 @@ async fn test_video_only_webrtc_offerer_rtc_answerer() -> Result<()> { /// sansio RTC correctly rejects audio (port=0) while accepting video. #[tokio::test] async fn test_sdp_answer_rejects_audio_correctly() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs b/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs index d56d7570..3504eb33 100644 --- a/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs +++ b/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs @@ -53,6 +53,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); const RTP_OUTBOUND_MTU: usize = 1200; @@ -60,6 +62,7 @@ const RTP_OUTBOUND_MTU: usize = 1200; /// Test webrtc as RecvOnly offerer, rtc as answerer streaming from disk #[tokio::test] async fn test_play_from_disk_rtc_set_remote_before_add_track() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/play_from_disk_vpx_interop.rs b/tests/play_from_disk_vpx_interop.rs index e4f75eea..5285249e 100644 --- a/tests/play_from_disk_vpx_interop.rs +++ b/tests/play_from_disk_vpx_interop.rs @@ -55,6 +55,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); const RTP_OUTBOUND_MTU: usize = 1200; @@ -62,6 +64,7 @@ const RTP_OUTBOUND_MTU: usize = 1200; /// Test streaming media from disk: rtc streams from disk -> webrtc receives #[tokio::test] async fn test_play_from_disk_vpx_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/reflect_rtc_to_webrtc_interop.rs b/tests/reflect_rtc_to_webrtc_interop.rs index b9d5e875..0ccc0a71 100644 --- a/tests/reflect_rtc_to_webrtc_interop.rs +++ b/tests/reflect_rtc_to_webrtc_interop.rs @@ -51,11 +51,14 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test reflect functionality: rtc sends RTP -> webrtc reflects -> rtc receives #[tokio::test] async fn test_reflect_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/reflect_webrtc_to_rtc_interop.rs b/tests/reflect_webrtc_to_rtc_interop.rs index fc656609..2171059f 100644 --- a/tests/reflect_webrtc_to_rtc_interop.rs +++ b/tests/reflect_webrtc_to_rtc_interop.rs @@ -45,11 +45,14 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test reflect functionality: webrtc sends RTP -> rtc reflects -> webrtc receives #[tokio::test] async fn test_reflect_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/rtcp_processing_boxed_interop.rs b/tests/rtcp_processing_boxed_interop.rs index 89ade00c..106425aa 100644 --- a/tests/rtcp_processing_boxed_interop.rs +++ b/tests/rtcp_processing_boxed_interop.rs @@ -70,6 +70,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // ============================================================================ @@ -444,6 +446,7 @@ fn dummy_rtp(ssrc: u32, seq: u32, payload_type: u8) -> rtc::rtp::packet::Packet /// surfaces RTCP via `poll_read()`, and the default interceptors still process RTP. #[tokio::test] async fn test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -591,6 +594,7 @@ async fn test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> /// exactly what an SFU needs in order to relay PLI/FIR upstream to a publisher. #[tokio::test] async fn test_boxed_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -713,6 +717,7 @@ async fn test_boxed_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> /// the default chain consumes RTCP internally. #[tokio::test] async fn test_boxed_rtc_to_rtc_heterogeneous_chains() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/rtcp_processing_interop.rs b/tests/rtcp_processing_interop.rs index 0f900adb..3b0dac61 100644 --- a/tests/rtcp_processing_interop.rs +++ b/tests/rtcp_processing_interop.rs @@ -50,6 +50,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // ============================================================================ @@ -213,6 +215,7 @@ fn create_rtc_peer_config_with_rtcp_forwarder( /// - RTCP packets can be parsed and inspected #[tokio::test] async fn test_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -474,6 +477,7 @@ async fn test_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { /// This test verifies RTCP processing when roles are reversed. #[tokio::test] async fn test_rtcp_processing_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -750,6 +754,7 @@ async fn test_rtcp_processing_rtc_offerer_webrtc_answerer() -> Result<()> { /// Asserts the RTC peer receives RTCP about its sent stream, tagged with the sender's track id. #[tokio::test] async fn test_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/save_to_disk_vpx_interop.rs b/tests/save_to_disk_vpx_interop.rs index 163c7414..2748ee1a 100644 --- a/tests/save_to_disk_vpx_interop.rs +++ b/tests/save_to_disk_vpx_interop.rs @@ -47,11 +47,14 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::TrackLocal; use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test streaming media to disk: webrtc streams -> rtc receives #[tokio::test] async fn test_save_to_disk_vpx_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/simulcast_rtc_to_webrtc_interop.rs b/tests/simulcast_rtc_to_webrtc_interop.rs index ad7dd61c..05d40403 100644 --- a/tests/simulcast_rtc_to_webrtc_interop.rs +++ b/tests/simulcast_rtc_to_webrtc_interop.rs @@ -61,11 +61,14 @@ use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test simulcast: rtc sends 3 layers with RIDs -> webrtc receives all 3 layers #[tokio::test] async fn test_simulcast_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/simulcast_webrtc_to_rtc_interop.rs b/tests/simulcast_webrtc_to_rtc_interop.rs index 0d450f2f..191384cc 100644 --- a/tests/simulcast_webrtc_to_rtc_interop.rs +++ b/tests/simulcast_webrtc_to_rtc_interop.rs @@ -56,6 +56,8 @@ use webrtc::rtp_transceiver::rtp_codec::{ use webrtc::track::track_local::TrackLocal; use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Integration test for multi-track video streaming (webrtc → rtc) @@ -92,6 +94,7 @@ const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); #[tokio::test] #[ignore] async fn test_simulcast_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/trickle_ice_interop.rs b/tests/trickle_ice_interop.rs index 6017e769..61dfda4a 100644 --- a/tests/trickle_ice_interop.rs +++ b/tests/trickle_ice_interop.rs @@ -45,6 +45,8 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Helper function to create a webrtc peer connection (no STUN - local only) @@ -101,6 +103,7 @@ fn create_rtc_peer_config( /// - Data channel communication works with trickle ICE #[tokio::test] async fn test_trickle_ice_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -391,6 +394,7 @@ async fn test_trickle_ice_webrtc_offerer_rtc_answerer() -> Result<()> { /// - Data channel communication works with trickle ICE #[tokio::test] async fn test_trickle_ice_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) From c62bd441b976c69fc21c43121ac6ac986de20b5a Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 21:12:42 -0700 Subject: [PATCH 13/40] update test coverage to run packages: --workspace for features: aws-lc-rs --- .github/workflows/grcov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 44ae7c6f..3439a9ba 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -31,8 +31,8 @@ jobs: test_args: "" - name: aws-lc-rs features: aws-lc-rs - packages: --package rtc - test_args: --lib + packages: --workspace + test_args: "" steps: - name: Checkout source code uses: actions/checkout@v4 From 039f625814601f411e3a76d7d75d2389d051c7ec Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Wed, 29 Jul 2026 21:19:39 -0700 Subject: [PATCH 14/40] update rkyv = "0.8.17" to avoid potential Security Vulnerabilities --- rtc-dtls/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index af53a007..fd9fae4f 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -36,7 +36,7 @@ rcgen.workspace = true ring = { workspace = true, optional = true } aws-lc-rs = { workspace = true, optional = true } rustls = { version = "0.23.27", default-features = false, features = ["std"] } -rkyv = "0.8" +rkyv = "0.8.17" bytecheck = "0.8" subtle = "2.5.0" log.workspace = true From 444e2c94c2b1502efb1f59e26474c7a8281777b1 Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:30:50 -0700 Subject: [PATCH 15/40] Create semver.yml --- .github/workflows/semver.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/semver.yml diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml new file mode 100644 index 00000000..57a006af --- /dev/null +++ b/.github/workflows/semver.yml @@ -0,0 +1,25 @@ +name: SemVer Check + +on: + pull_request: + branches: [ "master" ] + +jobs: + semver: + name: Check SemVer + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and build artifacts + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-semver-checks + run: cargo install cargo-semver-checks --locked + + - name: Run cargo-semver-checks + run: cargo semver-checks --workspace --default-features From d8365becd176b39a64cfb0759d0cccc490e6578d Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:32:12 -0700 Subject: [PATCH 16/40] Update semver.yml --- .github/workflows/semver.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index 57a006af..8e4b3dc0 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -1,8 +1,10 @@ name: SemVer Check on: + push: + branches: [master] pull_request: - branches: [ "master" ] + branches: [master] jobs: semver: From c866c6cf94ed9edea838e172ae1e3fb0d6da18b1 Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:48:01 -0700 Subject: [PATCH 17/40] Update semver.yml --- .github/workflows/semver.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index 8e4b3dc0..5fc47e4b 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -14,14 +14,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo registry and build artifacts - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-semver-checks - run: cargo install cargo-semver-checks --locked - - name: Run cargo-semver-checks - run: cargo semver-checks --workspace --default-features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + # Overrides the default heuristic to ONLY use default features + feature-group: default-features From ffc3dfb5ddee5ed00e12733800c79978c453de02 Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:54:50 -0700 Subject: [PATCH 18/40] Update semver.yml --- .github/workflows/semver.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index 5fc47e4b..8e4b3dc0 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -14,8 +14,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and build artifacts + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-semver-checks + run: cargo install cargo-semver-checks --locked + - name: Run cargo-semver-checks - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - # Overrides the default heuristic to ONLY use default features - feature-group: default-features + run: cargo semver-checks --workspace --default-features From 487d2fc4852a83f00204c39c7f4e1bfd2d03f37b Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Thu, 30 Jul 2026 20:03:36 -0700 Subject: [PATCH 19/40] fix webrtc issue 808: No public API to configure DTLS cipher suites; default set offers ECDHE_RSA suites incompatible with an ECDSA certificate (handshake stalls) --- rtc-dtls/src/config.rs | 6 +- rtc-stun/src/message.rs | 37 ++++++--- .../configuration/setting_engine.rs | 34 ++++++++ src/peer_connection/internal.rs | 1 + src/peer_connection/transport/dtls/mod.rs | 79 +++++++++++++++++++ 5 files changed, 141 insertions(+), 16 deletions(-) diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 0c7692db..c681745b 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -49,13 +49,13 @@ use rustls::server::danger::ClientCertVerifier; fn crypto_provider() -> Option> { #[cfg(feature = "aws-lc-rs")] { - return Some(std::sync::Arc::new( + Some(std::sync::Arc::new( rustls::crypto::aws_lc_rs::default_provider(), - )); + )) } #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] { - return Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())); + Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())) } #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] { diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index c599951e..63a02858 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -463,21 +463,32 @@ impl Message { } } - /// Build resets message and applies setters to it in batch, returning on - /// first error. To prevent allocations, pass pointers to values. + /// Resets the message and applies `setters` to it in order, returning on the first + /// error. /// - /// Example: - /// var ( - /// t = BindingRequest - /// username = NewUsername("username") - /// nonce = NewNonce("nonce") - /// realm = NewRealm("example.org") - /// ) - /// m := new(Message) - /// m.Build(t, username, nonce, realm) // 4 allocations - /// m.Build(&t, &username, &nonce, &realm) // 0 allocations + /// Each setter writes its attribute into the message as it is applied, so the encoded + /// [`raw`](Self::raw) bytes are complete once this returns. Order matters for setters + /// that cover the attributes before them — a + /// [`MessageIntegrity`](crate::integrity::MessageIntegrity) or + /// [`FINGERPRINT`](crate::fingerprint::FINGERPRINT) therefore goes last. /// - /// See BenchmarkBuildOverhead. + /// ``` + /// use rtc_stun::attributes::ATTR_SOFTWARE; + /// use rtc_stun::message::{BINDING_REQUEST, Message, TransactionId}; + /// use rtc_stun::textattrs::TextAttribute; + /// + /// # fn example() -> Result<(), Box> { + /// let mut m = Message::new(); + /// m.build(&[ + /// Box::new(BINDING_REQUEST), + /// Box::new(TransactionId::new()), + /// Box::new(TextAttribute::new(ATTR_SOFTWARE, "webrtc-rs".to_owned())), + /// ])?; + /// + /// assert!(!m.raw.is_empty()); + /// # Ok(()) + /// # } + /// ``` pub fn build(&mut self, setters: &[Box]) -> Result<()> { self.reset(); self.write_header(); diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 43a707d6..8d172732 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -108,6 +108,7 @@ use std::net::IpAddr; use std::sync::Arc; +use dtls::cipher_suite::CipherSuiteId; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; //TODO: use ice::agent::agent_config::{InterfaceFilterFn, IpFilterFn}; //TODO: use ice::mdns::MulticastDnsMode; @@ -346,6 +347,7 @@ pub struct SettingEngine { pub(crate) disable_media_engine_copy: bool, pub(crate) disable_media_engine_multiple_codecs: bool, pub(crate) srtp_protection_profiles: Vec, + pub(crate) dtls_cipher_suites: Vec, pub(crate) receive_mtu: usize, pub(crate) mid_generator: Option String + Send + Sync>>, /// Determines the max size of any message that may be sent through an SCTP transport. @@ -397,6 +399,38 @@ impl SettingEngine { self.srtp_protection_profiles = profiles } + /// Restricts the DTLS cipher suites offered during the handshake. + /// + /// An empty list (the default) uses the `dtls` crate's built-in set, which offers + /// **both** ECDHE_ECDSA and ECDHE_RSA suites. That is deliberate — the local + /// certificate is not known when the list is compiled — but it means a remote peer may + /// select an ECDHE_RSA suite that an ECDSA certificate cannot satisfy, and the + /// handshake then stalls with the connection stuck in `Connecting`. + /// + /// Certificates generated by this crate are ECDSA (P-256), so an application that does + /// not supply its own RSA certificate can pin the ECDSA suites and remove that + /// possibility: + /// + /// ``` + /// use dtls::cipher_suite::CipherSuiteId; + /// use rtc::peer_connection::configuration::setting_engine::SettingEngine; + /// + /// let mut setting_engine = SettingEngine::default(); + /// + /// setting_engine.set_dtls_cipher_suites(vec![ + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + /// ]); + /// ``` + /// + /// The order is a preference order. Every suite named must be one the `dtls` crate + /// implements, or building the transport fails with `ErrInvalidCipherSuite`; a list + /// that filters down to nothing usable fails with `ErrNoAvailableCipherSuites`. + pub fn set_dtls_cipher_suites(&mut self, cipher_suites: Vec) { + self.dtls_cipher_suites = cipher_suites + } + /// Configures ICE timeout behavior for connection health monitoring. /// /// These timeouts control when ICE transitions between connection states diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index 8230f0ca..8cd50fd6 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -91,6 +91,7 @@ where certificates, setting_engine.answering_dtls_role, setting_engine.srtp_protection_profiles.clone(), + setting_engine.dtls_cipher_suites.clone(), setting_engine.allow_insecure_verification_algorithm, setting_engine.disable_certificate_fingerprint_verification, setting_engine.replay_protection, diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index a2d8b87b..51081036 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -4,6 +4,7 @@ use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters; use crate::peer_connection::transport::dtls::role::{DEFAULT_DTLS_ROLE_ANSWER, RTCDtlsRole}; use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState; use crate::peer_connection::transport::ice::role::RTCIceRole; +use dtls::cipher_suite::CipherSuiteId; use dtls::config::{ClientAuthType, VerifyPeerCertificateFn}; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; use rcgen::KeyPair; @@ -44,6 +45,9 @@ pub(crate) struct RTCDtlsTransport { // From SettingEngine pub(crate) answering_dtls_role: RTCDtlsRole, pub(crate) srtp_protection_profiles: Vec, + /// Empty means "use the `dtls` crate's default set" (see + /// [`SettingEngine::set_dtls_cipher_suites`](crate::peer_connection::configuration::setting_engine::SettingEngine::set_dtls_cipher_suites)). + pub(crate) dtls_cipher_suites: Vec, pub(crate) allow_insecure_verification_algorithm: bool, pub(crate) disable_certificate_fingerprint_verification: bool, pub(crate) replay_protection: ReplayProtection, @@ -54,6 +58,7 @@ impl RTCDtlsTransport { mut certificates: Vec, answering_dtls_role: RTCDtlsRole, srtp_protection_profiles: Vec, + dtls_cipher_suites: Vec, allow_insecure_verification_algorithm: bool, disable_certificate_fingerprint_verification: bool, replay_protection: ReplayProtection, @@ -80,6 +85,7 @@ impl RTCDtlsTransport { answering_dtls_role, srtp_protection_profiles, + dtls_cipher_suites, allow_insecure_verification_algorithm, disable_certificate_fingerprint_verification, replay_protection, @@ -182,6 +188,8 @@ impl RTCDtlsTransport { } else { default_srtp_protection_profiles() }) + // Empty leaves `dtls`'s default set in place; a non-empty list replaces it. + .with_cipher_suites(self.dtls_cipher_suites.clone()) .with_client_auth(ClientAuthType::RequireAnyClientCert) .with_insecure_skip_verify(true) .with_insecure_verification(self.allow_insecure_verification_algorithm) @@ -227,3 +235,74 @@ impl RTCDtlsTransport { Ok(()) } } + +#[cfg(test)] +mod tests { + //! Cipher-suite plumbing for issue #808. + //! + //! `HandshakeConfig::local_cipher_suites` is `pub(crate)` to `rtc-dtls`, so these assert + //! the setting reached the config *behaviourally*: a list that cannot be satisfied is + //! rejected, which can only happen if it was applied rather than ignored. + + use super::*; + use crate::peer_connection::configuration::setting_engine::ReplayProtection; + + fn transport(dtls_cipher_suites: Vec) -> RTCDtlsTransport { + RTCDtlsTransport::new( + vec![], + DEFAULT_DTLS_ROLE_ANSWER, + vec![], + dtls_cipher_suites, + false, + false, + ReplayProtection::default(), + ) + .expect("a self-signed ECDSA certificate is generated when none is supplied") + } + + fn remote_params() -> RTCDtlsParameters { + RTCDtlsParameters { + role: RTCDtlsRole::Client, + fingerprints: vec![], + } + } + + #[test] + fn empty_cipher_suites_keeps_the_dtls_defaults() { + // The pre-#808 behaviour, and what every existing caller gets. + assert!( + transport(vec![]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + } + + #[test] + fn ecdsa_only_cipher_suites_are_accepted() { + // The fix for #808: pin the suites an ECDSA certificate can actually satisfy, so a + // peer cannot select an ECDHE_RSA suite and stall the handshake. + assert!( + transport(vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + ]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + } + + #[test] + fn unsatisfiable_cipher_suites_are_rejected_rather_than_ignored() { + // This is the assertion that proves plumbing. PSK suites are filtered out when no + // PSK is configured, leaving nothing usable. If `set_dtls_cipher_suites` were + // dropped on the floor, the default set would be used and this would succeed. + let err = transport(vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .expect_err("a PSK-only list with no PSK leaves no usable suite"); + assert!( + err.to_string().contains("CipherSuite"), + "expected a cipher-suite error, got: {err}" + ); + } +} From ff9af2f16b24a92ad1d7a473ed845a93d5459ad1 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Fri, 31 Jul 2026 06:55:06 -0700 Subject: [PATCH 20/40] =?UTF-8?q?ICE=20restart=20after=20TURN=20credential?= =?UTF-8?q?=20rotation=20re-allocates=20on=20the=20same=205-tuple=20?= =?UTF-8?q?=E2=86=92=20437=20(Allocation=20Mismatch)=20#835?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rtc-turn/src/client/mod.rs | 54 +++++++++++++++++++++++++++++++++++- rtc-turn/src/client/relay.rs | 4 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index ea1ee47f..645d9500 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -24,7 +24,7 @@ use bytes::BytesMut; use log::{debug, trace}; use std::collections::{HashMap, VecDeque}; use std::net::SocketAddr; -use std::time::Instant; +use std::time::{Duration, Instant}; use stun::attributes::*; use stun::integrity::*; @@ -477,6 +477,58 @@ impl Client { | XOR-MAPPED-ADDRESS=192.0.2.1:7000 | | | MESSAGE-INTEGRITY-SHA256=... | | | */ + /// Replaces the long-term credential used to sign subsequent requests, **keeping any + /// existing allocation**. + /// + /// A TURN allocation is a property of the 5-tuple, not of the credential that created + /// it: [RFC 5766 §6.2] identifies an allocation by 5-tuple, and a server's + /// `Refresh` handling looks it up the same way. So when credentials are rotated on the + /// same server there is no need to give up the allocation and re-`Allocate` — which + /// would in fact be rejected with **437 (Allocation Mismatch)**, since the server still + /// holds the previous allocation for that 5-tuple. Re-signing the existing allocation is + /// both correct and seamless: permissions and channel bindings survive. + /// + /// The realm is *not* re-negotiated. It was learned from the server's 401 during the + /// first `Allocate`, and a credential rotation keeps the same server, so it still + /// applies. Follow this with [`Relay::refresh`] so the server sees the new credential + /// before the allocation would otherwise expire. + /// + /// [RFC 5766 §6.2]: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 + pub fn update_credentials(&mut self, username: String, password: String) { + self.username = Username::new(ATTR_USERNAME, username); + self.password = password; + self.integrity = MessageIntegrity::new_long_term_integrity( + self.username.text.clone(), + self.realm.text.clone(), + self.password.clone(), + ); + + // Each allocation carries the integrity it will sign its own Refresh / + // CreatePermission / ChannelBind with, so they have to be re-signed too — otherwise + // the next refresh would still present the retired credential. + for relay in self.relays.values_mut() { + relay.integrity = self.integrity.clone(); + } + } + + /// Refreshes every live allocation, re-signing each with the current credential. + /// + /// Each allocation is refreshed with its own current lifetime, so this extends rather + /// than changes it. Intended to follow [`update_credentials`](Self::update_credentials). + pub fn refresh_allocations(&mut self) -> Result<()> { + let relays: Vec<(RelayedAddr, Duration)> = self + .relays + .iter() + .map(|(addr, relay)| (*addr, relay.lifetime)) + .collect(); + + for (relayed_addr, lifetime) in relays { + self.relay(relayed_addr)?.refresh_allocation(lifetime)?; + } + + Ok(()) + } + /// Allocate sends a TURN allocation request to the given transport address pub fn allocate(&mut self) -> Result { let mut msg = Message::new(); diff --git a/rtc-turn/src/client/relay.rs b/rtc-turn/src/client/relay.rs index aadc6c18..638d8581 100644 --- a/rtc-turn/src/client/relay.rs +++ b/rtc-turn/src/client/relay.rs @@ -344,7 +344,7 @@ impl Relay<'_> { } } - fn refresh_allocation(&mut self, lifetime: Duration) -> Result<()> { + pub(super) fn refresh_allocation(&mut self, lifetime: Duration) -> Result<()> { let (username, realm) = (self.client.username(), self.client.realm()); if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { let mut msg = Message::new(); @@ -400,7 +400,7 @@ impl Relay<'_> { } } - fn refresh_permissions(&mut self) -> Result<()> { + pub(super) fn refresh_permissions(&mut self) -> Result<()> { if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { #[allow(clippy::map_clone)] let addrs: Vec = relay.perm_map.keys().map(|addr| *addr).collect(); From efc88871673879e02472701ca92ff16ddf0acf54 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Thu, 30 Jul 2026 21:59:24 -0700 Subject: [PATCH 21/40] bump version to v0.20.0 --- Cargo.toml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5705e806..7ec06f26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ resolver = "2" opt-level = 0 [workspace.package] -version = "0.20.0-rc.4" +version = "0.20.0" authors = ["Rain Liu "] edition = "2024" license = "MIT/Apache-2.0" @@ -32,21 +32,21 @@ keywords = ["sansio", "networking", "protocols"] categories = ["network-programming"] [workspace.dependencies] -datachannel = { version = "0.20.0-rc.4", path = "rtc-datachannel", package = "rtc-datachannel" } -dtls = { version = "0.20.0-rc.4", path = "rtc-dtls", package = "rtc-dtls", default-features = false } -ice = { version = "0.20.0-rc.4", path = "rtc-ice", package = "rtc-ice", default-features = false } -interceptor = { version = "0.20.0-rc.4", path = "rtc-interceptor", package = "rtc-interceptor" } -interceptor-derive = { version = "0.20.0-rc.4", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } -mdns = { version = "0.20.0-rc.4", path = "rtc-mdns", package = "rtc-mdns" } -media = { version = "0.20.0-rc.4", path = "rtc-media", package = "rtc-media" } -rtcp = { version = "0.20.0-rc.4", path = "rtc-rtcp", package = "rtc-rtcp" } -rtp = { version = "0.20.0-rc.4", path = "rtc-rtp", package = "rtc-rtp" } -sctp = { version = "0.20.0-rc.4", path = "rtc-sctp", package = "rtc-sctp" } -sdp = { version = "0.20.0-rc.4", path = "rtc-sdp", package = "rtc-sdp" } -shared = { version = "0.20.0-rc.4", path = "rtc-shared", package = "rtc-shared", default-features = false } -srtp = { version = "0.20.0-rc.4", path = "rtc-srtp", package = "rtc-srtp", default-features = false } -stun = { version = "0.20.0-rc.4", path = "rtc-stun", package = "rtc-stun", default-features = false } -turn = { version = "0.20.0-rc.4", path = "rtc-turn", package = "rtc-turn", default-features = false } +datachannel = { version = "0.20.0", path = "rtc-datachannel", package = "rtc-datachannel" } +dtls = { version = "0.20.0", path = "rtc-dtls", package = "rtc-dtls", default-features = false } +ice = { version = "0.20.0", path = "rtc-ice", package = "rtc-ice", default-features = false } +interceptor = { version = "0.20.0", path = "rtc-interceptor", package = "rtc-interceptor" } +interceptor-derive = { version = "0.20.0", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } +mdns = { version = "0.20.0", path = "rtc-mdns", package = "rtc-mdns" } +media = { version = "0.20.0", path = "rtc-media", package = "rtc-media" } +rtcp = { version = "0.20.0", path = "rtc-rtcp", package = "rtc-rtcp" } +rtp = { version = "0.20.0", path = "rtc-rtp", package = "rtc-rtp" } +sctp = { version = "0.20.0", path = "rtc-sctp", package = "rtc-sctp" } +sdp = { version = "0.20.0", path = "rtc-sdp", package = "rtc-sdp" } +shared = { version = "0.20.0", path = "rtc-shared", package = "rtc-shared", default-features = false } +srtp = { version = "0.20.0", path = "rtc-srtp", package = "rtc-srtp", default-features = false } +stun = { version = "0.20.0", path = "rtc-stun", package = "rtc-stun", default-features = false } +turn = { version = "0.20.0", path = "rtc-turn", package = "rtc-turn", default-features = false } # common dependencies sansio = "1" @@ -127,7 +127,7 @@ sansio.workspace = true shared.workspace = true ice.workspace = true webrtc = "0.14.0" -signal = { version = "0.20.0-rc.4", path = "examples/signal", package = "rtc-signal" } +signal = { version = "0.20.0", path = "examples/signal", package = "rtc-signal" } tokio.workspace = true env_logger.workspace = true From 892ad3b6bbcbb518381dcc0874c8b7f074118d3e Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Fri, 31 Jul 2026 11:36:10 -0700 Subject: [PATCH 22/40] dd CHANGELOG.md --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..19edf82d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- + +### Changed + +- + +### Deprecated + +- + +### Removed + +- + +### Fixed + +- + +### Security + +- + +## [0.20.0] - 2026-07-31 + +### Added + +- The `rtc` v0.20.0 is Sans-I/O protocol core with complete WebRTC stack (95%+ W3C API compliance) + +[Unreleased]: https://github.com/webrtc-rs/rtc/compare/0.20.0...HEAD + +[0.20.1]: https://github.com/webrtc-rs/rtc/compare/0.20.0...0.20.1 + +[0.20.0]: https://github.com/webrtc-rs/rtc/releases/tag/0.20.0 From db1aa0ac1b274e40d03e0f026253c1175a6bdcac Mon Sep 17 00:00:00 2001 From: dominik2m Date: Fri, 31 Jul 2026 15:02:25 -0700 Subject: [PATCH 23/40] Add RSA as an allowed private key kind in `rtc_dtls::ConfigBuilder::validate` (#141) * Allow RSA key for DTLS * Cleanup * Move related peer connection functionality out of existing DTLS test * Add integration test for RSA dtls keys * Prevent unused warning --- rtc-dtls/src/config.rs | 3 +- tests/dtls_common/mod.rs | 131 +++++++++++++++ .../dtls_disable_fingerprint_verification.rs | 130 +++------------ tests/dtls_rsa_certificate.rs | 153 ++++++++++++++++++ tests/testdata/rsa_2048_answerer_key.pem | 28 ++++ tests/testdata/rsa_2048_offerer_key.pem | 28 ++++ 6 files changed, 361 insertions(+), 112 deletions(-) create mode 100644 tests/dtls_common/mod.rs create mode 100644 tests/dtls_rsa_certificate.rs create mode 100644 tests/testdata/rsa_2048_answerer_key.pem create mode 100644 tests/testdata/rsa_2048_offerer_key.pem diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index c681745b..f1d66f6c 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -342,12 +342,13 @@ impl ConfigBuilder { return Err(Error::ErrIdentityNoPsk); } + // Gates future private key kinds from being automatically allowed. for cert in &self.certificates { match cert.private_key.kind { CryptoPrivateKeyKind::Ed25519(_) => {} CryptoPrivateKeyKind::Ecdsa256(_) => {} + CryptoPrivateKeyKind::Rsa256(_) => {} CryptoPrivateKeyKind::Custom(_) => {} - _ => return Err(Error::ErrInvalidPrivateKey), } } diff --git a/tests/dtls_common/mod.rs b/tests/dtls_common/mod.rs new file mode 100644 index 00000000..3bfc3926 --- /dev/null +++ b/tests/dtls_common/mod.rs @@ -0,0 +1,131 @@ +//! Shared helpers for DTLS related integration tests. + +use std::{ + net::SocketAddr, + time::{Duration, Instant}, +}; + +use anyhow::Result; +use bytes::BytesMut; +use rtc::peer_connection::{ + RTCPeerConnection, event::RTCPeerConnectionEvent, state::RTCPeerConnectionState, +}; +use sansio::Protocol; +use shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use tokio::net::UdpSocket; + +/// Connection timeout used during [`TestPeer::connect`], long enough for a DTLS handshake over +/// loopback, short enough to keep the negative test from dominating the suite. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +pub trait TestPeer { + /// Mutable reference to the underlying [`RTCPeerConnection`]. + fn pc(&mut self) -> &mut RTCPeerConnection; + /// Reference to the underlying UDP socket. + fn socket(&self) -> &UdpSocket; + /// The local socket address. + fn local_addr(&self) -> SocketAddr; + + /// Drives both peers until each reports `Connected`, or the timeout expires. + /// + /// Returns whether the DTLS handshake completed on both ends. + async fn connect(&mut self, answer: &mut impl TestPeer) -> Result { + let (mut offer_connected, mut answer_connected) = (false, false); + let mut offer_buf = vec![0u8; 2000]; + let mut answer_buf = vec![0u8; 2000]; + let start = Instant::now(); + + while start.elapsed() < CONNECT_TIMEOUT && !(offer_connected && answer_connected) { + while let Some(msg) = self.pc().poll_write() { + self.socket() + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = self.pc().poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + offer_connected = true; + } + } + + while let Some(msg) = answer.pc().poll_write() { + answer + .socket() + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = answer.pc().poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + answer_connected = true; + } + } + + let next_timeout = self + .pc() + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT) + .min( + answer + .pc() + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT), + ); + let delay = next_timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + + if delay.is_zero() { + self.pc().handle_timeout(Instant::now()).ok(); + answer.pc().handle_timeout(Instant::now()).ok(); + continue; + } + + let sleep = tokio::time::sleep(delay); + tokio::pin!(sleep); + tokio::select! { + _ = sleep => { + self.pc().handle_timeout(Instant::now()).ok(); + answer.pc().handle_timeout(Instant::now()).ok(); + } + Ok((n, peer_addr)) = self.socket().recv_from(&mut offer_buf) => { + let local_addr = self.local_addr(); + self.pc().handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&offer_buf[..n]), + }).ok(); + } + + Ok((n, peer_addr)) = answer.socket().recv_from(&mut answer_buf) => { + let local_addr = answer.local_addr(); + answer.pc().handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&answer_buf[..n]), + }).ok(); + } + } + } + + Ok(offer_connected && answer_connected) + } +} diff --git a/tests/dtls_disable_fingerprint_verification.rs b/tests/dtls_disable_fingerprint_verification.rs index ff69a5cd..c51cd17f 100644 --- a/tests/dtls_disable_fingerprint_verification.rs +++ b/tests/dtls_disable_fingerprint_verification.rs @@ -15,32 +15,27 @@ //! fingerprint connects, and with it left at the default the same setup still fails. use anyhow::Result; -use bytes::BytesMut; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::configuration::setting_engine::SettingEngine; -use rtc::peer_connection::event::RTCPeerConnectionEvent; -use rtc::peer_connection::state::RTCPeerConnectionState; use rtc::peer_connection::transport::{ CandidateConfig, CandidateHostConfig, RTCDtlsRole, RTCIceCandidate, }; use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; use rtc::sansio::Protocol; -use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; use std::net::SocketAddr; use std::sync::Arc; -use std::time::{Duration, Instant}; use tokio::net::UdpSocket; +use crate::dtls_common::TestPeer; + +mod dtls_common; + /// A fingerprint that matches no certificate, standing in for the placeholder a /// WebRTC-Direct server puts in the offer it synthesizes for the client. const PLACEHOLDER_FINGERPRINT: &str = "a=fingerprint:sha-256 \ FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:\ FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"; -/// Long enough for a DTLS handshake over loopback, short enough to keep the -/// negative test from dominating the suite. -const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); - struct Peer { pc: RTCPeerConnection, socket: Arc, @@ -79,6 +74,20 @@ impl Peer { } } +impl TestPeer for Peer { + fn pc(&mut self) -> &mut RTCPeerConnection { + &mut self.pc + } + + fn socket(&self) -> &UdpSocket { + &self.socket + } + + fn local_addr(&self) -> SocketAddr { + self.local_addr + } +} + /// Replaces the `a=fingerprint` line so the answerer is told to expect a /// certificate the offerer will never present. fn with_placeholder_fingerprint(sdp: &str) -> String { @@ -95,107 +104,6 @@ fn with_placeholder_fingerprint(sdp: &str) -> String { + "\r\n" } -/// Drives both peers until each reports `Connected`, or the timeout expires. -/// -/// Returns whether the DTLS handshake completed on both ends. -async fn connect(offer: &mut Peer, answer: &mut Peer) -> Result { - let (mut offer_connected, mut answer_connected) = (false, false); - let mut offer_buf = vec![0u8; 2000]; - let mut answer_buf = vec![0u8; 2000]; - let start = Instant::now(); - - while start.elapsed() < CONNECT_TIMEOUT && !(offer_connected && answer_connected) { - while let Some(msg) = offer.pc.poll_write() { - offer - .socket - .send_to(&msg.message, msg.transport.peer_addr) - .await?; - } - while let Some(event) = offer.pc.poll_event() { - if matches!( - event, - RTCPeerConnectionEvent::OnConnectionStateChangeEvent( - RTCPeerConnectionState::Connected - ) - ) { - offer_connected = true; - } - } - - while let Some(msg) = answer.pc.poll_write() { - answer - .socket - .send_to(&msg.message, msg.transport.peer_addr) - .await?; - } - while let Some(event) = answer.pc.poll_event() { - if matches!( - event, - RTCPeerConnectionEvent::OnConnectionStateChangeEvent( - RTCPeerConnectionState::Connected - ) - ) { - answer_connected = true; - } - } - - let next_timeout = offer - .pc - .poll_timeout() - .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT) - .min( - answer - .pc - .poll_timeout() - .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT), - ); - let delay = next_timeout - .saturating_duration_since(Instant::now()) - .min(Duration::from_millis(10)); - - if delay.is_zero() { - offer.pc.handle_timeout(Instant::now()).ok(); - answer.pc.handle_timeout(Instant::now()).ok(); - continue; - } - - let sleep = tokio::time::sleep(delay); - tokio::pin!(sleep); - tokio::select! { - _ = sleep => { - offer.pc.handle_timeout(Instant::now()).ok(); - answer.pc.handle_timeout(Instant::now()).ok(); - } - Ok((n, peer_addr)) = offer.socket.recv_from(&mut offer_buf) => { - offer.pc.handle_read(TaggedBytesMut { - now: Instant::now(), - transport: TransportContext { - local_addr: offer.local_addr, - peer_addr, - ecn: None, - transport_protocol: TransportProtocol::UDP, - }, - message: BytesMut::from(&offer_buf[..n]), - }).ok(); - } - Ok((n, peer_addr)) = answer.socket.recv_from(&mut answer_buf) => { - answer.pc.handle_read(TaggedBytesMut { - now: Instant::now(), - transport: TransportContext { - local_addr: answer.local_addr, - peer_addr, - ecn: None, - transport_protocol: TransportProtocol::UDP, - }, - message: BytesMut::from(&answer_buf[..n]), - }).ok(); - } - } - } - - Ok(offer_connected && answer_connected) -} - /// Runs the handshake with the answerer given a fingerprint that cannot match. /// /// `disable_verification` selects whether the answerer opts out of fingerprint @@ -226,7 +134,7 @@ async fn handshake_with_mismatched_fingerprint(disable_verification: bool) -> Re answer.pc.set_local_description(local_answer.clone())?; offer.pc.set_remote_description(local_answer)?; - let connected = connect(&mut offer, &mut answer).await?; + let connected = offer.connect(&mut answer).await?; offer.pc.close().ok(); answer.pc.close().ok(); diff --git a/tests/dtls_rsa_certificate.rs b/tests/dtls_rsa_certificate.rs new file mode 100644 index 00000000..e3cfa21b --- /dev/null +++ b/tests/dtls_rsa_certificate.rs @@ -0,0 +1,153 @@ +//! Integration coverage for RSA certificates over DTLS. +//! +//! `ConfigBuilder::validate` used to reject every private key that was not Ed25519 or ECDSA +//! P-256. This restriction has been removed with this test validating expected behavior with +//! RSA keys. +//! +//! The keys are fixtures rather than freshly generated because rcgen cannot generate RSA keys +//! under the `ring` backend. + +use anyhow::Result; +use rcgen::KeyPair; +use rtc::peer_connection::certificate::RTCCertificate; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::sansio::Protocol; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; + +use crate::dtls_common::TestPeer; + +mod dtls_common; + +const RSA_OFFERER_KEY: &str = include_str!("testdata/rsa_2048_offerer_key.pem"); +const RSA_ANSWERER_KEY: &str = include_str!("testdata/rsa_2048_answerer_key.pem"); + +/// The key type a peer authenticates with. +#[derive(Copy, Clone, Debug)] +enum KeyType { + Rsa2048(&'static str), + EcdsaP256, +} + +impl KeyType { + fn certificate(self) -> Result { + let key_pair = match self { + KeyType::Rsa2048(pem) => { + KeyPair::from_pkcs8_pem_and_sign_algo(pem, &rcgen::PKCS_RSA_SHA256)? + } + KeyType::EcdsaP256 => KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?, + }; + + Ok(RTCCertificate::from_key_pair(key_pair)?) + } +} + +struct Peer { + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, +} + +impl TestPeer for Peer { + fn pc(&mut self) -> &mut RTCPeerConnection { + &mut self.pc + } + + fn socket(&self) -> &UdpSocket { + &self.socket + } + + fn local_addr(&self) -> SocketAddr { + self.local_addr + } +} + +impl Peer { + async fn new(key_type: KeyType) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration( + RTCConfigurationBuilder::new() + .with_certificates(vec![key_type.certificate()?]) + .build(), + ) + .build()?; + + // Host candidate only: the peers talk over loopback, so no STUN is needed. + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + pc, + socket: Arc::new(socket), + local_addr, + }) + } +} + +/// Negotiates a connection between two peers with the given certificate key types. +async fn handshake_between(offer_key: KeyType, answer_key: KeyType) -> Result { + let mut offer = Peer::new(offer_key).await?; + let mut answer = Peer::new(answer_key).await?; + + // A data channel is needed for the m-line that carries the DTLS parameters. + offer.pc.create_data_channel("test", None)?; + + let local_offer = offer.pc.create_offer(None)?; + offer.pc.set_local_description(local_offer.clone())?; + answer.pc.set_remote_description(local_offer)?; + + let local_answer = answer.pc.create_answer(None)?; + offer.pc.set_remote_description(local_answer.clone())?; + answer.pc.set_local_description(local_answer)?; + + let connected = offer.connect(&mut answer).await?; + + offer.pc.close().ok(); + answer.pc.close().ok(); + + Ok(connected) +} + +/// An RSA certificate on both ends must complete the handshake. +#[tokio::test] +async fn rsa_certificates_complete_dtls_handshake() -> Result<()> { + assert!( + handshake_between( + KeyType::Rsa2048(RSA_OFFERER_KEY), + KeyType::Rsa2048(RSA_ANSWERER_KEY) + ) + .await?, + "DTLS should complete when both peers authenticate with an RSA certificate" + ); + Ok(()) +} + +/// Mixed private key case must complete the handshake. +#[tokio::test] +async fn rsa_certificate_interoperates_with_ecdsa_peer() -> Result<()> { + assert!( + handshake_between(KeyType::Rsa2048(RSA_OFFERER_KEY), KeyType::EcdsaP256).await?, + "DTLS should complete with an RSA offerer and an ECDSA answerer" + ); + assert!( + handshake_between(KeyType::EcdsaP256, KeyType::Rsa2048(RSA_ANSWERER_KEY)).await?, + "DTLS should complete with an ECDSA offerer and an RSA answerer" + ); + Ok(()) +} diff --git a/tests/testdata/rsa_2048_answerer_key.pem b/tests/testdata/rsa_2048_answerer_key.pem new file mode 100644 index 00000000..6d2a2d5f --- /dev/null +++ b/tests/testdata/rsa_2048_answerer_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCk0WjLWHTJi+I8 +jJbf80B+9R2FCxaHDYE9AYFxB+PPMa4qW/tj85SvgdcmBZGU8i8T0mmGTyd85AUQ +SaLvrvdaft2qyePbYZK8fxxHjuAwUR1m+FW59qSt0Qzw5cOZWqYRSYqYb4xpnl08 +5UauKO5y52tP9QUQiONMPxvTQPTRPJ7XHf4pJL50MDA8oEp/kE0oMpqmARgZtAvo +rp3/O81AY57EErhyTSmRir4VXv0/1V5zGaZO5JBvYd5RAOhCPqXRD6CdHbYtnJhL +6xBmFKDflhiZWbVt/jQe1f25aY5j9YQAwuInFgeXEC9h7NJhe6CQwpomVz4689PF +Pq7cQINfAgMBAAECggEAJyC5zrCYxTJKytIbjV4b3SgG0otv1S8NE/UUDEfxusIS +FWdR1ohAr6vU7mdbCO+34m5M9AA9oSYm15eLsXgpq9e7eyzDxdSzt8E7bveJ3J6d +KtFg1K2rqcIb3uLFHVaKX0dEumyc+oQIoYjSz0zTUv8CmHBUO/krcPH4gp4EoNlw +NpGps7TBjLoy6VKVERivBH0uphf+XVlZKuXu1d03JECpTjml4+xqEIyLNuaufEXk +0XdWD6YGeRuzuqeLmMFP+G7/hdIm2EnyEyfPaODgoDVzbhAsBio+8mF8bSrLJUZt +xU+HpR3EiAhHFuEm2bJ/wOuTfuBAYGiH1fH7NvS2KQKBgQDYtNx8Enw3JKF7ch2r +ZVKL7XS7LY/rc4Q883rhMxmdRJsFMtK4um3XqikA0n6w+h+hu4SMwLpFL6T32w34 +sP6PvEb10jseeR5ZfgURmzv+pmjrcEMzX7ckW6wpxOOfNseVjoxOSM6u2ktz8fzf +rbRu8iniuB3QmAf2vxLurelp9wKBgQDCs/mM3B2WJw9bpI0HzgWlCnZYUh5JtaZo +pp2ylTFVUXifxb02T8EkuL/RsJfk05psB2PgjPRUuWu3/zZRs9aUTeE98aGpis1V +d9IxGPLi0gh7TOo2XJglKr4bSgGnu2HToV+sGkedngLFJH3aH1cvD+9IVXDEaY5P +WvYmKgOX2QKBgBNJnaopVwbtarX7BSsBHKS5oL8cIggZwvZN0W0DyE7YEVgYGus4 +JJiiW33oPkBwMqqJbxW7/Zg00mEBDTBKSI3wevxcFsjyjMH4VmknwOI8W3BBEKoc +5ccFpcAd9whvrrnf8xrwnfywDzElo5Ug98M8KIA2VOkA7wyGGyBFOFYnAoGAHMgu +I7yGQyQTCSESGA1EmFkrlXLON2c66k13Emcrm8knY7b/eX/gtWT6Ni08xv7g3Qda +pF8x3zRp0BFSHlHPbqz0lwuZMk7qe6/yyn89qHoT9uYv2Ulm1Fe1MqOGCx2QIJDC +a9aWr54kGrCCSjRH1xUr8i1vNhnEh8aSjKG5VkECgYEAzyHUxK/PdxkGvz04LFLB +BjA8MJqyULH/WEPPl4aYvY2XyDpYNcgZzvyXigZ/RvZsXyB8Nj26zTozGeBpInOL +mVZfqhIl7i5IsbM7KWVbMVu7vgHP/7qn2ggkjIjnK1wsaXi3yYf7HTN36N+ciuf+ +5ugmEIxGUt5ZFnoTy8UTgsk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/rsa_2048_offerer_key.pem b/tests/testdata/rsa_2048_offerer_key.pem new file mode 100644 index 00000000..8a1cf1b3 --- /dev/null +++ b/tests/testdata/rsa_2048_offerer_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw+MSGjOm81NEW +MhYsA3a7Cdzi+sraonptStAbIXwKKeA2sb/QBSJU7D00k4MBnY/S1ciVrGeQvMbE +38p8JrvPVw7jvf9wgm+A6CVHdtjnO0MeK+zj19UV7dI62ojixxNqx5ZoXfikDtOM +5Z6u/CUJ4bJ3oDY5OOuOmYo/n9by7QEblp5I2J12UIkElh+Ng3VkZu/w8f1eRiHo +/wg/0Naqh7MeVgwYWhhiBZug+pX4ElrJa/x+BRw5llh9wScbs+v0kwMxTuiIzMhE +HeE6WbBkbZN137y9ZsFDU5jhZLxGcv1jou7xYqfBrH3bPpzbaM/qz/AfA2pVdHe0 +UXJZNyf1AgMBAAECggEABFM1bQW5RKh9f6mztFmqp4GN3heQ/kI69GbVVS8T+qTx +Wi+CkjvojeNuz3/Mvi0H8c17EdAH1pgL0jiXQ0IoaYsleFgRYReJpWsxq2Byhpwt +KhSavSrDhhNXhaFc97szyfKcxhTd2cHpq6eE9vPUl+bl11nurqapzcS02z81yp6z +rqbb0QfufF5TywinK78a0rKyY3mtF83z8kw9D3e1QF5r2AfrAK8vcGkZfOlK8mhk +3q3SaLKsWvB7emQ4ldigCM282jT3x+4vmPhOGbvtjsEpJEm28VTzIChqttHv2Rpg ++IVZccPVlyy4OEZoLqbnwP14rm5tX0R5cNjrxVIzQQKBgQD2yBbJ0UR/Cp3wKS3j +NTTGYDn7VYNBMlaXL4u2NKkNUGXHaUMykORx0IlQt1VpplX7qs4QHVwBekOJbgRa +aIjlT7QO8Z3EEQj/1Ba6fMmVad/fo6ToI2JAvT04gaSInYO2PkRyHNyi89+E2GpG +2yrY+2FGE2aNe+UdmjuW2zXvhQKBgQC3lRq9haGD+TvC/tGjzMgsMo0AaEaGqDm+ +k7M/+eCLsoT7xznloizM1UZ/jKg7jSOvqfzZs1Wps08DPZrCPyYJc9ak+vBimyWE +RTVHVxrmmqUpR8iehGdrTVAmbFOss2TnUK+io5+JCb3wRjt2RyQPWm5zSpHdl1wy +1oZICKNpsQKBgQCwOwlTDDd/BcTt6WpUk/1hIPynCFUYLOt7Qb/i2U5ULLLSKdCL +/r60rHgzBQlgziEe/MX06hJ3F6m9Lay8J2SDZVyvQ0on5wZnMz0b5dtK8PWnzkQI +ZqRWmQ1sGeC2ks2pSmQ0nXnOgJuBUc7rVL4Pf8zibx5QMUbX0fl17ItixQKBgHoB +oyLfg6c05Y3DUkodF8+fzOu/YVeux6mreY6EH8JX4199WTIO5N1AxLiSH2BsfZIK +VBvOvpiorVNHBuofk8Tmcnl0uHugBn/wiucdsagekLNtnJwU/LJoUGMozTdShjXg +/skFG0q06cGcu3nw77swa4U9wtFU/ZZf0iBfdVMRAoGAcZ2FcL4UsrBbIg6t8b/4 +K4wM5zNEt2o7kdVWLrrM0aBbWL1EEXFXLPXqhOEguSM4QL1ZCC8fxozYG0bNuiuj +y2R3K/AaeJDN/W9rM6x+Dbuj7QoOLDP9cSUf9iwaYtbrQcbDTk8SCzsyXle/bCli +L3Tuvt9w14f5auU05DpIfYQ= +-----END PRIVATE KEY----- From 4d0c50703f97fd6b9924c335493b6c7108348712 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sat, 1 Aug 2026 13:43:16 -0700 Subject: [PATCH 24/40] =?UTF-8?q?[Pre-1.0]=20G1=20=E2=80=94=20Stop=20adver?= =?UTF-8?q?tising=20ULPFEC=20by=20default=20#837?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 +- .../configuration/media_engine.rs | 74 ++++++++++++++----- tests/default_codecs_no_ulpfec.rs | 41 ++++++++++ 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 tests/default_codecs_no_ulpfec.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 19edf82d..8fc57b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- +- **`MediaEngine::register_default_codecs` no longer registers `video/ulpfec`** + ([#837](https://github.com/webrtc-rs/webrtc/issues/837)). The receive path does not recover + media from ULPFEC packets, so offering the codec invited peers to send repair packets that + could not be used. Applications that want it can still register it explicitly with + `MIME_TYPE_ULP_FEC`, which remains public. **This changes the default offer's SDP**: payload + type 116 is no longer present. ULPFEC will return to the defaults once receive-side recovery + is implemented. ### Deprecated diff --git a/src/peer_connection/configuration/media_engine.rs b/src/peer_connection/configuration/media_engine.rs index 1c301c3b..a715410c 100644 --- a/src/peer_connection/configuration/media_engine.rs +++ b/src/peer_connection/configuration/media_engine.rs @@ -403,9 +403,7 @@ impl MediaEngine { // type) fmtp parameter points at the primary codec this RTX stream // repairs. Browsers offer an RTX codec for every video codec by default // and drop RTX negotiation for any primary we do not pair one with, so - // register one RTX codec per primary video codec (mirroring pion's - // RegisterDefaultCodecs). ulpfec is a FEC codec, not a media codec, and - // does not get an RTX pairing. + // register one RTX codec per primary video codec. let rtx_codec = |payload_type: PayloadType, apt: PayloadType| RTCRtpCodecParameters { rtp_codec: RTCRtpCodec { mime_type: MIME_TYPE_RTX.to_owned(), @@ -550,16 +548,6 @@ impl MediaEngine { payload_type: 126, }, rtx_codec(107, 126), - RTCRtpCodecParameters { - rtp_codec: RTCRtpCodec { - mime_type: "video/ulpfec".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 116, - }, ] { self.register_codec(codec, RtpCodecKind::Video)?; } @@ -1166,6 +1154,61 @@ impl MediaEngine { } } +#[cfg(test)] +mod default_fec_codec_tests { + use super::*; + + // Issue #837: the default media engine must not offer ULPFEC. The receive path does + // not recover media from ULPFEC packets, so advertising it invites a peer to send + // repair packets we cannot use. `MIME_TYPE_ULP_FEC` stays exported for applications + // that register it deliberately; only the *default* registration is removed. + #[test] + fn default_codecs_do_not_offer_ulpfec() { + let mut me = MediaEngine::default(); + me.register_default_codecs().unwrap(); + + let ulpfec: Vec<_> = me + .video_codecs + .iter() + .filter(|c| { + UniCase::new(c.rtp_codec.mime_type.as_str()) == UniCase::new(MIME_TYPE_ULP_FEC) + }) + .map(|c| c.payload_type) + .collect(); + + assert!( + ulpfec.is_empty(), + "default codecs must not advertise ULPFEC while the receive path cannot \ + recover it; found payload type(s) {ulpfec:?}" + ); + } + + // The constant remains part of the public API so an application can opt in. + #[test] + fn ulpfec_remains_registerable_explicitly() { + let mut me = MediaEngine::default(); + me.register_default_codecs().unwrap(); + me.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_ULP_FEC.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + payload_type: 116, + }, + RtpCodecKind::Video, + ) + .expect("an application may still register ULPFEC itself"); + + assert!(me.video_codecs.iter().any(|c| { + UniCase::new(c.rtp_codec.mime_type.as_str()) == UniCase::new(MIME_TYPE_ULP_FEC) + })); + } +} + #[cfg(test)] mod default_rtx_codec_tests { use super::*; @@ -1186,10 +1229,7 @@ mod default_rtx_codec_tests { let apt = parse_rtx_apt(&codec.rtp_codec.sdp_fmtp_line) .expect("rtx codec must carry an apt= parameter"); rtx_apts.push(apt); - } else if UniCase::new(codec.rtp_codec.mime_type.as_str()) - != UniCase::new("video/ulpfec") - { - // ulpfec is a FEC codec and has no RTX pairing. + } else { primary_payload_types.push(codec.payload_type); } } diff --git a/tests/default_codecs_no_ulpfec.rs b/tests/default_codecs_no_ulpfec.rs new file mode 100644 index 00000000..8b0ac17b --- /dev/null +++ b/tests/default_codecs_no_ulpfec.rs @@ -0,0 +1,41 @@ +//! Issue #837 end-to-end: the generated SDP must carry no ULPFEC rtpmap. +use rtc::peer_connection::RTCPeerConnectionBuilder; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::media_engine::MediaEngine; +use rtc::rtp_transceiver::rtp_sender::RtpCodecKind; +use rtc::rtp_transceiver::{RTCRtpTransceiverDirection, RTCRtpTransceiverInit}; + +#[test] +fn default_offer_sdp_contains_no_ulpfec() { + let mut me = MediaEngine::default(); + me.register_default_codecs() + .expect("register default codecs"); + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration(RTCConfigurationBuilder::new().build()) + .with_media_engine(me) + .build() + .expect("build peer connection"); + pc.add_transceiver_from_kind( + RtpCodecKind::Video, + Some(RTCRtpTransceiverInit { + direction: RTCRtpTransceiverDirection::Recvonly, + ..Default::default() + }), + ) + .expect("add video transceiver"); + + let offer = pc.create_offer(None).expect("create offer"); + let lower = offer.sdp.to_lowercase(); + assert!( + !lower.contains("ulpfec"), + "issue #837: the default offer still advertises ULPFEC:\n{}", + offer.sdp + ); + // sanity: the offer really does carry video codecs, so the assertion above is meaningful + assert!( + lower.contains("vp8"), + "expected a video offer, got:\n{}", + offer.sdp + ); +} From 8d331c6c8b86c532ddedad15ef75b29eaefbf202 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sat, 1 Aug 2026 14:21:43 -0700 Subject: [PATCH 25/40] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc57b18..bc14f975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- +- Add RSA as an allowed private key kind in rtc_dtls::ConfigBuilder:: + validate- [PR #141](https://github.com/webrtc-rs/rtc/pull/141) ### Security From 02d2f9a22645058a4536de3cfb4ee68a6cabadf5 Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:24:36 -0700 Subject: [PATCH 26/40] =?UTF-8?q?[Pre-1.0]=20G2=20=E2=80=94=20Make=20the?= =?UTF-8?q?=20API=20extensible=20before=20freezing=20it=20#838=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 34 +++--- docs/semver.md | 107 ++++++++++++++++++ examples/broadcast/broadcast.rs | 1 + .../data-channels-close.rs | 1 + .../data-channels-create.rs | 1 + .../data-channels-flow-control.rs | 34 +++--- .../data-channels-answer.rs | 24 ++-- .../data-channels-offer.rs | 24 ++-- examples/data-channels/data-channels.rs | 1 + examples/ice-restart/ice-restart.rs | 18 +-- .../ice-tcp-active-offer.rs | 16 ++- .../ice-tcp-passive-answer.rs | 22 ++-- examples/ice-tcp/ice-tcp.rs | 28 ++--- .../mdns-query-and-gather.rs | 1 + .../play-from-disk-playlist-control.rs | 1 + examples/reflect/reflect.rs | 1 + .../rtcp-processing-boxed.rs | 1 + examples/rtcp-processing/rtcp-processing.rs | 1 + examples/rtp-forwarder/rtp-forwarder.rs | 1 + examples/rtp-to-webrtc/rtp-to-webrtc.rs | 1 + examples/save-to-disk-av1/save-to-disk-av1.rs | 1 + .../save-to-disk-h26x/save-to-disk-h26x.rs | 1 + examples/save-to-disk-vpx/save-to-disk-vpx.rs | 1 + examples/simulcast/simulcast.rs | 1 + .../simulcast_bidirection.rs | 1 + examples/stats/stats.rs | 1 + examples/swap-tracks/swap-tracks.rs | 1 + examples/trickle-ice-host/trickle-ice-host.rs | 28 ++--- .../trickle-ice-relay/trickle-ice-relay.rs | 100 ++++++++-------- .../trickle-ice-srflx/trickle-ice-srflx.rs | 28 ++--- examples/trickle-ice/trickle-ice.rs | 67 ++++++----- .../src/message/message_channel_open.rs | 1 + rtc-datachannel/src/message/message_type.rs | 1 + rtc-datachannel/src/message/mod.rs | 1 + rtc-dtls/src/cipher_suite/mod.rs | 2 + rtc-dtls/src/client_certificate_type.rs | 1 + rtc-dtls/src/compression_methods.rs | 1 + rtc-dtls/src/config.rs | 1 + rtc-dtls/src/content.rs | 2 + rtc-dtls/src/crypto/mod.rs | 1 + rtc-dtls/src/curve/mod.rs | 1 + rtc-dtls/src/curve/named_curve.rs | 1 + rtc-dtls/src/endpoint.rs | 1 + rtc-dtls/src/extension/extension_use_srtp.rs | 1 + rtc-dtls/src/extension/mod.rs | 2 + rtc-dtls/src/handshake/mod.rs | 2 + rtc-dtls/src/signature_hash_algorithm/mod.rs | 3 + rtc-ice/src/agent/agent_proto.rs | 2 + rtc-ice/src/agent/mod.rs | 1 + rtc-ice/src/candidate/candidate_pair.rs | 1 + rtc-ice/src/candidate/mod.rs | 1 + rtc-ice/src/mdns/mod.rs | 1 + rtc-ice/src/network_type/mod.rs | 1 + rtc-ice/src/state/mod.rs | 2 + rtc-ice/src/tcp_type/mod.rs | 1 + rtc-ice/src/url/mod.rs | 2 + rtc-interceptor/src/lib.rs | 1 + rtc-mdns/examples/mdns_query.rs | 1 + rtc-mdns/examples/mdns_server_query.rs | 1 + rtc-mdns/src/proto/mod.rs | 3 + rtc-mdns/tests/integration_test.rs | 1 + rtc-media/src/audio/buffer.rs | 1 + rtc-media/src/io/h26x_reader/mod.rs | 3 + rtc-media/src/io/h26x_writer/mod.rs | 2 + rtc-media/src/io/ivf_writer/mod.rs | 1 + rtc-media/src/io/ogg_reader/mod.rs | 1 + rtc-rtcp/src/extended_report/mod.rs | 1 + rtc-rtcp/src/header.rs | 1 + rtc-rtcp/src/source_description/mod.rs | 1 + rtc-rtp/src/codec/h265/mod.rs | 2 + rtc-rtp/src/extension/mod.rs | 1 + rtc-sctp/examples/sctp_e2e.rs | 1 + rtc-sctp/src/association/mod.rs | 1 + rtc-sctp/src/association/stream.rs | 2 + rtc-sctp/src/chunk/chunk_payload_data.rs | 1 + rtc-sctp/src/endpoint/mod.rs | 2 + rtc-sctp/src/lib.rs | 1 + rtc-shared/src/ifaces/ffi/windows/mod.rs | 6 + rtc-shared/src/ifaces/mod.rs | 2 + rtc-shared/src/transport.rs | 1 + rtc-srtp/src/protection_profile.rs | 1 + rtc-stun/src/agent.rs | 2 + rtc-turn/examples/turn_client_udp.rs | 1 + rtc-turn/src/client/mod.rs | 1 + src/data_channel/state.rs | 1 + src/lib.rs | 2 + src/media_stream/track_state.rs | 1 + .../configuration/bundle_policy.rs | 1 + .../configuration/ice_transport_policy.rs | 1 + .../configuration/rtcp_mux_policy.rs | 1 + .../configuration/sdp_semantics.rs | 1 + .../configuration/setting_engine.rs | 1 + .../event/data_channel_event.rs | 1 + src/peer_connection/event/mod.rs | 2 + src/peer_connection/event/track_event.rs | 1 + src/peer_connection/handler/dtls.rs | 1 + src/peer_connection/handler/ice.rs | 1 + src/peer_connection/handler/interceptor.rs | 1 + src/peer_connection/handler/mod.rs | 1 + src/peer_connection/handler/sctp.rs | 4 + src/peer_connection/handler/srtp.rs | 5 + src/peer_connection/message/internal.rs | 2 + src/peer_connection/message/mod.rs | 7 ++ src/peer_connection/sdp/sdp_type.rs | 3 + .../state/ice_connection_state.rs | 1 + .../state/ice_gathering_state.rs | 1 + .../state/peer_connection_state.rs | 1 + src/peer_connection/state/signaling_state.rs | 1 + src/peer_connection/transport/dtls/role.rs | 1 + src/peer_connection/transport/dtls/state.rs | 1 + .../transport/ice/candidate.rs | 3 + .../transport/ice/candidate_type.rs | 1 + src/peer_connection/transport/ice/protocol.rs | 1 + src/peer_connection/transport/ice/role.rs | 1 + src/peer_connection/transport/ice/state.rs | 1 + src/peer_connection/transport/sctp/state.rs | 1 + src/rtp_transceiver/direction.rs | 1 + src/rtp_transceiver/rtp_sender/rtp_codec.rs | 1 + src/statistics/accumulator/codec.rs | 1 + src/statistics/mod.rs | 1 + src/statistics/report.rs | 1 + src/statistics/stats/ice_candidate_pair.rs | 2 + src/statistics/stats/mod.rs | 2 + .../data_channels_close_by_webrtc_interop.rs | 1 + tests/data_channels_create_interop.rs | 1 + tests/data_channels_interop.rs | 1 + tests/ice_restart_by_webrtc_interop.rs | 17 +-- tests/media_only_negotiation_no_sctp.rs | 17 +-- tests/offer_answer_rtc2rtc.rs | 22 ++-- .../one_media_section_rtc_to_rtc_simulcast.rs | 1 + tests/one_media_section_rtc_to_rtc_unicast.rs | 1 + tests/reflect_rtc_to_webrtc_interop.rs | 1 + tests/reflect_webrtc_to_rtc_interop.rs | 1 + tests/rtcp_processing_boxed_interop.rs | 1 + tests/rtcp_processing_interop.rs | 8 +- tests/save_to_disk_vpx_interop.rs | 1 + tests/simulcast_rtc_to_rtc_interop.rs | 1 + tests/simulcast_webrtc_to_rtc_interop.rs | 1 + tests/statistics_rtc_to_rtc.rs | 17 +-- 139 files changed, 516 insertions(+), 262 deletions(-) create mode 100644 docs/semver.md diff --git a/Cargo.toml b/Cargo.toml index 7ec06f26..152e230c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ resolver = "2" opt-level = 0 [workspace.package] -version = "0.20.0" +version = "0.21.0" authors = ["Rain Liu "] edition = "2024" license = "MIT/Apache-2.0" @@ -32,21 +32,21 @@ keywords = ["sansio", "networking", "protocols"] categories = ["network-programming"] [workspace.dependencies] -datachannel = { version = "0.20.0", path = "rtc-datachannel", package = "rtc-datachannel" } -dtls = { version = "0.20.0", path = "rtc-dtls", package = "rtc-dtls", default-features = false } -ice = { version = "0.20.0", path = "rtc-ice", package = "rtc-ice", default-features = false } -interceptor = { version = "0.20.0", path = "rtc-interceptor", package = "rtc-interceptor" } -interceptor-derive = { version = "0.20.0", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } -mdns = { version = "0.20.0", path = "rtc-mdns", package = "rtc-mdns" } -media = { version = "0.20.0", path = "rtc-media", package = "rtc-media" } -rtcp = { version = "0.20.0", path = "rtc-rtcp", package = "rtc-rtcp" } -rtp = { version = "0.20.0", path = "rtc-rtp", package = "rtc-rtp" } -sctp = { version = "0.20.0", path = "rtc-sctp", package = "rtc-sctp" } -sdp = { version = "0.20.0", path = "rtc-sdp", package = "rtc-sdp" } -shared = { version = "0.20.0", path = "rtc-shared", package = "rtc-shared", default-features = false } -srtp = { version = "0.20.0", path = "rtc-srtp", package = "rtc-srtp", default-features = false } -stun = { version = "0.20.0", path = "rtc-stun", package = "rtc-stun", default-features = false } -turn = { version = "0.20.0", path = "rtc-turn", package = "rtc-turn", default-features = false } +datachannel = { version = "0.21.0", path = "rtc-datachannel", package = "rtc-datachannel" } +dtls = { version = "0.21.0", path = "rtc-dtls", package = "rtc-dtls", default-features = false } +ice = { version = "0.21.0", path = "rtc-ice", package = "rtc-ice", default-features = false } +interceptor = { version = "0.21.0", path = "rtc-interceptor", package = "rtc-interceptor" } +interceptor-derive = { version = "0.21.0", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } +mdns = { version = "0.21.0", path = "rtc-mdns", package = "rtc-mdns" } +media = { version = "0.21.0", path = "rtc-media", package = "rtc-media" } +rtcp = { version = "0.21.0", path = "rtc-rtcp", package = "rtc-rtcp" } +rtp = { version = "0.21.0", path = "rtc-rtp", package = "rtc-rtp" } +sctp = { version = "0.21.0", path = "rtc-sctp", package = "rtc-sctp" } +sdp = { version = "0.21.0", path = "rtc-sdp", package = "rtc-sdp" } +shared = { version = "0.21.0", path = "rtc-shared", package = "rtc-shared", default-features = false } +srtp = { version = "0.21.0", path = "rtc-srtp", package = "rtc-srtp", default-features = false } +stun = { version = "0.21.0", path = "rtc-stun", package = "rtc-stun", default-features = false } +turn = { version = "0.21.0", path = "rtc-turn", package = "rtc-turn", default-features = false } # common dependencies sansio = "1" @@ -127,7 +127,7 @@ sansio.workspace = true shared.workspace = true ice.workspace = true webrtc = "0.14.0" -signal = { version = "0.20.0", path = "examples/signal", package = "rtc-signal" } +signal = { version = "0.21.0", path = "examples/signal", package = "rtc-signal" } tokio.workspace = true env_logger.workspace = true diff --git a/docs/semver.md b/docs/semver.md new file mode 100644 index 00000000..a6fc09d5 --- /dev/null +++ b/docs/semver.md @@ -0,0 +1,107 @@ +# API stability policy + +This document records how `rtc` and the protocol subcrates keep their public API extensible, +and the specific decisions taken before 1.0. It exists because `#[non_exhaustive]` and trait +sealing **cannot be introduced after 1.0** — adding either is itself a breaking change, so +the window closes when the major version lands. + +The async wrapper keeps its own copy of this policy plus its trait decisions in +`webrtc/docs/semver.md`. + +## The rule + +> Mark a public enum `#[non_exhaustive]` when the set of variants is defined by something +> outside this codebase and can grow — an IANA registry, a protocol state machine, an error +> taxonomy, an event stream. Leave it exhaustive when the set is closed by construction — a +> fixed-width wire field, a binary role, a mathematically complete set — and where losing +> exhaustiveness checking would cost callers more than a future variant would. + +`#[non_exhaustive]` does **not** affect matches inside the defining crate, but it does affect +that crate's examples, integration tests, and benchmarks, which are separate compilation +units. It also blocks downstream struct-literal construction of a non-exhaustive variant, and +makes an irrefutable `let` destructure of a single-variant enum illegal. + +Because `rtc` re-exports all 15 protocol subcrates wholesale — + +```rust +pub use {datachannel, dtls, ice, interceptor, mdns, media, rtcp, rtp, + sansio, sctp, sdp, shared, srtp, stun, turn}; +``` + +— their entire public surface is part of `rtc`'s public API and is frozen at 1.0 alongside it. +The subcrates are in scope for this policy, not an afterthought. + +## Inventory + +122 public enums, all with an explicit decision: + +| Area | `#[non_exhaustive]` | Kept exhaustive | +|---|---:|---:| +| `rtc/src` | 34 | 0 | +| protocol subcrates | 69 | 19 | +| **total** | **103** | **19** | + +### `rtc/src` — all 34 marked + +Every directly declared enum in the core is `#[non_exhaustive]`: the W3C-facing state, +configuration, transport, statistics, event, and message enums. All of these track external +specifications (W3C WebRTC, IANA registries, RFC state machines) that add values over time. + +### Protocol subcrates — kept exhaustive (19) + +These are the deliberate exceptions. Each is closed by construction, and exhaustive matching +is worth more to callers than room for a variant we have no way to add. + +| Crate | Enum | Why it cannot grow | +|---|---|---| +| `rtc-dtls` | `ExtendedMasterSecretType` | Request / Require / Disable — a complete policy triple. | +| `rtc-dtls` | `CryptoCcmTagLen` | CCM tag is 8 or 16 bytes. | +| `rtc-dtls` | `DtlsPadding` | Padding scheme marker. | +| `rtc-ice` | `Role` | Controlling / Controlled (RFC 8445). | +| `rtc-shared` | `EcnCodepoint` | Exactly four values in a two-bit field (RFC 3168). | +| `rtc-rtcp` | `ChunkType` | One-bit RLE discriminator. | +| `rtc-rtcp` | `TTLorHopLimitType` | Two-bit field. | +| `rtc-rtcp` | `StatusChunkTypeTcc` | One-bit field. | +| `rtc-rtcp` | `SymbolTypeTcc` | Two-bit field. | +| `rtc-rtcp` | `SymbolSizeTypeTcc` | One-bit field. | +| `rtc-rtcp` | `PacketStatusChunk` | Closed by its one-bit discriminator. | +| `rtc-media` | `Deinterleaved`, `Interleaved` | Buffer-layout markers. | +| `rtc-sctp` | `Side` | Client / Server. | +| `rtc-datachannel` | `DataChannelThreshold` | Low / High. | +| `rtc-rtp` | `CameraDirection` | Front / Back. | +| `rtc-rtp` | `VideoRotation` | 0° / 90° / 180° / 270°. | +| `rtc-sdp` | `Direction` | sendrecv / sendonly / recvonly / inactive (RFC 4566). | +| `rtc-sdp` | `ConnectionRole` | active / passive / actpass / holdconn (RFC 4145). | + +### Protocol subcrates — marked (69) + +Everything else, falling into four families: + +- **IANA / protocol registries**, which grow by design: `CipherSuiteId`, `NamedCurve`, + `SignatureScheme`, `HashAlgorithm`, `SignatureAlgorithm`, `SrtpProtectionProfile`, + `ExtensionValue`, `HandshakeType`, `ContentType`, `ClientCertificateType`, + `CompressionMethodId`, `EllipticCurveType`, `PacketType`, `SdesType`, `BlockType`, + `PayloadProtocolIdentifier`, `MessageType`, `ChannelType`, `ProtectionProfile`, + `H264NalUnitType`, `H265NalUnitType`, `UnitType`, `CandidateType`, `NetworkType`, + `TcpType`, `SchemeType`, `ProtoType`, … +- **State machines**: `ConnectionState`, `GatheringState`, `CandidatePairState`, + `RecvSendState`, `ReliabilityType`, … +- **Event streams**: `Event` (ice, turn, sctp), `EndpointEvent`, `DatagramEvent`, + `StunEvent`, `ClientAgent`, `MdnsEvent`, `StreamEvent`, … +- **Error taxonomies**: `Error` (shared, media), `AssociationError`, `ConnectError`. + +Also marked: wire sum types that gain arms when the protocol does — `Content`, +`HandshakeMessage`, `Extension`, `Message`, `Payload`, `H265Payload`, `HeaderExtension`, +`H26xNAL`, `Packet`, `CryptoPrivateKeyKind`, `NextHop`, `Kind`, `TransportProtocol`, +`IvfCodec`, `OggHeaderType`, `MulticastDnsMode`, `ClientAuthType`, `CipherSuiteHash`, and the +Windows interface-info enums in `rtc-shared::ifaces`. + +## When adding a new public item + +- **New public enum**: decide exhaustive-or-not *at the point of introduction* and record it + here. After 1.0 the decision is frozen. +- **New variant on a `#[non_exhaustive]` enum**: minor release, no breakage. +- **New variant on an exhaustive enum**: major release. Reconsider the classification first. +- Matching a `#[non_exhaustive]` enum from another crate in this workspace — including from + `rtc` onto a subcrate enum — requires a `_` arm. Prefer a fallback that degrades safely + (skip the packet, return `Unspecified`) over `unreachable!()`. diff --git a/examples/broadcast/broadcast.rs b/examples/broadcast/broadcast.rs index b016164b..1d52b053 100644 --- a/examples/broadcast/broadcast.rs +++ b/examples/broadcast/broadcast.rs @@ -326,6 +326,7 @@ async fn run_broadcaster( trace!("[Receiver] Received RTCP packets"); } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/data-channels-close/data-channels-close.rs b/examples/data-channels-close/data-channels-close.rs index 2527054a..88ff645b 100644 --- a/examples/data-channels-close/data-channels-close.rs +++ b/examples/data-channels-close/data-channels-close.rs @@ -280,6 +280,7 @@ async fn run( let msg_str = String::from_utf8(data_channel_message.data.to_vec())?; println!("Message from DataChannel '{}': '{}'", dc.label(), msg_str); } + _ => {} } } diff --git a/examples/data-channels-create/data-channels-create.rs b/examples/data-channels-create/data-channels-create.rs index 70921544..ccec57a0 100644 --- a/examples/data-channels-create/data-channels-create.rs +++ b/examples/data-channels-create/data-channels-create.rs @@ -271,6 +271,7 @@ async fn run( let msg_str = String::from_utf8(data_channel_message.data.to_vec())?; println!("Message from DataChannel '{}': '{}'", dc.label(), msg_str); } + _ => {} } } diff --git a/examples/data-channels-flow-control/data-channels-flow-control.rs b/examples/data-channels-flow-control/data-channels-flow-control.rs index 25370bc3..59e3144b 100644 --- a/examples/data-channels-flow-control/data-channels-flow-control.rs +++ b/examples/data-channels-flow-control/data-channels-flow-control.rs @@ -200,11 +200,11 @@ async fn run_requester( // Poll requester events while let Some(event) = requester.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Failed { - eprintln!("Requester peer connection failed"); - break 'EventLoop; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Failed => + { + eprintln!("Requester peer connection failed"); + break 'EventLoop; } RTCPeerConnectionEvent::OnDataChannel(data_channel_event) => { match data_channel_event { @@ -358,21 +358,16 @@ async fn run_responder( // Poll responder events while let Some(event) = responder.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Failed { - eprintln!("Responder peer connection failed"); - break 'EventLoop; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Failed => + { + eprintln!("Responder peer connection failed"); + break 'EventLoop; } - RTCPeerConnectionEvent::OnDataChannel(data_channel_event) => { - match data_channel_event { - RTCDataChannelEvent::OnOpen(_channel_id) => { - println!("Responder: Data channel opened"); - resp_data_channel_opened = true; - throughput_start = Instant::now(); - } - _ => {} - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(_channel_id)) => { + println!("Responder: Data channel opened"); + resp_data_channel_opened = true; + throughput_start = Instant::now(); } _ => {} } @@ -385,6 +380,7 @@ async fn run_responder( RTCMessage::DataChannelMessage(_channel_id, data_channel_message) => { total_bytes_received += data_channel_message.data.len(); } + _ => {} } } diff --git a/examples/data-channels-offer-answer/data-channels-answer.rs b/examples/data-channels-offer-answer/data-channels-answer.rs index 2cca7596..4a8b20ce 100644 --- a/examples/data-channels-offer-answer/data-channels-answer.rs +++ b/examples/data-channels-offer-answer/data-channels-answer.rs @@ -233,20 +233,17 @@ async fn main() -> Result<()> { break 'EventLoop; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!( - "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", - dc.label(), - dc.id() - ); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!( + "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", + dc.label(), + dc.id() + ); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -260,6 +257,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/data-channels-offer-answer/data-channels-offer.rs b/examples/data-channels-offer-answer/data-channels-offer.rs index b66034a3..32b04627 100644 --- a/examples/data-channels-offer-answer/data-channels-offer.rs +++ b/examples/data-channels-offer-answer/data-channels-offer.rs @@ -257,20 +257,17 @@ async fn main() -> Result<()> { break 'EventLoop; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!( - "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", - dc.label(), - dc.id() - ); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!( + "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", + dc.label(), + dc.id() + ); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -284,6 +281,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/data-channels/data-channels.rs b/examples/data-channels/data-channels.rs index 10d7af37..894bce8d 100644 --- a/examples/data-channels/data-channels.rs +++ b/examples/data-channels/data-channels.rs @@ -263,6 +263,7 @@ async fn run( ); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/examples/ice-restart/ice-restart.rs b/examples/ice-restart/ice-restart.rs index 89b5a585..96dfeeff 100644 --- a/examples/ice-restart/ice-restart.rs +++ b/examples/ice-restart/ice-restart.rs @@ -249,16 +249,15 @@ async fn main() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!("Data channel '{}'-'{}' open", dc.label(), dc.id()); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!("Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -272,6 +271,7 @@ async fn main() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs b/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs index e771a306..f330fcb8 100644 --- a/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs +++ b/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs @@ -319,16 +319,13 @@ async fn main() -> Result<()> { println!("[Offer] Connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!("[Offer] Data channel '{}'-'{}' open", dc.label(), dc.id()); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!("[Offer] Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -343,6 +340,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("[Offer] Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs b/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs index 94bd158a..0eb580e9 100644 --- a/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs +++ b/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs @@ -238,20 +238,15 @@ async fn main() -> Result<()> { println!("[Answer] Connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "[Answer] Data channel '{}'-'{}' open", - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!("[Answer] Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -266,6 +261,7 @@ async fn main() -> Result<()> { .unwrap_or_default(); println!("[Answer] Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp/ice-tcp.rs b/examples/ice-tcp/ice-tcp.rs index 1c0970d4..3cbde756 100644 --- a/examples/ice-tcp/ice-tcp.rs +++ b/examples/ice-tcp/ice-tcp.rs @@ -231,21 +231,20 @@ async fn run_main_loop( println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -260,6 +259,7 @@ async fn run_main_loop( .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/mdns-query-and-gather/mdns-query-and-gather.rs b/examples/mdns-query-and-gather/mdns-query-and-gather.rs index dbb113f1..55052cc4 100644 --- a/examples/mdns-query-and-gather/mdns-query-and-gather.rs +++ b/examples/mdns-query-and-gather/mdns-query-and-gather.rs @@ -311,6 +311,7 @@ async fn run( ); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs b/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs index 4f99100a..a98fa482 100644 --- a/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs +++ b/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs @@ -286,6 +286,7 @@ fn parse_playlist(path: &str) -> Result> { } continue; } + _ => {} } } diff --git a/examples/reflect/reflect.rs b/examples/reflect/reflect.rs index 57596b4c..c1eeeacf 100644 --- a/examples/reflect/reflect.rs +++ b/examples/reflect/reflect.rs @@ -426,6 +426,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs index da526810..0bbe66d3 100644 --- a/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs +++ b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs @@ -393,6 +393,7 @@ impl RtcpSession { println!(); } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } } diff --git a/examples/rtcp-processing/rtcp-processing.rs b/examples/rtcp-processing/rtcp-processing.rs index 101ed679..5f82f815 100644 --- a/examples/rtcp-processing/rtcp-processing.rs +++ b/examples/rtcp-processing/rtcp-processing.rs @@ -391,6 +391,7 @@ async fn run(input_sdp_file: String) -> Result<()> { println!(); } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtp-forwarder/rtp-forwarder.rs b/examples/rtp-forwarder/rtp-forwarder.rs index db9e6299..5560ce7f 100644 --- a/examples/rtp-forwarder/rtp-forwarder.rs +++ b/examples/rtp-forwarder/rtp-forwarder.rs @@ -314,6 +314,7 @@ async fn run_peer_connection( trace!("Received RTCP packets"); } rtc::peer_connection::message::RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtp-to-webrtc/rtp-to-webrtc.rs b/examples/rtp-to-webrtc/rtp-to-webrtc.rs index 41b8f141..7299d44b 100644 --- a/examples/rtp-to-webrtc/rtp-to-webrtc.rs +++ b/examples/rtp-to-webrtc/rtp-to-webrtc.rs @@ -245,6 +245,7 @@ async fn run_peer_connection(offer: RTCSessionDescription, rtp_listener: UdpSock trace!("Received RTCP packets"); } rtc::peer_connection::message::RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-av1/save-to-disk-av1.rs b/examples/save-to-disk-av1/save-to-disk-av1.rs index c265efae..3f15aca5 100644 --- a/examples/save-to-disk-av1/save-to-disk-av1.rs +++ b/examples/save-to-disk-av1/save-to-disk-av1.rs @@ -354,6 +354,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-h26x/save-to-disk-h26x.rs b/examples/save-to-disk-h26x/save-to-disk-h26x.rs index 18894a0e..16dfb535 100644 --- a/examples/save-to-disk-h26x/save-to-disk-h26x.rs +++ b/examples/save-to-disk-h26x/save-to-disk-h26x.rs @@ -419,6 +419,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-vpx/save-to-disk-vpx.rs b/examples/save-to-disk-vpx/save-to-disk-vpx.rs index 3fd375f6..c1b2f1d2 100644 --- a/examples/save-to-disk-vpx/save-to-disk-vpx.rs +++ b/examples/save-to-disk-vpx/save-to-disk-vpx.rs @@ -430,6 +430,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/simulcast/simulcast.rs b/examples/simulcast/simulcast.rs index 96f1afc1..7dbae51c 100644 --- a/examples/simulcast/simulcast.rs +++ b/examples/simulcast/simulcast.rs @@ -408,6 +408,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/simulcast_bidirection/simulcast_bidirection.rs b/examples/simulcast_bidirection/simulcast_bidirection.rs index e4ef2453..389e3da6 100644 --- a/examples/simulcast_bidirection/simulcast_bidirection.rs +++ b/examples/simulcast_bidirection/simulcast_bidirection.rs @@ -400,6 +400,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/stats/stats.rs b/examples/stats/stats.rs index 31ea6dcc..cbdaf362 100644 --- a/examples/stats/stats.rs +++ b/examples/stats/stats.rs @@ -318,6 +318,7 @@ async fn run( // Read incoming RTCP packets } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/swap-tracks/swap-tracks.rs b/examples/swap-tracks/swap-tracks.rs index 5a96619a..c4d0335f 100644 --- a/examples/swap-tracks/swap-tracks.rs +++ b/examples/swap-tracks/swap-tracks.rs @@ -447,6 +447,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/trickle-ice-host/trickle-ice-host.rs b/examples/trickle-ice-host/trickle-ice-host.rs index ccd79a10..358bbbcb 100644 --- a/examples/trickle-ice-host/trickle-ice-host.rs +++ b/examples/trickle-ice-host/trickle-ice-host.rs @@ -186,21 +186,20 @@ async fn run_main_loop() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -215,6 +214,7 @@ async fn run_main_loop() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice-relay/trickle-ice-relay.rs b/examples/trickle-ice-relay/trickle-ice-relay.rs index 33ca5c07..08867c97 100644 --- a/examples/trickle-ice-relay/trickle-ice-relay.rs +++ b/examples/trickle-ice-relay/trickle-ice-relay.rs @@ -225,44 +225,42 @@ async fn run_main_loop( Event::TransactionTimeout(_) => { error!("TURN transaction timeout"); } - Event::AllocateResponse(tid, addr) => { - if tid == allocate_tid { - println!("TURN allocation successful, relay address: {}", addr); - relay_addr = Some(addr); - - // If peer connection already exists and we haven't added the relay candidate yet, add it now - if let Some(pc) = peer_connection.as_mut() { - if !relay_candidate_added { - match add_relay_candidate(pc, addr, relay_local_addr) { - Ok(local_candidate_init) => { - relay_candidate_added = true; - println!( - "Added local Relay ICE candidate: {}", - local_candidate_init.candidate - ); - - // Send to browser via WebSocket - if let Some(ref mut ws) = ws_stream { - if let Ok(json) = - serde_json::to_string(&local_candidate_init) + Event::AllocateResponse(tid, addr) if tid == allocate_tid => { + println!("TURN allocation successful, relay address: {}", addr); + relay_addr = Some(addr); + + // If peer connection already exists and we haven't added the relay candidate yet, add it now + if let Some(pc) = peer_connection.as_mut() { + if !relay_candidate_added { + match add_relay_candidate(pc, addr, relay_local_addr) { + Ok(local_candidate_init) => { + relay_candidate_added = true; + println!( + "Added local Relay ICE candidate: {}", + local_candidate_init.candidate + ); + + // Send to browser via WebSocket + if let Some(ref mut ws) = ws_stream { + if let Ok(json) = + serde_json::to_string(&local_candidate_init) + { + info!( + "Sending local ICE candidate: {}", + local_candidate_init.candidate + ); + if let Err(e) = + ws.send(Message::Text(json.into())).await { - info!( - "Sending local ICE candidate: {}", - local_candidate_init.candidate + error!( + "Failed to send relay candidate to browser: {}", + e ); - if let Err(e) = - ws.send(Message::Text(json.into())).await - { - error!( - "Failed to send relay candidate to browser: {}", - e - ); - } } } } - Err(e) => error!("Failed to add relay candidate: {}", e), } + Err(e) => error!("Failed to add relay candidate: {}", e), } } } @@ -270,11 +268,11 @@ async fn run_main_loop( Event::AllocateError(_, err) => { error!("TURN allocation error: {}", err); } - Event::CreatePermissionResponse(tid, peer_addr) => { - if pending_permissions.remove(&tid).is_some() { - println!("CreatePermission for peer addr {} is granted", peer_addr); - granted_permissions.insert(peer_addr); - } + Event::CreatePermissionResponse(tid, peer_addr) + if pending_permissions.remove(&tid).is_some() => + { + println!("CreatePermission for peer addr {} is granted", peer_addr); + granted_permissions.insert(peer_addr); } Event::CreatePermissionError(_, err) => { error!("CreatePermission error: {}", err); @@ -349,21 +347,20 @@ async fn run_main_loop( println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -378,6 +375,7 @@ async fn run_main_loop( .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice-srflx/trickle-ice-srflx.rs b/examples/trickle-ice-srflx/trickle-ice-srflx.rs index d4e4673b..261d9424 100644 --- a/examples/trickle-ice-srflx/trickle-ice-srflx.rs +++ b/examples/trickle-ice-srflx/trickle-ice-srflx.rs @@ -193,21 +193,20 @@ async fn run_main_loop() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -222,6 +221,7 @@ async fn run_main_loop() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice/trickle-ice.rs b/examples/trickle-ice/trickle-ice.rs index 9e18c8a3..877fcec0 100644 --- a/examples/trickle-ice/trickle-ice.rs +++ b/examples/trickle-ice/trickle-ice.rs @@ -387,22 +387,19 @@ async fn run_main_loop(cli: Cli) -> Result<()> { TurnEvent::TransactionTimeout(_) => { error!("TURN transaction timeout"); } - TurnEvent::AllocateResponse(tid, addr) => { - if Some(tid) == allocate_tid { - println!("TURN allocation successful, relay address: {}", addr); - relay_addr = Some(addr); - - // Add relay candidate if peer connection exists - if let Some(pc) = peer_connection.as_mut() { - if !relay_candidate_added { - if let Err(e) = - add_relay_candidate(pc, addr, local_addr, &mut ws_stream) - .await - { - error!("Failed to add relay candidate: {}", e); - } else { - relay_candidate_added = true; - } + TurnEvent::AllocateResponse(tid, addr) if Some(tid) == allocate_tid => { + println!("TURN allocation successful, relay address: {}", addr); + relay_addr = Some(addr); + + // Add relay candidate if peer connection exists + if let Some(pc) = peer_connection.as_mut() { + if !relay_candidate_added { + if let Err(e) = + add_relay_candidate(pc, addr, local_addr, &mut ws_stream).await + { + error!("Failed to add relay candidate: {}", e); + } else { + relay_candidate_added = true; } } } @@ -410,11 +407,11 @@ async fn run_main_loop(cli: Cli) -> Result<()> { TurnEvent::AllocateError(_, err) => { error!("TURN allocation error: {}", err); } - TurnEvent::CreatePermissionResponse(tid, peer_addr) => { - if pending_permissions.remove(&tid).is_some() { - println!("CreatePermission for peer addr {} is granted", peer_addr); - granted_permissions.insert(peer_addr); - } + TurnEvent::CreatePermissionResponse(tid, peer_addr) + if pending_permissions.remove(&tid).is_some() => + { + println!("CreatePermission for peer addr {} is granted", peer_addr); + granted_permissions.insert(peer_addr); } TurnEvent::CreatePermissionError(_, err) => { error!("CreatePermission error: {}", err); @@ -498,21 +495,20 @@ async fn run_main_loop(cli: Cli) -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -527,6 +523,7 @@ async fn run_main_loop(cli: Cli) -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/rtc-datachannel/src/message/message_channel_open.rs b/rtc-datachannel/src/message/message_channel_open.rs index 7292913d..d628ffa2 100644 --- a/rtc-datachannel/src/message/message_channel_open.rs +++ b/rtc-datachannel/src/message/message_channel_open.rs @@ -25,6 +25,7 @@ pub const CHANNEL_PRIORITY_EXTRA_HIGH: u16 = 1024; /// supplies the retransmission count or lifetime for the partial-reliability variants. /// /// [RFC 8832]: https://datatracker.ietf.org/doc/html/rfc8832 +#[non_exhaustive] pub enum ChannelType { /// Reliable, in-order delivery — the SCTP default, and what `RTCDataChannel` gives you /// unless you ask otherwise. diff --git a/rtc-datachannel/src/message/message_type.rs b/rtc-datachannel/src/message/message_type.rs index bcb692ec..4ecdc2e7 100644 --- a/rtc-datachannel/src/message/message_type.rs +++ b/rtc-datachannel/src/message/message_type.rs @@ -10,6 +10,7 @@ pub(crate) const MESSAGE_TYPE_LEN: usize = 1; /// The one-byte type that prefixes a DCEP message. #[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[non_exhaustive] pub enum MessageType { /// A buffered-amount threshold crossing. Internal to this crate. DataChannelThreshold, // internal usage only diff --git a/rtc-datachannel/src/message/mod.rs b/rtc-datachannel/src/message/mod.rs index fc465325..d655ce76 100644 --- a/rtc-datachannel/src/message/mod.rs +++ b/rtc-datachannel/src/message/mod.rs @@ -23,6 +23,7 @@ use shared::marshal::*; /// A parsed DataChannel message #[derive(Eq, PartialEq, Clone, Debug)] +#[non_exhaustive] pub enum Message { /// A buffered-amount threshold crossing. Internal to this crate — not a DCEP message. DataChannelThreshold(DataChannelThreshold), // internal usage only diff --git a/rtc-dtls/src/cipher_suite/mod.rs b/rtc-dtls/src/cipher_suite/mod.rs index 6552ee82..0183644d 100644 --- a/rtc-dtls/src/cipher_suite/mod.rs +++ b/rtc-dtls/src/cipher_suite/mod.rs @@ -38,6 +38,7 @@ use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*; #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// The cipher suites this crate can negotiate, by their IANA code points. +#[non_exhaustive] pub enum CipherSuiteId { // AES-128-CCM /// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` (`0xc0ac`). @@ -175,6 +176,7 @@ impl From<&str> for CipherSuiteId { #[derive(Copy, Clone, Debug)] /// The hash a suite uses in its PRF and `Finished` computation. +#[non_exhaustive] pub enum CipherSuiteHash { /// SHA-256. Sha256, diff --git a/rtc-dtls/src/client_certificate_type.rs b/rtc-dtls/src/client_certificate_type.rs index 567d3dcf..6b064fb7 100644 --- a/rtc-dtls/src/client_certificate_type.rs +++ b/rtc-dtls/src/client_certificate_type.rs @@ -1,5 +1,6 @@ #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// The certificate types a server may request from a client. +#[non_exhaustive] pub enum ClientCertificateType { /// `RSA_SIGN` (`1`). RsaSign = 1, diff --git a/rtc-dtls/src/compression_methods.rs b/rtc-dtls/src/compression_methods.rs index 7a5dee9f..f6c8b8e6 100644 --- a/rtc-dtls/src/compression_methods.rs +++ b/rtc-dtls/src/compression_methods.rs @@ -5,6 +5,7 @@ use std::io::{Read, Write}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// Compression methods. DTLS in WebRTC always negotiates `Null`. +#[non_exhaustive] pub enum CompressionMethodId { /// `NULL` (`0`). Null = 0, diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index f1d66f6c..f3def5be 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -296,6 +296,7 @@ pub(crate) type PskCallback = Arc Result>) + Send + Sy /// ClientAuthType declares the policy the server will follow for /// TLS Client Authentication. #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ClientAuthType { #[default] /// `NO_CLIENT_CERT` (`0`). diff --git a/rtc-dtls/src/content.rs b/rtc-dtls/src/content.rs index 52194d4a..5567f749 100644 --- a/rtc-dtls/src/content.rs +++ b/rtc-dtls/src/content.rs @@ -12,6 +12,7 @@ use shared::error::*; /// /// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] +#[non_exhaustive] pub enum ContentType { /// `CHANGE_CIPHER_SPEC` (`20`). ChangeCipherSpec = 20, @@ -40,6 +41,7 @@ impl From for ContentType { #[derive(PartialEq, Debug, Clone)] /// The parsed body of a DTLS record. +#[non_exhaustive] pub enum Content { /// A ChangeCipherSpec record. ChangeCipherSpec(ChangeCipherSpec), diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index 02474fa0..2642e3c5 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -167,6 +167,7 @@ pub trait CustomSigner: Send + Sync + std::fmt::Debug { /// Either ED25519, ECDSA, RSA keypair, or a custom external signer. #[derive(Debug)] +#[non_exhaustive] pub enum CryptoPrivateKeyKind { /// An Ed25519 key pair. Ed25519(Ed25519KeyPair), diff --git a/rtc-dtls/src/curve/mod.rs b/rtc-dtls/src/curve/mod.rs index 8245bc35..5dafb80a 100644 --- a/rtc-dtls/src/curve/mod.rs +++ b/rtc-dtls/src/curve/mod.rs @@ -4,6 +4,7 @@ pub mod named_curve; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10 #[derive(Copy, Clone, PartialEq, Eq, Debug)] /// How an elliptic curve is identified in a key exchange — by name, or explicitly. +#[non_exhaustive] pub enum EllipticCurveType { /// `NAMED_CURVE` (`0x03`). NamedCurve = 0x03, diff --git a/rtc-dtls/src/curve/named_curve.rs b/rtc-dtls/src/curve/named_curve.rs index b12cff6e..322476e6 100644 --- a/rtc-dtls/src/curve/named_curve.rs +++ b/rtc-dtls/src/curve/named_curve.rs @@ -6,6 +6,7 @@ use shared::error::*; #[repr(u16)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] /// The named elliptic curves this crate can perform ECDHE over. +#[non_exhaustive] pub enum NamedCurve { /// `UNSUPPORTED` (`0x0000`). Unsupported = 0x0000, diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index 0c03de9f..a6b52adc 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use std::time::Instant; /// What the endpoint reports to its caller. +#[non_exhaustive] pub enum EndpointEvent { /// The handshake finished; application data may now be sent, and SRTP keys can be exported. HandshakeComplete, diff --git a/rtc-dtls/src/extension/extension_use_srtp.rs b/rtc-dtls/src/extension/extension_use_srtp.rs index af7ca060..eeb5fa88 100644 --- a/rtc-dtls/src/extension/extension_use_srtp.rs +++ b/rtc-dtls/src/extension/extension_use_srtp.rs @@ -11,6 +11,7 @@ use super::*; /// [RFC 5764 §4.1.2]: https://tools.ietf.org/html/rfc5764#section-4.1.2 #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum SrtpProtectionProfile { /// `SRTP_AES128_CM_HMAC_SHA1_80` (`0x0001`). Srtp_Aes128_Cm_Hmac_Sha1_80 = 0x0001, diff --git a/rtc-dtls/src/extension/mod.rs b/rtc-dtls/src/extension/mod.rs index 8a981178..6d5fe678 100644 --- a/rtc-dtls/src/extension/mod.rs +++ b/rtc-dtls/src/extension/mod.rs @@ -31,6 +31,7 @@ use std::io::{Read, Write}; // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml #[derive(Clone, Debug, PartialEq, Eq)] /// The extension type code points this crate understands. +#[non_exhaustive] pub enum ExtensionValue { /// `SERVER_NAME` (`0`). ServerName = 0, @@ -67,6 +68,7 @@ impl From for ExtensionValue { #[derive(PartialEq, Eq, Debug, Clone)] /// A parsed hello extension. +#[non_exhaustive] pub enum Extension { /// Server Name Indication. ServerName(ExtensionServerName), diff --git a/rtc-dtls/src/handshake/mod.rs b/rtc-dtls/src/handshake/mod.rs index 2aa52995..22ed57e5 100644 --- a/rtc-dtls/src/handshake/mod.rs +++ b/rtc-dtls/src/handshake/mod.rs @@ -65,6 +65,7 @@ use handshake_message_server_key_exchange::*; /// /// [RFC 5246 §7.4]: https://tools.ietf.org/html/rfc5246#section-7.4 #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum HandshakeType { /// `HELLO_REQUEST` (`0`). HelloRequest = 0, @@ -133,6 +134,7 @@ impl From for HandshakeType { #[derive(PartialEq, Debug, Clone)] /// A parsed handshake message. +#[non_exhaustive] pub enum HandshakeMessage { //HelloRequest(errNotImplemented), /// ClientHello, which opens the handshake. diff --git a/rtc-dtls/src/signature_hash_algorithm/mod.rs b/rtc-dtls/src/signature_hash_algorithm/mod.rs index cf879a71..1fc13565 100644 --- a/rtc-dtls/src/signature_hash_algorithm/mod.rs +++ b/rtc-dtls/src/signature_hash_algorithm/mod.rs @@ -11,6 +11,7 @@ use shared::error::*; // Supported hash hash algorithms #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// The hash algorithms that may be paired with a signature algorithm. +#[non_exhaustive] pub enum HashAlgorithm { /// `MD2` (`0`). Md2 = 0, // Blacklisted @@ -80,6 +81,7 @@ impl HashAlgorithm { // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-16 #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// The signature algorithms this crate can verify and produce. +#[non_exhaustive] pub enum SignatureAlgorithm { /// `RSA` (`1`). Rsa = 1, @@ -174,6 +176,7 @@ pub(crate) fn select_signature_scheme( // RFC 8446, Section 4.2.3. #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// A TLS signature scheme, which names a signature and hash together ([RFC 8446] §4.2.3). +#[non_exhaustive] pub enum SignatureScheme { // RSASSA-PKCS1-v1_5 algorithms. /// `PKCS1_WITH_SHA256` (`0x0401`). diff --git a/rtc-ice/src/agent/agent_proto.rs b/rtc-ice/src/agent/agent_proto.rs index c85ef9d4..f90c40f0 100644 --- a/rtc-ice/src/agent/agent_proto.rs +++ b/rtc-ice/src/agent/agent_proto.rs @@ -36,6 +36,7 @@ impl sansio::Protocol for Agent { error!("mDNS Query {} timed out for {}", id, c.address()); } } + _ => {} } } remote_candidates @@ -113,6 +114,7 @@ impl sansio::Protocol for Agent { error!("mDNS Query {} timed out for {}", id, c.address()); } } + _ => {} } } remote_candidates diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index 24a973e4..8fa3b4a0 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -104,6 +104,7 @@ fn assert_inbound_message_integrity(m: &mut Message, key: &[u8]) -> Result<()> { } /// What the agent reports to its caller. +#[non_exhaustive] pub enum Event { /// The agent's connection state changed. ConnectionStateChange(ConnectionState), diff --git a/rtc-ice/src/candidate/candidate_pair.rs b/rtc-ice/src/candidate/candidate_pair.rs index 306a98db..f224f0f3 100644 --- a/rtc-ice/src/candidate/candidate_pair.rs +++ b/rtc-ice/src/candidate/candidate_pair.rs @@ -13,6 +13,7 @@ use std::time::Duration; /// Represent the ICE candidate pair state. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum CandidatePairState { #[default] #[serde(rename = "unspecified")] diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index b5a8c585..ff278822 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -59,6 +59,7 @@ pub(crate) const COMPONENT_RTCP: u16 = 0; /// Represents the type of candidate `CandidateType` enum. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum CandidateType { #[default] #[serde(rename = "unspecified")] diff --git a/rtc-ice/src/mdns/mod.rs b/rtc-ice/src/mdns/mod.rs index 591158ac..e6ddb187 100644 --- a/rtc-ice/src/mdns/mod.rs +++ b/rtc-ice/src/mdns/mod.rs @@ -11,6 +11,7 @@ use shared::error::Result; /// Represents the different Multicast modes that ICE can run. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum MulticastDnsMode { /// Means remote mDNS candidates will be discarded, and local host candidates will use IPs. Disabled, diff --git a/rtc-ice/src/network_type/mod.rs b/rtc-ice/src/network_type/mod.rs index 5f3069a2..d598396f 100644 --- a/rtc-ice/src/network_type/mod.rs +++ b/rtc-ice/src/network_type/mod.rs @@ -30,6 +30,7 @@ pub fn supported_network_types() -> Vec { /// Represents the type of network. #[derive(Default, PartialEq, Debug, Copy, Clone, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] pub enum NetworkType { #[serde(rename = "unspecified")] #[default] diff --git a/rtc-ice/src/state/mod.rs b/rtc-ice/src/state/mod.rs index aa3c0d95..6433bb7c 100644 --- a/rtc-ice/src/state/mod.rs +++ b/rtc-ice/src/state/mod.rs @@ -5,6 +5,7 @@ use std::fmt; /// An enum showing the state of a ICE Connection List of supported States. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ConnectionState { /// No state was set. #[default] @@ -65,6 +66,7 @@ impl From for ConnectionState { /// Describes the state of the candidate gathering process. #[derive(Default, PartialEq, Eq, Copy, Clone)] +#[non_exhaustive] pub enum GatheringState { /// No state was set. #[default] diff --git a/rtc-ice/src/tcp_type/mod.rs b/rtc-ice/src/tcp_type/mod.rs index 8d074c01..677366a2 100644 --- a/rtc-ice/src/tcp_type/mod.rs +++ b/rtc-ice/src/tcp_type/mod.rs @@ -9,6 +9,7 @@ use std::fmt; /// The role of an ICE-TCP candidate, per [RFC 6544] §4.5. /// /// [RFC 6544]: https://datatracker.ietf.org/doc/html/rfc6544#section-4.5 +#[non_exhaustive] pub enum TcpType { /// The default value. For example UDP candidates do not need this field. #[default] diff --git a/rtc-ice/src/url/mod.rs b/rtc-ice/src/url/mod.rs index 3d86d90b..5261b275 100644 --- a/rtc-ice/src/url/mod.rs +++ b/rtc-ice/src/url/mod.rs @@ -9,6 +9,7 @@ use shared::error::*; /// The type of server used in the ice.URL structure. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum SchemeType { /// The URL represents a STUN server. Stun, @@ -57,6 +58,7 @@ impl fmt::Display for SchemeType { /// The transport protocol type that is used in the `ice::url::Url` structure. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum ProtoType { /// The URL uses a UDP transport. #[default] diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index b10d4b73..9969559a 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -246,6 +246,7 @@ pub use interceptor_derive::{Interceptor, interceptor}; /// An enum representing either an RTP or RTCP packet that can be processed /// by interceptors in the chain. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum Packet { /// RTP (Real-time Transport Protocol) packet containing media data Rtp(rtp::Packet), diff --git a/rtc-mdns/examples/mdns_query.rs b/rtc-mdns/examples/mdns_query.rs index 993d5d97..9fe67a87 100644 --- a/rtc-mdns/examples/mdns_query.rs +++ b/rtc-mdns/examples/mdns_query.rs @@ -157,6 +157,7 @@ async fn main() -> Result<(), Box> { conn.close()?; return Err(format!("Query timed out after {} seconds", args.timeout).into()); } + _ => {} } } } diff --git a/rtc-mdns/examples/mdns_server_query.rs b/rtc-mdns/examples/mdns_server_query.rs index de00f876..e78e1ac8 100644 --- a/rtc-mdns/examples/mdns_server_query.rs +++ b/rtc-mdns/examples/mdns_server_query.rs @@ -255,6 +255,7 @@ async fn main() -> Result<(), Box> { format!("Query {} timed out after {} seconds", id, args.timeout).into(), ); } + _ => {} } } } diff --git a/rtc-mdns/src/proto/mod.rs b/rtc-mdns/src/proto/mod.rs index 74a569e9..9a2be429 100644 --- a/rtc-mdns/src/proto/mod.rs +++ b/rtc-mdns/src/proto/mod.rs @@ -85,6 +85,7 @@ pub struct Query { /// } /// ``` #[derive(Debug)] +#[non_exhaustive] pub enum MdnsEvent { /// A query was successfully answered. /// @@ -163,6 +164,8 @@ pub enum MdnsEvent { /// MdnsEvent::QueryTimeout(id) => { /// println!("Query {} timed out", id); /// } +/// // MdnsEvent is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// } /// ``` diff --git a/rtc-mdns/tests/integration_test.rs b/rtc-mdns/tests/integration_test.rs index 5d21645a..3891e914 100644 --- a/rtc-mdns/tests/integration_test.rs +++ b/rtc-mdns/tests/integration_test.rs @@ -130,6 +130,7 @@ fn test_server_responds_to_query() { assert_eq!(addr, server_addr.ip()); } MdnsEvent::QueryTimeout(_) => panic!("Unexpected QueryTimeout"), + other => panic!("Unexpected event: {other:?}"), } // Query should no longer be pending diff --git a/rtc-media/src/audio/buffer.rs b/rtc-media/src/audio/buffer.rs index e37383b3..d7a490cd 100644 --- a/rtc-media/src/audio/buffer.rs +++ b/rtc-media/src/audio/buffer.rs @@ -58,6 +58,7 @@ pub trait ToByteBufferRef: Sized { #[derive(Debug, Error, PartialEq, Eq)] /// Errors from converting between buffers and raw bytes. +#[non_exhaustive] pub enum Error { #[error("Unexpected end of buffer: (expected: {expected}, actual: {actual})")] /// The byte slice was too short to hold the expected number of samples. diff --git a/rtc-media/src/io/h26x_reader/mod.rs b/rtc-media/src/io/h26x_reader/mod.rs index da528ddd..6cea4743 100644 --- a/rtc-media/src/io/h26x_reader/mod.rs +++ b/rtc-media/src/io/h26x_reader/mod.rs @@ -62,6 +62,7 @@ impl ReadBuffer { /// H264NalUnitType is the type of a NAL /// Enums for H264NalUnitType #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H264NalUnitType { /// Unspecified #[default] @@ -188,6 +189,7 @@ impl H264NAL { /// H265NalUnitType is the type of a NAL unit in H.265/HEVC /// Based on ITU-T H.265 (04/2013) Table 7-1 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H265NalUnitType { /// Coded slice of a non-TSA, non-STSA trailing picture #[default] @@ -365,6 +367,7 @@ impl H265NAL { } /// H26xNAL represents either an H264 or H265 NAL unit +#[non_exhaustive] pub enum H26xNAL { /// An H.264 NAL unit. H264(H264NAL), diff --git a/rtc-media/src/io/h26x_writer/mod.rs b/rtc-media/src/io/h26x_writer/mod.rs index d3332e8f..b27385a9 100644 --- a/rtc-media/src/io/h26x_writer/mod.rs +++ b/rtc-media/src/io/h26x_writer/mod.rs @@ -181,6 +181,8 @@ impl H26xWriter { self.buffer.clear(); } } + // Unknown payload kind: skip it rather than corrupt the Annex-B stream. + _ => {} } } diff --git a/rtc-media/src/io/ivf_writer/mod.rs b/rtc-media/src/io/ivf_writer/mod.rs index 8f35c0be..eee3061c 100644 --- a/rtc-media/src/io/ivf_writer/mod.rs +++ b/rtc-media/src/io/ivf_writer/mod.rs @@ -14,6 +14,7 @@ use shared::error::Result; /// Codec type for IVF writer #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] pub enum IvfCodec { #[default] /// VP8, written with the `VP80` FourCC. diff --git a/rtc-media/src/io/ogg_reader/mod.rs b/rtc-media/src/io/ogg_reader/mod.rs index 49238af2..51545302 100644 --- a/rtc-media/src/io/ogg_reader/mod.rs +++ b/rtc-media/src/io/ogg_reader/mod.rs @@ -40,6 +40,7 @@ pub const ID_PAGE_PAYLOAD_SIZE: usize = 19; /// Header type classification for Opus pages #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum OggHeaderType { /// OpusHead - Opus ID page OpusHead, diff --git a/rtc-rtcp/src/extended_report/mod.rs b/rtc-rtcp/src/extended_report/mod.rs index 048aaadf..4fe463b2 100644 --- a/rtc-rtcp/src/extended_report/mod.rs +++ b/rtc-rtcp/src/extended_report/mod.rs @@ -40,6 +40,7 @@ const XR_HEADER_LENGTH: usize = 4; /// BlockType specifies the type of report in a report block /// Extended Report block types from RFC 3611. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum BlockType { #[default] /// A block type this crate does not model. diff --git a/rtc-rtcp/src/header.rs b/rtc-rtcp/src/header.rs index ed96210a..2b0b3946 100644 --- a/rtc-rtcp/src/header.rs +++ b/rtc-rtcp/src/header.rs @@ -17,6 +17,7 @@ use bytes::{Buf, BufMut}; /// RTCP packet types registered with IANA. See: #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum PacketType { #[default] /// A packet type this crate does not model. diff --git a/rtc-rtcp/src/source_description/mod.rs b/rtc-rtcp/src/source_description/mod.rs index ec41279b..1ccc696a 100644 --- a/rtc-rtcp/src/source_description/mod.rs +++ b/rtc-rtcp/src/source_description/mod.rs @@ -23,6 +23,7 @@ const SDES_TEXT_OFFSET: usize = 2; /// RTP SDES item types registered with IANA. See: #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum SdesType { #[default] /// End of the SDES item list ([RFC 3550] §6.5). diff --git a/rtc-rtp/src/codec/h265/mod.rs b/rtc-rtp/src/codec/h265/mod.rs index b766571d..08b74aa0 100644 --- a/rtc-rtp/src/codec/h265/mod.rs +++ b/rtc-rtp/src/codec/h265/mod.rs @@ -55,6 +55,7 @@ pub const NAL_HEADER_SIZE: usize = 2; #[derive(PartialEq, Hash, Debug, Copy, Clone)] /// The H.265 NAL unit types this payloader distinguishes. +#[non_exhaustive] pub enum UnitType { /// Video parameter set. VPS = 32, @@ -1023,6 +1024,7 @@ impl H265TSCI { /// H265 Payload Enum /// #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H265Payload { /// One NAL unit carried whole in a single packet. H265SingleNALUnitPacket(H265SingleNALUnitPacket), diff --git a/rtc-rtp/src/extension/mod.rs b/rtc-rtp/src/extension/mod.rs index 733b54f8..a56bac08 100644 --- a/rtc-rtp/src/extension/mod.rs +++ b/rtc-rtp/src/extension/mod.rs @@ -18,6 +18,7 @@ pub mod transport_cc_extension; pub mod video_orientation_extension; /// A generic RTP header extension. +#[non_exhaustive] pub enum HeaderExtension { /// The absolute-send-time extension. AbsSendTime(abs_send_time_extension::AbsSendTimeExtension), diff --git a/rtc-sctp/examples/sctp_e2e.rs b/rtc-sctp/examples/sctp_e2e.rs index 9f55a6ad..8e5c74c5 100644 --- a/rtc-sctp/examples/sctp_e2e.rs +++ b/rtc-sctp/examples/sctp_e2e.rs @@ -69,6 +69,7 @@ impl Node { assoc.handle_event(event); } } + _ => {} } } } diff --git a/rtc-sctp/src/association/mod.rs b/rtc-sctp/src/association/mod.rs index 6e0ea069..8ddeacc4 100644 --- a/rtc-sctp/src/association/mod.rs +++ b/rtc-sctp/src/association/mod.rs @@ -55,6 +55,7 @@ mod association_test; /// Reasons why an association might be lost #[derive(Debug, Error, Clone, PartialEq)] +#[non_exhaustive] pub enum AssociationError { /// Handshake failed #[error("handshake failed due to {0}")] diff --git a/rtc-sctp/src/association/stream.rs b/rtc-sctp/src/association/stream.rs index ff8f6446..f2697bdc 100644 --- a/rtc-sctp/src/association/stream.rs +++ b/rtc-sctp/src/association/stream.rs @@ -73,6 +73,7 @@ pub enum StreamEvent { /// Reliability type for stream #[derive(Default, Debug, Copy, Clone, PartialEq)] +#[non_exhaustive] pub enum ReliabilityType { /// ReliabilityTypeReliable is used for reliable transmission #[default] @@ -392,6 +393,7 @@ impl Stream<'_> { } #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] pub enum RecvSendState { #[default] Closed = 0, diff --git a/rtc-sctp/src/chunk/chunk_payload_data.rs b/rtc-sctp/src/chunk/chunk_payload_data.rs index 024c4a53..06f2a759 100644 --- a/rtc-sctp/src/chunk/chunk_payload_data.rs +++ b/rtc-sctp/src/chunk/chunk_payload_data.rs @@ -15,6 +15,7 @@ pub(crate) const PAYLOAD_DATA_HEADER_SIZE: usize = 12; /// #[derive(Default, Debug, Copy, Clone, PartialEq)] #[repr(C)] +#[non_exhaustive] pub enum PayloadProtocolIdentifier { /// `WebRTC DCEP` (50): a Data Channel Establishment Protocol control message. Dcep = 50, diff --git a/rtc-sctp/src/endpoint/mod.rs b/rtc-sctp/src/endpoint/mod.rs index 41a303f0..15085363 100644 --- a/rtc-sctp/src/endpoint/mod.rs +++ b/rtc-sctp/src/endpoint/mod.rs @@ -355,6 +355,7 @@ impl IndexMut for Slab { /// Event resulting from processing a single datagram #[allow(clippy::large_enum_variant)] // Not passed around extensively +#[non_exhaustive] pub enum DatagramEvent { /// The datagram is redirected to its `Association` AssociationEvent(AssociationEvent), @@ -366,6 +367,7 @@ pub enum DatagramEvent { /// /// These arise before any I/O has been performed. #[derive(Debug, Error, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ConnectError { /// The endpoint can no longer create new associations /// diff --git a/rtc-sctp/src/lib.rs b/rtc-sctp/src/lib.rs index b0e95bbd..3d299b75 100644 --- a/rtc-sctp/src/lib.rs +++ b/rtc-sctp/src/lib.rs @@ -143,6 +143,7 @@ use crate::packet::PartialDecode; /// Payload in Incoming/outgoing Transmit #[derive(Debug)] +#[non_exhaustive] pub enum Payload { /// An inbound packet whose header has been decoded but whose chunks have not. PartialDecode(PartialDecode), diff --git a/rtc-shared/src/ifaces/ffi/windows/mod.rs b/rtc-shared/src/ifaces/ffi/windows/mod.rs index c30f04fa..b6cdb0b3 100644 --- a/rtc-shared/src/ifaces/ffi/windows/mod.rs +++ b/rtc-shared/src/ifaces/ffi/windows/mod.rs @@ -182,6 +182,7 @@ bitflags! { } #[repr(C)] +#[non_exhaustive] pub enum IpPrefixOrigin { Other = 0, Manual, @@ -192,6 +193,7 @@ pub enum IpPrefixOrigin { } #[repr(C)] +#[non_exhaustive] pub enum IpSuffixOrigin { Other = 0, Manual, @@ -204,6 +206,7 @@ pub enum IpSuffixOrigin { #[derive(PartialEq, Eq)] #[repr(C)] +#[non_exhaustive] pub enum IpDadState { Invalid = 0, Tentative, @@ -213,6 +216,7 @@ pub enum IpDadState { } #[repr(C)] +#[non_exhaustive] pub enum IfOperStatus { Up = 1, Down = 2, @@ -224,6 +228,7 @@ pub enum IfOperStatus { } #[repr(C)] +#[non_exhaustive] pub enum NetIfConnectionType { Dedicated = 1, Passive = 2, @@ -232,6 +237,7 @@ pub enum NetIfConnectionType { } #[repr(C)] +#[non_exhaustive] pub enum TunnelType { None = 0, Other = 1, diff --git a/rtc-shared/src/ifaces/mod.rs b/rtc-shared/src/ifaces/mod.rs index de10fcfd..5cf8a068 100644 --- a/rtc-shared/src/ifaces/mod.rs +++ b/rtc-shared/src/ifaces/mod.rs @@ -4,6 +4,7 @@ pub use ffi::ifaces; #[derive(PartialEq, Eq, Debug, Clone)] /// The next hop configured for an interface address. +#[non_exhaustive] pub enum NextHop { /// The broadcast address of the attached network. Broadcast(::std::net::SocketAddr), @@ -13,6 +14,7 @@ pub enum NextHop { #[derive(PartialEq, Eq, Debug, Clone)] /// The address family or link type an [`Interface`] entry describes. +#[non_exhaustive] pub enum Kind { /// A raw packet-level (link layer) address. Packet, diff --git a/rtc-shared/src/transport.rs b/rtc-shared/src/transport.rs index ad150c3d..9293ed37 100644 --- a/rtc-shared/src/transport.rs +++ b/rtc-shared/src/transport.rs @@ -31,6 +31,7 @@ impl EcnCodepoint { /// Type of transport protocol, either UDP or TCP #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[non_exhaustive] pub enum TransportProtocol { /// UDP #[default] diff --git a/rtc-srtp/src/protection_profile.rs b/rtc-srtp/src/protection_profile.rs index 01c1266a..67dba6ed 100644 --- a/rtc-srtp/src/protection_profile.rs +++ b/rtc-srtp/src/protection_profile.rs @@ -1,6 +1,7 @@ /// ProtectionProfile specifies Cipher and AuthTag details, similar to TLS cipher suite #[derive(Default, Debug, Clone, Copy)] #[repr(u8)] +#[non_exhaustive] pub enum ProtectionProfile { #[default] /// `SRTP_AES128_CM_HMAC_SHA1_80`: AES-128 counter mode with an 80-bit HMAC-SHA1 tag. diff --git a/rtc-stun/src/agent.rs b/rtc-stun/src/agent.rs index cb932492..12777433 100644 --- a/rtc-stun/src/agent.rs +++ b/rtc-stun/src/agent.rs @@ -43,6 +43,7 @@ pub struct Event { #[derive(Debug)] //Clone /// What became of a STUN transaction. +#[non_exhaustive] pub enum StunEvent { /// The agent was closed, abandoning this transaction. AgentClosed, @@ -68,6 +69,7 @@ const AGENT_COLLECT_CAP: usize = 100; /// ClientAgent is Agent implementation that is used by Client to /// process transactions. #[derive(Debug)] +#[non_exhaustive] pub enum ClientAgent { /// Hand an inbound message to the agent for matching against a transaction. Process(Message), diff --git a/rtc-turn/examples/turn_client_udp.rs b/rtc-turn/examples/turn_client_udp.rs index f5f7cfeb..c661c486 100644 --- a/rtc-turn/examples/turn_client_udp.rs +++ b/rtc-turn/examples/turn_client_udp.rs @@ -176,6 +176,7 @@ fn main() -> Result<()> { client.relay(relay_addr)?.send_to(&data[..], from)?; } } + _ => {} } } diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 645d9500..947b9f95 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -68,6 +68,7 @@ pub type PeerAddr = SocketAddr; /// /// Every variant carries the [`TransactionId`] of the request it answers, so a caller can /// match responses to the requests it issued. +#[non_exhaustive] pub enum Event { /// A request exhausted its retransmissions without a response. TransactionTimeout(TransactionId), diff --git a/src/data_channel/state.rs b/src/data_channel/state.rs index 45ef598e..e48c46c1 100644 --- a/src/data_channel/state.rs +++ b/src/data_channel/state.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcdatachannelstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDataChannelState { /// The state is unspecified. #[serde(rename = "unspecified")] diff --git a/src/lib.rs b/src/lib.rs index c55fc26c..23cfaba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,8 @@ //! println!("Received data channel message on channel {:?}", channel_id); //! // Process data channel message //! } +//! // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +//! _ => {} //! } //! } //! diff --git a/src/media_stream/track_state.rs b/src/media_stream/track_state.rs index 6085a6f1..112fc0ce 100644 --- a/src/media_stream/track_state.rs +++ b/src/media_stream/track_state.rs @@ -31,6 +31,7 @@ use std::fmt; /// assert_eq!(state.to_string(), "ended"); /// ``` #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum MediaStreamTrackState { /// Unspecified or unknown state. Unspecified, diff --git a/src/peer_connection/configuration/bundle_policy.rs b/src/peer_connection/configuration/bundle_policy.rs index 9ab86ab6..755f6c1b 100644 --- a/src/peer_connection/configuration/bundle_policy.rs +++ b/src/peer_connection/configuration/bundle_policy.rs @@ -32,6 +32,7 @@ use serde::{Deserialize, Serialize}; /// /// * [W3C RTCBundlePolicy](https://w3c.github.io/webrtc-pc/#rtcbundlepolicy-enum) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCBundlePolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/ice_transport_policy.rs b/src/peer_connection/configuration/ice_transport_policy.rs index d783ca73..a482031e 100644 --- a/src/peer_connection/configuration/ice_transport_policy.rs +++ b/src/peer_connection/configuration/ice_transport_policy.rs @@ -57,6 +57,7 @@ use serde::{Deserialize, Serialize}; /// * [W3C RTCIceTransportPolicy](https://w3c.github.io/webrtc-pc/#rtcicetransportpolicy-enum) /// * [RFC 8445 - ICE](https://tools.ietf.org/html/rfc8445) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTransportPolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/rtcp_mux_policy.rs b/src/peer_connection/configuration/rtcp_mux_policy.rs index 9bf83ea5..d584f2b7 100644 --- a/src/peer_connection/configuration/rtcp_mux_policy.rs +++ b/src/peer_connection/configuration/rtcp_mux_policy.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; /// * [W3C RTCRtcpMuxPolicy](https://w3c.github.io/webrtc-pc/#rtcrtcpmuxpolicy-enum) /// * [RFC 5761 - Multiplexing RTP and RTCP](https://tools.ietf.org/html/rfc5761) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCRtcpMuxPolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/sdp_semantics.rs b/src/peer_connection/configuration/sdp_semantics.rs index 10e93b12..c17d063e 100644 --- a/src/peer_connection/configuration/sdp_semantics.rs +++ b/src/peer_connection/configuration/sdp_semantics.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; /// * [Unified Plan](https://tools.ietf.org/html/draft-roach-mmusic-unified-plan-00) /// * [Plan B (deprecated)](https://tools.ietf.org/html/draft-uberti-rtcweb-plan-00) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCSdpSemantics { /// Unspecified - not a valid semantic Unspecified = 0, diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 8d172732..6cdfc80e 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -251,6 +251,7 @@ pub struct ReplayProtection { /// Controls the maximum size of messages that can be sent through data channels. /// Per [RFC 8841](https://datatracker.ietf.org/doc/html/rfc8841), the default is 64KB. #[derive(Copy, Clone)] +#[non_exhaustive] pub enum SctpMaxMessageSize { /// Fixed maximum message size in bytes. Bounded(u32), diff --git a/src/peer_connection/event/data_channel_event.rs b/src/peer_connection/event/data_channel_event.rs index 92a9a5b8..2bb2b4c9 100644 --- a/src/peer_connection/event/data_channel_event.rs +++ b/src/peer_connection/event/data_channel_event.rs @@ -81,6 +81,7 @@ use crate::data_channel::RTCDataChannelId; /// - [W3C RTCDataChannel](https://www.w3.org/TR/webrtc/#rtcdatachannel) #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCDataChannelEvent { /// Data channel has opened and is ready to send/receive data. /// diff --git a/src/peer_connection/event/mod.rs b/src/peer_connection/event/mod.rs index 3d6fc6f4..8641ff62 100644 --- a/src/peer_connection/event/mod.rs +++ b/src/peer_connection/event/mod.rs @@ -224,6 +224,7 @@ pub use track_event::{RTCTrackEvent, RTCTrackEventInit}; /// See [RTCPeerConnection Events](https://www.w3.org/TR/webrtc/#rtcpeerconnection-interface) #[allow(clippy::enum_variant_names)] #[derive(Default, Clone, Debug)] +#[non_exhaustive] pub enum RTCPeerConnectionEvent { /// Fired when negotiation is needed to maintain the connection. /// @@ -520,6 +521,7 @@ pub enum RTCPeerConnectionEvent { /// /// This enum is currently empty but reserved for potential future event types. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCEvent {} /// Internal event types for WebRTC implementation. diff --git a/src/peer_connection/event/track_event.rs b/src/peer_connection/event/track_event.rs index 5eeb7975..7461ae66 100644 --- a/src/peer_connection/event/track_event.rs +++ b/src/peer_connection/event/track_event.rs @@ -167,6 +167,7 @@ pub struct RTCTrackEventInit { /// See [RTCTrackEvent](https://www.w3.org/TR/webrtc/#rtctrackevent) #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCTrackEvent { /// Track has opened and is ready to receive media. /// diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index 4215b8b6..560bb22d 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -208,6 +208,7 @@ impl<'a> sansio::Protocol {} } } diff --git a/src/peer_connection/handler/ice.rs b/src/peer_connection/handler/ice.rs index 490313e3..9e7a57b9 100644 --- a/src/peer_connection/handler/ice.rs +++ b/src/peer_connection/handler/ice.rs @@ -214,6 +214,7 @@ impl<'a> sansio::Protocol { self.stats.transport.on_ice_role_changed(is_controlling); } + _ => {} } } diff --git a/src/peer_connection/handler/interceptor.rs b/src/peer_connection/handler/interceptor.rs index 85f09b5e..69e5f62e 100644 --- a/src/peer_connection/handler/interceptor.rs +++ b/src/peer_connection/handler/interceptor.rs @@ -260,6 +260,7 @@ where ); } } + _ => {} } self.ctx.write_outs.push_back(TaggedRTCMessageInternal { diff --git a/src/peer_connection/handler/mod.rs b/src/peer_connection/handler/mod.rs index f212fca0..e70be5de 100644 --- a/src/peer_connection/handler/mod.rs +++ b/src/peer_connection/handler/mod.rs @@ -242,6 +242,7 @@ where Packet::Rtcp(packet) => { Some(RTCMessage::RtcpPacket(track_packet.track_id, packet)) } + _ => None, } } _ => None, diff --git a/src/peer_connection/handler/sctp.rs b/src/peer_connection/handler/sctp.rs index b6db0866..13f9c4e4 100644 --- a/src/peer_connection/handler/sctp.rs +++ b/src/peer_connection/handler/sctp.rs @@ -143,6 +143,7 @@ impl<'a> sansio::Protocol { sctp_events.entry(ch).or_default().push_back(event); } + _ => {} } } @@ -670,6 +671,7 @@ mod tests { sc.handle_event(e); } } + _ => {} } } } @@ -774,6 +776,7 @@ mod tests { c.handle_event(e); } } + _ => {} } } } @@ -789,6 +792,7 @@ mod tests { c.handle_event(e); } } + _ => {} } } } diff --git a/src/peer_connection/handler/srtp.rs b/src/peer_connection/handler/srtp.rs index 2fe88b7b..6dcace5b 100644 --- a/src/peer_connection/handler/srtp.rs +++ b/src/peer_connection/handler/srtp.rs @@ -143,6 +143,11 @@ impl<'a> sansio::Protocol { + return Err(Error::Other( + "unsupported packet kind for srtp write".to_string(), + )); + } }; self.ctx.write_outs.push_back(TaggedRTCMessageInternal { diff --git a/src/peer_connection/message/internal.rs b/src/peer_connection/message/internal.rs index d8eee476..7aad2f16 100644 --- a/src/peer_connection/message/internal.rs +++ b/src/peer_connection/message/internal.rs @@ -81,6 +81,7 @@ impl RTCMessageInternal { } rtcp_packet_size } + _ => 0, // Future Packet variants: size unknown; treat as 0 for accounting. }, RTPMessage::TrackPacket(tp) => match &tp.packet { Packet::Rtp(rtp) => rtp.marshal_size(), @@ -91,6 +92,7 @@ impl RTCMessageInternal { } rtcp_packet_size } + _ => 0, }, }, } diff --git a/src/peer_connection/message/mod.rs b/src/peer_connection/message/mod.rs index 44e721bc..62f4c7bc 100644 --- a/src/peer_connection/message/mod.rs +++ b/src/peer_connection/message/mod.rs @@ -87,6 +87,8 @@ //! println!("Binary message"); //! } //! } +//! // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +//! _ => {} //! } //! # } //! ``` @@ -329,6 +331,8 @@ pub(crate) mod internal; /// println!("Received data on channel {}", channel_id); /// // Process application data /// } +/// // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # } /// ``` @@ -381,6 +385,8 @@ pub(crate) mod internal; /// println!("Binary: {} bytes", msg.data.len()); /// } /// } +/// // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # } /// ``` @@ -409,6 +415,7 @@ pub(crate) mod internal; /// [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 /// [RFC 8831]: https://datatracker.ietf.org/doc/html/rfc8831 #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCMessage { /// RTP packet for a specific media track. /// diff --git a/src/peer_connection/sdp/sdp_type.rs b/src/peer_connection/sdp/sdp_type.rs index 1c7e71e1..f0e2bcd4 100644 --- a/src/peer_connection/sdp/sdp_type.rs +++ b/src/peer_connection/sdp/sdp_type.rs @@ -52,6 +52,8 @@ use serde::{Deserialize, Serialize}; /// RTCSdpType::Pranswer => println!("This is a provisional answer"), /// RTCSdpType::Rollback => println!("This is a rollback"), /// RTCSdpType::Unspecified => println!("Type not specified"), +/// // RTCSdpType is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # Ok(()) /// # } @@ -85,6 +87,7 @@ use serde::{Deserialize, Serialize}; /// [MDN RTCSessionDescription.type]: https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription/type /// [RFC 3264]: https://datatracker.ietf.org/doc/html/rfc3264 #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCSdpType { /// Type not specified. This is the default value and should not be used /// in actual WebRTC negotiation. diff --git a/src/peer_connection/state/ice_connection_state.rs b/src/peer_connection/state/ice_connection_state.rs index a702e58e..d075fda6 100644 --- a/src/peer_connection/state/ice_connection_state.rs +++ b/src/peer_connection/state/ice_connection_state.rs @@ -105,6 +105,7 @@ use std::fmt; /// [MDN RTCPeerConnection.iceConnectionState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState /// [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCIceConnectionState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/ice_gathering_state.rs b/src/peer_connection/state/ice_gathering_state.rs index 831ff874..a33e184f 100644 --- a/src/peer_connection/state/ice_gathering_state.rs +++ b/src/peer_connection/state/ice_gathering_state.rs @@ -115,6 +115,7 @@ use std::fmt; /// [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 /// [RFC 8838]: https://datatracker.ietf.org/doc/html/rfc8838 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCIceGatheringState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/peer_connection_state.rs b/src/peer_connection/state/peer_connection_state.rs index 8cc9fd84..bcbf8cf8 100644 --- a/src/peer_connection/state/peer_connection_state.rs +++ b/src/peer_connection/state/peer_connection_state.rs @@ -142,6 +142,7 @@ use std::fmt; /// [W3C RTCPeerConnection.connectionState]: https://w3c.github.io/webrtc-pc/#dom-peerconnection-connection-state /// [MDN RTCPeerConnection.connectionState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCPeerConnectionState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/signaling_state.rs b/src/peer_connection/state/signaling_state.rs index 3f75ee1e..e1c85bde 100644 --- a/src/peer_connection/state/signaling_state.rs +++ b/src/peer_connection/state/signaling_state.rs @@ -184,6 +184,7 @@ impl fmt::Display for StateChangeOp { /// [MDN RTCPeerConnection.signalingState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/signalingState /// [RFC 3264]: https://datatracker.ietf.org/doc/html/rfc3264 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCSignalingState { /// State not specified. This should not occur in normal operation. Unspecified = 0, diff --git a/src/peer_connection/transport/dtls/role.rs b/src/peer_connection/transport/dtls/role.rs index de82f6d4..cab32a27 100644 --- a/src/peer_connection/transport/dtls/role.rs +++ b/src/peer_connection/transport/dtls/role.rs @@ -76,6 +76,7 @@ use serde::{Deserialize, Serialize}; /// [RFC 5763]: https://datatracker.ietf.org/doc/html/rfc5763 /// [RFC 8122]: https://datatracker.ietf.org/doc/html/rfc8122 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDtlsRole { /// Role not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/dtls/state.rs b/src/peer_connection/transport/dtls/state.rs index a2233395..4783f142 100644 --- a/src/peer_connection/transport/dtls/state.rs +++ b/src/peer_connection/transport/dtls/state.rs @@ -85,6 +85,7 @@ use std::fmt; /// [MDN RTCDtlsTransport.state]: https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state /// [RFC 6347]: https://datatracker.ietf.org/doc/html/rfc6347 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDtlsTransportState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/ice/candidate.rs b/src/peer_connection/transport/ice/candidate.rs index 4cd2c97d..6d5ea21a 100644 --- a/src/peer_connection/transport/ice/candidate.rs +++ b/src/peer_connection/transport/ice/candidate.rs @@ -13,6 +13,7 @@ pub use ice::candidate::{ }; #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTcpCandidateType { #[default] Unspecified, @@ -34,6 +35,7 @@ impl From for RTCIceTcpCandidateType { TcpType::Active => RTCIceTcpCandidateType::Active, TcpType::Passive => RTCIceTcpCandidateType::Passive, TcpType::SimultaneousOpen => RTCIceTcpCandidateType::SimultaneousOpen, + _ => RTCIceTcpCandidateType::Unspecified, } } } @@ -50,6 +52,7 @@ impl RTCIceTcpCandidateType { } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceServerTransportProtocol { #[default] Unspecified, diff --git a/src/peer_connection/transport/ice/candidate_type.rs b/src/peer_connection/transport/ice/candidate_type.rs index d8a0a6bb..84737e22 100644 --- a/src/peer_connection/transport/ice/candidate_type.rs +++ b/src/peer_connection/transport/ice/candidate_type.rs @@ -105,6 +105,7 @@ use serde::{Deserialize, Serialize}; /// [W3C RTCIceCandidateStats.candidateType]: https://w3c.github.io/webrtc-stats/#dom-rtcicecandidatestats-candidatetype /// [MDN RTCIceCandidateStats.candidateType]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidateStats/candidateType #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceCandidateType { /// Type not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/ice/protocol.rs b/src/peer_connection/transport/ice/protocol.rs index 9740524b..b4fdea0e 100644 --- a/src/peer_connection/transport/ice/protocol.rs +++ b/src/peer_connection/transport/ice/protocol.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/protocol /// [W3C]: https://w3c.github.io/webrtc-pc/#rtciceprotocol-enum #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceProtocol { /// The protocol is unspecified. #[default] diff --git a/src/peer_connection/transport/ice/role.rs b/src/peer_connection/transport/ice/role.rs index 410d168f..58d8567d 100644 --- a/src/peer_connection/transport/ice/role.rs +++ b/src/peer_connection/transport/ice/role.rs @@ -12,6 +12,7 @@ use std::fmt; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceTransport/role /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcicerole #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceRole { /// The ICE role is unspecified. #[default] diff --git a/src/peer_connection/transport/ice/state.rs b/src/peer_connection/transport/ice/state.rs index 3cb55f12..7cb26246 100644 --- a/src/peer_connection/transport/ice/state.rs +++ b/src/peer_connection/transport/ice/state.rs @@ -12,6 +12,7 @@ use std::fmt; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceTransport/state /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcicetransportstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTransportState { /// The transport state is unspecified. #[default] diff --git a/src/peer_connection/transport/sctp/state.rs b/src/peer_connection/transport/sctp/state.rs index fb323a9b..cfad9fd1 100644 --- a/src/peer_connection/transport/sctp/state.rs +++ b/src/peer_connection/transport/sctp/state.rs @@ -9,6 +9,7 @@ use std::fmt; /// [W3C]: https://w3c.github.io/webrtc-pc/#rtcsctptransportstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum RTCSctpTransportState { /// The transport state is unspecified. #[default] diff --git a/src/rtp_transceiver/direction.rs b/src/rtp_transceiver/direction.rs index c61422fb..95fa9c95 100644 --- a/src/rtp_transceiver/direction.rs +++ b/src/rtp_transceiver/direction.rs @@ -14,6 +14,7 @@ use std::fmt; /// /// See [RTCRtpTransceiver.direction](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/direction). #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCRtpTransceiverDirection { /// Direction is not specified (internal use only). #[default] diff --git a/src/rtp_transceiver/rtp_sender/rtp_codec.rs b/src/rtp_transceiver/rtp_sender/rtp_codec.rs index 0519b21c..c5fe1c22 100644 --- a/src/rtp_transceiver/rtp_sender/rtp_codec.rs +++ b/src/rtp_transceiver/rtp_sender/rtp_codec.rs @@ -10,6 +10,7 @@ use std::fmt; /// Codec kind identifying the media type. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] pub enum RtpCodecKind { /// Unspecified or unknown codec type #[default] diff --git a/src/statistics/accumulator/codec.rs b/src/statistics/accumulator/codec.rs index 4a9d4ee4..9fe0b640 100644 --- a/src/statistics/accumulator/codec.rs +++ b/src/statistics/accumulator/codec.rs @@ -9,6 +9,7 @@ use std::time::Instant; /// Direction qualifier for codec stats IDs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum CodecDirection { /// Codec used for sending (encoding). Send, diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 940c3c44..0cd5988e 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -51,6 +51,7 @@ pub mod stats; /// - `None` - Return all statistics for the entire connection /// - `Sender` - Return statistics for a specific RTP sender and referenced objects /// - `Receiver` - Return statistics for a specific RTP receiver and referenced objects +#[non_exhaustive] pub enum StatsSelector { /// Gather stats for the whole connection. /// diff --git a/src/statistics/report.rs b/src/statistics/report.rs index 1be4f5bd..0cee62d8 100644 --- a/src/statistics/report.rs +++ b/src/statistics/report.rs @@ -24,6 +24,7 @@ use std::collections::HashMap; /// /// Each variant corresponds to a different W3C WebRTC stats dictionary type. #[derive(Debug)] +#[non_exhaustive] pub enum RTCStatsReportEntry { /// Peer connection level statistics. PeerConnection(RTCPeerConnectionStats), diff --git a/src/statistics/stats/ice_candidate_pair.rs b/src/statistics/stats/ice_candidate_pair.rs index c279e31e..b6a8fb07 100644 --- a/src/statistics/stats/ice_candidate_pair.rs +++ b/src/statistics/stats/ice_candidate_pair.rs @@ -14,6 +14,7 @@ use std::time::Instant; /// This enum represents the current state of a candidate pair /// in the ICE connectivity check process. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCStatsIceCandidatePairState { /// State has not been set. #[default] @@ -48,6 +49,7 @@ impl From for RTCStatsIceCandidatePairState { CandidatePairState::InProgress => RTCStatsIceCandidatePairState::InProgress, CandidatePairState::Failed => RTCStatsIceCandidatePairState::Failed, CandidatePairState::Succeeded => RTCStatsIceCandidatePairState::Succeeded, + _ => RTCStatsIceCandidatePairState::Unspecified, } } } diff --git a/src/statistics/stats/mod.rs b/src/statistics/stats/mod.rs index 00dbb116..58caa66a 100644 --- a/src/statistics/stats/mod.rs +++ b/src/statistics/stats/mod.rs @@ -59,6 +59,7 @@ pub mod transport; /// Values are serialized using the W3C-specified lowercase hyphenated format /// (e.g., `InboundRTP` serializes to `"inbound-rtp"`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCStatsType { /// Statistics for a media codec. #[serde(rename = "codec")] @@ -142,6 +143,7 @@ pub struct RTCStats { /// This enum indicates why the video encoder may have reduced quality /// (resolution, frame rate, or bitrate) during encoding. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCQualityLimitationReason { /// No quality limitation is active. #[default] diff --git a/tests/data_channels_close_by_webrtc_interop.rs b/tests/data_channels_close_by_webrtc_interop.rs index ffcdb220..cc0ffe3e 100644 --- a/tests/data_channels_close_by_webrtc_interop.rs +++ b/tests/data_channels_close_by_webrtc_interop.rs @@ -274,6 +274,7 @@ async fn test_data_channel_close_by_webrtc_interop() -> Result<()> { log::info!("RTC received message on channel {}: '{}'", channel_id, data); rtc_received_messages.push(data); } + _ => {} } } diff --git a/tests/data_channels_create_interop.rs b/tests/data_channels_create_interop.rs index b279adc4..6cbd0ee1 100644 --- a/tests/data_channels_create_interop.rs +++ b/tests/data_channels_create_interop.rs @@ -273,6 +273,7 @@ async fn test_data_channel_create_rtc_to_webrtc() -> Result<()> { let mut rtc_msgs = rtc_received_messages.lock().await; rtc_msgs.push(msg_str.clone()); } + _ => {} } } diff --git a/tests/data_channels_interop.rs b/tests/data_channels_interop.rs index 8c4c858b..dd2e0dec 100644 --- a/tests/data_channels_interop.rs +++ b/tests/data_channels_interop.rs @@ -260,6 +260,7 @@ async fn test_data_channel_rtc_to_webrtc() -> Result<()> { log::info!("RTC echoing message back: '{}'", msg_str); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/tests/ice_restart_by_webrtc_interop.rs b/tests/ice_restart_by_webrtc_interop.rs index 52e45275..f4834f47 100644 --- a/tests/ice_restart_by_webrtc_interop.rs +++ b/tests/ice_restart_by_webrtc_interop.rs @@ -243,15 +243,12 @@ async fn test_ice_restart_interop() -> Result<()> { rtc_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = rtc_pc.data_channel(channel_id) { - log::info!("RTC data channel '{}'-'{}' opened", dc.label(), dc.id()); - rtc_dc_id = Some(channel_id); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = rtc_pc.data_channel(channel_id) { + log::info!("RTC data channel '{}'-'{}' opened", dc.label(), dc.id()); + rtc_dc_id = Some(channel_id); } - _ => {} - }, + } _ => {} } } @@ -265,6 +262,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -392,6 +390,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -552,6 +551,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message after restart: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -624,6 +624,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message after restart: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } diff --git a/tests/media_only_negotiation_no_sctp.rs b/tests/media_only_negotiation_no_sctp.rs index 961845ba..feb8669e 100644 --- a/tests/media_only_negotiation_no_sctp.rs +++ b/tests/media_only_negotiation_no_sctp.rs @@ -180,10 +180,10 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { while let Some(event) = offerer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("offerer ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("offerer ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { if state == RTCPeerConnectionState::Failed { @@ -202,10 +202,10 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { while let Some(event) = answerer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("answerer ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("answerer ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { if state == RTCPeerConnectionState::Failed { @@ -240,6 +240,7 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { unexpected_data_channel_messages += 1; } RTCMessage::RtcpPacket(_, _) => {} + _ => {} } } diff --git a/tests/offer_answer_rtc2rtc.rs b/tests/offer_answer_rtc2rtc.rs index 7dd99d29..f4126c37 100644 --- a/tests/offer_answer_rtc2rtc.rs +++ b/tests/offer_answer_rtc2rtc.rs @@ -215,13 +215,10 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { offer_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - log::info!("Offer data channel {} opened", channel_id); - offer_dc_id = Some(channel_id); - } - _ => {} - }, + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + log::info!("Offer data channel {} opened", channel_id); + offer_dc_id = Some(channel_id); + } _ => {} } } @@ -236,6 +233,7 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { let mut msgs = offer_received_messages.lock().await; msgs.push(msg_str); } + _ => {} } } @@ -272,12 +270,9 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { answer_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - log::info!("Answer data channel {} opened", channel_id); - } - _ => {} - }, + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + log::info!("Answer data channel {} opened", channel_id); + } _ => {} } } @@ -301,6 +296,7 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { } } } + _ => {} } } diff --git a/tests/one_media_section_rtc_to_rtc_simulcast.rs b/tests/one_media_section_rtc_to_rtc_simulcast.rs index b5355cc4..25e859c6 100644 --- a/tests/one_media_section_rtc_to_rtc_simulcast.rs +++ b/tests/one_media_section_rtc_to_rtc_simulcast.rs @@ -402,6 +402,7 @@ async fn test_one_media_section_rtc_to_rtc_simulcast() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/one_media_section_rtc_to_rtc_unicast.rs b/tests/one_media_section_rtc_to_rtc_unicast.rs index 7f699ebc..961373fd 100644 --- a/tests/one_media_section_rtc_to_rtc_unicast.rs +++ b/tests/one_media_section_rtc_to_rtc_unicast.rs @@ -342,6 +342,7 @@ async fn test_one_media_section_rtc_to_rtc_unicast() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/reflect_rtc_to_webrtc_interop.rs b/tests/reflect_rtc_to_webrtc_interop.rs index 0ccc0a71..a95be8d1 100644 --- a/tests/reflect_rtc_to_webrtc_interop.rs +++ b/tests/reflect_rtc_to_webrtc_interop.rs @@ -323,6 +323,7 @@ async fn test_reflect_rtc_to_webrtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/reflect_webrtc_to_rtc_interop.rs b/tests/reflect_webrtc_to_rtc_interop.rs index 2171059f..07fb6e4d 100644 --- a/tests/reflect_webrtc_to_rtc_interop.rs +++ b/tests/reflect_webrtc_to_rtc_interop.rs @@ -378,6 +378,7 @@ async fn test_reflect_webrtc_to_rtc() -> Result<()> { // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/rtcp_processing_boxed_interop.rs b/tests/rtcp_processing_boxed_interop.rs index 106425aa..f48592b2 100644 --- a/tests/rtcp_processing_boxed_interop.rs +++ b/tests/rtcp_processing_boxed_interop.rs @@ -342,6 +342,7 @@ impl RtcpPeer { } } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } } diff --git a/tests/rtcp_processing_interop.rs b/tests/rtcp_processing_interop.rs index 3b0dac61..ed66759f 100644 --- a/tests/rtcp_processing_interop.rs +++ b/tests/rtcp_processing_interop.rs @@ -864,10 +864,10 @@ async fn test_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { while let Some(event) = rtc_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("RTC ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("RTC ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { log::info!("RTC connection state: {}", state); diff --git a/tests/save_to_disk_vpx_interop.rs b/tests/save_to_disk_vpx_interop.rs index 2748ee1a..4c752ec0 100644 --- a/tests/save_to_disk_vpx_interop.rs +++ b/tests/save_to_disk_vpx_interop.rs @@ -416,6 +416,7 @@ async fn test_save_to_disk_vpx_webrtc_to_rtc() -> Result<()> { // Process RTCP packets } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/simulcast_rtc_to_rtc_interop.rs b/tests/simulcast_rtc_to_rtc_interop.rs index ae987fe7..3eb12fd0 100644 --- a/tests/simulcast_rtc_to_rtc_interop.rs +++ b/tests/simulcast_rtc_to_rtc_interop.rs @@ -427,6 +427,7 @@ async fn test_simulcast_rtc_to_rtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/simulcast_webrtc_to_rtc_interop.rs b/tests/simulcast_webrtc_to_rtc_interop.rs index 191384cc..c7694ecd 100644 --- a/tests/simulcast_webrtc_to_rtc_interop.rs +++ b/tests/simulcast_webrtc_to_rtc_interop.rs @@ -502,6 +502,7 @@ async fn test_simulcast_webrtc_to_rtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/statistics_rtc_to_rtc.rs b/tests/statistics_rtc_to_rtc.rs index 8ddbeea2..5599b6ba 100644 --- a/tests/statistics_rtc_to_rtc.rs +++ b/tests/statistics_rtc_to_rtc.rs @@ -185,10 +185,10 @@ async fn test_data_channel_statistics_collection() -> Result<()> { // Process offer peer events while let Some(event) = runner.offer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Connected { - offer_connected = true; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Connected => + { + offer_connected = true; } RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { offer_dc_id = Some(channel_id); @@ -215,10 +215,10 @@ async fn test_data_channel_statistics_collection() -> Result<()> { // Process answer peer events while let Some(event) = runner.answer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Connected { - answer_connected = true; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Connected => + { + answer_connected = true; } RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { _answer_dc_id = Some(channel_id); @@ -891,6 +891,7 @@ async fn test_stats_json_serialization() -> Result<()> { RTCStatsReportEntry::AudioSource(s) => serde_json::to_string(s), RTCStatsReportEntry::VideoSource(s) => serde_json::to_string(s), RTCStatsReportEntry::AudioPlayout(s) => serde_json::to_string(s), + _ => continue, // Skip unknown variants for forward compatibility }; assert!( From b47f82fe8f705b57cf9583df9a1e9c0ff4b551c3 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sat, 1 Aug 2026 17:42:23 -0700 Subject: [PATCH 27/40] make v0.20.x branch run github actions --- .github/workflows/cargo.yml | 6 +++--- .github/workflows/grcov.yml | 4 ++-- .github/workflows/semver.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index fcd2ed5e..ac600d0e 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -2,9 +2,9 @@ name: cargo on: push: - branches: [master] + branches: [ master, v0.20.x ] pull_request: - branches: [master] + branches: [ master, v0.20.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,7 +18,7 @@ jobs: name: Build strategy: matrix: - os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] + os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 3439a9ba..7141b7da 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -2,9 +2,9 @@ name: coverage on: push: - branches: [ master ] + branches: [ master, v0.20.x ] pull_request: - branches: [ master ] + branches: [ master, v0.20.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index 8e4b3dc0..b1b25992 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -2,9 +2,9 @@ name: SemVer Check on: push: - branches: [master] + branches: [ master, v0.20.x ] pull_request: - branches: [master] + branches: [ master, v0.20.x ] jobs: semver: From 9aa477c524dff9f43d99bc5a7d104bf34d3dced2 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 11:31:51 -0700 Subject: [PATCH 28/40] make v0.21.x branch run github actions --- .github/workflows/cargo.yml | 4 ++-- .github/workflows/grcov.yml | 4 ++-- .github/workflows/semver.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index ac600d0e..3ab766f3 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -2,9 +2,9 @@ name: cargo on: push: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] pull_request: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 7141b7da..c43bbb11 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -2,9 +2,9 @@ name: coverage on: push: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] pull_request: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index b1b25992..b337e439 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -2,9 +2,9 @@ name: SemVer Check on: push: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] pull_request: - branches: [ master, v0.20.x ] + branches: [ master, v0.20.x, v0.21.x ] jobs: semver: From 82fda92c6a441ebe94335e380841e3b2c3bd671e Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 16:10:00 -0700 Subject: [PATCH 29/40] =?UTF-8?q?P0=20=E2=80=94=20Baseline=20and=20design?= =?UTF-8?q?=20freeze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cargo.yml | 27 ++++++- docs/crypto-provider-baseline.md | 120 ++++++++++++++++++++++++++++++ docs/crypto-provider-decisions.md | 44 +++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 docs/crypto-provider-baseline.md create mode 100644 docs/crypto-provider-decisions.md diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 3ab766f3..759aaffb 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -26,7 +26,7 @@ jobs: run: cargo build --verbose test: - name: Test + name: Test (default backend) strategy: matrix: os: [ 'ubuntu-latest' ] #, 'macos-latest', 'windows-latest' ] @@ -36,6 +36,31 @@ jobs: - name: Run tests run: cargo test --verbose + crypto_backend_baseline: + name: Crypto baseline (${{ matrix.backend }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + backend: [ ring, aws-lc-rs ] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test workspace + run: cargo test --workspace --no-fail-fast --lib --bins --examples --tests --no-default-features --features rtc/${{ matrix.backend }} --verbose + - name: Test workspace documentation + run: cargo test --workspace --doc --no-default-features --features rtc/${{ matrix.backend }} --verbose + - name: Build standalone crypto consumers + shell: bash + run: | + cargo check --package rtc-dtls --no-default-features --features ${{ matrix.backend }} --verbose + cargo check --package rtc-srtp --no-default-features --features ${{ matrix.backend }} --verbose + cargo check --package rtc-stun --no-default-features --features ${{ matrix.backend }} --verbose + + # TODO(P1-09): add a passing ring + aws-lc-rs job after backend features become additive. + # TODO(P1-09): add a no-built-in-features job with a downstream-style custom provider. + rustfmt_and_clippy: name: Check rustfmt style && run clippy runs-on: ubuntu-latest diff --git a/docs/crypto-provider-baseline.md b/docs/crypto-provider-baseline.md new file mode 100644 index 00000000..f64d5088 --- /dev/null +++ b/docs/crypto-provider-baseline.md @@ -0,0 +1,120 @@ +# Crypto provider baseline + +This document records the crypto behavior and compatibility surface before the `rtc-crypto` provider migration. It is the baseline for G3 and must be updated when a migration intentionally changes an algorithm, preference, public adapter, or test expectation. + +## DTLS + +### Cipher suites + +All current suites use SHA-256 for the TLS 1.2 PRF and Finished calculation. CBC suites additionally use HMAC-SHA1 for record authentication. + +| Preference | Cipher suite | Code point | Record protection | Authentication | +|---:|---|---:|---|---| +| 1 | `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` | `0xc02b` | AES-128-GCM | ECDSA certificate | +| 2 | `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` | `0xc00a` | AES-256-CBC plus HMAC-SHA1 | ECDSA certificate | +| 3 | `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | `0xc02f` | AES-128-GCM | RSA certificate | +| 4 | `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` | `0xc014` | AES-256-CBC plus HMAC-SHA1 | RSA certificate | +| 5 | `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` | `0xcca9` | ChaCha20-Poly1305 | ECDSA certificate | +| Explicit only | `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` | `0xc0ac` | AES-128-CCM, 16-byte tag | ECDSA certificate | +| Explicit only | `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` | `0xc0ae` | AES-128-CCM, 8-byte tag | ECDSA certificate | +| Explicit only | `TLS_PSK_WITH_AES_128_CCM` | `0xc0a4` | AES-128-CCM, 16-byte tag | PSK | +| Explicit only | `TLS_PSK_WITH_AES_128_CCM_8` | `0xc0a8` | AES-128-CCM, 8-byte tag | PSK | +| Explicit only | `TLS_PSK_WITH_AES_128_GCM_SHA256` | `0x00a8` | AES-128-GCM | PSK | +| Explicit only | `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | `0xcca8` | ChaCha20-Poly1305 | RSA certificate | + +The first five rows are the order returned by `default_cipher_suites()`. G3 must preserve this order unless a separate compatibility change explicitly changes it. + +### Named groups + +| Group | Code point | Current use | +|---|---:|---| +| P-256 | `0x0017` | Advertised first | +| P-384 | `0x0018` | Advertised third | +| X25519 | `0x001d` | Advertised second and defined as `DEFAULT_NAMED_CURVE` | + +The provider API therefore needs P-256, P-384, and X25519 key agreement. The current concrete `NamedCurvePrivateKey` remains crate-private. + +### Signatures and key encodings + +The default advertised signature/hash list is ECDSA with SHA-256, SHA-384, and SHA-512; RSA PKCS#1 with SHA-256, SHA-384, and SHA-512; and Ed25519. Current signing supports Ed25519, ECDSA P-256/SHA-256, RSA PKCS#1/SHA-256, and `CustomSigner`. Current verification additionally accepts ECDSA P-384/SHA-384 and RSA PKCS#1/SHA-1, SHA-384, and SHA-512. + +The `SignatureScheme` enum names RSA-PSS and SHA-1 combinations beyond that set, but they are not complete negotiated implementations. They are not part of the initial provider contract. + +| Signature operation | Current verifier input | +|---|---| +| Ed25519 | Raw 32-byte public key | +| ECDSA P-256/SHA-256 | Uncompressed SEC1 point and ASN.1 DER signature | +| ECDSA P-384/SHA-384 | Uncompressed SEC1 point and ASN.1 DER signature | +| RSA PKCS#1/SHA-1, SHA-256, SHA-384, or SHA-512 | PKCS#1 DER `RSAPublicKey` and PKCS#1 v1.5 signature | + +Certificate parsing currently extracts the subject-public-key bit string from X.509. The provider-neutral certificate boundary will instead use complete SPKI DER, with built-in adapters converting it to the operation-specific encodings above. + +## SRTP + +| Protection profile | Master key | Master salt | RTP auth tag | RTCP auth tag | AEAD tag | Auth key | +|---|---:|---:|---:|---:|---:|---:| +| `SRTP_AES128_CM_HMAC_SHA1_80` | 16 | 14 | 10 | 10 | 0 | 20 | +| `SRTP_AES128_CM_HMAC_SHA1_32` | 16 | 14 | 4 | 10 | 0 | 20 | +| `SRTP_AES256_CM_HMAC_SHA1_80` | 32 | 14 | 10 | 10 | 0 | 20 | +| `SRTP_AES256_CM_HMAC_SHA1_32` | 32 | 14 | 4 | 10 | 0 | 20 | +| `SRTP_AEAD_AES_128_GCM` | 16 | 12 | 0 | 0 | 16 | 0 | +| `SRTP_AEAD_AES_256_GCM` | 32 | 12 | 0 | 0 | 16 | 0 | + +Lengths are bytes. DTLS-SRTP negotiation currently exposes AES-128-CM HMAC-SHA1 80/32 and AEAD AES-128/256-GCM; the two AES-256-CM profiles are implemented by `rtc-srtp` but are not currently represented in the DTLS `use_srtp` extension. + +## STUN + +| Operation | Current call site and behavior | +|---|---| +| MD5 | `rtc-stun/src/integrity.rs` derives a long-term credential key from `username:realm:password`. | +| HMAC-SHA1 | `rtc-stun/src/integrity.rs` calculates and verifies `MESSAGE-INTEGRITY`. | +| Constant-time equality | `rtc-stun/src/checks.rs` compares complete integrity values with `subtle::ConstantTimeEq`. | +| Transaction randomness | `TransactionId::new()` and `Message::new_transaction_id()` use `rand::rng().fill` for the 96-bit ID. | + +`ATTR_MESSAGE_INTEGRITY_SHA256` is defined, but the current implementation only implements HMAC-SHA1 `MESSAGE-INTEGRITY`. Adding STUN SHA-256 integrity is not implicit G3 work. `MessageIntegrity` publicly wraps a `Vec`, and its current `Display` implementation exposes the key; migration must replace this with a redacted secret type without changing wire behavior. + +## Public backend-bound compatibility surface + +| Surface | Current dependency | G3 disposition | +|---|---|---| +| `rtc_dtls::crypto::CryptoPrivateKeyKind` | Ring-compatible Ed25519/ECDSA key pairs, RSA key pair, or `CustomSigner` | Compatibility adapter during migration; remove before 1.0. | +| `rtc_dtls::crypto::CryptoPrivateKey` and `Certificate` | Concrete key variants and X.509 parsing | Replace internals with provider-neutral signing keys and SPKI DER. | +| `rtc_dtls::crypto::CustomSigner` | DTLS-specific signing extension | Adapt temporarily to `SigningKey`; remove before 1.0. | +| DTLS `Config` certificate verification | rustls `RootCertStore`, verifier traits, and certificate types | Keep behind an explicit certificate-verification adapter; do not put X.509 policy into `RTCCrypto`. | +| Top-level `RTCCertificate::from_key_pair` | `rcgen::KeyPair` | Keep as a deprecated migration adapter until provider-neutral import/generation lands; remove before 1.0. | +| `rtc_shared::crypto::KeyingMaterialExporter` | DTLS-to-SRTP trait coupling | Replace with byte-oriented keying material and remove. | +| `ring` and `aws-lc-rs` feature guards and aliases | DTLS, SRTP, STUN, ICE, TURN, and top-level RTC | Replace with additive provider selection in P1-09 and remove per-crate direct dependencies after migration. | +| `openssl` and `vendored-openssl` | Partial SRTP AES-CTR implementation only | Deprecate and remove before 1.0; no partial provider will remain. | +| Shared backend error variants | Ring, AWS-LC-RS, and OpenSSL errors in `rtc-shared` | Map provider failures at protocol boundaries, then remove backend-specific variants. | + +## Initial provider operation-to-caller map + +Only operations with a current caller belong in the initial trait. Algorithm identifiers used inside a signature or HMAC operation do not require an equivalent standalone hash operation. + +| Provider operation | Initial algorithms | Current callers | +|---|---|---| +| Hash | MD5, SHA-256 | STUN long-term credentials; RTC fingerprints and DTLS transcript/PRF composition. | +| HMAC | SHA-1, SHA-256 | STUN and SRTP authentication and DTLS CBC record MAC; DTLS TLS 1.2 PRF. | +| Constant-time equality | Byte slices | STUN, SRTP, and DTLS authentication checks. | +| Random bytes | CSPRNG bytes | DTLS protocol randoms and key generation; optional STUN transaction-ID generation where provider propagation is practical. | +| AES block encryption | AES-128, AES-256 | SRTP key derivation. | +| Stream cipher | AES-128-CTR, AES-256-CTR | SRTP AES-CM profiles. | +| AEAD | AES-128-GCM, AES-256-GCM, AES-128-CCM, AES-128-CCM-8, ChaCha20-Poly1305 | DTLS record protection and SRTP AEAD profiles. | +| CBC | AES-256-CBC | DTLS CBC record protection. | +| Key agreement | P-256, P-384, X25519 | DTLS ECDHE. | +| Signature verification | Ed25519; ECDSA P-256/SHA-256 and P-384/SHA-384; RSA PKCS#1/SHA-1, SHA-256, SHA-384, SHA-512 | DTLS certificate authentication. | +| Signing | Ed25519, ECDSA P-256/SHA-256, RSA PKCS#1/SHA-256 | DTLS CertificateVerify. | +| Signing-key generation | Ed25519, ECDSA P-256 | Current certificate generation paths. | +| Signing-key import | Ed25519, ECDSA P-256, RSA | Current PKCS#8/key-pair import paths. | + +Standalone SHA-1, SHA-384, and SHA-512 hashing has no current caller and is excluded from the initial `HashAlgorithm`. RSA-PSS is also excluded until an independently tested protocol requirement exists. + +## Existing and missing validation + +| Area | Existing baseline | Explicit follow-up gap | +|---|---|---| +| DTLS | Cipher-suite unit tests, handshake tests, key-exchange/signature tests, and repository interoperability coverage | Provider-level vectors for every primitive and cross-provider DTLS handshakes belong to P1-08 and P6-06. | +| SRTP AES-CM | RFC 3711 Appendix B.3 AES-128 derivation vector and RFC 6188 section 7.2 AES-256 derivation vector; RTP/RTCP round trips, replay, rollover, and bad-auth tests | Cross-provider packets for every profile belong to P1-08/P4; retain exact ciphertext and tag fixtures during migration. | +| SRTP AEAD | AES-128-GCM and AES-256-GCM tests, including RFC 7714-derived layouts and RTP/RTCP round trips | Consolidated published known-answer fixtures for every AEAD packet shape belong to P1-08/P4. | +| STUN | Long-term MD5 key expectations, HMAC behavior, tamper rejection, fingerprint ordering, and message round trips | Add RFC 5769 full-message vectors and cross-provider equality in P1-08/P2. | +| Browser/interoperability | Existing repository integration workflows | Add a provider-by-provider DTLS-SRTP browser/interoperability matrix in P7-03/P8-02. | diff --git a/docs/crypto-provider-decisions.md b/docs/crypto-provider-decisions.md new file mode 100644 index 00000000..f04f3ab0 --- /dev/null +++ b/docs/crypto-provider-decisions.md @@ -0,0 +1,44 @@ +# Crypto provider migration decisions + +These decisions freeze the boundaries needed to start G3 implementation. Reopening one requires an explicit design change because provider implementations and protocol migrations depend on them. + +## SRTP OpenSSL features + +The `openssl` and `vendored-openssl` features are partial SRTP implementation choices, not complete RTC crypto providers. They will be deprecated during the migration and removed before 1.0 in P4-04. A future OpenSSL provider is possible only as a complete downstream implementation of the public provider traits that passes the same conformance suite as the built-in providers; it will not preserve these partial features by accident. + +The deprecation must be called out in the changelog when it begins, and removal must be included in the pre-1.0 migration guide. + +## Crypto errors and protocol boundaries + +`rtc-crypto` owns a backend-neutral, non-exhaustive `CryptoError`. The initial variants are `NoDefaultProvider`, `UnsupportedAlgorithm(CryptoAlgorithm)`, `InvalidKeyLength { expected, actual }`, `InvalidNonceLength { expected, actual }`, `InvalidTagLength { expected, actual }`, `InvalidPublicKey`, `InvalidPrivateKey`, `AuthenticationFailed`, `InvalidSignature`, `RandomnessFailed`, `OutputTooSmall { required, actual }`, and `Provider(String)`. Authentication, tag, and padding failures from decryption converge on `AuthenticationFailed`; signature verification uses `InvalidSignature`. These failures must not expose backend-specific distinctions that could become an oracle. + +Protocol crates convert `CryptoError` at their own boundary. `rtc-shared` must not depend on `rtc-crypto`, and G3 will not introduce that dependency merely to obtain an automatic `From` conversion. During migration, each call site maps a provider failure to the protocol's existing semantic error where one exists; otherwise it maps to a backend-neutral shared crypto error without retaining the backend error type. This mapping is explicit rather than a blanket conversion. + +The string in `Provider(String)` is sanitized local diagnostic context, is not a stable matching surface, and may be logged at trace level. It must not be serialized into packets, alerts, or peer-visible protocol messages. All other variants use stable provider-neutral wording. Secret material, keys, plaintext, nonces, tags, and complete signatures are never included in `Debug`, `Display`, or error sources. + +The existing shared public error type remains available during G3 to avoid mixing a workspace-wide error redesign into provider migration. Backend-specific shared variants are removed in P7-01. Moving protocols to crate-local error types can be considered separately and is not required to finish G3. + +## Public keys and certificate adapters + +The initial non-exhaustive `PublicKeyEncoding` distinguishes these encodings: + +| Variant | Encoding | +|---|---| +| `SubjectPublicKeyInfoDer` | Complete DER-encoded SubjectPublicKeyInfo | +| `EcUncompressedPoint` | SEC1 uncompressed P-256 or P-384 point; the signature scheme determines the curve | +| `Ed25519Raw` | Raw 32-byte Ed25519 public key | +| `RsaPkcs1Der` | PKCS#1 DER `RSAPublicKey` | + +The canonical certificate-facing public-key boundary is complete DER-encoded SubjectPublicKeyInfo. Certificate parsing and policy remain outside `RTCCrypto`; adapters parse SPKI and construct the operation-specific `PublicKey` passed to signature verification. This preserves an unambiguous public boundary while matching the encodings accepted by current providers. + +A `SigningKey` may be non-exportable. Public-key access and signing are mandatory; `to_pkcs8_der()` returns `Ok(None)` for a non-exportable key. PEM/private-key serialization built on that operation returns an explicit adapter error rather than fabricating bytes. Key equality or certificate identity must use public material or a stable provider-neutral identifier, never private-key export. + +The current `rcgen::KeyPair`, `CryptoPrivateKeyKind`, and `CustomSigner` APIs remain only as migration adapters. Provider-neutral key generation/import and signing land first; the old adapters are then deprecated and removed before 1.0 in P7-02. Internal DTLS named-curve private-key types are replaced by provider-owned active key-exchange objects and do not become public compatibility APIs. + +rustls/webpki certificate-chain verification remains an explicitly separate adapter and policy layer. `RTCCrypto` performs cryptographic primitives and signing-key operations; it does not own trust stores, certificate path building, hostname validation, revocation policy, or application identity policy. + +## Feature and provider selection invariants + +`ring` remains the default built-in provider. `ring` and `aws-lc-rs` become additive in P1-09: enabling both compiles both and does not silently select AWS-LC-RS. Construction resolves the chosen provider once and protocol objects store an `Arc`; packet paths do not read a mutable global or repeatedly resolve defaults. + +The pre-migration workspace intentionally rejects both built-ins together and cannot yet build the full RTC stack without one. Baseline CI names those two future configurations but does not pretend they pass. P1-09 replaces the placeholders with passing both-provider and no-built-in/custom-provider jobs. From b8bb313ac86fddb8f8abd28efd784a52c9a322c6 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 17:53:04 -0700 Subject: [PATCH 30/40] =?UTF-8?q?P1=20=E2=80=94=20Build=20rtc-crypto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cargo.yml | 35 +- Cargo.toml | 3 + README.md | 1 + docs/crypto-provider-decisions.md | 2 +- rtc-crypto/Cargo.toml | 32 ++ rtc-crypto/README.md | 19 + rtc-crypto/src/algorithm.rs | 155 +++++ rtc-crypto/src/common.rs | 344 +++++++++++ rtc-crypto/src/conformance.rs | 539 ++++++++++++++++++ rtc-crypto/src/error.rs | 63 ++ rtc-crypto/src/lib.rs | 86 +++ rtc-crypto/src/provider.rs | 24 + rtc-crypto/src/providers/aws_lc_rs.rs | 524 +++++++++++++++++ rtc-crypto/src/providers/mod.rs | 9 + rtc-crypto/src/providers/ring.rs | 538 +++++++++++++++++ rtc-crypto/src/secret.rs | 66 +++ rtc-crypto/src/traits.rs | 224 ++++++++ rtc-crypto/tests/conformance.rs | 13 + rtc-crypto/tests/cross_provider.rs | 171 ++++++ rtc-crypto/tests/custom_provider.rs | 76 +++ rtc-crypto/tests/data/rsa-2048.pkcs8.pem | 28 + rtc-crypto/tests/default_provider.rs | 11 + rtc-crypto/tests/rsa_import.rs | 40 ++ .../receiver_estimated_maximum_bitrate/mod.rs | 2 +- src/peer_connection/handler/sctp.rs | 2 +- 25 files changed, 3002 insertions(+), 5 deletions(-) create mode 100644 rtc-crypto/Cargo.toml create mode 100644 rtc-crypto/README.md create mode 100644 rtc-crypto/src/algorithm.rs create mode 100644 rtc-crypto/src/common.rs create mode 100644 rtc-crypto/src/conformance.rs create mode 100644 rtc-crypto/src/error.rs create mode 100644 rtc-crypto/src/lib.rs create mode 100644 rtc-crypto/src/provider.rs create mode 100644 rtc-crypto/src/providers/aws_lc_rs.rs create mode 100644 rtc-crypto/src/providers/mod.rs create mode 100644 rtc-crypto/src/providers/ring.rs create mode 100644 rtc-crypto/src/secret.rs create mode 100644 rtc-crypto/src/traits.rs create mode 100644 rtc-crypto/tests/conformance.rs create mode 100644 rtc-crypto/tests/cross_provider.rs create mode 100644 rtc-crypto/tests/custom_provider.rs create mode 100644 rtc-crypto/tests/data/rsa-2048.pkcs8.pem create mode 100644 rtc-crypto/tests/default_provider.rs create mode 100644 rtc-crypto/tests/rsa_import.rs diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 759aaffb..7f40f246 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -58,8 +58,39 @@ jobs: cargo check --package rtc-srtp --no-default-features --features ${{ matrix.backend }} --verbose cargo check --package rtc-stun --no-default-features --features ${{ matrix.backend }} --verbose - # TODO(P1-09): add a passing ring + aws-lc-rs job after backend features become additive. - # TODO(P1-09): add a no-built-in-features job with a downstream-style custom provider. + rtc_crypto_provider_matrix: + name: rtc-crypto (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + args: --features test-support + - name: ring + args: --no-default-features --features ring,test-support + - name: aws-lc-rs + args: --no-default-features --features aws-lc-rs,test-support + - name: ring + aws-lc-rs + args: --no-default-features --features ring,aws-lc-rs,test-support + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test provider configuration + run: cargo test --package rtc-crypto ${{ matrix.args }} --verbose + + rtc_crypto_custom_provider: + name: rtc-crypto (no built-in, custom provider) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test downstream-style provider + run: cargo test --package rtc-crypto --no-default-features --features test-support --test custom_provider --verbose + - name: Verify publishable package + run: cargo package --package rtc-crypto --no-default-features rustfmt_and_clippy: name: Check rustfmt style && run clippy diff --git a/Cargo.toml b/Cargo.toml index 152e230c..4f87da46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "rtc-crypto", "rtc-datachannel", "rtc-dtls", "rtc-ice", @@ -32,6 +33,7 @@ keywords = ["sansio", "networking", "protocols"] categories = ["network-programming"] [workspace.dependencies] +crypto = { version = "0.21.0", path = "rtc-crypto", package = "rtc-crypto", default-features = false } datachannel = { version = "0.21.0", path = "rtc-datachannel", package = "rtc-datachannel" } dtls = { version = "0.21.0", path = "rtc-dtls", package = "rtc-dtls", default-features = false } ice = { version = "0.21.0", path = "rtc-ice", package = "rtc-ice", default-features = false } @@ -59,6 +61,7 @@ aws-lc-rs = { version = "1.17.3", features = ["aws-lc-sys"], default-features = rand = "0.10.1" serde = { version = "1.0.228", features = ["derive"] } thiserror = "2.0.18" +zeroize = "1.8.2" # dev dependencies env_logger = "0.11.11" diff --git a/README.md b/README.md index f3e1f1ea..166f2176 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,7 @@ RTC is built from composable crates, each implementing a specific protocol: SCTP
DTLS + Crypto
mDNS STUN diff --git a/docs/crypto-provider-decisions.md b/docs/crypto-provider-decisions.md index f04f3ab0..03db73ea 100644 --- a/docs/crypto-provider-decisions.md +++ b/docs/crypto-provider-decisions.md @@ -41,4 +41,4 @@ rustls/webpki certificate-chain verification remains an explicitly separate adap `ring` remains the default built-in provider. `ring` and `aws-lc-rs` become additive in P1-09: enabling both compiles both and does not silently select AWS-LC-RS. Construction resolves the chosen provider once and protocol objects store an `Arc`; packet paths do not read a mutable global or repeatedly resolve defaults. -The pre-migration workspace intentionally rejects both built-ins together and cannot yet build the full RTC stack without one. Baseline CI names those two future configurations but does not pretend they pass. P1-09 replaces the placeholders with passing both-provider and no-built-in/custom-provider jobs. +The protocol crates still intentionally reject both built-ins together and cannot yet build the full RTC stack without one while their direct backend calls remain. P1-09 makes these configurations pass for `rtc-crypto`; P2 through P6 migrate the consumers, and P7 adds the corresponding full-workspace matrix. This sequencing keeps the new provider contract independently testable without claiming that unmigrated protocol crates are already additive. diff --git a/rtc-crypto/Cargo.toml b/rtc-crypto/Cargo.toml new file mode 100644 index 00000000..5b1474cc --- /dev/null +++ b/rtc-crypto/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rtc-crypto" +version.workspace = true +authors.workspace = true +edition.workspace = true +description = "Provider-neutral cryptography for the webrtc-rs RTC stack" +license.workspace = true +documentation = "https://docs.rs/rtc-crypto" +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +readme = "README.md" + +[features] +default = ["ring"] +ring = ["dep:ring", "dep:aes", "dep:ccm", "dep:md-5"] +aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ccm", "dep:md-5"] +test-support = [] + +[dependencies] +aes = { version = "0.8.4", optional = true } +ccm = { version = "0.5.0", optional = true } +md-5 = { version = "0.10.6", optional = true } +subtle = "2.6.1" +thiserror.workspace = true +zeroize.workspace = true +ring = { workspace = true, optional = true } +aws-lc-rs = { workspace = true, optional = true } + +[dev-dependencies] +pem = "3.0.3" diff --git a/rtc-crypto/README.md b/rtc-crypto/README.md new file mode 100644 index 00000000..e74880b2 --- /dev/null +++ b/rtc-crypto/README.md @@ -0,0 +1,19 @@ +# rtc-crypto + +`rtc-crypto` provides the provider-neutral cryptographic operations used by the webrtc-rs RTC stack. Applications can use a built-in Ring or AWS-LC-RS provider, or implement the public provider traits without registration or sealing. + +The crate owns primitives and opaque keyed state. DTLS, SRTP, STUN, certificate policy, and wire-format composition remain in their protocol crates. + +Enable `test-support` to run the reusable provider conformance suite from a downstream provider's tests. + +```rust +#[test] +fn provider_conforms() { + let provider = MyProvider::new(); + rtc_crypto::conformance::assert_provider(&provider); +} +``` + +Partial providers can invoke the public operation-family helpers that match their advertised capabilities. The feature adds no built-in backend, so a downstream provider can test with `default-features = false, features = ["test-support"]`. + +The `ring` and `aws-lc-rs` features are additive. Ring is the default when enabled, including builds that enable both backends. With neither backend enabled, applications construct their own `Arc` and `default_provider()` returns `CryptoError::NoDefaultProvider`. diff --git a/rtc-crypto/src/algorithm.rs b/rtc-crypto/src/algorithm.rs new file mode 100644 index 00000000..704e1a29 --- /dev/null +++ b/rtc-crypto/src/algorithm.rs @@ -0,0 +1,155 @@ +/// Algorithms accepted by [`crate::RTCCrypto::hash`]. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HashAlgorithm { + /// MD5, retained only for STUN long-term credential derivation. + Md5, + /// SHA-256. + Sha256, +} + +/// Algorithms accepted by the HMAC operations. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HmacAlgorithm { + /// HMAC-SHA1. + Sha1, + /// HMAC-SHA256. + Sha256, +} + +impl HmacAlgorithm { + /// Returns the native tag length in bytes. + #[must_use] + pub const fn output_len(self) -> usize { + match self { + Self::Sha1 => 20, + Self::Sha256 => 32, + } + } +} + +/// Authenticated-encryption algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AeadAlgorithm { + /// AES-128-GCM. + Aes128Gcm, + /// AES-256-GCM. + Aes256Gcm, + /// AES-128-CCM with a 16-byte tag. + Aes128Ccm, + /// AES-128-CCM with an 8-byte tag. + Aes128Ccm8, + /// ChaCha20-Poly1305. + ChaCha20Poly1305, +} + +/// Stream-cipher algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StreamCipherAlgorithm { + /// AES-128 in counter mode. + Aes128Ctr, + /// AES-256 in counter mode. + Aes256Ctr, +} + +/// Single-block encryption algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BlockCipherAlgorithm { + /// AES-128. + Aes128, + /// AES-256. + Aes256, +} + +/// CBC algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CbcAlgorithm { + /// AES-256-CBC. + Aes256Cbc, +} + +/// Ephemeral key-agreement algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyExchangeAlgorithm { + /// ECDH over NIST P-256. + P256, + /// ECDH over NIST P-384. + P384, + /// X25519. + X25519, +} + +/// Signature schemes currently used by DTLS. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignatureScheme { + /// Ed25519. + Ed25519, + /// ECDSA P-256 with SHA-256 and ASN.1 DER signatures. + EcdsaP256Sha256, + /// ECDSA P-384 with SHA-384 and ASN.1 DER signatures. + EcdsaP384Sha384, + /// RSA PKCS#1 v1.5 with SHA-1, for legacy verification only. + RsaPkcs1Sha1, + /// RSA PKCS#1 v1.5 with SHA-256. + RsaPkcs1Sha256, + /// RSA PKCS#1 v1.5 with SHA-384. + RsaPkcs1Sha384, + /// RSA PKCS#1 v1.5 with SHA-512. + RsaPkcs1Sha512, +} + +/// The encoding of public-key bytes. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PublicKeyEncoding { + /// Complete DER-encoded SubjectPublicKeyInfo. + SubjectPublicKeyInfoDer, + /// SEC1 uncompressed elliptic-curve point. + EcUncompressedPoint, + /// Raw 32-byte Ed25519 public key. + Ed25519Raw, + /// PKCS#1 DER `RSAPublicKey`. + RsaPkcs1Der, +} + +/// Borrowed public-key bytes with an explicit encoding. +#[derive(Debug, Clone, Copy)] +pub struct PublicKey<'a> { + /// Encoding of `bytes`. + pub encoding: PublicKeyEncoding, + /// Encoded public key. + pub bytes: &'a [u8], +} + +/// A provider capability identifier. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CryptoAlgorithm { + /// Stateless hash operation. + Hash(HashAlgorithm), + /// HMAC generation and verification. + Hmac(HmacAlgorithm), + /// Authenticated encryption. + Aead(AeadAlgorithm), + /// Stream encryption. + StreamCipher(StreamCipherAlgorithm), + /// Single-block encryption. + BlockCipher(BlockCipherAlgorithm), + /// CBC encryption and decryption. + Cbc(CbcAlgorithm), + /// Ephemeral key agreement. + KeyExchange(KeyExchangeAlgorithm), + /// Signature verification. + Signature(SignatureScheme), + /// Signing-key generation. + SigningKeyGeneration(SignatureScheme), + /// PKCS#8 signing-key import. + SigningKeyImport(SignatureScheme), +} diff --git a/rtc-crypto/src/common.rs b/rtc-crypto/src/common.rs new file mode 100644 index 00000000..2745198d --- /dev/null +++ b/rtc-crypto/src/common.rs @@ -0,0 +1,344 @@ +use aes::cipher::generic_array::GenericArray; +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; +use aes::{Aes128, Aes256}; +use ccm::Ccm; +use ccm::aead::AeadInPlace; +use ccm::consts::{U8, U12, U16}; +use md5::{Digest, Md5}; + +use crate::{ + AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoError, + StreamCipher, StreamCipherAlgorithm, +}; + +const AES_BLOCK_LEN: usize = 16; +const CCM_NONCE_LEN: usize = 12; + +type Aes128Ccm = Ccm; +type Aes128Ccm8 = Ccm; + +pub(crate) fn md5(data: &[u8]) -> Vec { + Md5::digest(data).to_vec() +} + +pub(crate) fn block_encrypt( + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], +) -> Result<(), CryptoError> { + check_len(AES_BLOCK_LEN, block.len(), LengthKind::Output)?; + match algorithm { + BlockCipherAlgorithm::Aes128 => { + check_key_len(16, key.len())?; + Aes128::new_from_slice(key) + .map_err(|_| invalid_key(16, key.len()))? + .encrypt_block(GenericArray::from_mut_slice(block)); + } + BlockCipherAlgorithm::Aes256 => { + check_key_len(32, key.len())?; + Aes256::new_from_slice(key) + .map_err(|_| invalid_key(32, key.len()))? + .encrypt_block(GenericArray::from_mut_slice(block)); + } + } + Ok(()) +} + +pub(crate) fn new_stream_cipher( + algorithm: StreamCipherAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + let key = match algorithm { + StreamCipherAlgorithm::Aes128Ctr => ExpandedAesKey::new_128(key)?, + StreamCipherAlgorithm::Aes256Ctr => ExpandedAesKey::new_256(key)?, + }; + Ok(Box::new(AesCtr { key })) +} + +pub(crate) fn new_cbc( + algorithm: CbcAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + match algorithm { + CbcAlgorithm::Aes256Cbc => Ok(Box::new(AesCbc { + key: ExpandedAesKey::new_256(key)?, + })), + } +} + +pub(crate) fn new_ccm( + algorithm: AeadAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + check_key_len(16, key.len())?; + let cipher = match algorithm { + AeadAlgorithm::Aes128Ccm => { + CommonCcm::Full(Aes128Ccm::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?) + } + AeadAlgorithm::Aes128Ccm8 => CommonCcm::Short( + Aes128Ccm8::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + crate::CryptoAlgorithm::Aead(algorithm), + )); + } + }; + Ok(Box::new(cipher)) +} + +// Both variants store an expanded key inside an already boxed cipher object. Keeping them inline +// avoids another allocation in every constructed state object. +#[allow(clippy::large_enum_variant)] +enum ExpandedAesKey { + Aes128(Aes128), + Aes256(Aes256), +} + +impl ExpandedAesKey { + fn new_128(key: &[u8]) -> Result { + check_key_len(16, key.len())?; + Ok(Self::Aes128( + Aes128::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?, + )) + } + + fn new_256(key: &[u8]) -> Result { + check_key_len(32, key.len())?; + Ok(Self::Aes256( + Aes256::new_from_slice(key).map_err(|_| invalid_key(32, key.len()))?, + )) + } + + fn encrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { + match self { + Self::Aes128(cipher) => cipher.encrypt_block(GenericArray::from_mut_slice(block)), + Self::Aes256(cipher) => cipher.encrypt_block(GenericArray::from_mut_slice(block)), + } + } + + fn decrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { + match self { + Self::Aes128(cipher) => cipher.decrypt_block(GenericArray::from_mut_slice(block)), + Self::Aes256(cipher) => cipher.decrypt_block(GenericArray::from_mut_slice(block)), + } + } +} + +struct AesCtr { + key: ExpandedAesKey, +} + +impl StreamCipher for AesCtr { + fn apply_keystream(&mut self, iv: &[u8], data: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + let mut counter: [u8; AES_BLOCK_LEN] = iv + .try_into() + .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; + + for chunk in data.chunks_mut(AES_BLOCK_LEN) { + let mut stream_block = counter; + self.key.encrypt(&mut stream_block); + for (byte, mask) in chunk.iter_mut().zip(stream_block) { + *byte ^= mask; + } + increment_be(&mut counter); + } + Ok(()) + } +} + +struct AesCbc { + key: ExpandedAesKey, +} + +impl CbcCipher for AesCbc { + fn block_len(&self) -> usize { + AES_BLOCK_LEN + } + + fn encrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + check_blocks(blocks)?; + let mut previous: [u8; AES_BLOCK_LEN] = iv + .try_into() + .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; + + for chunk in blocks.chunks_exact_mut(AES_BLOCK_LEN) { + for (byte, prior) in chunk.iter_mut().zip(previous) { + *byte ^= prior; + } + let block: &mut [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + self.key.encrypt(block); + previous.copy_from_slice(block); + } + Ok(()) + } + + fn decrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + check_blocks(blocks)?; + let mut previous: [u8; AES_BLOCK_LEN] = iv + .try_into() + .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; + + for chunk in blocks.chunks_exact_mut(AES_BLOCK_LEN) { + let ciphertext: [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + let block: &mut [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + self.key.decrypt(block); + for (byte, prior) in block.iter_mut().zip(previous) { + *byte ^= prior; + } + previous = ciphertext; + } + Ok(()) + } +} + +enum CommonCcm { + Full(Aes128Ccm), + Short(Aes128Ccm8), +} + +impl AeadCipher for CommonCcm { + fn tag_len(&self) -> usize { + match self { + Self::Full(_) => 16, + Self::Short(_) => 8, + } + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + check_nonce_len(CCM_NONCE_LEN, nonce.len())?; + check_tag_len(self.tag_len(), tag_out.len())?; + match self { + Self::Full(cipher) => { + let tag = cipher + .encrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + plaintext_and_ciphertext, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(&tag); + } + Self::Short(cipher) => { + let tag = cipher + .encrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + plaintext_and_ciphertext, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(&tag); + } + } + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + check_nonce_len(CCM_NONCE_LEN, nonce.len())?; + check_tag_len(self.tag_len(), tag.len())?; + match self { + Self::Full(cipher) => cipher + .decrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + ciphertext_and_plaintext, + GenericArray::from_slice(tag), + ) + .map_err(|_| CryptoError::AuthenticationFailed), + Self::Short(cipher) => cipher + .decrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + ciphertext_and_plaintext, + GenericArray::from_slice(tag), + ) + .map_err(|_| CryptoError::AuthenticationFailed), + } + } +} + +fn increment_be(counter: &mut [u8; AES_BLOCK_LEN]) { + for byte in counter.iter_mut().rev() { + let (next, overflow) = byte.overflowing_add(1); + *byte = next; + if !overflow { + break; + } + } +} + +fn check_blocks(blocks: &[u8]) -> Result<(), CryptoError> { + if blocks.is_empty() || !blocks.len().is_multiple_of(AES_BLOCK_LEN) { + return Err(CryptoError::OutputTooSmall { + required: blocks + .len() + .next_multiple_of(AES_BLOCK_LEN) + .max(AES_BLOCK_LEN), + actual: blocks.len(), + }); + } + Ok(()) +} + +enum LengthKind { + Output, +} + +fn check_len(expected: usize, actual: usize, kind: LengthKind) -> Result<(), CryptoError> { + if expected == actual { + return Ok(()); + } + match kind { + LengthKind::Output => Err(CryptoError::OutputTooSmall { + required: expected, + actual, + }), + } +} + +pub(crate) fn check_key_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(invalid_key(expected, actual)) + } +} + +pub(crate) fn check_nonce_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(invalid_nonce(expected, actual)) + } +} + +pub(crate) fn check_tag_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(CryptoError::InvalidTagLength { expected, actual }) + } +} + +fn invalid_key(expected: usize, actual: usize) -> CryptoError { + CryptoError::InvalidKeyLength { expected, actual } +} + +fn invalid_nonce(expected: usize, actual: usize) -> CryptoError { + CryptoError::InvalidNonceLength { expected, actual } +} diff --git a/rtc-crypto/src/conformance.rs b/rtc-crypto/src/conformance.rs new file mode 100644 index 00000000..e9236010 --- /dev/null +++ b/rtc-crypto/src/conformance.rs @@ -0,0 +1,539 @@ +//! Reusable conformance assertions for built-in and application-provided crypto providers. + +use crate::{ + AeadAlgorithm, BlockCipherAlgorithm, CbcAlgorithm, CryptoAlgorithm, CryptoError, HashAlgorithm, + HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, PublicKeyEncoding, RTCCrypto, + RTCCryptoProvider, SignatureScheme, StreamCipherAlgorithm, +}; + +/// Exercises the complete initial RTC crypto contract implemented by the built-in providers. +/// +/// This helper panics on a contract violation so provider authors can call it directly from a +/// normal `#[test]` function. +pub fn assert_provider(provider: &dyn RTCCryptoProvider) { + assert_basic_capabilities(provider.crypto()); + assert_hashes_and_hmac(provider.crypto()); + assert_block_and_stream_ciphers(provider.crypto()); + assert_cbc(provider.crypto()); + assert_aead(provider.crypto()); + assert_key_exchange(provider.crypto()); + assert_signatures(provider.crypto()); + assert_random(provider); +} + +fn assert_basic_capabilities(crypto: &dyn RTCCrypto) { + let capabilities = [ + CryptoAlgorithm::Hash(HashAlgorithm::Md5), + CryptoAlgorithm::Hash(HashAlgorithm::Sha256), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha256), + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes128Ctr), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes256Ctr), + CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes256Gcm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm8), + CryptoAlgorithm::Aead(AeadAlgorithm::ChaCha20Poly1305), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::P256), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::P384), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::X25519), + CryptoAlgorithm::Signature(SignatureScheme::Ed25519), + CryptoAlgorithm::Signature(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::Signature(SignatureScheme::EcdsaP384Sha384), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha1), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha256), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha384), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha512), + CryptoAlgorithm::SigningKeyGeneration(SignatureScheme::Ed25519), + CryptoAlgorithm::SigningKeyGeneration(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::Ed25519), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::RsaPkcs1Sha256), + ]; + for capability in capabilities { + assert!( + crypto.supports(capability), + "missing capability: {capability:?}" + ); + } + assert!(!crypto.supports(CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::RsaPkcs1Sha256 + ))); + assert!(matches!( + crypto.generate_signing_key(SignatureScheme::RsaPkcs1Sha256), + Err(CryptoError::UnsupportedAlgorithm(_)) + )); + for scheme in [ + SignatureScheme::Ed25519, + SignatureScheme::EcdsaP256Sha256, + SignatureScheme::RsaPkcs1Sha256, + ] { + assert!(matches!( + crypto.import_signing_key(scheme, b"not a PKCS#8 key"), + Err(CryptoError::InvalidPrivateKey) + )); + } +} + +/// Checks hash and HMAC known-answer vectors and their error contract. +pub fn assert_hashes_and_hmac(crypto: &dyn RTCCrypto) { + // RFC 1321, FIPS 180-4, RFC 2202, and RFC 4231 known-answer vectors. + assert_eq!( + crypto.hash(HashAlgorithm::Md5, b"abc").unwrap(), + bytes("900150983cd24fb0d6963f7d28e17f72") + ); + assert_eq!( + crypto.hash(HashAlgorithm::Sha256, b"abc").unwrap(), + bytes("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + ); + + let key = [0x0b; 20]; + let mut sha1 = [0; 20]; + crypto + .hmac(HmacAlgorithm::Sha1, &key, &[b"Hi ", b"There"], &mut sha1) + .unwrap(); + assert_eq!( + sha1.as_slice(), + bytes("b617318655057264e28bc0b6fb378c8ef146be00") + ); + + let mut sha256 = [0; 32]; + crypto + .hmac( + HmacAlgorithm::Sha256, + &key, + &[b"Hi ", b"There"], + &mut sha256, + ) + .unwrap(); + assert_eq!( + sha256.as_slice(), + bytes("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") + ); + crypto + .verify_hmac(HmacAlgorithm::Sha256, &key, &[b"Hi There"], &sha256) + .unwrap(); + let mut bad_tag = sha256; + bad_tag[0] ^= 1; + assert_eq!( + crypto.verify_hmac(HmacAlgorithm::Sha256, &key, &[b"Hi There"], &bad_tag), + Err(CryptoError::AuthenticationFailed) + ); + assert!(matches!( + crypto.hmac(HmacAlgorithm::Sha256, &key, &[b"x"], &mut [0; 31]), + Err(CryptoError::InvalidTagLength { .. }) + )); +} + +/// Checks AES block and stream-cipher known-answer vectors and malformed inputs. +pub fn assert_block_and_stream_ciphers(crypto: &dyn RTCCrypto) { + // FIPS 197 and NIST SP 800-38A known-answer vectors. + let mut block = bytes("00112233445566778899aabbccddeeff"); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes128, + &bytes("000102030405060708090a0b0c0d0e0f"), + &mut block, + ) + .unwrap(); + assert_eq!(block, bytes("69c4e0d86a7b0430d8cdb78070b4c55a")); + + let mut block = bytes("00112233445566778899aabbccddeeff"); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes256, + &bytes("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"), + &mut block, + ) + .unwrap(); + assert_eq!(block, bytes("8ea2b7ca516745bfeafc49904b496089")); + + let key = bytes("2b7e151628aed2a6abf7158809cf4f3c"); + let iv = bytes("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + let plaintext = bytes("6bc1bee22e409f96e93d7e117393172a"); + let mut encrypted = plaintext.clone(); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, bytes("874d6191b620e3261bef6864990db6ce")); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, plaintext); + + let key = bytes("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + let mut encrypted = plaintext.clone(); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes256Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, bytes("601ec313775789a5b7a7f504bbf3d228")); + + assert!(matches!( + crypto.block_encrypt(BlockCipherAlgorithm::Aes128, &[0; 15], &mut [0; 16]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + crypto.block_encrypt(BlockCipherAlgorithm::Aes128, &[0; 16], &mut [0; 15]), + Err(CryptoError::OutputTooSmall { .. }) + )); + assert!(matches!( + crypto.new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &[0; 15]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + let mut stream = crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &[0; 16]) + .unwrap(); + assert!(matches!( + stream.apply_keystream(&[0; 15], &mut [0; 1]), + Err(CryptoError::InvalidNonceLength { .. }) + )); +} + +/// Checks AES-CBC known-answer vectors and malformed inputs. +pub fn assert_cbc(crypto: &dyn RTCCrypto) { + // NIST SP 800-38A F.2.5. + let key = bytes("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + let iv = bytes("000102030405060708090a0b0c0d0e0f"); + let plaintext = bytes("6bc1bee22e409f96e93d7e117393172a"); + let mut blocks = plaintext.clone(); + let mut cipher = crypto.new_cbc(CbcAlgorithm::Aes256Cbc, &key).unwrap(); + assert_eq!(cipher.block_len(), 16); + cipher.encrypt_blocks(&iv, &mut blocks).unwrap(); + assert_eq!(blocks, bytes("f58c4c04d6e5f1ba779eabfb5f7bfbd6")); + cipher.decrypt_blocks(&iv, &mut blocks).unwrap(); + assert_eq!(blocks, plaintext); + + assert!(matches!( + crypto.new_cbc(CbcAlgorithm::Aes256Cbc, &[0; 31]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + cipher.encrypt_blocks(&[0; 15], &mut [0; 16]), + Err(CryptoError::InvalidNonceLength { .. }) + )); + assert!(matches!( + cipher.encrypt_blocks(&[0; 16], &mut []), + Err(CryptoError::OutputTooSmall { .. }) + )); + assert!(matches!( + cipher.decrypt_blocks(&[0; 16], &mut [0; 15]), + Err(CryptoError::OutputTooSmall { .. }) + )); +} + +/// Checks AEAD known-answer vectors, round trips, authentication failures, and malformed sizes. +pub fn assert_aead(crypto: &dyn RTCCrypto) { + // NIST SP 800-38D, NIST SP 800-38C, and RFC 8439 known-answer vectors. + let mut gcm = crypto.new_aead(AeadAlgorithm::Aes128Gcm, &[0; 16]).unwrap(); + let mut block = vec![0; 16]; + let mut tag = vec![0; gcm.tag_len()]; + gcm.seal_in_place(&[0; 12], &[], &mut block, &mut tag) + .unwrap(); + assert_eq!(block, bytes("0388dace60b6a392f328c2b971b2fe78")); + assert_eq!(tag, bytes("ab6e47d42cec13bdf53a67b21257bddf")); + gcm.open_in_place(&[0; 12], &[], &mut block, &tag).unwrap(); + assert_eq!(block, vec![0; 16]); + + let mut gcm = crypto.new_aead(AeadAlgorithm::Aes256Gcm, &[0; 32]).unwrap(); + let mut block = vec![0; 16]; + let mut tag = vec![0; gcm.tag_len()]; + gcm.seal_in_place(&[0; 12], &[], &mut block, &mut tag) + .unwrap(); + assert_eq!(block, bytes("cea7403d4d606b6e074ec5d3baf39d18")); + assert_eq!(tag, bytes("d0d1c8a799996bf0265b98b5d48ab919")); + + let mut ccm8 = crypto + .new_aead( + AeadAlgorithm::Aes128Ccm8, + &bytes("404142434445464748494a4b4c4d4e4f"), + ) + .unwrap(); + let mut ccm8_buffer = bytes("202122232425262728292a2b2c2d2e2f3031323334353637"); + let mut ccm8_tag = vec![0; ccm8.tag_len()]; + ccm8.seal_in_place( + &bytes("101112131415161718191a1b"), + &bytes("000102030405060708090a0b0c0d0e0f10111213"), + &mut ccm8_buffer, + &mut ccm8_tag, + ) + .unwrap(); + assert_eq!( + ccm8_buffer, + bytes("e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5") + ); + assert_eq!(ccm8_tag, bytes("484392fbc1b09951")); + + let mut chacha = crypto + .new_aead( + AeadAlgorithm::ChaCha20Poly1305, + &bytes("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"), + ) + .unwrap(); + let mut chacha_buffer = bytes(concat!( + "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393", + "a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f722074", + "6865206675747572652c2073756e73637265656e20776f756c642062652069742e" + )); + let mut chacha_tag = vec![0; chacha.tag_len()]; + chacha + .seal_in_place( + &bytes("070000004041424344454647"), + &bytes("50515253c0c1c2c3c4c5c6c7"), + &mut chacha_buffer, + &mut chacha_tag, + ) + .unwrap(); + assert_eq!( + chacha_buffer, + bytes(concat!( + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca967128", + "2fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fa", + "b324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116" + )) + ); + assert_eq!(chacha_tag, bytes("1ae10b594f09e26a7e902ecbd0600691")); + assert!(matches!( + crypto.new_aead(AeadAlgorithm::Aes128Gcm, &[0; 15]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + gcm.seal_in_place(&[0; 11], &[], &mut [], &mut [0; 16]), + Err(CryptoError::InvalidNonceLength { .. }) + )); + assert!(matches!( + gcm.seal_in_place(&[0; 12], &[], &mut [], &mut [0; 15]), + Err(CryptoError::InvalidTagLength { .. }) + )); + + let cases = [ + (AeadAlgorithm::Aes256Gcm, 32), + (AeadAlgorithm::Aes128Ccm, 16), + (AeadAlgorithm::Aes128Ccm8, 16), + (AeadAlgorithm::ChaCha20Poly1305, 32), + ]; + for (algorithm, key_len) in cases { + let mut cipher = crypto.new_aead(algorithm, &vec![7; key_len]).unwrap(); + let plaintext = b"provider conformance".to_vec(); + let mut encrypted = plaintext.clone(); + let mut tag = vec![0; cipher.tag_len()]; + cipher + .seal_in_place(&[3; 12], b"aad", &mut encrypted, &mut tag) + .unwrap(); + assert_ne!(encrypted, plaintext); + cipher + .open_in_place(&[3; 12], b"aad", &mut encrypted, &tag) + .unwrap(); + assert_eq!(encrypted, plaintext); + + let mut ciphertext = plaintext.clone(); + let mut valid_tag = vec![0; cipher.tag_len()]; + cipher + .seal_in_place(&[3; 12], b"aad", &mut ciphertext, &mut valid_tag) + .unwrap(); + + let mut tampered = ciphertext.clone(); + tag[0] ^= 1; + assert_eq!( + cipher.open_in_place(&[3; 12], b"aad", &mut tampered, &tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut wrong_aad = ciphertext.clone(); + assert_eq!( + cipher.open_in_place(&[3; 12], b"bad", &mut wrong_aad, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut changed_ciphertext = ciphertext.clone(); + changed_ciphertext[0] ^= 1; + assert_eq!( + cipher.open_in_place(&[3; 12], b"aad", &mut changed_ciphertext, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut wrong_key_cipher = crypto.new_aead(algorithm, &vec![8; key_len]).unwrap(); + assert_eq!( + wrong_key_cipher.open_in_place(&[3; 12], b"aad", &mut ciphertext, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + } +} + +/// Checks every supported one-shot key exchange and malformed peer keys. +pub fn assert_key_exchange(crypto: &dyn RTCCrypto) { + for algorithm in [ + KeyExchangeAlgorithm::P256, + KeyExchangeAlgorithm::P384, + KeyExchangeAlgorithm::X25519, + ] { + let left = crypto.start_key_exchange(algorithm).unwrap(); + let right = crypto.start_key_exchange(algorithm).unwrap(); + assert_eq!(left.algorithm(), algorithm); + let left_public = left.public_key().to_vec(); + let right_public = right.public_key().to_vec(); + let left_secret = left.complete(&right_public).unwrap(); + let right_secret = right.complete(&left_public).unwrap(); + assert_eq!(left_secret.as_ref(), right_secret.as_ref()); + assert!(!left_secret.is_empty()); + + let invalid = crypto.start_key_exchange(algorithm).unwrap(); + assert!(matches!( + invalid.complete(&[0; 1]), + Err(CryptoError::InvalidPublicKey) + )); + } +} + +/// Checks signing, verification, key import/export, and invalid signatures and encodings. +pub fn assert_signatures(crypto: &dyn RTCCrypto) { + for scheme in [SignatureScheme::Ed25519, SignatureScheme::EcdsaP256Sha256] { + let key = crypto.generate_signing_key(scheme).unwrap(); + let message = b"rtc-crypto provider conformance"; + let signature = key.sign(scheme, message).unwrap(); + crypto + .verify_signature(scheme, key.public_key(), message, &signature) + .unwrap(); + assert_eq!( + crypto.verify_signature(scheme, key.public_key(), b"changed", &signature), + Err(CryptoError::InvalidSignature) + ); + + let exported = key.to_pkcs8_der().unwrap().unwrap(); + let imported = crypto + .import_signing_key(scheme, exported.as_ref()) + .unwrap(); + let imported_signature = imported.sign(scheme, message).unwrap(); + crypto + .verify_signature(scheme, imported.public_key(), message, &imported_signature) + .unwrap(); + assert!(matches!( + imported.sign(SignatureScheme::RsaPkcs1Sha256, message), + Err(CryptoError::UnsupportedAlgorithm(_)) + )); + } + + assert_eq!( + crypto.verify_signature( + SignatureScheme::Ed25519, + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[0; 32], + }, + b"message", + &[0; 64], + ), + Err(CryptoError::InvalidPublicKey) + ); + + assert_verification_only_schemes(crypto); +} + +fn assert_verification_only_schemes(crypto: &dyn RTCCrypto) { + let message = b"rtc-crypto verification vector"; + let p384_public_key = bytes(concat!( + "04c298b589fdd33f544610d13c277e0c703b2e3a72c0dfa2a81725e761614bd8c4", + "cb80ecf40bba853f37aec2f4e13c7b5d05e7be9231d651bd1dc2848050bcd19858", + "e448d27bf2418b350626a1f241c4914795c404aa35afab97e15e202296244a" + )); + let p384_signature = bytes(concat!( + "306502302c6f36a6a01282982213b037f73ec8f935e1fcf4dc63035824c2bcb6", + "aaa378f716d15f63df23e85d60f7d5e46c028ad1023100877055a3a8849e179", + "ad94da98dc5125f1e78852cf9017795087b90751b99985b989786d2a537f84b08cdf7243820c313" + )); + crypto + .verify_signature( + SignatureScheme::EcdsaP384Sha384, + PublicKey { + encoding: PublicKeyEncoding::EcUncompressedPoint, + bytes: &p384_public_key, + }, + message, + &p384_signature, + ) + .unwrap(); + + let rsa_public_key = bytes( + "3082010a0282010100b0f8c4868ce9bcd4d11632162c0376bb09dce2facadaa27a6d4ad01b217c0a29e036b1bfd0052254ec3d349383019d8fd2d5c895ac6790bcc6c4dfca7c26bbcf570ee3bdff70826f80e8254776d8e73b431e2bece3d7d515edd23ada88e2c7136ac796685df8a40ed38ce59eaefc2509e1b277a0363938eb8e998a3f9fd6f2ed011b969e48d89d76508904961f8d83756466eff0f1fd5e4621e8ff083fd0d6aa87b31e560c185a1862059ba0fa95f8125ac96bfc7e051c3996587dc1271bb3ebf49303314ee888ccc8441de13a59b0646d9375dfbcbd66c1435398e164bc4672fd63a2eef162a7c1ac7ddb3e9cdb68cfeacff01f036a557477b45172593727f50203010001", + ); + let rsa_vectors = [ + ( + SignatureScheme::RsaPkcs1Sha1, + "967636ce08cc1923f2b6fa792d2a7cd6521f4a793adb4a0f94cfc6543e483d5383a6b36dbcef3a5cfd5cc12c333ebc6a22f2d452cac61a352111247e26f5e13595f0b78a9e94be1ff7eb4ab60ef48fff3a0e8c1a70ea041f63413c7dc4a5219d2ddb8349058ce1b7d2c02eb98f285d589f858a18f6473a1d3af47de653d520ceba6825e1a1bd5a0065ef0e4d9ac5929bc1cc3ffab014081224db7e7787c67f1580913fb98a870c1355c86a33770d17f654f24dccd781f3bf8a7de29e00198a99dd2bf1a1298630da433982ae63cbf71c265abebdd003c1c2ee870f7f8ea96c465dedb158a1e2e5048763d308a69eefc91424ad0795e8320fa0e753673f657a45", + ), + ( + SignatureScheme::RsaPkcs1Sha256, + "19b21fd70c4dacec4c3cc6f6aac961e4ea0fb9b5d0ee3862cc35b60849388f2f461ab4697c59c25abb251f88b1de312ced6861ed90152c911356dff768cc4eafdadaa2f8d6b5d70c630d2739f1178fe5cbaff62cee343a20bb38404d35c58d247befa486dd5a3c69affdad5bdecaab876799dd297bd9cc4700e099b92d18d8ad78b64f570e4a398b0b8e9baf9a1b01aaa873b23b7c381917f3383482b7780e0e11e734409fc18daacb0428082789f791af2c4a79d5c75cde45c201e46ae347ab7624e0137940945e174dfab59888892c25b8e10abee1a72f2f31fb3f0e8840db490ba752e1df966979f267756767c88ea0d7909145f379811412f68465e08fd2", + ), + ( + SignatureScheme::RsaPkcs1Sha384, + "0095d1ef58850cbf09dd1219a8678d7fa3f8c490a4a35feb19f50d85991caf659131dc90c52f14466c429a099a53e1d5f321a49065e85f250b85e08158ee46328515acee03bd215d610ec2335e1cbd1525058b950fb8ecb5d073f9fc474613c830c1111868efc3554c2ec62d9efd4694db281a6b3e48ed68d934278f5ba9a6fb19e8d648baa6b5f48d126def83986166ed05d710d5cdccd457092649b08d5e54c5ccfe42786852c98113b78291e3e2ced51ff3ed51ddef8cb81d311a2983cc6c93f0bb583537c067c6afc63e8e4a9d15635882b14d189a217d9588a469ac1899e3adeafdd276150b1023d7b64fea94b50a9ba22cc47c0d8d6ca4faf6b23f76d0", + ), + ( + SignatureScheme::RsaPkcs1Sha512, + "7373b3eaf66ba56bda1ae85a466c2f0321d15c37ce293642f6b69fddbea0e6f4dfa76dd0ba274afaabf71ba9d85198c0e75af95af6c64cb9bb304ea1ea3c04e653649891af78145b823d3ed1179fdbdc4edbedcf1eb3201b44a9a40930bf005da1bbe138dfac2d272364a991fcadcfba9093e889fa03f153771fdd51525ffae16cb2155eac8f6da49b602ae8fb34a5a131693459cacbc5adbc9b6002473732a7e205c2150bd25afe850c29845054e17d56a028d81f293b1dc634ba11a6790aa478f0f36bcc64787e356384ae90ef83c5226183afca64c9ac28c83ca803e01ff801f1ee12c806005d9f5d3562afae08e358722ca606d46d500d43901256625fc4", + ), + ]; + for (scheme, signature) in rsa_vectors { + crypto + .verify_signature( + scheme, + PublicKey { + encoding: PublicKeyEncoding::RsaPkcs1Der, + bytes: &rsa_public_key, + }, + message, + &bytes(signature), + ) + .unwrap(); + } +} + +/// Checks that a provider's secure random source returns fresh output. +pub fn assert_random(provider: &dyn RTCCryptoProvider) { + let mut first = [0; 32]; + let mut second = [0; 32]; + provider.random().fill(&mut first).unwrap(); + provider.random().fill(&mut second).unwrap(); + assert_ne!(first, second); +} + +/// Checks the default unsupported-operation behavior required for partial providers. +pub fn assert_unsupported_hash(crypto: &dyn RTCCrypto) { + assert!(!crypto.supports(CryptoAlgorithm::Hash(HashAlgorithm::Sha256))); + assert_eq!( + crypto.hash(HashAlgorithm::Sha256, b"input"), + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + HashAlgorithm::Sha256 + ))) + ); +} + +/// Checks the provider-neutral failure returned by a deliberately failing random source. +pub fn assert_random_failure(random: &dyn crate::RTCRandom) { + assert_eq!(random.fill(&mut [0; 1]), Err(CryptoError::RandomnessFailed)); +} + +fn bytes(hex: &str) -> Vec { + assert!(hex.len().is_multiple_of(2)); + hex.as_bytes() + .chunks_exact(2) + .map(|pair| (nibble(pair[0]) << 4) | nibble(pair[1])) + .collect() +} + +fn nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + _ => panic!("invalid hexadecimal test vector"), + } +} diff --git a/rtc-crypto/src/error.rs b/rtc-crypto/src/error.rs new file mode 100644 index 00000000..c8cf15af --- /dev/null +++ b/rtc-crypto/src/error.rs @@ -0,0 +1,63 @@ +use crate::CryptoAlgorithm; + +/// A provider-neutral cryptographic failure. +#[non_exhaustive] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum CryptoError { + /// No built-in default provider was compiled. + #[error("no default crypto provider is enabled")] + NoDefaultProvider, + /// The provider does not implement an algorithm. + #[error("unsupported algorithm: {0:?}")] + UnsupportedAlgorithm(CryptoAlgorithm), + /// A key has the wrong length. + #[error("invalid key length: expected {expected}, got {actual}")] + InvalidKeyLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// A nonce or IV has the wrong length. + #[error("invalid nonce length: expected {expected}, got {actual}")] + InvalidNonceLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// An authentication tag has the wrong length. + #[error("invalid tag length: expected {expected}, got {actual}")] + InvalidTagLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// Public-key bytes are malformed or use the wrong encoding. + #[error("invalid public key")] + InvalidPublicKey, + /// Private-key bytes are malformed or incompatible with the scheme. + #[error("invalid private key")] + InvalidPrivateKey, + /// Decryption, padding, or tag authentication failed. + #[error("authentication failed")] + AuthenticationFailed, + /// Signature verification failed. + #[error("signature verification failed")] + InvalidSignature, + /// The cryptographically secure random source failed. + #[error("randomness source failed")] + RandomnessFailed, + /// A caller-owned output buffer is too small. + #[error("output buffer is too small: required {required}, got {actual}")] + OutputTooSmall { + /// Required length. + required: usize, + /// Supplied length. + actual: usize, + }, + /// Sanitized provider diagnostic context. + #[error("provider failure: {0}")] + Provider(String), +} diff --git a/rtc-crypto/src/lib.rs b/rtc-crypto/src/lib.rs new file mode 100644 index 00000000..8536e4b3 --- /dev/null +++ b/rtc-crypto/src/lib.rs @@ -0,0 +1,86 @@ +//! Provider-neutral cryptography for the webrtc-rs RTC stack. +//! +//! The open traits in this crate allow applications to supply cryptography and randomness without +//! registering global state. Built-in providers are selected with additive Cargo features. + +mod algorithm; +mod error; +mod provider; +mod secret; +mod traits; + +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] +mod common; +pub mod providers; + +#[cfg(feature = "test-support")] +pub mod conformance; + +pub use algorithm::*; +pub use error::CryptoError; +pub use provider::default_provider; +pub use secret::SecretVec; +pub use traits::*; + +/// Compares equal-length byte strings without data-dependent early exit. +#[must_use] +pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { + use subtle::ConstantTimeEq; + + left.len() == right.len() && bool::from(left.ct_eq(right)) +} + +const _: () = { + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + fn assert_dyn_compatible( + _provider: &dyn RTCCryptoProvider, + _crypto: &dyn RTCCrypto, + _random: &dyn RTCRandom, + _stream: &dyn StreamCipher, + _aead: &dyn AeadCipher, + _cbc: &dyn CbcCipher, + _exchange: &dyn ActiveKeyExchange, + _signing_key: &dyn SigningKey, + ) { + } +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn secret_debug_is_redacted() { + let secret = SecretVec::new(vec![1, 2, 3, 4]); + let debug = format!("{secret:?}"); + assert!(debug.contains("REDACTED")); + assert!(debug.contains("len: 4")); + assert!(!debug.contains("1, 2, 3, 4")); + } + + #[test] + fn secret_into_bytes_is_explicit() { + let secret = SecretVec::new(vec![1, 2, 3]); + assert_eq!(secret.into_bytes(), vec![1, 2, 3]); + } + + #[test] + fn errors_have_provider_neutral_text() { + assert_eq!( + CryptoError::AuthenticationFailed.to_string(), + "authentication failed" + ); + assert_eq!( + CryptoError::InvalidSignature.to_string(), + "signature verification failed" + ); + } + + #[test] + fn constant_time_equality_checks_length_and_content() { + assert!(constant_time_eq(b"same", b"same")); + assert!(!constant_time_eq(b"same", b"diff")); + assert!(!constant_time_eq(b"same", b"same-longer")); + } +} diff --git a/rtc-crypto/src/provider.rs b/rtc-crypto/src/provider.rs new file mode 100644 index 00000000..70b72fbe --- /dev/null +++ b/rtc-crypto/src/provider.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use crate::{CryptoError, RTCCryptoProvider}; + +/// Constructs the built-in default provider. +/// +/// Ring remains the default whenever its feature is enabled. AWS-LC-RS is selected only when it is +/// the sole built-in. With no built-in features this returns [`CryptoError::NoDefaultProvider`]. +pub fn default_provider() -> Result, CryptoError> { + #[cfg(feature = "ring")] + { + Ok(Arc::new(crate::providers::RingProvider::new())) + } + + #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] + { + Ok(Arc::new(crate::providers::AwsLcRsProvider::new())) + } + + #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + { + Err(CryptoError::NoDefaultProvider) + } +} diff --git a/rtc-crypto/src/providers/aws_lc_rs.rs b/rtc-crypto/src/providers/aws_lc_rs.rs new file mode 100644 index 00000000..4ab4a4d4 --- /dev/null +++ b/rtc-crypto/src/providers/aws_lc_rs.rs @@ -0,0 +1,524 @@ +use std::sync::Arc; + +use aws_lc_rs::rand::{SecureRandom, SystemRandom}; +use aws_lc_rs::signature::{self, KeyPair}; +use aws_lc_rs::{aead, agreement, digest, hmac}; + +use crate::common; +use crate::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, + PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, SignatureScheme, + SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, +}; + +/// The built-in AWS-LC-RS provider bundle. +#[derive(Default)] +pub struct AwsLcRsProvider { + crypto: AwsLcRsCrypto, + random: AwsLcRsRandom, +} + +impl AwsLcRsProvider { + /// Creates an AWS-LC-RS provider. + #[must_use] + pub const fn new() -> Self { + Self { + crypto: AwsLcRsCrypto, + random: AwsLcRsRandom, + } + } +} + +impl RTCCryptoProvider for AwsLcRsProvider { + fn name(&self) -> &'static str { + "aws-lc-rs" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +/// AWS-LC-RS-backed operations, with RustCrypto fallbacks for primitives that are not exposed by +/// its stable high-level API: MD5, AES block/CTR/CBC, and AES-CCM. +#[derive(Default)] +pub struct AwsLcRsCrypto; + +/// AWS-LC-RS's operating-system-backed secure random source. +#[derive(Default)] +pub struct AwsLcRsRandom; + +impl RTCRandom for AwsLcRsRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + SystemRandom::new() + .fill(output) + .map_err(|_| CryptoError::RandomnessFailed) + } +} + +impl RTCCrypto for AwsLcRsCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5 | HashAlgorithm::Sha256) + | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1 | HmacAlgorithm::Sha256) + | CryptoAlgorithm::Aead( + AeadAlgorithm::Aes128Gcm + | AeadAlgorithm::Aes256Gcm + | AeadAlgorithm::Aes128Ccm + | AeadAlgorithm::Aes128Ccm8 + | AeadAlgorithm::ChaCha20Poly1305, + ) + | CryptoAlgorithm::StreamCipher( + StreamCipherAlgorithm::Aes128Ctr | StreamCipherAlgorithm::Aes256Ctr, + ) + | CryptoAlgorithm::BlockCipher( + BlockCipherAlgorithm::Aes128 | BlockCipherAlgorithm::Aes256, + ) + | CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + | CryptoAlgorithm::KeyExchange( + KeyExchangeAlgorithm::P256 + | KeyExchangeAlgorithm::P384 + | KeyExchangeAlgorithm::X25519, + ) + | CryptoAlgorithm::Signature( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::EcdsaP384Sha384 + | SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + ) + | CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::Ed25519 | SignatureScheme::EcdsaP256Sha256, + ) + | CryptoAlgorithm::SigningKeyImport( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::RsaPkcs1Sha256, + ) + ) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + match algorithm { + HashAlgorithm::Md5 => Ok(common::md5(data)), + HashAlgorithm::Sha256 => Ok(digest::digest(&digest::SHA256, data).as_ref().to_vec()), + } + } + + fn hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + output: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_tag_len(algorithm.output_len(), output.len())?; + let key = hmac::Key::new(hmac_algorithm(algorithm), key); + let mut context = hmac::Context::with_key(&key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + expected: &[u8], + ) -> Result<(), CryptoError> { + common::check_tag_len(algorithm.output_len(), expected.len())?; + let mut actual = vec![0; algorithm.output_len()]; + self.hmac(algorithm, key, input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + common::block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + match algorithm { + AeadAlgorithm::Aes128Ccm | AeadAlgorithm::Aes128Ccm8 => common::new_ccm(algorithm, key), + AeadAlgorithm::Aes128Gcm => AwsLcRsAead::create(&aead::AES_128_GCM, key), + AeadAlgorithm::Aes256Gcm => AwsLcRsAead::create(&aead::AES_256_GCM, key), + AeadAlgorithm::ChaCha20Poly1305 => AwsLcRsAead::create(&aead::CHACHA20_POLY1305, key), + } + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + AwsLcRsKeyExchange::start(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + AwsLcRsSigningKey::generate(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + AwsLcRsSigningKey::import(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + verify_public_key_encoding(scheme, public_key.encoding)?; + signature::UnparsedPublicKey::new(verification_algorithm(scheme), public_key.bytes) + .verify(message, signature) + .map_err(|_| CryptoError::InvalidSignature) + } +} + +fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { + match algorithm { + HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + HmacAlgorithm::Sha256 => hmac::HMAC_SHA256, + } +} + +struct AwsLcRsAead { + key: aead::LessSafeKey, +} + +impl AwsLcRsAead { + fn create( + algorithm: &'static aead::Algorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::check_key_len(algorithm.key_len(), key.len())?; + let key = aead::UnboundKey::new(algorithm, key) + .map(aead::LessSafeKey::new) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + Ok(Box::new(Self { key })) + } +} + +impl AeadCipher for AwsLcRsAead { + fn tag_len(&self) -> usize { + 16 + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag_out.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = self + .key + .seal_in_place_separate_tag(nonce, aead::Aad::from(aad), plaintext_and_ciphertext) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(tag.as_ref()); + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + self.key + .open_in_place_separate_tag(nonce, aead::Aad::from(aad), tag, ciphertext_and_plaintext) + .map(|_| ()) + .map_err(|_| CryptoError::AuthenticationFailed) + } +} + +struct AwsLcRsKeyExchange { + algorithm: KeyExchangeAlgorithm, + backend_algorithm: &'static agreement::Algorithm, + private_key: agreement::PrivateKey, + public_key: Vec, +} + +impl AwsLcRsKeyExchange { + fn start(algorithm: KeyExchangeAlgorithm) -> Result, CryptoError> { + let backend_algorithm = agreement_algorithm(algorithm); + let private_key = agreement::PrivateKey::generate(backend_algorithm) + .map_err(|_| CryptoError::RandomnessFailed)?; + let public_key = private_key + .compute_public_key() + .map_err(|_| CryptoError::Provider("key exchange public-key generation failed".into()))? + .as_ref() + .to_vec(); + Ok(Box::new(Self { + algorithm, + backend_algorithm, + private_key, + public_key, + })) + } +} + +impl ActiveKeyExchange for AwsLcRsKeyExchange { + fn algorithm(&self) -> KeyExchangeAlgorithm { + self.algorithm + } + + fn public_key(&self) -> &[u8] { + &self.public_key + } + + fn complete(self: Box, peer_public_key: &[u8]) -> Result { + let peer = agreement::UnparsedPublicKey::new(self.backend_algorithm, peer_public_key); + agreement::agree( + &self.private_key, + peer, + CryptoError::InvalidPublicKey, + |secret| Ok(SecretVec::new(secret.to_vec())), + ) + } +} + +enum AwsLcRsSigningKeyKind { + Ed25519(signature::Ed25519KeyPair), + EcdsaP256(signature::EcdsaKeyPair), + Rsa(signature::RsaKeyPair), +} + +struct AwsLcRsSigningKey { + scheme: SignatureScheme, + kind: AwsLcRsSigningKeyKind, + public_key: Vec, + public_key_encoding: PublicKeyEncoding, + pkcs8_der: SecretVec, +} + +impl AwsLcRsSigningKey { + fn generate(scheme: SignatureScheme) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let pkcs8 = match scheme { + SignatureScheme::Ed25519 => signature::Ed25519KeyPair::generate_pkcs8(&rng), + SignatureScheme::EcdsaP256Sha256 => signature::EcdsaKeyPair::generate_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &rng, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )); + } + } + .map_err(|_| CryptoError::RandomnessFailed)?; + Self::import(scheme, pkcs8.as_ref()) + } + + fn import( + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + let (kind, public_key, public_key_encoding) = match scheme { + SignatureScheme::Ed25519 => { + let key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::Ed25519(key), + public, + PublicKeyEncoding::Ed25519Raw, + ) + } + SignatureScheme::EcdsaP256Sha256 => { + let key = signature::EcdsaKeyPair::from_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + pkcs8_der, + ) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::EcdsaP256(key), + public, + PublicKeyEncoding::EcUncompressedPoint, + ) + } + SignatureScheme::RsaPkcs1Sha256 => { + let key = signature::RsaKeyPair::from_pkcs8(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::Rsa(key), + public, + PublicKeyEncoding::RsaPkcs1Der, + ) + } + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )); + } + }; + Ok(Arc::new(Self { + scheme, + kind, + public_key, + public_key_encoding, + pkcs8_der: SecretVec::new(pkcs8_der.to_vec()), + })) + } +} + +impl SigningKey for AwsLcRsSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.scheme == scheme + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: self.public_key_encoding, + bytes: &self.public_key, + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + if !self.supports(scheme) { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )); + } + match &self.kind { + AwsLcRsSigningKeyKind::Ed25519(key) => Ok(key.sign(message).as_ref().to_vec()), + AwsLcRsSigningKeyKind::EcdsaP256(key) => key + .sign(&SystemRandom::new(), message) + .map(|signature| signature.as_ref().to_vec()) + .map_err(|_| CryptoError::Provider("signature generation failed".into())), + AwsLcRsSigningKeyKind::Rsa(key) => { + let mut signature = vec![0; key.public_modulus_len()]; + key.sign( + &signature::RSA_PKCS1_SHA256, + &SystemRandom::new(), + message, + &mut signature, + ) + .map_err(|_| CryptoError::Provider("signature generation failed".into()))?; + Ok(signature) + } + } + } + + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(Some(self.pkcs8_der.clone())) + } +} + +fn agreement_algorithm(algorithm: KeyExchangeAlgorithm) -> &'static agreement::Algorithm { + match algorithm { + KeyExchangeAlgorithm::P256 => &agreement::ECDH_P256, + KeyExchangeAlgorithm::P384 => &agreement::ECDH_P384, + KeyExchangeAlgorithm::X25519 => &agreement::X25519, + } +} + +fn verification_algorithm( + scheme: SignatureScheme, +) -> &'static dyn signature::VerificationAlgorithm { + match scheme { + SignatureScheme::Ed25519 => &signature::ED25519, + SignatureScheme::EcdsaP256Sha256 => &signature::ECDSA_P256_SHA256_ASN1, + SignatureScheme::EcdsaP384Sha384 => &signature::ECDSA_P384_SHA384_ASN1, + SignatureScheme::RsaPkcs1Sha1 => &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, + SignatureScheme::RsaPkcs1Sha256 => { + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + } + SignatureScheme::RsaPkcs1Sha384 => &signature::RSA_PKCS1_2048_8192_SHA384, + SignatureScheme::RsaPkcs1Sha512 => { + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + } + } +} + +fn verify_public_key_encoding( + scheme: SignatureScheme, + encoding: PublicKeyEncoding, +) -> Result<(), CryptoError> { + let valid = matches!( + (scheme, encoding), + (SignatureScheme::Ed25519, PublicKeyEncoding::Ed25519Raw) + | ( + SignatureScheme::EcdsaP256Sha256 | SignatureScheme::EcdsaP384Sha384, + PublicKeyEncoding::EcUncompressedPoint + ) + | ( + SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + PublicKeyEncoding::RsaPkcs1Der + ) + ); + if valid { + Ok(()) + } else { + Err(CryptoError::InvalidPublicKey) + } +} diff --git a/rtc-crypto/src/providers/mod.rs b/rtc-crypto/src/providers/mod.rs new file mode 100644 index 00000000..b587050e --- /dev/null +++ b/rtc-crypto/src/providers/mod.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "aws-lc-rs")] +mod aws_lc_rs; +#[cfg(feature = "ring")] +mod ring; + +#[cfg(feature = "aws-lc-rs")] +pub use aws_lc_rs::{AwsLcRsCrypto, AwsLcRsProvider, AwsLcRsRandom}; +#[cfg(feature = "ring")] +pub use ring::{RingCrypto, RingProvider, RingRandom}; diff --git a/rtc-crypto/src/providers/ring.rs b/rtc-crypto/src/providers/ring.rs new file mode 100644 index 00000000..d53ce229 --- /dev/null +++ b/rtc-crypto/src/providers/ring.rs @@ -0,0 +1,538 @@ +use std::sync::Arc; + +use ring::aead; +use ring::agreement; +use ring::digest; +use ring::hmac; +use ring::rand::{SecureRandom, SystemRandom}; +use ring::signature::{self, KeyPair}; + +use crate::common; +use crate::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, + PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, SignatureScheme, + SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, +}; + +/// The built-in Ring provider bundle. +#[derive(Default)] +pub struct RingProvider { + crypto: RingCrypto, + random: RingRandom, +} + +impl RingProvider { + /// Creates a Ring provider. + #[must_use] + pub const fn new() -> Self { + Self { + crypto: RingCrypto, + random: RingRandom, + } + } +} + +impl RTCCryptoProvider for RingProvider { + fn name(&self) -> &'static str { + "ring" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +/// Ring-backed cryptographic operations, with documented RustCrypto fallbacks for primitives Ring +/// does not expose (MD5, AES block/CTR/CBC, and AES-CCM). +#[derive(Default)] +pub struct RingCrypto; + +/// Ring's operating-system-backed secure random source. +#[derive(Default)] +pub struct RingRandom; + +impl RTCRandom for RingRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + SystemRandom::new() + .fill(output) + .map_err(|_| CryptoError::RandomnessFailed) + } +} + +impl RTCCrypto for RingCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5 | HashAlgorithm::Sha256) + | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1 | HmacAlgorithm::Sha256) + | CryptoAlgorithm::Aead( + AeadAlgorithm::Aes128Gcm + | AeadAlgorithm::Aes256Gcm + | AeadAlgorithm::Aes128Ccm + | AeadAlgorithm::Aes128Ccm8 + | AeadAlgorithm::ChaCha20Poly1305, + ) + | CryptoAlgorithm::StreamCipher( + StreamCipherAlgorithm::Aes128Ctr | StreamCipherAlgorithm::Aes256Ctr, + ) + | CryptoAlgorithm::BlockCipher( + BlockCipherAlgorithm::Aes128 | BlockCipherAlgorithm::Aes256, + ) + | CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + | CryptoAlgorithm::KeyExchange( + KeyExchangeAlgorithm::P256 + | KeyExchangeAlgorithm::P384 + | KeyExchangeAlgorithm::X25519, + ) + | CryptoAlgorithm::Signature( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::EcdsaP384Sha384 + | SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + ) + | CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::Ed25519 | SignatureScheme::EcdsaP256Sha256, + ) + | CryptoAlgorithm::SigningKeyImport( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::RsaPkcs1Sha256, + ) + ) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + match algorithm { + HashAlgorithm::Md5 => Ok(common::md5(data)), + HashAlgorithm::Sha256 => Ok(digest::digest(&digest::SHA256, data).as_ref().to_vec()), + } + } + + fn hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + output: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_tag_len(algorithm.output_len(), output.len())?; + let key = hmac::Key::new(hmac_algorithm(algorithm), key); + let mut context = hmac::Context::with_key(&key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + expected: &[u8], + ) -> Result<(), CryptoError> { + common::check_tag_len(algorithm.output_len(), expected.len())?; + let mut actual = vec![0; algorithm.output_len()]; + self.hmac(algorithm, key, input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + common::block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + match algorithm { + AeadAlgorithm::Aes128Ccm | AeadAlgorithm::Aes128Ccm8 => common::new_ccm(algorithm, key), + AeadAlgorithm::Aes128Gcm => RingAead::create(&aead::AES_128_GCM, key), + AeadAlgorithm::Aes256Gcm => RingAead::create(&aead::AES_256_GCM, key), + AeadAlgorithm::ChaCha20Poly1305 => RingAead::create(&aead::CHACHA20_POLY1305, key), + } + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + RingKeyExchange::start(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + RingSigningKey::generate(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + RingSigningKey::import(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + verify_public_key_encoding(scheme, public_key.encoding)?; + signature::UnparsedPublicKey::new(verification_algorithm(scheme), public_key.bytes) + .verify(message, signature) + .map_err(|_| CryptoError::InvalidSignature) + } +} + +fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { + match algorithm { + HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + HmacAlgorithm::Sha256 => hmac::HMAC_SHA256, + } +} + +struct RingAead { + key: aead::LessSafeKey, +} + +impl RingAead { + fn create( + algorithm: &'static aead::Algorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::check_key_len(algorithm.key_len(), key.len())?; + let key = aead::UnboundKey::new(algorithm, key) + .map(aead::LessSafeKey::new) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + Ok(Box::new(Self { key })) + } +} + +impl AeadCipher for RingAead { + fn tag_len(&self) -> usize { + 16 + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag_out.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = self + .key + .seal_in_place_separate_tag(nonce, aead::Aad::from(aad), plaintext_and_ciphertext) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(tag.as_ref()); + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = aead::Tag::try_from(tag).map_err(|_| CryptoError::InvalidTagLength { + expected: self.tag_len(), + actual: tag.len(), + })?; + self.key + .open_in_place_separate_tag( + nonce, + aead::Aad::from(aad), + tag, + ciphertext_and_plaintext, + 0.., + ) + .map(|_| ()) + .map_err(|_| CryptoError::AuthenticationFailed) + } +} + +struct RingKeyExchange { + algorithm: KeyExchangeAlgorithm, + backend_algorithm: &'static agreement::Algorithm, + private_key: agreement::EphemeralPrivateKey, + public_key: Vec, +} + +impl RingKeyExchange { + fn start(algorithm: KeyExchangeAlgorithm) -> Result, CryptoError> { + let backend_algorithm = agreement_algorithm(algorithm); + let private_key = + agreement::EphemeralPrivateKey::generate(backend_algorithm, &SystemRandom::new()) + .map_err(|_| CryptoError::RandomnessFailed)?; + let public_key = private_key + .compute_public_key() + .map_err(|_| CryptoError::Provider("key exchange public-key generation failed".into()))? + .as_ref() + .to_vec(); + Ok(Box::new(Self { + algorithm, + backend_algorithm, + private_key, + public_key, + })) + } +} + +impl ActiveKeyExchange for RingKeyExchange { + fn algorithm(&self) -> KeyExchangeAlgorithm { + self.algorithm + } + + fn public_key(&self) -> &[u8] { + &self.public_key + } + + fn complete(self: Box, peer_public_key: &[u8]) -> Result { + let peer = agreement::UnparsedPublicKey::new(self.backend_algorithm, peer_public_key); + agreement::agree_ephemeral(self.private_key, &peer, |secret| { + SecretVec::new(secret.to_vec()) + }) + .map_err(|_| CryptoError::InvalidPublicKey) + } +} + +enum RingSigningKeyKind { + Ed25519(signature::Ed25519KeyPair), + EcdsaP256(signature::EcdsaKeyPair), + Rsa(signature::RsaKeyPair), +} + +struct RingSigningKey { + scheme: SignatureScheme, + kind: RingSigningKeyKind, + public_key: Vec, + public_key_encoding: PublicKeyEncoding, + pkcs8_der: SecretVec, +} + +impl RingSigningKey { + fn generate(scheme: SignatureScheme) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let pkcs8 = match scheme { + SignatureScheme::Ed25519 => signature::Ed25519KeyPair::generate_pkcs8(&rng), + SignatureScheme::EcdsaP256Sha256 => signature::EcdsaKeyPair::generate_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &rng, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )); + } + } + .map_err(|_| CryptoError::RandomnessFailed)?; + Self::import(scheme, pkcs8.as_ref()) + } + + fn import( + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let (kind, public_key, public_key_encoding) = match scheme { + SignatureScheme::Ed25519 => { + let key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + RingSigningKeyKind::Ed25519(key), + public, + PublicKeyEncoding::Ed25519Raw, + ) + } + SignatureScheme::EcdsaP256Sha256 => { + let key = signature::EcdsaKeyPair::from_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + pkcs8_der, + &rng, + ) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + RingSigningKeyKind::EcdsaP256(key), + public, + PublicKeyEncoding::EcUncompressedPoint, + ) + } + SignatureScheme::RsaPkcs1Sha256 => { + let key = signature::RsaKeyPair::from_pkcs8(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public().as_ref().to_vec(); + ( + RingSigningKeyKind::Rsa(key), + public, + PublicKeyEncoding::RsaPkcs1Der, + ) + } + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )); + } + }; + Ok(Arc::new(Self { + scheme, + kind, + public_key, + public_key_encoding, + pkcs8_der: SecretVec::new(pkcs8_der.to_vec()), + })) + } +} + +impl SigningKey for RingSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.scheme == scheme + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: self.public_key_encoding, + bytes: &self.public_key, + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + if !self.supports(scheme) { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )); + } + match &self.kind { + RingSigningKeyKind::Ed25519(key) => Ok(key.sign(message).as_ref().to_vec()), + RingSigningKeyKind::EcdsaP256(key) => key + .sign(&SystemRandom::new(), message) + .map(|signature| signature.as_ref().to_vec()) + .map_err(|_| CryptoError::Provider("signature generation failed".into())), + RingSigningKeyKind::Rsa(key) => { + let mut signature = vec![0; key.public().modulus_len()]; + key.sign( + &signature::RSA_PKCS1_SHA256, + &SystemRandom::new(), + message, + &mut signature, + ) + .map_err(|_| CryptoError::Provider("signature generation failed".into()))?; + Ok(signature) + } + } + } + + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(Some(self.pkcs8_der.clone())) + } +} + +fn agreement_algorithm(algorithm: KeyExchangeAlgorithm) -> &'static agreement::Algorithm { + match algorithm { + KeyExchangeAlgorithm::P256 => &agreement::ECDH_P256, + KeyExchangeAlgorithm::P384 => &agreement::ECDH_P384, + KeyExchangeAlgorithm::X25519 => &agreement::X25519, + } +} + +fn verification_algorithm( + scheme: SignatureScheme, +) -> &'static dyn signature::VerificationAlgorithm { + match scheme { + SignatureScheme::Ed25519 => &signature::ED25519, + SignatureScheme::EcdsaP256Sha256 => &signature::ECDSA_P256_SHA256_ASN1, + SignatureScheme::EcdsaP384Sha384 => &signature::ECDSA_P384_SHA384_ASN1, + SignatureScheme::RsaPkcs1Sha1 => &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, + SignatureScheme::RsaPkcs1Sha256 => { + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + } + SignatureScheme::RsaPkcs1Sha384 => &signature::RSA_PKCS1_2048_8192_SHA384, + SignatureScheme::RsaPkcs1Sha512 => { + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + } + } +} + +fn verify_public_key_encoding( + scheme: SignatureScheme, + encoding: PublicKeyEncoding, +) -> Result<(), CryptoError> { + let valid = matches!( + (scheme, encoding), + (SignatureScheme::Ed25519, PublicKeyEncoding::Ed25519Raw) + | ( + SignatureScheme::EcdsaP256Sha256 | SignatureScheme::EcdsaP384Sha384, + PublicKeyEncoding::EcUncompressedPoint + ) + | ( + SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + PublicKeyEncoding::RsaPkcs1Der + ) + ); + if valid { + Ok(()) + } else { + Err(CryptoError::InvalidPublicKey) + } +} diff --git a/rtc-crypto/src/secret.rs b/rtc-crypto/src/secret.rs new file mode 100644 index 00000000..547a6f1d --- /dev/null +++ b/rtc-crypto/src/secret.rs @@ -0,0 +1,66 @@ +use std::fmt; + +use zeroize::Zeroizing; + +/// An owned byte vector that zeroizes its allocation when dropped. +pub struct SecretVec(Zeroizing>); + +impl SecretVec { + /// Wraps secret bytes. + #[must_use] + pub fn new(bytes: Vec) -> Self { + Self(Zeroizing::new(bytes)) + } + + /// Returns the number of secret bytes. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether the secret is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Explicitly unwraps the secret bytes. The returned vector is no longer automatically zeroized. + #[must_use] + pub fn into_bytes(mut self) -> Vec { + std::mem::take(&mut *self.0) + } +} + +impl AsRef<[u8]> for SecretVec { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl AsMut<[u8]> for SecretVec { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut_slice() + } +} + +impl Clone for SecretVec { + fn clone(&self) -> Self { + Self::new(self.as_ref().to_vec()) + } +} + +impl fmt::Debug for SecretVec { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("SecretVec") + .field("len", &self.len()) + .field("bytes", &"[REDACTED]") + .finish() + } +} + +impl From> for SecretVec { + fn from(value: Vec) -> Self { + Self::new(value) + } +} diff --git a/rtc-crypto/src/traits.rs b/rtc-crypto/src/traits.rs new file mode 100644 index 00000000..17f8e686 --- /dev/null +++ b/rtc-crypto/src/traits.rs @@ -0,0 +1,224 @@ +use std::sync::Arc; + +use crate::{ + AeadAlgorithm, BlockCipherAlgorithm, CbcAlgorithm, CryptoAlgorithm, CryptoError, HashAlgorithm, + HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, SecretVec, SignatureScheme, + StreamCipherAlgorithm, +}; + +/// A bundle of cryptographic operations and cryptographically secure randomness. +pub trait RTCCryptoProvider: Send + Sync { + /// Returns a non-secret diagnostic name. + fn name(&self) -> &'static str; + + /// Returns the cryptographic operations implementation. + fn crypto(&self) -> &dyn RTCCrypto; + + /// Returns the cryptographically secure random source. + fn random(&self) -> &dyn RTCRandom; +} + +/// A cryptographically secure random byte generator. +pub trait RTCRandom: Send + Sync { + /// Fills all of `output` with random bytes. + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError>; +} + +/// Provider-neutral cryptographic operations. +pub trait RTCCrypto: Send + Sync { + /// Reports whether an operation is implemented. + fn supports(&self, algorithm: CryptoAlgorithm) -> bool; + + /// Hashes `data`. + fn hash(&self, algorithm: HashAlgorithm, _data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + algorithm, + ))) + } + + /// Computes a native-length HMAC into `output`. + fn hmac( + &self, + algorithm: HmacAlgorithm, + _key: &[u8], + _input: &[&[u8]], + _output: &mut [u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))) + } + + /// Verifies a complete native-length HMAC tag. + fn verify_hmac( + &self, + algorithm: HmacAlgorithm, + _key: &[u8], + _input: &[&[u8]], + _expected: &[u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))) + } + + /// Encrypts exactly one block in place. + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + _key: &[u8], + _block: &mut [u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::BlockCipher(algorithm), + )) + } + + /// Creates a keyed stream cipher. + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::StreamCipher(algorithm), + )) + } + + /// Creates a keyed AEAD cipher. + fn new_aead( + &self, + algorithm: AeadAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Aead( + algorithm, + ))) + } + + /// Creates a keyed CBC cipher. + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Cbc( + algorithm, + ))) + } + + /// Starts a one-shot ephemeral key exchange. + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::KeyExchange(algorithm), + )) + } + + /// Generates an exportable signing key. + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )) + } + + /// Imports an exportable PKCS#8 signing key. + fn import_signing_key( + &self, + scheme: SignatureScheme, + _pkcs8_der: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )) + } + + /// Verifies a signature. + fn verify_signature( + &self, + scheme: SignatureScheme, + _public_key: PublicKey<'_>, + _message: &[u8], + _signature: &[u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )) + } +} + +/// A keyed stream cipher with a reusable expanded key. +pub trait StreamCipher: Send { + /// Applies the keystream in place with a fresh IV. + fn apply_keystream(&mut self, iv: &[u8], data: &mut [u8]) -> Result<(), CryptoError>; +} + +/// A keyed authenticated cipher with detached tags. +pub trait AeadCipher: Send { + /// Returns the detached tag length in bytes. + fn tag_len(&self) -> usize; + + /// Encrypts and authenticates a caller-owned buffer. + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError>; + + /// Authenticates and decrypts a caller-owned buffer. + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError>; +} + +/// A keyed CBC block cipher with a reusable expanded key. +pub trait CbcCipher: Send { + /// Returns the block and IV length in bytes. + fn block_len(&self) -> usize; + + /// Encrypts whole blocks in place without applying padding. + fn encrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError>; + + /// Decrypts whole blocks in place without removing padding. + fn decrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError>; +} + +/// Provider-owned one-shot ephemeral key exchange. +pub trait ActiveKeyExchange: Send { + /// Returns the key-exchange algorithm. + fn algorithm(&self) -> KeyExchangeAlgorithm; + + /// Returns the encoded wire public key. + fn public_key(&self) -> &[u8]; + + /// Consumes the private key and derives the shared secret. + fn complete(self: Box, peer_public_key: &[u8]) -> Result; +} + +/// A provider-owned signing key, including external or non-exportable keys. +pub trait SigningKey: Send + Sync { + /// Reports whether this key can sign with `scheme`. + fn supports(&self, scheme: SignatureScheme) -> bool; + + /// Returns the public key with explicit encoding. + fn public_key(&self) -> PublicKey<'_>; + + /// Signs `message`. + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError>; + + /// Exports PKCS#8 when supported. `Ok(None)` means the key is non-exportable. + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(None) + } +} diff --git a/rtc-crypto/tests/conformance.rs b/rtc-crypto/tests/conformance.rs new file mode 100644 index 00000000..9a09c1dc --- /dev/null +++ b/rtc-crypto/tests/conformance.rs @@ -0,0 +1,13 @@ +#![cfg(feature = "test-support")] + +#[cfg(feature = "ring")] +#[test] +fn ring_provider_conforms() { + rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::RingProvider::new()); +} + +#[cfg(feature = "aws-lc-rs")] +#[test] +fn aws_lc_rs_provider_conforms() { + rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::AwsLcRsProvider::new()); +} diff --git a/rtc-crypto/tests/cross_provider.rs b/rtc-crypto/tests/cross_provider.rs new file mode 100644 index 00000000..ada63a3f --- /dev/null +++ b/rtc-crypto/tests/cross_provider.rs @@ -0,0 +1,171 @@ +#![cfg(all(feature = "ring", feature = "aws-lc-rs"))] + +use rtc_crypto::providers::{AwsLcRsProvider, RingProvider}; +use rtc_crypto::{ + AeadAlgorithm, CbcAlgorithm, KeyExchangeAlgorithm, RTCCrypto, RTCCryptoProvider, + SignatureScheme, StreamCipherAlgorithm, +}; + +fn providers() -> (RingProvider, AwsLcRsProvider) { + (RingProvider::new(), AwsLcRsProvider::new()) +} + +#[test] +fn symmetric_ciphertext_is_interoperable() { + let (ring, aws) = providers(); + cross_symmetric(ring.crypto(), aws.crypto()); + cross_symmetric(aws.crypto(), ring.crypto()); +} + +fn cross_symmetric(sealer: &dyn RTCCrypto, opener: &dyn RTCCrypto) { + for (algorithm, key_len) in [ + (AeadAlgorithm::Aes128Gcm, 16), + (AeadAlgorithm::Aes256Gcm, 32), + (AeadAlgorithm::Aes128Ccm, 16), + (AeadAlgorithm::Aes128Ccm8, 16), + (AeadAlgorithm::ChaCha20Poly1305, 32), + ] { + let key = vec![0x23; key_len]; + let plaintext = b"cross-provider authenticated encryption".to_vec(); + let mut buffer = plaintext.clone(); + let mut sealer = sealer.new_aead(algorithm, &key).unwrap(); + let mut tag = vec![0; sealer.tag_len()]; + sealer + .seal_in_place(&[0x45; 12], b"rtc", &mut buffer, &mut tag) + .unwrap(); + opener + .new_aead(algorithm, &key) + .unwrap() + .open_in_place(&[0x45; 12], b"rtc", &mut buffer, &tag) + .unwrap(); + assert_eq!(buffer, plaintext); + } + + for (algorithm, key_len) in [ + (StreamCipherAlgorithm::Aes128Ctr, 16), + (StreamCipherAlgorithm::Aes256Ctr, 32), + ] { + let key = vec![0x34; key_len]; + let plaintext = b"cross-provider stream cipher".to_vec(); + let mut buffer = plaintext.clone(); + sealer + .new_stream_cipher(algorithm, &key) + .unwrap() + .apply_keystream(&[0x56; 16], &mut buffer) + .unwrap(); + opener + .new_stream_cipher(algorithm, &key) + .unwrap() + .apply_keystream(&[0x56; 16], &mut buffer) + .unwrap(); + assert_eq!(buffer, plaintext); + } + + let key = [0x67; 32]; + let iv = [0x78; 16]; + let plaintext = [0x89; 32]; + let mut blocks = plaintext; + sealer + .new_cbc(CbcAlgorithm::Aes256Cbc, &key) + .unwrap() + .encrypt_blocks(&iv, &mut blocks) + .unwrap(); + opener + .new_cbc(CbcAlgorithm::Aes256Cbc, &key) + .unwrap() + .decrypt_blocks(&iv, &mut blocks) + .unwrap(); + assert_eq!(blocks, plaintext); +} + +#[test] +fn key_exchange_is_interoperable() { + let (ring, aws) = providers(); + for algorithm in [ + KeyExchangeAlgorithm::P256, + KeyExchangeAlgorithm::P384, + KeyExchangeAlgorithm::X25519, + ] { + let left = ring.crypto().start_key_exchange(algorithm).unwrap(); + let right = aws.crypto().start_key_exchange(algorithm).unwrap(); + let left_public = left.public_key().to_vec(); + let right_public = right.public_key().to_vec(); + let left_secret = left.complete(&right_public).unwrap(); + let right_secret = right.complete(&left_public).unwrap(); + assert_eq!(left_secret.as_ref(), right_secret.as_ref()); + } +} + +#[test] +fn generated_and_imported_signatures_are_interoperable() { + let (ring, aws) = providers(); + cross_signatures(ring.crypto(), aws.crypto()); + cross_signatures(aws.crypto(), ring.crypto()); + + let rsa_der = pem::parse(include_str!("data/rsa-2048.pkcs8.pem")) + .unwrap() + .into_contents(); + let ring_key = ring + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &rsa_der) + .unwrap(); + let aws_key = aws + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &rsa_der) + .unwrap(); + let message = b"cross-provider RSA"; + let ring_signature = ring_key + .sign(SignatureScheme::RsaPkcs1Sha256, message) + .unwrap(); + aws.crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + ring_key.public_key(), + message, + &ring_signature, + ) + .unwrap(); + let aws_signature = aws_key + .sign(SignatureScheme::RsaPkcs1Sha256, message) + .unwrap(); + ring.crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + aws_key.public_key(), + message, + &aws_signature, + ) + .unwrap(); +} + +fn cross_signatures(generator: &dyn RTCCrypto, verifier: &dyn RTCCrypto) { + for scheme in [SignatureScheme::Ed25519, SignatureScheme::EcdsaP256Sha256] { + let generated = generator.generate_signing_key(scheme).unwrap(); + let message = b"cross-provider signature"; + let signature = generated.sign(scheme, message).unwrap(); + verifier + .verify_signature(scheme, generated.public_key(), message, &signature) + .unwrap(); + + let exported = generated.to_pkcs8_der().unwrap().unwrap(); + let imported = verifier + .import_signing_key(scheme, exported.as_ref()) + .unwrap(); + let signature = imported.sign(scheme, message).unwrap(); + generator + .verify_signature(scheme, imported.public_key(), message, &signature) + .unwrap(); + } +} + +#[test] +fn ring_remains_the_default_when_both_backends_are_enabled() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "ring"); + + let providers: Vec> = vec![ + Box::new(RingProvider::new()), + Box::new(AwsLcRsProvider::new()), + ]; + assert_eq!(providers[0].name(), "ring"); + assert_eq!(providers[1].name(), "aws-lc-rs"); +} diff --git a/rtc-crypto/tests/custom_provider.rs b/rtc-crypto/tests/custom_provider.rs new file mode 100644 index 00000000..5ac889ac --- /dev/null +++ b/rtc-crypto/tests/custom_provider.rs @@ -0,0 +1,76 @@ +use rtc_crypto::{CryptoAlgorithm, CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; + +#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] +use rtc_crypto::default_provider; + +struct CustomProvider { + crypto: CustomCrypto, + random: CustomRandom, +} + +struct CustomCrypto; + +impl RTCCrypto for CustomCrypto { + fn supports(&self, _algorithm: CryptoAlgorithm) -> bool { + false + } +} + +struct CustomRandom; + +impl RTCRandom for CustomRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + output.fill(0x5a); + Ok(()) + } +} + +#[cfg(feature = "test-support")] +struct FailingRandom; + +#[cfg(feature = "test-support")] +impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +impl RTCCryptoProvider for CustomProvider { + fn name(&self) -> &'static str { + "application-provider" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[test] +fn downstream_provider_requires_no_registration_or_backend_types() { + let provider: &dyn RTCCryptoProvider = &CustomProvider { + crypto: CustomCrypto, + random: CustomRandom, + }; + assert_eq!(provider.name(), "application-provider"); + let mut output = [0; 4]; + provider.random().fill(&mut output).unwrap(); + assert_eq!(output, [0x5a; 4]); + #[cfg(feature = "test-support")] + { + rtc_crypto::conformance::assert_unsupported_hash(provider.crypto()); + rtc_crypto::conformance::assert_random_failure(&FailingRandom); + } +} + +#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] +#[test] +fn no_builtin_provider_is_a_normal_error() { + assert!(matches!( + default_provider(), + Err(CryptoError::NoDefaultProvider) + )); +} diff --git a/rtc-crypto/tests/data/rsa-2048.pkcs8.pem b/rtc-crypto/tests/data/rsa-2048.pkcs8.pem new file mode 100644 index 00000000..8a1cf1b3 --- /dev/null +++ b/rtc-crypto/tests/data/rsa-2048.pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw+MSGjOm81NEW +MhYsA3a7Cdzi+sraonptStAbIXwKKeA2sb/QBSJU7D00k4MBnY/S1ciVrGeQvMbE +38p8JrvPVw7jvf9wgm+A6CVHdtjnO0MeK+zj19UV7dI62ojixxNqx5ZoXfikDtOM +5Z6u/CUJ4bJ3oDY5OOuOmYo/n9by7QEblp5I2J12UIkElh+Ng3VkZu/w8f1eRiHo +/wg/0Naqh7MeVgwYWhhiBZug+pX4ElrJa/x+BRw5llh9wScbs+v0kwMxTuiIzMhE +HeE6WbBkbZN137y9ZsFDU5jhZLxGcv1jou7xYqfBrH3bPpzbaM/qz/AfA2pVdHe0 +UXJZNyf1AgMBAAECggEABFM1bQW5RKh9f6mztFmqp4GN3heQ/kI69GbVVS8T+qTx +Wi+CkjvojeNuz3/Mvi0H8c17EdAH1pgL0jiXQ0IoaYsleFgRYReJpWsxq2Byhpwt +KhSavSrDhhNXhaFc97szyfKcxhTd2cHpq6eE9vPUl+bl11nurqapzcS02z81yp6z +rqbb0QfufF5TywinK78a0rKyY3mtF83z8kw9D3e1QF5r2AfrAK8vcGkZfOlK8mhk +3q3SaLKsWvB7emQ4ldigCM282jT3x+4vmPhOGbvtjsEpJEm28VTzIChqttHv2Rpg ++IVZccPVlyy4OEZoLqbnwP14rm5tX0R5cNjrxVIzQQKBgQD2yBbJ0UR/Cp3wKS3j +NTTGYDn7VYNBMlaXL4u2NKkNUGXHaUMykORx0IlQt1VpplX7qs4QHVwBekOJbgRa +aIjlT7QO8Z3EEQj/1Ba6fMmVad/fo6ToI2JAvT04gaSInYO2PkRyHNyi89+E2GpG +2yrY+2FGE2aNe+UdmjuW2zXvhQKBgQC3lRq9haGD+TvC/tGjzMgsMo0AaEaGqDm+ +k7M/+eCLsoT7xznloizM1UZ/jKg7jSOvqfzZs1Wps08DPZrCPyYJc9ak+vBimyWE +RTVHVxrmmqUpR8iehGdrTVAmbFOss2TnUK+io5+JCb3wRjt2RyQPWm5zSpHdl1wy +1oZICKNpsQKBgQCwOwlTDDd/BcTt6WpUk/1hIPynCFUYLOt7Qb/i2U5ULLLSKdCL +/r60rHgzBQlgziEe/MX06hJ3F6m9Lay8J2SDZVyvQ0on5wZnMz0b5dtK8PWnzkQI +ZqRWmQ1sGeC2ks2pSmQ0nXnOgJuBUc7rVL4Pf8zibx5QMUbX0fl17ItixQKBgHoB +oyLfg6c05Y3DUkodF8+fzOu/YVeux6mreY6EH8JX4199WTIO5N1AxLiSH2BsfZIK +VBvOvpiorVNHBuofk8Tmcnl0uHugBn/wiucdsagekLNtnJwU/LJoUGMozTdShjXg +/skFG0q06cGcu3nw77swa4U9wtFU/ZZf0iBfdVMRAoGAcZ2FcL4UsrBbIg6t8b/4 +K4wM5zNEt2o7kdVWLrrM0aBbWL1EEXFXLPXqhOEguSM4QL1ZCC8fxozYG0bNuiuj +y2R3K/AaeJDN/W9rM6x+Dbuj7QoOLDP9cSUf9iwaYtbrQcbDTk8SCzsyXle/bCli +L3Tuvt9w14f5auU05DpIfYQ= +-----END PRIVATE KEY----- diff --git a/rtc-crypto/tests/default_provider.rs b/rtc-crypto/tests/default_provider.rs new file mode 100644 index 00000000..f9792107 --- /dev/null +++ b/rtc-crypto/tests/default_provider.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "ring")] +#[test] +fn ring_is_the_default_when_enabled() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "ring"); +} + +#[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] +#[test] +fn aws_lc_rs_is_the_default_when_it_is_the_only_builtin() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "aws-lc-rs"); +} diff --git a/rtc-crypto/tests/rsa_import.rs b/rtc-crypto/tests/rsa_import.rs new file mode 100644 index 00000000..6db87d33 --- /dev/null +++ b/rtc-crypto/tests/rsa_import.rs @@ -0,0 +1,40 @@ +#![cfg(any(feature = "ring", feature = "aws-lc-rs"))] + +use rtc_crypto::{RTCCryptoProvider, SignatureScheme}; + +#[cfg(feature = "ring")] +#[test] +fn ring_imports_and_uses_rsa_pkcs8() { + assert_rsa_import(&rtc_crypto::providers::RingProvider::new()); +} + +#[cfg(feature = "aws-lc-rs")] +#[test] +fn aws_lc_rs_imports_and_uses_rsa_pkcs8() { + assert_rsa_import(&rtc_crypto::providers::AwsLcRsProvider::new()); +} + +fn assert_rsa_import(provider: &dyn RTCCryptoProvider) { + let pkcs8 = pem::parse(include_str!("data/rsa-2048.pkcs8.pem")) + .unwrap() + .into_contents(); + let key = provider + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &pkcs8) + .unwrap(); + let message = b"RSA PKCS#8 import conformance"; + let signature = key.sign(SignatureScheme::RsaPkcs1Sha256, message).unwrap(); + provider + .crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + key.public_key(), + message, + &signature, + ) + .unwrap(); + assert_eq!( + key.to_pkcs8_der().unwrap().unwrap().as_ref(), + pkcs8.as_slice() + ); +} diff --git a/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs b/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs index 4d15aca2..291f2f15 100644 --- a/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs @@ -35,7 +35,7 @@ const SSRC_ENTRY_OFFSET: usize = 20; /// Keep a table of powers to units for fast conversion. const BIT_UNITS: [&str; 7] = ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"]; -const UNIQUE_IDENTIFIER: [u8; 4] = [b'R', b'E', b'M', b'B']; +const UNIQUE_IDENTIFIER: [u8; 4] = *b"REMB"; /// String prints the REMB packet in a human-readable format. impl fmt::Display for ReceiverEstimatedMaximumBitrate { diff --git a/src/peer_connection/handler/sctp.rs b/src/peer_connection/handler/sctp.rs index 13f9c4e4..3ed53edb 100644 --- a/src/peer_connection/handler/sctp.rs +++ b/src/peer_connection/handler/sctp.rs @@ -72,7 +72,7 @@ impl<'a> SctpHandler<'a> { /// `flush_dirty` is set, after a burst of inbound packets has been ingested, so /// their SACKs coalesce into a single datagram. fn flush_transmits(&mut self, now: Instant) { - for (_ch, conn) in self.ctx.sctp_transport.sctp_associations.iter_mut() { + for conn in self.ctx.sctp_transport.sctp_associations.values_mut() { while let Some(x) = conn.poll_transmit(now) { for transmit in split_transmit(x) { if let Payload::RawEncode(raw_data) = transmit.message { From f8298ce989587393cf378b2722ee7bf2cf7fc6bb Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 18:36:02 -0700 Subject: [PATCH 31/40] =?UTF-8?q?P2=20=E2=80=94=20Migrate=20STUN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rtc-ice/Cargo.toml | 6 +- rtc-ice/src/agent/agent_selector.rs | 9 +- rtc-ice/src/agent/mod.rs | 42 +++++- rtc-shared/src/error.rs | 3 + rtc-stun/Cargo.toml | 9 +- rtc-stun/src/checks.rs | 10 -- rtc-stun/src/integrity.rs | 139 ++++++++++++++++---- rtc-stun/src/integrity/integrity_test.rs | 159 ++++++++++++++++++++++- rtc-stun/src/lib.rs | 7 - rtc-stun/src/message.rs | 12 +- rtc-turn/Cargo.toml | 5 +- rtc-turn/src/client/mod.rs | 47 +++++-- 12 files changed, 369 insertions(+), 79 deletions(-) diff --git a/rtc-ice/Cargo.toml b/rtc-ice/Cargo.toml index f25fc52e..569484e9 100644 --- a/rtc-ice/Cargo.toml +++ b/rtc-ice/Cargo.toml @@ -13,14 +13,15 @@ categories.workspace = true [features] default = ["ring"] -ring = ["stun/ring"] -aws-lc-rs = ["stun/aws-lc-rs"] +ring = ["crypto/ring", "stun/ring"] +aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = [] } sansio.workspace = true stun.workspace = true mdns.workspace = true +crypto.workspace = true crc = "3.0.1" log.workspace = true @@ -38,7 +39,6 @@ ipnet = "2.9.0" clap.workspace = true lazy_static = "1.4.0" hyper = { version = "0.14.28", features = ["full"] } -sha1 = "0.10.6" waitgroup = "0.1.2" serde_json = "1.0.114" tokio.workspace = true diff --git a/rtc-ice/src/agent/agent_selector.rs b/rtc-ice/src/agent/agent_selector.rs index 1610804e..34bfe8e2 100644 --- a/rtc-ice/src/agent/agent_selector.rs +++ b/rtc-ice/src/agent/agent_selector.rs @@ -113,8 +113,9 @@ impl Agent { Box::::default(), Box::new(AttrControlling(self.tie_breaker)), Box::new(PriorityAttr(pair.local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.clone(), )), Box::new(FINGERPRINT), ]); @@ -280,8 +281,9 @@ impl ControllingSelector for Agent { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(self.tie_breaker)), Box::new(PriorityAttr(self.local_candidates[local_index].priority())), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.clone(), )), Box::new(FINGERPRINT), ]); @@ -441,8 +443,9 @@ impl ControlledSelector for Agent { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(self.tie_breaker)), Box::new(PriorityAttr(self.local_candidates[local_index].priority())), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.clone(), )), Box::new(FINGERPRINT), ]); diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index 8fa3b4a0..c94b460b 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -21,6 +21,7 @@ pub mod agent_stats; use agent_config::*; use bytes::BytesMut; +use crypto::RTCCryptoProvider; use log::{debug, error, info, trace, warn}; use mdns::{Mdns, QueryId}; use sansio::Protocol; @@ -98,8 +99,13 @@ fn assert_inbound_username(m: &Message, expected_username: &str) -> Result<()> { Ok(()) } -fn assert_inbound_message_integrity(m: &mut Message, key: &[u8]) -> Result<()> { - let message_integrity_attr = MessageIntegrity(key.to_vec()); +fn assert_inbound_message_integrity( + m: &mut Message, + key: &[u8], + provider: Arc, +) -> Result<()> { + let message_integrity_attr = + MessageIntegrity::new_raw_integrity_with_provider(key.to_vec(), provider); message_integrity_attr.check(m) } @@ -119,6 +125,7 @@ pub enum Event { /// Represents the ICE agent. pub struct Agent { + pub(crate) crypto_provider: Arc, pub(crate) tie_breaker: u64, pub(crate) is_controlling: bool, pub(crate) lite: bool, @@ -187,6 +194,8 @@ pub struct Agent { impl Default for Agent { fn default() -> Self { Self { + crypto_provider: crypto::default_provider() + .expect("a default crypto provider is required"), tie_breaker: 0, is_controlling: false, lite: false, @@ -231,6 +240,16 @@ impl Default for Agent { impl Agent { /// Creates a new Agent. pub fn new(config: Arc) -> Result { + let provider = + crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + Self::new_with_provider(config, provider) + } + + /// Creates a new Agent using an explicitly selected crypto provider. + pub fn new_with_provider( + config: Arc, + crypto_provider: Arc, + ) -> Result { let mut mdns_local_name = config.multicast_dns_local_name.clone(); if mdns_local_name.is_empty() { mdns_local_name = generate_multicast_dns_name(); @@ -273,6 +292,7 @@ impl Agent { } let mut agent = Self { + crypto_provider, tie_breaker: rand::random::(), is_controlling: config.is_controlling, lite: config.lite, @@ -1030,7 +1050,10 @@ impl Agent { Box::new(m.clone()), Box::new(BINDING_SUCCESS), Box::new(XorMappedAddress { ip, port }), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + self.crypto_provider.clone(), + )), Box::new(FINGERPRINT), ]); (out, result) @@ -1071,7 +1094,10 @@ impl Agent { Box::new(m.clone()), Box::new(stun::message::BINDING_ERROR), Box::new(CODE_ROLE_CONFLICT), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + self.crypto_provider.clone(), + )), Box::new(FINGERPRINT), ]); (out, result) @@ -1287,8 +1313,11 @@ impl Agent { let mut remote_candidate_index = self.find_remote_candidate(remote_addr); if m.typ.class == CLASS_SUCCESS_RESPONSE { - if let Err(err) = assert_inbound_message_integrity(m, remote_credentials.pwd.as_bytes()) - { + if let Err(err) = assert_inbound_message_integrity( + m, + remote_credentials.pwd.as_bytes(), + self.crypto_provider.clone(), + ) { warn!( "[{}]: discard message from ({}), {}", self.get_name(), @@ -1324,6 +1353,7 @@ impl Agent { } else if let Err(err) = assert_inbound_message_integrity( m, self.ufrag_pwd.local_credentials.pwd.as_bytes(), + self.crypto_provider.clone(), ) { warn!( "[{}]: discard message from ({}), {}", diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index 5d0a3c39..d62973e5 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -2326,6 +2326,9 @@ pub enum Error { /// Other PeerConnection Err. #[error("Other PeerConnection Err: {0}")] OtherPeerConnectionErr(String), + /// A provider-neutral cryptographic operation failed. + #[error("crypto: {0}")] + Crypto(String), #[error("{0}")] /// An error that does not fit any other variant, carrying a description. Other(String), diff --git a/rtc-stun/Cargo.toml b/rtc-stun/Cargo.toml index 793cf64c..2c6030d4 100644 --- a/rtc-stun/Cargo.toml +++ b/rtc-stun/Cargo.toml @@ -14,23 +14,20 @@ categories.workspace = true [features] default = ["ring"] bench = [] -ring = ["dep:ring"] -aws-lc-rs = ["dep:aws-lc-rs"] +ring = ["crypto/ring"] +aws-lc-rs = ["crypto/aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = [] } sansio.workspace = true +crypto.workspace = true bytes.workspace = true lazy_static = "1.4.0" url = "2.5.0" rand.workspace = true base64 = "0.22.1" -subtle = "2.5.0" crc = "3.0.1" -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -md-5 = "0.10" [dev-dependencies] clap.workspace = true diff --git a/rtc-stun/src/checks.rs b/rtc-stun/src/checks.rs index 07312017..a7f35140 100644 --- a/rtc-stun/src/checks.rs +++ b/rtc-stun/src/checks.rs @@ -1,8 +1,6 @@ use crate::attributes::*; use shared::error::*; -use subtle::ConstantTimeEq; - /// Check_size returns ErrAttrSizeInvalid if got is not equal to expected. pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { if got == expected { @@ -17,14 +15,6 @@ pub fn is_attr_size_invalid(err: &Error) -> bool { Error::ErrAttributeSizeInvalid == *err } -pub(crate) fn check_hmac(got: &[u8], expected: &[u8]) -> Result<()> { - if got.ct_eq(expected).unwrap_u8() != 1 { - Err(Error::ErrIntegrityMismatch) - } else { - Ok(()) - } -} - pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { if got == expected { Ok(()) diff --git a/rtc-stun/src/integrity.rs b/rtc-stun/src/integrity.rs index 38d918a3..fa881a6d 100644 --- a/rtc-stun/src/integrity.rs +++ b/rtc-stun/src/integrity.rs @@ -2,13 +2,12 @@ mod integrity_test; use crate::attributes::*; -use crate::checks::*; use crate::message::*; -use md5::{Digest, Md5}; +use crypto::{CryptoError, HashAlgorithm, HmacAlgorithm, RTCCryptoProvider, SecretVec}; use shared::error::*; -use ring::hmac; use std::fmt; +use std::sync::Arc; // separator for credentials. pub(crate) const CREDENTIALS_SEP: &str = ":"; @@ -19,20 +18,26 @@ pub(crate) const CREDENTIALS_SEP: &str = ":"; // newHMAC function and internal/hmac/pool.go. // // RFC 5389 Section 15.4 -#[derive(Default, Clone)] +#[derive(Clone)] /// The `MESSAGE-INTEGRITY` key: an HMAC-SHA1 is computed over the message with it. /// /// Built from a short-term password, or from a long-term username/realm/password triple. -pub struct MessageIntegrity(pub Vec); +pub struct MessageIntegrity { + key: SecretVec, + provider: Arc, +} -fn new_hmac(key: &[u8], message: &[u8]) -> Vec { - let mac = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key); - hmac::sign(&mac, message).as_ref().to_vec() +fn crypto_error(error: CryptoError) -> Error { + Error::Crypto(error.to_string()) } impl fmt::Display for MessageIntegrity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "KEY: 0x{:x?}", self.0) + write!( + f, + "MESSAGE-INTEGRITY key: [REDACTED; {} bytes]", + self.key.len() + ) } } @@ -55,10 +60,18 @@ impl Setter for MessageIntegrity { // Adjusting m.Length to contain MESSAGE-INTEGRITY TLV. m.length += (MESSAGE_INTEGRITY_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; m.write_length(); // writing length to m.Raw - let v = new_hmac(&self.0, &m.raw); // calculating HMAC for adjusted m.Raw + let mut value = [0_u8; MESSAGE_INTEGRITY_SIZE]; + let result = self.provider.crypto().hmac( + HmacAlgorithm::Sha1, + self.key.as_ref(), + &[&m.raw], + &mut value, + ); m.length = length; // changing m.Length back + m.write_length(); + result.map_err(crypto_error)?; - m.add(ATTR_MESSAGE_INTEGRITY, &v); + m.add(ATTR_MESSAGE_INTEGRITY, &value); Ok(()) } @@ -67,21 +80,86 @@ impl Setter for MessageIntegrity { pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; impl MessageIntegrity { - /// New_long_term_integrity returns new MessageIntegrity with key for long-term - /// credentials. Password, username, and realm must be SASL-prepared. - pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { - let s = [username, realm, password].join(CREDENTIALS_SEP); + /// Creates a raw-key integrity attribute with an explicit crypto provider. + #[must_use] + pub fn new_raw_integrity_with_provider( + key: impl Into>, + provider: Arc, + ) -> Self { + Self { + key: SecretVec::new(key.into()), + provider, + } + } + + /// Creates a short-term integrity attribute with an explicit crypto provider. + #[must_use] + pub fn new_short_term_integrity_with_provider( + password: String, + provider: Arc, + ) -> Self { + Self::new_raw_integrity_with_provider(password.into_bytes(), provider) + } + + /// Creates a long-term integrity attribute with an explicit crypto provider. + pub fn new_long_term_integrity_with_provider( + username: String, + realm: String, + password: String, + provider: Arc, + ) -> Result { + let credentials = [username, realm, password].join(CREDENTIALS_SEP); + let key = provider + .crypto() + .hash(HashAlgorithm::Md5, credentials.as_bytes()) + .map_err(crypto_error)?; + if key.len() != 16 { + return Err(Error::Crypto(format!( + "provider returned an invalid MD5 digest length: {}", + key.len() + ))); + } + Ok(Self::new_raw_integrity_with_provider(key, provider)) + } - let mut h = Md5::new(); - h.update(s.as_bytes()); + /// Creates a raw-key integrity attribute using the built-in default provider. + /// + /// This compatibility adapter resolves the default once during construction and panics when + /// no built-in provider is enabled. New code should use + /// [`Self::new_raw_integrity_with_provider`]. + #[must_use] + pub fn new_raw_integrity(key: impl Into>) -> Self { + Self::new_raw_integrity_with_provider( + key, + crypto::default_provider().expect("a default crypto provider is required"), + ) + } - MessageIntegrity(h.finalize().as_slice().to_vec()) + /// Creates a long-term integrity attribute using the built-in default provider. + /// + /// Password, username, and realm must be SASL-prepared. This compatibility adapter resolves + /// the default once during construction and panics when no built-in provider is enabled. New + /// code should use [`Self::new_long_term_integrity_with_provider`]. + pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { + Self::new_long_term_integrity_with_provider( + username, + realm, + password, + crypto::default_provider().expect("a default crypto provider is required"), + ) + .expect("the default crypto provider must support STUN long-term credentials") } - /// New_short_term_integrity returns new MessageIntegrity with key for short-term - /// credentials. Password must be SASL-prepared. + /// Creates a short-term integrity attribute using the built-in default provider. + /// + /// Password must be SASL-prepared. This compatibility adapter resolves the default once during + /// construction and panics when no built-in provider is enabled. New code should use + /// [`Self::new_short_term_integrity_with_provider`]. pub fn new_short_term_integrity(password: String) -> Self { - MessageIntegrity(password.as_bytes().to_vec()) + Self::new_short_term_integrity_with_provider( + password, + crypto::default_provider().expect("a default crypto provider is required"), + ) } /// Check checks MESSAGE-INTEGRITY attribute. @@ -112,9 +190,24 @@ impl MessageIntegrity { let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); let b = &m.raw[..start_of_hmac]; // data before integrity attribute - let expected = new_hmac(&self.0, b); + let result = + self.provider + .crypto() + .verify_hmac(HmacAlgorithm::Sha1, self.key.as_ref(), &[b], &v); m.length = length as u32; m.write_length(); // writing length back - check_hmac(&v, &expected) + match result { + Ok(()) => Ok(()), + Err(CryptoError::AuthenticationFailed | CryptoError::InvalidTagLength { .. }) => { + Err(Error::ErrIntegrityMismatch) + } + Err(error) => Err(crypto_error(error)), + } + } +} + +impl Default for MessageIntegrity { + fn default() -> Self { + Self::new_raw_integrity(Vec::new()) } } diff --git a/rtc-stun/src/integrity/integrity_test.rs b/rtc-stun/src/integrity/integrity_test.rs index a3992e69..7aee6da2 100644 --- a/rtc-stun/src/integrity/integrity_test.rs +++ b/rtc-stun/src/integrity/integrity_test.rs @@ -3,6 +3,155 @@ use crate::attributes::ATTR_SOFTWARE; use crate::fingerprint::FINGERPRINT; use crate::message::TransactionId; use crate::textattrs::TextAttribute; +use crypto::{ + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, RTCCrypto, RTCCryptoProvider, + RTCRandom, constant_time_eq, +}; +use std::sync::Arc; + +struct TestProvider { + crypto: TestCrypto, + random: TestRandom, +} + +struct TestCrypto; + +struct TestRandom; + +impl RTCCryptoProvider for TestProvider { + fn name(&self) -> &'static str { + "test" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +impl RTCRandom for TestRandom { + fn fill(&self, output: &mut [u8]) -> std::result::Result<(), CryptoError> { + output.fill(0x42); + Ok(()) + } +} + +impl RTCCrypto for TestCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5) | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1) + ) + } + + fn hash( + &self, + algorithm: HashAlgorithm, + data: &[u8], + ) -> std::result::Result, CryptoError> { + if algorithm != HashAlgorithm::Md5 { + return Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + algorithm, + ))); + } + let mut output = vec![0_u8; 16]; + let output_len = output.len(); + for (index, byte) in data.iter().enumerate() { + output[index % output_len] ^= byte; + } + Ok(output) + } + + fn hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + output: &mut [u8], + ) -> std::result::Result<(), CryptoError> { + if algorithm != HmacAlgorithm::Sha1 { + return Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))); + } + if output.len() != algorithm.output_len() { + return Err(CryptoError::InvalidTagLength { + expected: algorithm.output_len(), + actual: output.len(), + }); + } + output.fill(0); + for (index, byte) in key + .iter() + .chain(input.iter().flat_map(|part| part.iter())) + .enumerate() + { + output[index % algorithm.output_len()] ^= byte; + } + Ok(()) + } + + fn verify_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + expected: &[u8], + ) -> std::result::Result<(), CryptoError> { + if expected.len() != algorithm.output_len() { + return Err(CryptoError::InvalidTagLength { + expected: algorithm.output_len(), + actual: expected.len(), + }); + } + let mut actual = vec![0_u8; algorithm.output_len()]; + self.hmac(algorithm, key, input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + +fn test_provider() -> Arc { + Arc::new(TestProvider { + crypto: TestCrypto, + random: TestRandom, + }) +} + +#[test] +fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<()> { + let integrity = MessageIntegrity::new_long_term_integrity_with_provider( + "user".to_owned(), + "realm".to_owned(), + "password".to_owned(), + test_provider(), + )?; + let mut message = Message::new(); + message.write_header(); + integrity.add_to(&mut message)?; + integrity.check(&mut message)?; + + let attribute = message + .attributes + .0 + .iter_mut() + .find(|attribute| attribute.typ == ATTR_MESSAGE_INTEGRITY) + .expect("MESSAGE-INTEGRITY attribute"); + attribute.value.pop(); + attribute.length -= 1; + assert_eq!( + integrity.check(&mut message), + Err(Error::ErrIntegrityMismatch) + ); + + Ok(()) +} #[test] fn test_message_integrity_add_to_simple() -> Result<()> { @@ -15,7 +164,7 @@ fn test_message_integrity_add_to_simple() -> Result<()> { let expected = vec![ 104, 228, 91, 113, 61, 154, 222, 34, 101, 61, 181, 146, 177, 90, 4, 29, ]; - assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); + assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); } let i = MessageIntegrity::new_long_term_integrity( @@ -27,7 +176,7 @@ fn test_message_integrity_add_to_simple() -> Result<()> { 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, 0xeb, ]; - assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); + assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); //"Check" { @@ -67,7 +216,11 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { a.add_to(&mut m)?; let i = MessageIntegrity::new_short_term_integrity("pwd".to_owned()); - assert_eq!(i.to_string(), "KEY: 0x[70, 77, 64]", "bad string {i}"); + assert_eq!( + i.to_string(), + "MESSAGE-INTEGRITY key: [REDACTED; 3 bytes]", + "bad string {i}" + ); let result = i.check(&mut m); assert!(result.is_err(), "should error"); diff --git a/rtc-stun/src/lib.rs b/rtc-stun/src/lib.rs index f853a8a6..d4dc63f7 100644 --- a/rtc-stun/src/lib.rs +++ b/rtc-stun/src/lib.rs @@ -92,10 +92,3 @@ pub mod xoraddr; pub const DEFAULT_PORT: u16 = 3478; /// The default port for `stuns:` (STUN over TLS/DTLS). pub const DEFAULT_TLS_PORT: u16 = 5349; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index 63a02858..16a3b80b 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -43,8 +43,10 @@ pub const TRANSACTION_ID_SIZE: usize = 12; pub struct TransactionId(pub [u8; TRANSACTION_ID_SIZE]); impl TransactionId { - /// new returns new random transaction ID using crypto/rand - /// as source. + /// Creates a random transaction ID with `rand`'s cryptographically secure thread RNG. + /// + /// Transaction IDs remain independent of [`crypto::RTCCryptoProvider`] because ordinary STUN + /// message construction does not otherwise own a provider. pub fn new() -> Self { let mut b = TransactionId([0u8; TRANSACTION_ID_SIZE]); rand::rng().fill(&mut b.0); @@ -197,8 +199,10 @@ impl Message { self.decode() } - /// NewTransactionID sets m.TransactionID to random value from crypto/rand - /// and returns error if any. + /// Replaces the transaction ID using `rand`'s cryptographically secure thread RNG. + /// + /// Transaction IDs remain independent of [`crypto::RTCCryptoProvider`] because a general + /// [`Message`] does not otherwise own a provider. pub fn new_transaction_id(&mut self) -> Result<()> { rand::rng().fill(&mut self.transaction_id.0); self.write_transaction_id(); diff --git a/rtc-turn/Cargo.toml b/rtc-turn/Cargo.toml index e496ea36..1ee294ca 100644 --- a/rtc-turn/Cargo.toml +++ b/rtc-turn/Cargo.toml @@ -15,6 +15,7 @@ categories.workspace = true shared = { workspace = true, default-features = false, features = [] } stun.workspace = true sansio.workspace = true +crypto.workspace = true bytes.workspace = true log.workspace = true @@ -31,8 +32,8 @@ ctrlc.workspace = true [features] default = ["ring"] metrics = [] -ring = ["stun/ring"] -aws-lc-rs = ["stun/aws-lc-rs"] +ring = ["crypto/ring", "stun/ring"] +aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] [[bench]] name = "bench" diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 947b9f95..6c4a12a5 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -21,9 +21,11 @@ pub mod relay; pub mod transaction; use bytes::BytesMut; +use crypto::RTCCryptoProvider; use log::{debug, trace}; use std::collections::{HashMap, VecDeque}; use std::net::SocketAddr; +use std::sync::Arc; use std::time::{Duration, Instant}; use stun::attributes::*; @@ -134,6 +136,7 @@ pub struct ClientConfig { /// Client is a STUN client pub struct Client { + crypto_provider: Arc, stun_serv_addr: Option, turn_serv_addr: Option, local_addr: SocketAddr, @@ -155,6 +158,16 @@ pub struct Client { impl Client { /// new returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0" pub fn new(config: ClientConfig) -> Result { + let provider = + crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + Self::new_with_provider(config, provider) + } + + /// Creates a client using an explicitly selected crypto provider. + pub fn new_with_provider( + config: ClientConfig, + crypto_provider: Arc, + ) -> Result { let stun_serv_addr = if config.stun_serv_addr.is_empty() { None } else { @@ -174,6 +187,7 @@ impl Client { }; Ok(Client { + crypto_provider: crypto_provider.clone(), stun_serv_addr, turn_serv_addr, local_addr: config.local_addr, @@ -189,7 +203,10 @@ impl Client { } else { DEFAULT_RTO_IN_MS }, - integrity: MessageIntegrity::new_short_term_integrity(String::new()), + integrity: MessageIntegrity::new_short_term_integrity_with_provider( + String::new(), + crypto_provider, + ), relays: HashMap::new(), transmits: VecDeque::new(), @@ -491,18 +508,21 @@ impl Client { /// /// The realm is *not* re-negotiated. It was learned from the server's 401 during the /// first `Allocate`, and a credential rotation keeps the same server, so it still - /// applies. Follow this with [`Relay::refresh`] so the server sees the new credential - /// before the allocation would otherwise expire. + /// applies. Follow this with [`Self::refresh_allocations`] so the server sees the new + /// credential before the allocation would otherwise expire. /// /// [RFC 5766 §6.2]: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 - pub fn update_credentials(&mut self, username: String, password: String) { - self.username = Username::new(ATTR_USERNAME, username); - self.password = password; - self.integrity = MessageIntegrity::new_long_term_integrity( - self.username.text.clone(), + pub fn update_credentials(&mut self, username: String, password: String) -> Result<()> { + let username = Username::new(ATTR_USERNAME, username); + let integrity = MessageIntegrity::new_long_term_integrity_with_provider( + username.text.clone(), self.realm.text.clone(), - self.password.clone(), - ); + password.clone(), + self.crypto_provider.clone(), + )?; + self.username = username; + self.password = password; + self.integrity = integrity; // Each allocation carries the integrity it will sign its own Refresh / // CreatePermission / ChannelBind with, so they have to be re-signed too — otherwise @@ -510,6 +530,8 @@ impl Client { for relay in self.relays.values_mut() { relay.integrity = self.integrity.clone(); } + + Ok(()) } /// Refreshes every live allocation, re-signing each with the current credential. @@ -581,11 +603,12 @@ impl Client { } }; - self.integrity = MessageIntegrity::new_long_term_integrity( + self.integrity = MessageIntegrity::new_long_term_integrity_with_provider( self.username.text.clone(), self.realm.text.clone(), self.password.clone(), - ); + self.crypto_provider.clone(), + )?; let mut msg = Message::new(); From 425494cf54579b4722f84f8d6f55f85243bcff9f Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 19:14:28 -0700 Subject: [PATCH 32/40] =?UTF-8?q?P3=20=E2=80=94=20Audit=20ICE=20and=20SCTP?= =?UTF-8?q?=20randomness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/randomness-audit.md | 33 ++++++++++++ rtc-ice/Cargo.toml | 1 - rtc-ice/src/agent/mod.rs | 8 +-- rtc-ice/src/candidate/mod.rs | 3 ++ rtc-ice/src/mdns/mod.rs | 2 + rtc-ice/src/rand/mod.rs | 65 ++++++++++++++++++++++-- rtc-ice/src/rand/rand_test.rs | 56 ++++++++++++++++++++ rtc-sctp/src/association/mod.rs | 8 +-- rtc-sctp/src/param/param_state_cookie.rs | 20 ++++++++ rtc-sctp/src/util.rs | 21 +++++++- 10 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 docs/randomness-audit.md diff --git a/docs/randomness-audit.md b/docs/randomness-audit.md new file mode 100644 index 00000000..1f2352a5 --- /dev/null +++ b/docs/randomness-audit.md @@ -0,0 +1,33 @@ +# ICE and SCTP randomness audit + +## Scope + +`RTCRandom` is the workspace boundary for cryptographically secure randomness when a component naturally owns an `RTCCryptoProvider`. It is not a replacement for every random-looking value in the workspace. Standalone crates may retain a direct CSPRNG when adding provider ownership would distort their public API, while deterministic protocol fields and test fixtures should remain deterministic. + +## ICE + +| Value | Security role | Source and decision | +| --- | --- | --- | +| Local username fragment | Short-term ICE credential | `Agent` generates it through `RTCCryptoProvider::random()`. The standalone compatibility helper retains `rand`'s thread-local CSPRNG. An application-supplied value remains unchanged. | +| Local password | Short-term ICE credential | `Agent` generates it through `RTCCryptoProvider::random()`. The standalone compatibility helper retains `rand`'s thread-local CSPRNG. An application-supplied value remains unchanged. | +| Controlling/controlled tie breaker | Unpredictable role-conflict input | Generated through `RTCCryptoProvider::random()` because `Agent` already owns the provider. | +| STUN transaction ID | Request correlation with an unpredictability requirement | `rtc-stun` retains its documented direct CSPRNG path because a `Message` can be constructed without provider ownership. This is separate from STUN integrity and fingerprint operations, which use the configured provider. | +| Candidate ID | Local uniqueness and diagnostics | Retains the direct CSPRNG helper. It is neither a credential nor a cryptographic identity, and passing a provider solely for this field would add unnecessary coupling. | +| Candidate foundation | Candidate grouping and redundancy elimination | Deterministic CRC32C over candidate properties. It is intentionally not random or secret. | +| mDNS host name | Privacy-preserving local alias | Retains UUID v4 generation through the UUID crate's secure random source. The mDNS subsystem does not naturally own the provider. | +| Explicit configuration and test values | Application policy and deterministic fixtures | Preserved exactly as supplied. Tests may use deterministic `RTCRandom` implementations to verify provider routing and error propagation. | + +## SCTP + +| Value | Security role | Source and decision | +| --- | --- | --- | +| Verification tag / association ID | Unpredictable nonzero SCTP verification tag | Generated with `rand`'s thread-local CSPRNG and rejected if zero. `rtc-sctp` remains independently usable and does not otherwise need an RTC crypto provider. | +| Initial TSN | Unpredictable starting sequence number | Generated with `rand`'s thread-local CSPRNG and adjusted to be nonzero. Provider propagation would add ownership solely for this field. | +| State-cookie bytes | Unpredictable 256-bit challenge echoed by the peer | Generated with `rand`'s thread-local CSPRNG. Received cookies are compared in constant time. The current in-memory cookie design does not encrypt, authenticate, or serialize server state into the value. | +| Retransmission counters, sequence arithmetic, and timers | Derived protocol state | Deterministic values derived from negotiated state and elapsed time; no random source is appropriate. | +| `ParamRandom` | Peer-provided extension negotiation input | Parsed from the peer rather than generated locally. It is not an application randomness boundary. | +| Unit-test packets and fixtures | Reproducible protocol inputs | Fixed constants remain intentional. Tests for generated values check invariants rather than depending on exact output. | + +## Provider boundary decision + +ICE already owns `Arc` for STUN operations, so routing credential and tie-breaker generation through the same provider is a natural extension of that ownership. SCTP has no cryptographic operation requiring `RTCCrypto`, and its few security-sensitive random values are safely served by its existing direct CSPRNG. Therefore this phase does not add an `rtc-crypto` dependency or provider parameter to `rtc-sctp`. diff --git a/rtc-ice/Cargo.toml b/rtc-ice/Cargo.toml index 569484e9..051eb004 100644 --- a/rtc-ice/Cargo.toml +++ b/rtc-ice/Cargo.toml @@ -25,7 +25,6 @@ crypto.workspace = true crc = "3.0.1" log.workspace = true -rand.workspace = true serde.workspace = true url = "2.5.0" uuid = { version = "1", features = ["v4"] } diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index c94b460b..d31e34d2 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -250,6 +250,8 @@ impl Agent { config: Arc, crypto_provider: Arc, ) -> Result { + let tie_breaker = generate_tie_breaker(crypto_provider.random())?; + let mut mdns_local_name = config.multicast_dns_local_name.clone(); if mdns_local_name.is_empty() { mdns_local_name = generate_multicast_dns_name(); @@ -293,7 +295,7 @@ impl Agent { let mut agent = Self { crypto_provider, - tie_breaker: rand::random::(), + tie_breaker, is_controlling: config.is_controlling, lite: config.lite, @@ -688,10 +690,10 @@ impl Agent { keep_local_candidates: bool, ) -> Result<()> { if ufrag.is_empty() { - ufrag = generate_ufrag(); + ufrag = generate_ufrag_with_random(self.crypto_provider.random())?; } if pwd.is_empty() { - pwd = generate_pwd(); + pwd = generate_pwd_with_random(self.crypto_provider.random())?; } if ufrag.len() * 8 < 24 { diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index ff278822..0a20be83 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -254,6 +254,9 @@ impl fmt::Display for Candidate { impl Candidate { /// The candidate's foundation, computed from its type, base address and transport. + /// + /// A foundation groups equivalent candidates; it is deterministic bookkeeping rather than a + /// secret, random identifier. CRC-32C is therefore intentional here. pub fn foundation(&self) -> String { if !self.foundation_override.is_empty() { return self.foundation_override.clone(); diff --git a/rtc-ice/src/mdns/mod.rs b/rtc-ice/src/mdns/mod.rs index e6ddb187..1c37ce7a 100644 --- a/rtc-ice/src/mdns/mod.rs +++ b/rtc-ice/src/mdns/mod.rs @@ -27,6 +27,8 @@ pub enum MulticastDnsMode { pub(crate) fn generate_multicast_dns_name() -> String { // https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering // The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”. + // This is a short-lived privacy alias, not a credential. `Uuid::new_v4` obtains randomness + // from the UUID crate's secure random source without forcing provider ownership into mDNS. let u = Uuid::new_v4(); format!("{u}.local") } diff --git a/rtc-ice/src/rand/mod.rs b/rtc-ice/src/rand/mod.rs index 6fd04661..0b03b0bf 100644 --- a/rtc-ice/src/rand/mod.rs +++ b/rtc-ice/src/rand/mod.rs @@ -1,3 +1,5 @@ +use crypto::RTCRandom; +use shared::error::{Error, Result}; use shared::util::generate_crypto_random_string; #[cfg(test)] @@ -14,6 +16,10 @@ const LEN_PWD: usize = 32; /// candidate-id = "candidate" ":" foundation /// foundation = 1*32ice-char /// ice-char = ALPHA / DIGIT / "+" / "/" +/// +/// Candidate IDs provide local uniqueness and diagnostics; they are not credentials or +/// cryptographic identities. This standalone helper therefore uses `rand`'s thread-local CSPRNG +/// rather than requiring a crypto provider. pub fn generate_cand_id() -> String { format!( "candidate:{}", @@ -21,14 +27,65 @@ pub fn generate_cand_id() -> String { ) } -/// Generates ICE pwd. -/// This internally uses `generate_crypto_random_string`. +fn generate_string_with_random( + length: usize, + alphabet: &[u8], + random: &dyn RTCRandom, +) -> Result { + debug_assert!(!alphabet.is_empty() && alphabet.len() <= u8::MAX as usize + 1); + + let acceptance_limit = 256 - (256 % alphabet.len()); + let mut output = String::with_capacity(length); + let mut random_bytes = [0_u8; 64]; + while output.len() < length { + random + .fill(&mut random_bytes) + .map_err(|error| Error::Crypto(error.to_string()))?; + for byte in random_bytes { + if byte as usize >= acceptance_limit { + continue; + } + output.push(alphabet[byte as usize % alphabet.len()] as char); + if output.len() == length { + break; + } + } + } + Ok(output) +} + +/// Generates an ICE password with `rand`'s thread-local CSPRNG. +/// +/// ICE agents use the provider-backed internal variant. This function remains as a standalone +/// compatibility helper for callers that do not own an [`crypto::RTCCryptoProvider`]. pub fn generate_pwd() -> String { generate_crypto_random_string(LEN_PWD, RUNES_ALPHA) } -/// ICE user fragment. -/// This internally uses `generate_crypto_random_string`. +/// Generates an ICE password using an explicitly supplied cryptographically secure random source. +pub(crate) fn generate_pwd_with_random(random: &dyn RTCRandom) -> Result { + generate_string_with_random(LEN_PWD, RUNES_ALPHA, random) +} + +/// Generates the 64-bit ICE role-conflict tie breaker from the agent's random provider. +pub(crate) fn generate_tie_breaker(random: &dyn RTCRandom) -> Result { + let mut bytes = [0_u8; std::mem::size_of::()]; + random + .fill(&mut bytes) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(u64::from_be_bytes(bytes)) +} + +/// Generates an ICE username fragment with `rand`'s thread-local CSPRNG. +/// +/// ICE agents use the provider-backed internal variant. This function remains as a standalone +/// compatibility helper for callers that do not own an [`crypto::RTCCryptoProvider`]. pub fn generate_ufrag() -> String { generate_crypto_random_string(LEN_UFRAG, RUNES_ALPHA) } + +/// Generates an ICE username fragment using an explicitly supplied cryptographically secure random +/// source. +pub(crate) fn generate_ufrag_with_random(random: &dyn RTCRandom) -> Result { + generate_string_with_random(LEN_UFRAG, RUNES_ALPHA, random) +} diff --git a/rtc-ice/src/rand/rand_test.rs b/rtc-ice/src/rand/rand_test.rs index 568bfc5c..75fc5236 100644 --- a/rtc-ice/src/rand/rand_test.rs +++ b/rtc-ice/src/rand/rand_test.rs @@ -1,6 +1,62 @@ use super::*; +use crypto::{CryptoError, RTCRandom}; use shared::error::Result; +struct FixedRandom(u8); + +impl RTCRandom for FixedRandom { + fn fill(&self, output: &mut [u8]) -> std::result::Result<(), CryptoError> { + output.fill(self.0); + Ok(()) + } +} + +struct FailingRandom; + +impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +#[test] +fn provider_backed_credentials_have_required_lengths_and_alphabet() -> Result<()> { + let random = FixedRandom(0); + let ufrag = generate_ufrag_with_random(&random)?; + let password = generate_pwd_with_random(&random)?; + + assert_eq!(ufrag.len(), LEN_UFRAG); + assert_eq!(password.len(), LEN_PWD); + assert!(ufrag.bytes().all(|byte| RUNES_ALPHA.contains(&byte))); + assert!(password.bytes().all(|byte| RUNES_ALPHA.contains(&byte))); + Ok(()) +} + +#[test] +fn provider_backed_credentials_propagate_randomness_failure() { + assert!(matches!( + generate_pwd_with_random(&FailingRandom), + Err(Error::Crypto(_)) + )); +} + +#[test] +fn provider_backed_tie_breaker_uses_all_random_bytes() -> Result<()> { + assert_eq!( + generate_tie_breaker(&FixedRandom(0x2a))?, + u64::from_be_bytes([0x2a; 8]) + ); + Ok(()) +} + +#[test] +fn provider_backed_tie_breaker_propagates_randomness_failure() { + assert!(matches!( + generate_tie_breaker(&FailingRandom), + Err(Error::Crypto(_)) + )); +} + #[test] fn test_random_generator_collision() -> Result<()> { let test_cases = vec![ diff --git a/rtc-sctp/src/association/mod.rs b/rtc-sctp/src/association/mod.rs index 8ddeacc4..38a45367 100644 --- a/rtc-sctp/src/association/mod.rs +++ b/rtc-sctp/src/association/mod.rs @@ -26,7 +26,7 @@ use crate::param::{ }; use crate::queue::{payload_queue::PayloadQueue, pending_queue::PendingQueue}; use crate::shared::{AssociationEventInner, AssociationId, EndpointEvent, EndpointEventInner}; -use crate::util::{sna16lt, sna32gt, sna32gte, sna32lt, sna32lte}; +use crate::util::{constant_time_eq, sna16lt, sna32gt, sna32gte, sna32lt, sna32lte}; use crate::{AssociationEvent, Payload, Side}; use shared::error::{Error, Result}; use shared::{TransportContext, TransportMessage, TransportProtocol}; @@ -351,6 +351,8 @@ impl Association { // The initial cwnd before DATA transmission or after a sufficiently // long idle period MUST be set to min(4*MTU, max (2*MTU, 4380bytes)). let cwnd = (2 * mtu).clamp(4380, 4 * mtu); + // RFC 4960 requires an unpredictable initial TSN. SCTP remains usable without an RTC + // crypto provider, so this deliberately uses `rand`'s thread-local CSPRNG. let mut tsn = random::(); if tsn == 0 { tsn += 1; @@ -1141,14 +1143,14 @@ impl Association { if let Some(my_cookie) = &self.my_cookie { match state { AssociationState::Established => { - if my_cookie.cookie != c.cookie { + if !constant_time_eq(&my_cookie.cookie, &c.cookie) { return Ok(vec![]); } } AssociationState::Closed | AssociationState::CookieWait | AssociationState::CookieEchoed => { - if my_cookie.cookie != c.cookie { + if !constant_time_eq(&my_cookie.cookie, &c.cookie) { return Ok(vec![]); } diff --git a/rtc-sctp/src/param/param_state_cookie.rs b/rtc-sctp/src/param/param_state_cookie.rs index 8c861716..f9fd6d7f 100644 --- a/rtc-sctp/src/param/param_state_cookie.rs +++ b/rtc-sctp/src/param/param_state_cookie.rs @@ -51,6 +51,8 @@ impl Param for ParamStateCookie { impl ParamStateCookie { pub(crate) fn new() -> Self { + // This 256-bit value is an unpredictable challenge echoed by the peer. SCTP remains usable + // without an RTC crypto provider, so this deliberately uses `rand`'s thread-local CSPRNG. let mut cookie = BytesMut::new(); cookie.resize(32, 0); rand::rng().fill(cookie.as_mut()); @@ -60,3 +62,21 @@ impl ParamStateCookie { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_state_cookies_have_full_entropy_width() { + assert_eq!(ParamStateCookie::new().cookie.len(), 32); + } + + #[test] + fn generated_state_cookies_are_distinct() { + assert_ne!( + ParamStateCookie::new().cookie, + ParamStateCookie::new().cookie + ); + } +} diff --git a/rtc-sctp/src/util.rs b/rtc-sctp/src/util.rs index e96a2e98..3c945351 100644 --- a/rtc-sctp/src/util.rs +++ b/rtc-sctp/src/util.rs @@ -42,7 +42,11 @@ pub trait AssociationIdGenerator: Send + Sync { fn aid_lifetime(&self) -> Option; } -/// Generates purely random Association IDs of a certain length +/// Generates nonzero, unpredictable SCTP verification tags. +/// +/// SCTP is usable as a standalone crate and does not naturally own an RTC crypto provider. The +/// `rand` convenience API used here draws from its thread-local CSPRNG, preserving that standalone +/// API while satisfying the verification tag's security requirement. #[derive(Default, Debug, Clone, Copy)] pub struct RandomAssociationIdGenerator { lifetime: Option, @@ -63,7 +67,12 @@ impl RandomAssociationIdGenerator { impl AssociationIdGenerator for RandomAssociationIdGenerator { fn generate_aid(&mut self) -> AssociationId { - rand::random::() + loop { + let association_id = rand::random::(); + if association_id != 0 { + return association_id; + } + } } fn aid_lifetime(&self) -> Option { @@ -315,6 +324,14 @@ mod test { use super::*; + #[test] + fn random_association_ids_are_nonzero() { + let mut generator = RandomAssociationIdGenerator::new(); + for _ in 0..128 { + assert_ne!(generator.generate_aid(), 0); + } + } + #[test] fn test_bytes_chunk_pops_zero_copy_slices() { let data = Bytes::from(vec![0x5au8; 100]); From fd81f68fd08855facf439971e68f2f6cc187a7c0 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 20:07:58 -0700 Subject: [PATCH 33/40] =?UTF-8?q?P4=20=E2=80=94=20Migrate=20SRTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- Cargo.toml | 2 - docs/crypto-provider-decisions.md | 4 +- docs/crypto-provider-migration.md | 11 + rtc-srtp/Cargo.toml | 19 +- rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs | 133 +++--- ...trcipher.rs => cipher_aes_cm_hmac_sha1.rs} | 234 ++++++---- .../src/cipher/cipher_aes_cm_hmac_sha1/mod.rs | 135 ------ .../cipher_aes_cm_hmac_sha1/opensslcipher.rs | 308 ------------- rtc-srtp/src/cipher/mod.rs | 11 +- rtc-srtp/src/context/context_test.rs | 3 + rtc-srtp/src/context/mod.rs | 44 +- rtc-srtp/src/key_derivation.rs | 50 ++- rtc-srtp/src/lib.rs | 10 +- rtc-srtp/src/protection_profile.rs | 95 ++++ rtc-srtp/tests/provider_profiles.rs | 406 ++++++++++++++++++ 16 files changed, 816 insertions(+), 651 deletions(-) create mode 100644 docs/crypto-provider-migration.md rename rtc-srtp/src/cipher/{cipher_aes_cm_hmac_sha1/ctrcipher.rs => cipher_aes_cm_hmac_sha1.rs} (51%) delete mode 100644 rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs delete mode 100644 rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs create mode 100644 rtc-srtp/tests/provider_profiles.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bc14f975..0bc32402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -- +- Removed the partial `openssl` and `vendored-openssl` Cargo features from `rtc-srtp` and `rtc`; SRTP cryptography now uses the selected `rtc-crypto` provider for every protection profile. ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 4f87da46..74e544ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,8 +88,6 @@ readme = "README.md" [features] default = ["ring"] pem = ["dep:pem", "dtls/pem"] -openssl = ["srtp/openssl"] -vendored-openssl = ["srtp/vendored-openssl"] ring = ["dep:ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] aws-lc-rs = ["dep:aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] diff --git a/docs/crypto-provider-decisions.md b/docs/crypto-provider-decisions.md index 03db73ea..dc77164c 100644 --- a/docs/crypto-provider-decisions.md +++ b/docs/crypto-provider-decisions.md @@ -4,9 +4,9 @@ These decisions freeze the boundaries needed to start G3 implementation. Reopeni ## SRTP OpenSSL features -The `openssl` and `vendored-openssl` features are partial SRTP implementation choices, not complete RTC crypto providers. They will be deprecated during the migration and removed before 1.0 in P4-04. A future OpenSSL provider is possible only as a complete downstream implementation of the public provider traits that passes the same conformance suite as the built-in providers; it will not preserve these partial features by accident. +The `openssl` and `vendored-openssl` features were partial SRTP implementation choices, not complete RTC crypto providers, and were removed when SRTP migrated to `rtc-crypto`. A future OpenSSL provider is possible only as a complete downstream implementation of the public provider traits that passes the same conformance suite as the built-in providers; partial protocol-specific backend features will not return. -The deprecation must be called out in the changelog when it begins, and removal must be included in the pre-1.0 migration guide. +The removal is recorded in the changelog and the crypto-provider migration guide. This pre-1.0 cleanup is intentional: retaining the feature names would falsely imply that OpenSSL provided the complete algorithm surface now required by SRTP and the rest of the RTC stack. ## Crypto errors and protocol boundaries diff --git a/docs/crypto-provider-migration.md b/docs/crypto-provider-migration.md new file mode 100644 index 00000000..40d77865 --- /dev/null +++ b/docs/crypto-provider-migration.md @@ -0,0 +1,11 @@ +# Crypto provider migration + +## SRTP in 0.21 + +`rtc-srtp::context::Context::new_with_provider` is the provider-selecting constructor. It accepts an `Arc`, validates the selected protection profile against `RTCCrypto::supports`, derives the SRTP and SRTCP session material through that provider, and creates reusable keyed cipher objects once per one-way context. Packet indexes, rollover counters, replay windows, IVs, AAD, authentication-tag truncation, and wire layout remain owned by `rtc-srtp`. + +`Context::new` remains available as a compatibility constructor and resolves `rtc_crypto::default_provider()`. Applications that need deterministic provider selection, use both built-ins in one process, or supply their own implementation should migrate to `Context::new_with_provider`. + +The `ring` and `aws-lc-rs` features on `rtc-srtp` now forward additively to `rtc-crypto`. Ring remains the default when enabled, including builds that enable both features. A no-built-in build is supported when the application supplies its own provider. + +The former `openssl` and `vendored-openssl` features were removed. They selected only an alternate AES-CTR path inside SRTP and did not implement the complete `RTCCryptoProvider` contract, so retaining them would create a misleading partial-provider surface. An OpenSSL integration can be added in the future as a complete application or crate-provided `RTCCryptoProvider` that passes the public conformance suite. diff --git a/rtc-srtp/Cargo.toml b/rtc-srtp/Cargo.toml index b44cfed0..d3ac550f 100644 --- a/rtc-srtp/Cargo.toml +++ b/rtc-srtp/Cargo.toml @@ -13,28 +13,17 @@ categories.workspace = true [features] default = ["ring"] -ring = ["dep:ring"] -aws-lc-rs = ["dep:aws-lc-rs"] -openssl = ["dep:openssl"] -vendored-openssl = ["openssl/vendored"] +ring = ["crypto/ring"] +aws-lc-rs = ["crypto/aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } rtp.workspace = true rtcp.workspace = true +crypto.workspace = true byteorder.workspace = true bytes.workspace = true -hmac = { version = "0.12.1", features = ["std", "reset"] } -sha1 = "0.10.6" -ctr = "0.9.2" -aes = "0.8.4" -subtle = "2.5.0" -# AES-GCM (AEAD_AES_128/256_GCM profiles) runs on ring's hardware-accelerated, -# single-pass AES-GCM; the AES-CM-HMAC-SHA1 profiles below stay on RustCrypto. -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -openssl = { version = "0.10.72", optional = true } [dev-dependencies] criterion.workspace = true @@ -42,4 +31,4 @@ lazy_static = "1.4.0" [[bench]] name = "bench" -harness = false \ No newline at end of file +harness = false diff --git a/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs b/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs index 8d62d0f4..b0d87c36 100644 --- a/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs +++ b/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs @@ -1,8 +1,8 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::BytesMut; -use ring::aead::{AES_128_GCM, AES_256_GCM, Aad, Algorithm, LessSafeKey, Nonce, UnboundKey}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCrypto, SecretVec}; -use super::{Cipher, Kdf}; +use super::{Cipher, Kdf, crypto_error}; use crate::key_derivation::*; use crate::protection_profile::ProtectionProfile; use shared::{ @@ -14,19 +14,11 @@ pub const CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN: usize = 16; const RTCP_ENCRYPTION_FLAG: u8 = 0x80; -/// AEAD Cipher based on AES. -/// -/// The AES-GCM AEAD (both the AES block cipher and the GHASH universal hash) is -/// provided by `ring`, which ships hardware-accelerated single-pass assembly for -/// x86_64 (AES-NI + CLMUL) and aarch64 (ARMv8 AES + PMULL) with runtime feature -/// detection. That is materially faster than the pure-Rust RustCrypto `aes-gcm`, -/// whose two-pass (encrypt-then-GHASH) design and cfg-gated intrinsics leave it -/// on a software fallback in a default build. `ring` is already a workspace -/// dependency (DTLS handshake signatures, STUN integrity). +/// Provider-backed AEAD cipher based on AES-GCM. pub(crate) struct CipherAeadAesGcm { profile: ProtectionProfile, - srtp_cipher: LessSafeKey, - srtcp_cipher: LessSafeKey, + srtp_cipher: Box, + srtcp_cipher: Box, srtp_session_salt: Vec, srtcp_session_salt: Vec, } @@ -56,16 +48,12 @@ impl Cipher for CipherAeadAesGcm { writer.extend_from_slice(payload); let (aad, plaintext) = writer.split_at_mut(header_len); - let tag = self - .srtp_cipher - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - plaintext, - ) - .map_err(|_| Error::Other("SRTP AES-GCM seal failed".to_string()))?; - - writer.extend_from_slice(tag.as_ref()); + let mut tag = [0; CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN]; + self.srtp_cipher + .seal_in_place(&nonce, aad, plaintext, &mut tag) + .map_err(crypto_error)?; + + writer.extend_from_slice(&tag); Ok(writer) } @@ -89,23 +77,15 @@ impl Cipher for CipherAeadAesGcm { let nonce = self.rtp_initialization_vector(header, roc); - // ring's `open_in_place` decrypts a contiguous ciphertext||tag region in - // place and returns the plaintext slice; the header stays as AAD. Copy - // the wire packet once, decrypt, then drop the trailing tag. - let mut writer = BytesMut::with_capacity(ciphertext.len()); - writer.extend_from_slice(ciphertext); - let final_len = writer.len() - tag_len; + let tag_offset = ciphertext.len() - tag_len; + let tag = &ciphertext[tag_offset..]; + let mut writer = BytesMut::with_capacity(tag_offset); + writer.extend_from_slice(&ciphertext[..tag_offset]); - let (aad, ct_and_tag) = writer.split_at_mut(payload_offset); + let (aad, encrypted_payload) = writer.split_at_mut(payload_offset); self.srtp_cipher - .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - ct_and_tag, - ) + .open_in_place(&nonce, aad, encrypted_payload, tag) .map_err(|_| Error::ErrFailedToVerifyAuthTag)?; - - writer.truncate(final_len); Ok(writer) } @@ -122,16 +102,12 @@ impl Cipher for CipherAeadAesGcm { BytesMut::with_capacity(decrypted.len() + self.aead_auth_tag_len() + SRTCP_INDEX_SIZE); writer.extend_from_slice(decrypted); - let tag = self - .srtcp_cipher - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(iv), - Aad::from(&aad[..]), - &mut writer[8..], - ) - .map_err(|_| Error::Other("SRTCP AES-GCM seal failed".to_string()))?; + let mut tag = [0; CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN]; + self.srtcp_cipher + .seal_in_place(&iv, &aad, &mut writer[8..], &mut tag) + .map_err(crypto_error)?; - writer.extend_from_slice(tag.as_ref()); + writer.extend_from_slice(&tag); writer.extend_from_slice(&aad[8..]); Ok(writer) @@ -157,23 +133,16 @@ impl Cipher for CipherAeadAesGcm { return Err(Error::ErrFailedToVerifyAuthTag); } - // Copy header(8) || ciphertext || tag (dropping the trailing ESRTCP index - // word), decrypt the ciphertext+tag region in place, then drop the tag. - let mut writer = BytesMut::with_capacity(tag_start + tag_len); - writer.extend_from_slice(&encrypted[..tag_start + tag_len]); + let tag = &encrypted[tag_start..tag_start + tag_len]; + let mut writer = BytesMut::with_capacity(tag_start); + writer.extend_from_slice(&encrypted[..tag_start]); { - let (_, ct_and_tag) = writer.split_at_mut(8); + let (_, encrypted_payload) = writer.split_at_mut(8); self.srtcp_cipher - .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - ct_and_tag, - ) + .open_in_place(&nonce, &aad, encrypted_payload, tag) .map_err(|_| Error::ErrFailedToVerifyAuthTag)?; } - - writer.truncate(tag_start); Ok(writer) } @@ -191,11 +160,14 @@ impl CipherAeadAesGcm { profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8], + crypto: &dyn RTCCrypto, ) -> Result { - let (algorithm, kdf): (&'static Algorithm, Kdf) = match profile { - ProtectionProfile::AeadAes128Gcm => (&AES_128_GCM, aes_cm_key_derivation), + let (algorithm, kdf): (AeadAlgorithm, Kdf) = match profile { + ProtectionProfile::AeadAes128Gcm => (AeadAlgorithm::Aes128Gcm, aes_cm_key_derivation), // AES_256_GCM must use AES_256_CM_PRF as per https://datatracker.ietf.org/doc/html/rfc7714#section-11 - ProtectionProfile::AeadAes256Gcm => (&AES_256_GCM, aes_256_cm_key_derivation), + ProtectionProfile::AeadAes256Gcm => { + (AeadAlgorithm::Aes256Gcm, aes_256_cm_key_derivation) + } _ => unreachable!(), }; @@ -205,17 +177,33 @@ impl CipherAeadAesGcm { ); assert_eq!(profile.salt_len(), master_salt.len()); - let build_cipher = |label: u8| -> Result { - let session_key = kdf(label, master_key, master_salt, 0, master_key.len())?; - let unbound = UnboundKey::new(algorithm, &session_key) - .map_err(|_| Error::Other("invalid SRTP AES-GCM session key".to_string()))?; - Ok(LessSafeKey::new(unbound)) + let build_cipher = |label: u8| -> Result> { + let session_key = SecretVec::new(kdf( + crypto, + label, + master_key, + master_salt, + 0, + master_key.len(), + )?); + let cipher = crypto + .new_aead(algorithm, session_key.as_ref()) + .map_err(crypto_error)?; + if cipher.tag_len() != CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN { + return Err(Error::Crypto(format!( + "SRTP AES-GCM requires a {}-byte tag, provider returned {}", + CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN, + cipher.tag_len() + ))); + } + Ok(cipher) }; let srtp_cipher = build_cipher(LABEL_SRTP_ENCRYPTION)?; let srtcp_cipher = build_cipher(LABEL_SRTCP_ENCRYPTION)?; let srtp_session_salt = kdf( + crypto, LABEL_SRTP_SALT, master_key, master_salt, @@ -224,6 +212,7 @@ impl CipherAeadAesGcm { )?; let srtcp_session_salt = kdf( + crypto, LABEL_SRTCP_SALT, master_key, master_salt, @@ -310,7 +299,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, @@ -332,7 +323,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, @@ -353,7 +346,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs similarity index 51% rename from rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs rename to rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs index 23296ab2..8c568ff0 100644 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs +++ b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs @@ -1,33 +1,44 @@ -use aes::cipher::generic_array::GenericArray; -use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; +use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, BytesMut}; +use crypto::{ + HmacAlgorithm, RTCCryptoProvider, SecretVec, StreamCipher, StreamCipherAlgorithm, + constant_time_eq, +}; use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; use shared::marshal::*; -use subtle::ConstantTimeEq; +use std::sync::Arc; -use super::{Cipher, CipherInner}; -use crate::cipher::Kdf; +use super::{Cipher, Kdf, crypto_error}; use crate::key_derivation::*; -use crate::protection_profile::ProtectionProfile; +use crate::protection_profile::*; use shared::error::{Error, Result}; -type Aes128Ctr = ctr::Ctr128BE; -type Aes256Ctr = ctr::Ctr128BE; +pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - srtp_session_key: Vec, - srtcp_session_key: Vec, + profile: ProtectionProfile, + srtp_session_salt: Vec, + srtp_session_auth: SecretVec, + srtcp_session_salt: Vec, + srtcp_session_auth: SecretVec, + provider: Arc, + srtp_cipher: Box, + srtcp_cipher: Box, } impl CipherAesCmHmacSha1 { - pub fn new(profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8]) -> Result { - let kdf: Kdf = match profile { + pub fn new( + profile: ProtectionProfile, + master_key: &[u8], + master_salt: &[u8], + provider: Arc, + ) -> Result { + let (kdf, algorithm): (Kdf, StreamCipherAlgorithm) = match profile { ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 => { - aes_cm_key_derivation + (aes_cm_key_derivation, StreamCipherAlgorithm::Aes128Ctr) } ProtectionProfile::Aes256CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_32 => { - aes_256_cm_key_derivation + (aes_256_cm_key_derivation, StreamCipherAlgorithm::Aes256Ctr) } _ => { return Err(Error::Other(String::from( @@ -35,49 +46,128 @@ impl CipherAesCmHmacSha1 { ))); } }; - let inner = CipherInner::new(profile, kdf, master_key, master_salt)?; - - let srtp_session_key = kdf( + let srtp_session_key = SecretVec::new(kdf( + provider.crypto(), LABEL_SRTP_ENCRYPTION, master_key, master_salt, 0, master_key.len(), - )?; - let srtcp_session_key = kdf( + )?); + let srtcp_session_key = SecretVec::new(kdf( + provider.crypto(), LABEL_SRTCP_ENCRYPTION, master_key, master_salt, 0, master_key.len(), + )?); + + let srtp_cipher = provider + .crypto() + .new_stream_cipher(algorithm, srtp_session_key.as_ref()) + .map_err(crypto_error)?; + let srtcp_cipher = provider + .crypto() + .new_stream_cipher(algorithm, srtcp_session_key.as_ref()) + .map_err(crypto_error)?; + let srtp_session_salt = kdf( + provider.crypto(), + LABEL_SRTP_SALT, + master_key, + master_salt, + 0, + master_salt.len(), )?; - - Ok(CipherAesCmHmacSha1 { - inner, - srtp_session_key, - srtcp_session_key, + let srtcp_session_salt = kdf( + provider.crypto(), + LABEL_SRTCP_SALT, + master_key, + master_salt, + 0, + master_salt.len(), + )?; + let auth_key_len = profile.auth_key_len(); + let srtp_session_auth = SecretVec::new(kdf( + provider.crypto(), + LABEL_SRTP_AUTHENTICATION_TAG, + master_key, + master_salt, + 0, + auth_key_len, + )?); + let srtcp_session_auth = SecretVec::new(kdf( + provider.crypto(), + LABEL_SRTCP_AUTHENTICATION_TAG, + master_key, + master_salt, + 0, + auth_key_len, + )?); + + Ok(Self { + profile, + srtp_session_salt, + srtp_session_auth, + srtcp_session_salt, + srtcp_session_auth, + provider, + srtp_cipher, + srtcp_cipher, }) } + + /// Generate the SRTP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. + fn generate_srtp_auth_tag(&self, buf: &[u8], roc: u32) -> Result<[u8; 20]> { + let mut tag = [0; 20]; + self.provider + .crypto() + .hmac( + HmacAlgorithm::Sha1, + self.srtp_session_auth.as_ref(), + &[buf, &roc.to_be_bytes()], + &mut tag, + ) + .map_err(crypto_error)?; + Ok(tag) + } + + /// Generate the SRTCP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. + fn generate_srtcp_auth_tag(&self, buf: &[u8]) -> Result<[u8; 20]> { + let mut tag = [0; 20]; + self.provider + .crypto() + .hmac( + HmacAlgorithm::Sha1, + self.srtcp_session_auth.as_ref(), + &[buf], + &mut tag, + ) + .map_err(crypto_error)?; + Ok(tag) + } } impl Cipher for CipherAesCmHmacSha1 { /// Get RTP authenticated tag length. fn rtp_auth_tag_len(&self) -> usize { - self.inner.profile.rtp_auth_tag_len() + self.profile.rtp_auth_tag_len() } /// Get RTCP authenticated tag length. fn rtcp_auth_tag_len(&self) -> usize { - self.inner.profile.rtcp_auth_tag_len() + self.profile.rtcp_auth_tag_len() } /// Get AEAD auth key length of the cipher. fn aead_auth_tag_len(&self) -> usize { - self.inner.profile.aead_auth_tag_len() + self.profile.aead_auth_tag_len() } fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) + let tail_offset = input.len() - (self.profile.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); + (BigEndian::read_u32(&input[tail_offset..tail_offset + SRTCP_INDEX_SIZE]) & !(1 << 31)) + as usize } fn encrypt_rtp( @@ -96,23 +186,16 @@ impl Cipher for CipherAesCmHmacSha1 { header.sequence_number, roc, header.ssrc, - &self.inner.srtp_session_salt, + &self.srtp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } else { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } + self.srtp_cipher + .apply_keystream(&counter, &mut writer[header.marshal_size()..]) + .map_err(crypto_error)?; // Generate the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.rtp_auth_tag_len()]; + let full_auth_tag = self.generate_srtp_auth_tag(&writer, roc)?; + let auth_tag = &full_auth_tag[..self.rtp_auth_tag_len()]; writer.extend_from_slice(auth_tag); Ok(writer) @@ -136,12 +219,12 @@ impl Cipher for CipherAesCmHmacSha1 { let cipher_text = &encrypted[..encrypted_len - self.rtp_auth_tag_len()]; // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.rtp_auth_tag_len()]; + let full_expected_tag = self.generate_srtp_auth_tag(cipher_text, roc)?; + let expected_tag = &full_expected_tag[..self.rtp_auth_tag_len()]; // See if the auth tag actually matches. // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { + if !constant_time_eq(actual_tag, expected_tag) { return Err(Error::RtpFailedToVerifyAuthTag); } @@ -153,22 +236,12 @@ impl Cipher for CipherAesCmHmacSha1 { header.sequence_number, roc, header.ssrc, - &self.inner.srtp_session_salt, + &self.srtp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } else { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } + self.srtp_cipher + .apply_keystream(&counter, &mut writer[header.marshal_size()..]) + .map_err(crypto_error)?; Ok(writer) } @@ -190,26 +263,19 @@ impl Cipher for CipherAesCmHmacSha1 { (srtcp_index & 0xFFFF) as u16, (srtcp_index >> 16) as u32, ssrc, - &self.inner.srtcp_session_salt, + &self.srtcp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } else { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } + self.srtcp_cipher + .apply_keystream(&counter, &mut writer[HEADER_LENGTH + SSRC_LENGTH..]) + .map_err(crypto_error)?; // Add SRTCP index and set Encryption bit writer.put_u32(srtcp_index as u32 | (1u32 << 31)); // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.rtcp_auth_tag_len()]; + let full_auth_tag = self.generate_srtcp_auth_tag(&writer)?; + let auth_tag = &full_auth_tag[..self.rtcp_auth_tag_len()]; writer.extend_from_slice(auth_tag); Ok(writer) @@ -255,12 +321,12 @@ impl Cipher for CipherAesCmHmacSha1 { let cipher_text = &encrypted[..encrypted_len - self.rtcp_auth_tag_len()]; // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.rtcp_auth_tag_len()]; + let full_expected_tag = self.generate_srtcp_auth_tag(cipher_text)?; + let expected_tag = &full_expected_tag[..self.rtcp_auth_tag_len()]; // See if the auth tag actually matches. // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { + if !constant_time_eq(actual_tag, expected_tag) { return Err(Error::RtcpFailedToVerifyAuthTag); } @@ -268,22 +334,12 @@ impl Cipher for CipherAesCmHmacSha1 { (srtcp_index & 0xFFFF) as u16, (srtcp_index >> 16) as u32, ssrc, - &self.inner.srtcp_session_salt, + &self.srtcp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } else { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } + self.srtcp_cipher + .apply_keystream(&counter, &mut writer[HEADER_LENGTH + SSRC_LENGTH..]) + .map_err(crypto_error)?; Ok(writer) } diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs deleted file mode 100644 index 40376b17..00000000 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -use byteorder::{BigEndian, ByteOrder}; -use hmac::{Hmac, Mac}; -use sha1::Sha1; - -use super::{Cipher, Kdf}; -use crate::key_derivation::*; -use crate::protection_profile::*; -use shared::error::{Error, Result}; - -#[cfg(not(feature = "openssl"))] -mod ctrcipher; - -#[cfg(feature = "openssl")] -mod opensslcipher; - -#[cfg(not(feature = "openssl"))] -pub(crate) use ctrcipher::CipherAesCmHmacSha1; - -#[cfg(feature = "openssl")] -pub(crate) use opensslcipher::CipherAesCmHmacSha1; - -type HmacSha1 = Hmac; - -pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; - -pub(crate) struct CipherInner { - profile: ProtectionProfile, - srtp_session_salt: Vec, - srtp_session_auth: HmacSha1, - srtcp_session_salt: Vec, - srtcp_session_auth: HmacSha1, -} - -impl CipherInner { - pub fn new( - profile: ProtectionProfile, - kdf: Kdf, - master_key: &[u8], - master_salt: &[u8], - ) -> Result { - let srtp_session_salt = kdf( - LABEL_SRTP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - let srtcp_session_salt = kdf( - LABEL_SRTCP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - - let auth_key_len = profile.auth_key_len(); - let srtp_session_auth_tag = kdf( - LABEL_SRTP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - let srtcp_session_auth_tag = kdf( - LABEL_SRTCP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - - let srtp_session_auth = HmacSha1::new_from_slice(&srtp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - let srtcp_session_auth = HmacSha1::new_from_slice(&srtcp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - profile, - srtp_session_salt, - srtp_session_auth, - srtcp_session_salt, - srtcp_session_auth, - }) - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// In the case of SRTP, M SHALL consist of the Authenticated - /// Portion of the packet (as specified in Figure 1) concatenated with - /// the roc, M = Authenticated Portion || roc; - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtp_auth_tag(&self, buf: &[u8], roc: u32) -> [u8; 20] { - let mut signer = self.srtp_session_auth.clone(); - - signer.update(buf); - - // For SRTP only, we need to hash the rollover counter as well. - signer.update(&roc.to_be_bytes()); - - signer.finalize().into_bytes().into() - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtcp_auth_tag(&self, buf: &[u8]) -> [u8; 20] { - let mut signer = self.srtcp_session_auth.clone(); - - signer.update(buf); - - signer.finalize().into_bytes().into() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - let tail_offset = input.len() - (self.profile.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); - (BigEndian::read_u32(&input[tail_offset..tail_offset + SRTCP_INDEX_SIZE]) & !(1 << 31)) - as usize - } -} diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs deleted file mode 100644 index f767f109..00000000 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs +++ /dev/null @@ -1,308 +0,0 @@ -use super::{Cipher, CipherInner}; -use crate::cipher::Kdf; -use crate::key_derivation::*; -use crate::protection_profile::ProtectionProfile; -use bytes::{BufMut, BytesMut}; -use openssl::cipher_ctx::CipherCtx; -use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; -use shared::error::{Error, Result}; -use shared::marshal::*; -use subtle::ConstantTimeEq; - -pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - rtp_ctx: CipherCtx, - rtcp_ctx: CipherCtx, -} - -impl CipherAesCmHmacSha1 { - pub fn new(profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8]) -> Result { - let kdf: Kdf = match profile { - ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 => { - aes_cm_key_derivation - } - ProtectionProfile::Aes256CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_32 => { - aes_256_cm_key_derivation - } - _ => { - return Err(Error::Other(String::from( - "no AES protection profile passed to CipherAesCmHmacSha1", - ))); - } - }; - - let inner = CipherInner::new(profile, kdf, master_key, master_salt)?; - - let srtp_session_key = kdf( - LABEL_SRTP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - let srtcp_session_key = kdf( - LABEL_SRTCP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let t = if profile.key_len() == 16 { - openssl::cipher::Cipher::aes_128_ctr() - } else { - openssl::cipher::Cipher::aes_256_ctr() - }; - let mut rtp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtp_ctx - .encrypt_init(Some(t), Some(&srtp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - let t = if profile.key_len() == 16 { - openssl::cipher::Cipher::aes_128_ctr() - } else { - openssl::cipher::Cipher::aes_256_ctr() - }; - let mut rtcp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtcp_ctx - .encrypt_init(Some(t), Some(&srtcp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - inner, - rtp_ctx, - rtcp_ctx, - }) - } -} - -impl Cipher for CipherAesCmHmacSha1 { - /// Get RTP authenticated tag length. - fn rtp_auth_tag_len(&self) -> usize { - self.inner.profile.rtp_auth_tag_len() - } - - /// Get RTCP authenticated tag length. - fn rtcp_auth_tag_len(&self) -> usize { - self.inner.profile.rtcp_auth_tag_len() - } - - /// Get AEAD auth key length of the cipher. - fn aead_auth_tag_len(&self) -> usize { - self.inner.profile.aead_auth_tag_len() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) - } - - fn encrypt_rtp( - &mut self, - plaintext: &[u8], - header: &rtp::Header, - roc: u32, - ) -> Result { - let header_len = header.marshal_size(); - let mut writer = BytesMut::with_capacity(plaintext.len() + self.rtp_auth_tag_len()); - - // Copy the header unencrypted. - writer.extend_from_slice(&plaintext[..header_len]); - - // Encrypt the payload - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - writer.resize(plaintext.len(), 0); - self.rtp_ctx.encrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&plaintext[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - // Generate and write the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.rtp_auth_tag_len()]; - writer.extend_from_slice(auth_tag); - - Ok(writer) - } - - fn decrypt_rtp( - &mut self, - encrypted: &[u8], - header: &rtp::Header, - roc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - if encrypted_len < self.rtp_auth_tag_len() { - return Err(Error::SrtpTooSmall(encrypted_len, self.rtp_auth_tag_len())); - } - let header_len = header.marshal_size(); - - let mut writer = BytesMut::with_capacity(encrypted_len - self.rtp_auth_tag_len()); - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.rtp_auth_tag_len()..]; - let cipher_text = &encrypted[..encrypted_len - self.rtp_auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.rtp_auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtpFailedToVerifyAuthTag); - } - - // Write cipher_text to the destination buffer. - writer.extend_from_slice(&cipher_text[..header_len]); - - // Decrypt the ciphertext for the payload. - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - - writer.resize(encrypted_len - self.rtp_auth_tag_len(), 0); - self.rtp_ctx.decrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&cipher_text[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - Ok(writer) - } - - fn encrypt_rtcp( - &mut self, - decrypted: &[u8], - srtcp_index: usize, - ssrc: u32, - ) -> Result { - let decrypted_len = decrypted.len(); - - let mut writer = - BytesMut::with_capacity(decrypted_len + SRTCP_INDEX_SIZE + self.rtcp_auth_tag_len()); - - // Write the decrypted to the destination buffer. - writer.extend_from_slice(&decrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - // Encrypt everything after header - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(decrypted_len, 0); - self.rtcp_ctx - .encrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &decrypted[HEADER_LENGTH + SSRC_LENGTH..], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - // Add SRTCP index and set Encryption bit - writer.put_u32(srtcp_index as u32 | (1u32 << 31)); - - // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.rtcp_auth_tag_len()]; - writer.extend_from_slice(auth_tag); - - Ok(writer) - } - - fn decrypt_rtcp( - &mut self, - encrypted: &[u8], - srtcp_index: usize, - ssrc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - - if encrypted_len < self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE { - return Err(Error::SrtcpTooSmall( - encrypted_len, - self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE, - )); - } - - let tail_offset = encrypted_len - (self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); - if tail_offset < 8 { - return Err(Error::ErrTooShortRtcp); - } - - let mut writer = BytesMut::with_capacity(tail_offset); - - writer.extend_from_slice(&encrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - let is_encrypted = encrypted[tail_offset] >> 7; - if is_encrypted == 0 { - return Ok(writer); - } - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.rtcp_auth_tag_len()..]; - if actual_tag.len() != self.rtcp_auth_tag_len() { - return Err(Error::RtcpInvalidLengthAuthTag( - actual_tag.len(), - self.rtcp_auth_tag_len(), - )); - } - - let cipher_text = &encrypted[..encrypted_len - self.rtcp_auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.rtcp_auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtcpFailedToVerifyAuthTag); - } - - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(tail_offset, 0); - self.rtcp_ctx - .decrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &encrypted[HEADER_LENGTH + SSRC_LENGTH..tail_offset], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - Ok(writer) - } -} diff --git a/rtc-srtp/src/cipher/mod.rs b/rtc-srtp/src/cipher/mod.rs index ed21ff48..49629315 100644 --- a/rtc-srtp/src/cipher/mod.rs +++ b/rtc-srtp/src/cipher/mod.rs @@ -2,10 +2,15 @@ pub mod cipher_aead_aes_gcm; pub mod cipher_aes_cm_hmac_sha1; use bytes::BytesMut; +use crypto::{CryptoError, RTCCrypto}; -use shared::error::Result; +use shared::error::{Error, Result}; -type Kdf = fn(u8, &[u8], &[u8], usize, usize) -> Result>; +type Kdf = fn(&dyn RTCCrypto, u8, &[u8], &[u8], usize, usize) -> Result>; + +pub(crate) fn crypto_error(error: CryptoError) -> Error { + Error::Crypto(error.to_string()) +} ///NOTE: Auth tag and AEAD auth tag are placed at the different position in SRTCP /// @@ -32,7 +37,7 @@ type Kdf = fn(u8, &[u8], &[u8], usize, usize) -> Result>; /// /// Cipher represents a implementation of one /// of the SRTP Specific ciphers. -pub(crate) trait Cipher: Send + Sync { +pub(crate) trait Cipher: Send { /// Get RTP authenticated tag length. fn rtp_auth_tag_len(&self) -> usize; diff --git a/rtc-srtp/src/context/context_test.rs b/rtc-srtp/src/context/context_test.rs index 8e2ef7ca..88abc4b0 100644 --- a/rtc-srtp/src/context/context_test.rs +++ b/rtc-srtp/src/context/context_test.rs @@ -98,6 +98,9 @@ fn test_valid_packet_counter() -> Result<()> { ]; let srtp_session_salt = aes_cm_key_derivation( + crypto::default_provider() + .map_err(|error| Error::Crypto(error.to_string()))? + .crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, diff --git a/rtc-srtp/src/context/mod.rs b/rtc-srtp/src/context/mod.rs index ec8b5b20..70f97ba9 100644 --- a/rtc-srtp/src/context/mod.rs +++ b/rtc-srtp/src/context/mod.rs @@ -6,7 +6,9 @@ mod srtcp_test; mod srtp_test; use std::collections::HashMap; +use std::sync::Arc; +use crypto::RTCCryptoProvider; use shared::replay_detector::*; use crate::cipher::cipher_aead_aes_gcm::*; @@ -103,13 +105,36 @@ pub struct Context { } impl Context { - /// CreateContext creates a new SRTP Context + /// Creates an SRTP context with the built-in default crypto provider. + /// + /// Applications that select or implement a provider should use [`Self::new_with_provider`]. pub fn new( master_key: &[u8], master_salt: &[u8], profile: ProtectionProfile, srtp_ctx_opt: Option, srtcp_ctx_opt: Option, + ) -> Result { + let provider = + crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + Self::new_with_provider( + master_key, + master_salt, + profile, + srtp_ctx_opt, + srtcp_ctx_opt, + provider, + ) + } + + /// Creates an SRTP context with an explicit crypto provider. + pub fn new_with_provider( + master_key: &[u8], + master_salt: &[u8], + profile: ProtectionProfile, + srtp_ctx_opt: Option, + srtcp_ctx_opt: Option, + provider: Arc, ) -> Result { let key_len = profile.key_len(); let salt_len = profile.salt_len(); @@ -119,19 +144,28 @@ impl Context { } else if master_salt.len() != salt_len { return Err(Error::SrtpSaltLength(salt_len, master_salt.len())); } + profile.ensure_crypto_supported(provider.crypto())?; let cipher: Box = match profile { ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_80 - | ProtectionProfile::Aes256CmHmacSha1_32 => { - Box::new(CipherAesCmHmacSha1::new(profile, master_key, master_salt)?) - } + | ProtectionProfile::Aes256CmHmacSha1_32 => Box::new(CipherAesCmHmacSha1::new( + profile, + master_key, + master_salt, + provider, + )?), ProtectionProfile::AeadAes128Gcm | ProtectionProfile::AeadAes256Gcm => { // `CipherAeadAesGcm::new` selects AES-128 vs AES-256 from the // profile itself, so both GCM profiles share one arm. - Box::new(CipherAeadAesGcm::new(profile, master_key, master_salt)?) + Box::new(CipherAeadAesGcm::new( + profile, + master_key, + master_salt, + provider.crypto(), + )?) } }; diff --git a/rtc-srtp/src/key_derivation.rs b/rtc-srtp/src/key_derivation.rs index fff4e2c3..ac012570 100644 --- a/rtc-srtp/src/key_derivation.rs +++ b/rtc-srtp/src/key_derivation.rs @@ -1,8 +1,6 @@ -use aes::Aes256; -use aes::cipher::BlockEncrypt; -use aes::cipher::KeyInit; -use aes::{Aes128, cipher::generic_array::GenericArray}; +use crypto::{BlockCipherAlgorithm, RTCCrypto}; +use crate::cipher::crypto_error; use shared::error::{Error, Result}; pub const LABEL_SRTP_ENCRYPTION: u8 = 0x00; @@ -15,6 +13,7 @@ pub const LABEL_SRTCP_SALT: u8 = 0x05; pub(crate) const SRTCP_INDEX_SIZE: usize = 4; pub(crate) fn aes_cm_key_derivation( + crypto: &dyn RTCCrypto, label: u8, master_key: &[u8], master_salt: &[u8], @@ -40,9 +39,6 @@ pub(crate) fn aes_cm_key_derivation( prf_in[7] ^= label; //The resulting value is then AES encrypted using the master key to get the cipher key. - let key = GenericArray::from_slice(master_key); - let block = Aes128::new(key); - let mut out = vec![0u8; ((out_len + n_master_key) / n_master_key) * n_master_key]; for (i, n) in (0..out_len).step_by(n_master_key).enumerate() { //BigEndian.PutUint16(prfIn[nMasterKey-2:], i) @@ -50,8 +46,13 @@ pub(crate) fn aes_cm_key_derivation( prf_in[n_master_key - 1] = (i & 0xFF) as u8; out[n..n + n_master_key].copy_from_slice(&prf_in); - let out_key = GenericArray::from_mut_slice(&mut out[n..n + 16]); - block.encrypt_block(out_key); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes128, + master_key, + &mut out[n..n + 16], + ) + .map_err(crypto_error)?; } Ok(out[..out_len].to_vec()) @@ -61,6 +62,7 @@ pub(crate) fn aes_cm_key_derivation( // The key derivation rate is zero as per https://datatracker.ietf.org/doc/html/rfc5764 hence index_over-kdr is 0 const AES_256_BS: usize = 16; pub(crate) fn aes_256_cm_key_derivation( + crypto: &dyn RTCCrypto, label: u8, master_key: &[u8], master_salt: &[u8], @@ -93,16 +95,18 @@ pub(crate) fn aes_256_cm_key_derivation( } //The resulting value is then AES encrypted using the master key to get the cipher key. - let key = GenericArray::from_slice(master_key); - let block = Aes256::new(key); - let mut out = vec![0u8; ((out_len + AES_256_BS) / AES_256_BS) * AES_256_BS]; for (i, n) in (0..out_len).step_by(AES_256_BS).enumerate() { prf_in[AES_256_BS - 2..].copy_from_slice(&((i as u16).to_be_bytes())); out[n..n + AES_256_BS].copy_from_slice(&prf_in); - let out_key = GenericArray::from_mut_slice(&mut out[n..n + 16]); - block.encrypt_block(out_key); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes256, + master_key, + &mut out[n..n + AES_256_BS], + ) + .map_err(crypto_error)?; } Ok(out[..out_len].to_vec()) @@ -150,6 +154,7 @@ mod test { #[test] fn test_valid_session_keys() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; // Key Derivation Test Vectors from https://tools.ietf.org/html/rfc3711#appendix-B.3 let master_key = vec![ 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, @@ -172,6 +177,7 @@ mod test { ]; let session_key = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_ENCRYPTION, &master_key, &master_salt, @@ -184,6 +190,7 @@ mod test { ); let session_salt = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, @@ -198,6 +205,7 @@ mod test { let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); let session_auth_tag = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_AUTHENTICATION_TAG, &master_key, &master_salt, @@ -216,7 +224,15 @@ mod test { // Currently this isn't supported, but the API makes sure we can add this in the future #[test] fn test_index_over_kdr() -> Result<()> { - let result = aes_cm_key_derivation(LABEL_SRTP_AUTHENTICATION_TAG, &[], &[], 1, 0); + let provider = crypto::default_provider().map_err(crypto_error)?; + let result = aes_cm_key_derivation( + provider.crypto(), + LABEL_SRTP_AUTHENTICATION_TAG, + &[], + &[], + 1, + 0, + ); assert!(result.is_err()); Ok(()) @@ -224,6 +240,7 @@ mod test { #[test] fn test_aes_256_cm_key_derivation() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; // Key Derivation Test Vectors from https://datatracker.ietf.org/doc/html/rfc6188#section-7.2 let master_key = vec![ 0xF0, 0xF0, 0x49, 0x14, 0xB5, 0x13, 0xF2, 0x76, 0x3A, 0x1B, 0x1F, 0xA1, 0x30, 0xF1, @@ -248,6 +265,7 @@ mod test { ]; let session_key = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_ENCRYPTION, &master_key, &master_salt, @@ -260,6 +278,7 @@ mod test { ); let session_salt = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, @@ -274,6 +293,7 @@ mod test { let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); let session_auth_tag = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_AUTHENTICATION_TAG, &master_key, &master_salt, diff --git a/rtc-srtp/src/lib.rs b/rtc-srtp/src/lib.rs index 97444871..1b289411 100644 --- a/rtc-srtp/src/lib.rs +++ b/rtc-srtp/src/lib.rs @@ -37,6 +37,9 @@ //! Most applications do not depend on this crate directly — the //! [`rtc`](https://docs.rs/rtc) crate creates the contexts from the DTLS handshake and //! applies them to media as one layer of the peer-connection pipeline. +//! Applications constructing contexts directly can select cryptography explicitly with +//! [`context::Context::new_with_provider`]; [`context::Context::new`] retains default-provider +//! compatibility. //! //! [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 //! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 @@ -51,10 +54,3 @@ mod key_derivation; pub mod option; /// The DTLS-SRTP protection profiles and their key, salt and tag lengths. pub mod protection_profile; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/rtc-srtp/src/protection_profile.rs b/rtc-srtp/src/protection_profile.rs index 67dba6ed..de3ffbf2 100644 --- a/rtc-srtp/src/protection_profile.rs +++ b/rtc-srtp/src/protection_profile.rs @@ -1,3 +1,28 @@ +use crypto::{ + AeadAlgorithm, BlockCipherAlgorithm, CryptoAlgorithm, HmacAlgorithm, RTCCrypto, + StreamCipherAlgorithm, +}; +use shared::error::{Error, Result}; + +const AES_128_CM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes128Ctr), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), +]; +const AES_256_CM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes256Ctr), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), +]; +const AEAD_AES_128_GCM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm), +]; +const AEAD_AES_256_GCM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes256Gcm), +]; + /// ProtectionProfile specifies Cipher and AuthTag details, similar to TLS cipher suite #[derive(Default, Debug, Clone, Copy)] #[repr(u8)] @@ -23,6 +48,29 @@ pub enum ProtectionProfile { } impl ProtectionProfile { + /// Returns the provider operations required to construct this protection profile. + #[must_use] + pub const fn required_crypto_algorithms(self) -> &'static [CryptoAlgorithm] { + match self { + Self::Aes128CmHmacSha1_32 | Self::Aes128CmHmacSha1_80 => AES_128_CM_REQUIREMENTS, + Self::Aes256CmHmacSha1_32 | Self::Aes256CmHmacSha1_80 => AES_256_CM_REQUIREMENTS, + Self::AeadAes128Gcm => AEAD_AES_128_GCM_REQUIREMENTS, + Self::AeadAes256Gcm => AEAD_AES_256_GCM_REQUIREMENTS, + } + } + + /// Validates that `crypto` implements every operation required by this profile. + pub fn ensure_crypto_supported(self, crypto: &dyn RTCCrypto) -> Result<()> { + for algorithm in self.required_crypto_algorithms() { + if !crypto.supports(*algorithm) { + return Err(Error::Crypto(format!( + "SRTP protection profile {self:?} requires unsupported algorithm {algorithm:?}" + ))); + } + } + Ok(()) + } + /// The master key length in bytes for this profile. pub fn key_len(&self) -> usize { match *self { @@ -88,3 +136,50 @@ impl ProtectionProfile { } } } + +#[cfg(test)] +mod tests { + use super::*; + + struct CapabilityCrypto { + missing: Option, + } + + impl RTCCrypto for CapabilityCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.missing != Some(algorithm) + } + } + + const PROFILES: [ProtectionProfile; 6] = [ + ProtectionProfile::Aes128CmHmacSha1_80, + ProtectionProfile::Aes128CmHmacSha1_32, + ProtectionProfile::Aes256CmHmacSha1_80, + ProtectionProfile::Aes256CmHmacSha1_32, + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, + ]; + + #[test] + fn complete_provider_supports_every_profile() { + let crypto = CapabilityCrypto { missing: None }; + for profile in PROFILES { + profile.ensure_crypto_supported(&crypto).unwrap(); + } + } + + #[test] + fn every_required_capability_is_enforced() { + for profile in PROFILES { + for algorithm in profile.required_crypto_algorithms() { + let crypto = CapabilityCrypto { + missing: Some(*algorithm), + }; + let error = profile.ensure_crypto_supported(&crypto).unwrap_err(); + let message = error.to_string(); + assert!(message.contains(&format!("{profile:?}"))); + assert!(message.contains(&format!("{algorithm:?}"))); + } + } + } +} diff --git a/rtc-srtp/tests/provider_profiles.rs b/rtc-srtp/tests/provider_profiles.rs new file mode 100644 index 00000000..baa8d744 --- /dev/null +++ b/rtc-srtp/tests/provider_profiles.rs @@ -0,0 +1,406 @@ +use std::sync::Arc; +#[cfg(feature = "ring")] +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[cfg(feature = "ring")] +use crypto::{ + AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, HmacAlgorithm, StreamCipher, + StreamCipherAlgorithm, +}; +use crypto::{CryptoAlgorithm, CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; +use rtc_srtp::context::Context; +use rtc_srtp::option::{srtcp_replay_protection, srtp_replay_protection}; +use rtc_srtp::protection_profile::ProtectionProfile; +use shared::error::Result; +use shared::marshal::Marshal; + +const PROFILES: [ProtectionProfile; 6] = [ + ProtectionProfile::Aes128CmHmacSha1_80, + ProtectionProfile::Aes128CmHmacSha1_32, + ProtectionProfile::Aes256CmHmacSha1_80, + ProtectionProfile::Aes256CmHmacSha1_32, + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, +]; + +fn providers() -> Vec> { + #[cfg(all(feature = "ring", feature = "aws-lc-rs"))] + return vec![ + Arc::new(crypto::providers::RingProvider::new()), + Arc::new(crypto::providers::AwsLcRsProvider::new()), + ]; + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + return vec![Arc::new(crypto::providers::RingProvider::new())]; + #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] + return vec![Arc::new(crypto::providers::AwsLcRsProvider::new())]; + #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + Vec::new() +} + +struct IncompleteCrypto; + +impl RTCCrypto for IncompleteCrypto { + fn supports(&self, _algorithm: CryptoAlgorithm) -> bool { + false + } +} + +struct IncompleteRandom; + +impl RTCRandom for IncompleteRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +struct IncompleteProvider { + crypto: IncompleteCrypto, + random: IncompleteRandom, +} + +impl RTCCryptoProvider for IncompleteProvider { + fn name(&self) -> &'static str { + "incomplete" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[test] +fn explicit_incomplete_provider_returns_actionable_capability_error() { + let profile = ProtectionProfile::Aes128CmHmacSha1_80; + let (key, salt) = key_material(profile); + let error = Context::new_with_provider( + &key, + &salt, + profile, + None, + None, + Arc::new(IncompleteProvider { + crypto: IncompleteCrypto, + random: IncompleteRandom, + }), + ) + .err() + .expect("an incomplete provider must be rejected"); + let message = error.to_string(); + assert!(message.contains("Aes128CmHmacSha1_80")); + assert!(message.contains("BlockCipher(Aes128)")); +} + +fn key_material(profile: ProtectionProfile) -> (Vec, Vec) { + ( + (0..profile.key_len()).map(|index| index as u8).collect(), + (0..profile.salt_len()) + .map(|index| 0x80 | index as u8) + .collect(), + ) +} + +fn context( + profile: ProtectionProfile, + provider: Arc, + replay: bool, +) -> Result { + let (key, salt) = key_material(profile); + Context::new_with_provider( + &key, + &salt, + profile, + replay.then(|| srtp_replay_protection(64)), + replay.then(|| srtcp_replay_protection(64)), + provider, + ) +} + +fn rtp_packet(sequence_number: u16) -> Result> { + Ok(rtp::Packet { + header: rtp::Header { + version: 2, + sequence_number, + timestamp: 0x1234_5678, + ssrc: 0x1122_3344, + ..Default::default() + }, + payload: vec![0x41; 48].into(), + } + .marshal()? + .to_vec()) +} + +fn rtcp_packet() -> [u8; 8] { + [0x80, 200, 0, 1, 0x11, 0x22, 0x33, 0x44] +} + +#[test] +fn every_profile_round_trips_with_every_enabled_provider() -> Result<()> { + for provider in providers() { + for profile in PROFILES { + let mut sender = context(profile, provider.clone(), false)?; + let mut receiver = context(profile, provider.clone(), false)?; + + let rtp = rtp_packet(7)?; + let protected_rtp = sender.encrypt_rtp(&rtp)?; + assert_eq!(receiver.decrypt_rtp(&protected_rtp)?.as_ref(), rtp); + + if profile.aead_auth_tag_len() == 0 { + let mut wrong_tag = protected_rtp.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_tag) + .is_err() + ); + } + + let rtcp = rtcp_packet(); + let protected_rtcp = sender.encrypt_rtcp(&rtcp)?; + assert_eq!(receiver.decrypt_rtcp(&protected_rtcp)?.as_ref(), rtcp); + if profile.aead_auth_tag_len() == 0 { + let mut wrong_tag = protected_rtcp.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_tag) + .is_err() + ); + } + } + } + Ok(()) +} + +#[test] +fn aead_profiles_reject_wrong_aad_tag_rollover_and_replay() -> Result<()> { + for provider in providers() { + for profile in [ + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, + ] { + let mut sender = context(profile, provider.clone(), false)?; + let first = rtp_packet(u16::MAX)?; + let first_protected = sender.encrypt_rtp(&first)?; + let wrapped = rtp_packet(0)?; + let wrapped_protected = sender.encrypt_rtp(&wrapped)?; + + let mut receiver = context(profile, provider.clone(), true)?; + assert_eq!(receiver.decrypt_rtp(&first_protected)?.as_ref(), first); + assert_eq!(receiver.decrypt_rtp(&wrapped_protected)?.as_ref(), wrapped); + assert!(receiver.decrypt_rtp(&wrapped_protected).is_err()); + + let mut wrong_rollover = context(profile, provider.clone(), false)?; + assert!(wrong_rollover.decrypt_rtp(&wrapped_protected).is_err()); + + let mut wrong_aad = first_protected.to_vec(); + wrong_aad[8] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_aad) + .is_err() + ); + + let mut wrong_tag = first_protected.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_tag) + .is_err() + ); + + let rtcp = rtcp_packet(); + let mut rtcp_sender = context(profile, provider.clone(), false)?; + let protected_rtcp = rtcp_sender.encrypt_rtcp(&rtcp)?; + let mut rtcp_receiver = context(profile, provider.clone(), true)?; + assert_eq!(rtcp_receiver.decrypt_rtcp(&protected_rtcp)?.as_ref(), rtcp); + assert!(rtcp_receiver.decrypt_rtcp(&protected_rtcp).is_err()); + + let mut wrong_rtcp_aad = protected_rtcp.to_vec(); + wrong_rtcp_aad[4] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_rtcp_aad) + .is_err() + ); + + let mut wrong_rtcp_tag = protected_rtcp.to_vec(); + let tag_byte = wrong_rtcp_tag.len() - 5; + wrong_rtcp_tag[tag_byte] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_rtcp_tag) + .is_err() + ); + } + } + Ok(()) +} + +#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[test] +fn providers_produce_identical_packets_and_interoperate() -> Result<()> { + let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); + let aws: Arc = Arc::new(crypto::providers::AwsLcRsProvider::new()); + + for profile in PROFILES { + let rtp = rtp_packet(42)?; + let mut ring_sender = context(profile, ring.clone(), false)?; + let mut aws_sender = context(profile, aws.clone(), false)?; + let ring_packet = ring_sender.encrypt_rtp(&rtp)?; + let aws_packet = aws_sender.encrypt_rtp(&rtp)?; + assert_eq!(ring_packet, aws_packet); + + let mut ring_receiver = context(profile, ring.clone(), false)?; + let mut aws_receiver = context(profile, aws.clone(), false)?; + assert_eq!(ring_receiver.decrypt_rtp(&aws_packet)?.as_ref(), rtp); + assert_eq!(aws_receiver.decrypt_rtp(&ring_packet)?.as_ref(), rtp); + } + Ok(()) +} + +#[cfg(feature = "ring")] +struct CountingCrypto { + inner: crypto::providers::RingCrypto, + stream_constructions: AtomicUsize, + aead_constructions: AtomicUsize, +} + +#[cfg(feature = "ring")] +impl RTCCrypto for CountingCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.inner.supports(algorithm) + } + + fn hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + output: &mut [u8], + ) -> std::result::Result<(), CryptoError> { + self.inner.hmac(algorithm, key, input, output) + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> std::result::Result<(), CryptoError> { + self.inner.block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.stream_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.aead_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_aead(algorithm, key) + } +} + +#[cfg(feature = "ring")] +struct CountingProvider { + crypto: CountingCrypto, + random: crypto::providers::RingRandom, +} + +#[cfg(feature = "ring")] +impl CountingProvider { + fn new() -> Self { + Self { + crypto: CountingCrypto { + inner: crypto::providers::RingCrypto, + stream_constructions: AtomicUsize::new(0), + aead_constructions: AtomicUsize::new(0), + }, + random: crypto::providers::RingRandom, + } + } +} + +#[cfg(feature = "ring")] +impl RTCCryptoProvider for CountingProvider { + fn name(&self) -> &'static str { + "counting-ring" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[cfg(feature = "ring")] +#[test] +fn keyed_ciphers_are_constructed_once_per_context_not_per_packet() -> Result<()> { + let stream_provider = Arc::new(CountingProvider::new()); + let mut stream_context = context( + ProtectionProfile::Aes128CmHmacSha1_80, + stream_provider.clone(), + false, + )?; + assert_eq!( + stream_provider + .crypto + .stream_constructions + .load(Ordering::Relaxed), + 2 + ); + for sequence_number in 0..4 { + stream_context.encrypt_rtp(&rtp_packet(sequence_number)?)?; + } + assert_eq!( + stream_provider + .crypto + .stream_constructions + .load(Ordering::Relaxed), + 2 + ); + + let aead_provider = Arc::new(CountingProvider::new()); + let mut aead_context = context( + ProtectionProfile::AeadAes128Gcm, + aead_provider.clone(), + false, + )?; + assert_eq!( + aead_provider + .crypto + .aead_constructions + .load(Ordering::Relaxed), + 2 + ); + for sequence_number in 0..4 { + aead_context.encrypt_rtp(&rtp_packet(sequence_number)?)?; + } + assert_eq!( + aead_provider + .crypto + .aead_constructions + .load(Ordering::Relaxed), + 2 + ); + Ok(()) +} From 219788a20462d77af45d6fa362425f894f230c0a Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 21:06:20 -0700 Subject: [PATCH 34/40] =?UTF-8?q?P5=20=E2=80=94=20Migrate=20DTLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 10 +- rtc-dtls/Cargo.toml | 32 +- rtc-dtls/examples/dtls_micro.rs | 13 +- .../cipher_suite/cipher_suite_aes_128_ccm.rs | 25 +- .../cipher_suite_aes_128_gcm_sha256.rs | 25 +- .../cipher_suite_aes_256_cbc_sha.rs | 21 +- .../cipher_suite_chacha20_poly1305_sha256.rs | 25 +- ...r_suite_tls_psk_with_aes_128_gcm_sha256.rs | 26 +- rtc-dtls/src/cipher_suite/mod.rs | 44 +- rtc-dtls/src/config.rs | 115 ++++- rtc-dtls/src/config/config_test.rs | 74 ++- rtc-dtls/src/conn/conn_test.rs | 32 +- rtc-dtls/src/conn/mod.rs | 10 +- rtc-dtls/src/crypto/crypto_cbc.rs | 136 ++++-- rtc-dtls/src/crypto/crypto_ccm.rs | 146 ++---- rtc-dtls/src/crypto/crypto_chacha20.rs | 68 ++- rtc-dtls/src/crypto/crypto_gcm.rs | 119 +++-- rtc-dtls/src/crypto/crypto_test.rs | 235 ++++++---- rtc-dtls/src/crypto/mod.rs | 430 ++++++++---------- rtc-dtls/src/crypto/padding.rs | 125 ----- rtc-dtls/src/curve/named_curve.rs | 89 ++-- rtc-dtls/src/endpoint.rs | 202 ++++++++ rtc-dtls/src/flight/flight0.rs | 22 +- rtc-dtls/src/flight/flight1.rs | 7 +- rtc-dtls/src/flight/flight3.rs | 38 +- rtc-dtls/src/flight/flight4.rs | 30 +- rtc-dtls/src/flight/flight5.rs | 18 +- rtc-dtls/src/flight/flight6.rs | 1 + rtc-dtls/src/handshake/handshake_cache.rs | 15 +- .../handshake_cache/handshake_cache_test.rs | 3 +- rtc-dtls/src/handshake/handshake_random.rs | 9 +- rtc-dtls/src/lib.rs | 7 - rtc-dtls/src/prf/mod.rs | 154 +++---- rtc-dtls/src/prf/prf_test.rs | 72 +-- rtc-dtls/src/signature_hash_algorithm/mod.rs | 33 +- rtc-dtls/src/state.rs | 35 +- rtc-stun/Cargo.toml | 2 +- rtc-turn/Cargo.toml | 12 +- src/lib.rs | 2 - src/peer_connection/certificate/mod.rs | 56 +-- 40 files changed, 1429 insertions(+), 1089 deletions(-) delete mode 100644 rtc-dtls/src/crypto/padding.rs diff --git a/Cargo.toml b/Cargo.toml index 74e544ba..64218ddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ rand = "0.10.1" serde = { version = "1.0.228", features = ["derive"] } thiserror = "2.0.18" zeroize = "1.8.2" +pem = "3.0.3" # dev dependencies env_logger = "0.11.11" @@ -87,9 +88,8 @@ readme = "README.md" [features] default = ["ring"] -pem = ["dep:pem", "dtls/pem"] -ring = ["dep:ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] -aws-lc-rs = ["dep:aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] +ring = ["dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] +aws-lc-rs = ["dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } @@ -113,13 +113,11 @@ log.workspace = true serde = "1" serde_json = { version = "1", features = [] } rcgen.workspace = true -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } sha2 = "0.10" rustls = { version = "0.23.35", default-features = false, features = ["std"] } url = { version = "2", features = [] } hex = { version = "0.4", features = [] } -pem = { version = "3", optional = true } +pem.workspace = true unicase = "2.8" rand.workspace = true diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index fd9fae4f..f9d5ec02 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -11,37 +11,25 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[features] +default = ["ring"] +ring = ["crypto/ring", "rustls/ring", "rcgen/ring"] +aws-lc-rs = ["crypto/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] + [dependencies] shared = { workspace = true, default-features = false, features = ["crypto", "replay"] } +crypto.workspace = true bytes.workspace = true byteorder.workspace = true -rand_core = "0.6.4" -p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } -p384 = "0.13.0" -rand.workspace = true -hmac = "0.12.1" -sec1 = { version = "0.7", features = ["std"] } -sha1 = "0.10.6" -sha2 = "0.10.8" -aes = "0.8.4" -cbc = { version = "0.1.2", features = ["block-padding", "alloc"] } -# AES-GCM cipher suites use ring's hardware-accelerated AES-GCM (see -# crypto/crypto_gcm.rs); CCM/CBC/ChaCha20-Poly1305 stay on RustCrypto. -ccm = "0.5.0" -x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } x509-parser = "0.16.0" der-parser = "9.0.0" rcgen.workspace = true -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } rustls = { version = "0.23.27", default-features = false, features = ["std"] } rkyv = "0.8.17" bytecheck = "0.8" -subtle = "2.5.0" log.workspace = true -pem = { version = "3.0.3", optional = true } -chacha20poly1305 = "0.10.1" +pem.workspace = true [dev-dependencies] local-sync = "0.1.1" @@ -53,12 +41,6 @@ anyhow = "1.0.80" ctrlc.workspace = true futures = "0.3.30" -[features] -default = ["ring"] -pem = ["dep:pem"] -ring = ["dep:ring", "rustls/ring", "rcgen/ring"] -aws-lc-rs = ["dep:aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] - #[[example]] #name = "dtls_chat_server" #path = "examples/dtls_chat_server.rs" diff --git a/rtc-dtls/examples/dtls_micro.rs b/rtc-dtls/examples/dtls_micro.rs index e5df6451..e23c35c7 100644 --- a/rtc-dtls/examples/dtls_micro.rs +++ b/rtc-dtls/examples/dtls_micro.rs @@ -25,8 +25,17 @@ fn main() { let remote_iv = [0x44u8; 4]; // Encrypt-side and decrypt-side contexts that mirror each other. - let sender = CryptoGcm::new(&local_key, &local_iv, &remote_key, &remote_iv); - let receiver = CryptoGcm::new(&remote_key, &remote_iv, &local_key, &local_iv); + let provider = crypto::default_provider().expect("a default crypto provider"); + let mut sender = CryptoGcm::new( + provider.clone(), + &local_key, + &local_iv, + &remote_key, + &remote_iv, + ) + .expect("create sender cipher"); + let mut receiver = CryptoGcm::new(provider, &remote_key, &remote_iv, &local_key, &local_iv) + .expect("create receiver cipher"); let header = RecordLayerHeader { content_type: ContentType::ApplicationData, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs index dd566977..0389724d 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs @@ -3,7 +3,6 @@ use crate::client_certificate_type::ClientCertificateType; use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen}; use crate::prf::*; -#[derive(Clone)] /// The shared AES-128-CCM implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128Ccm { ccm: Option, @@ -62,51 +61,57 @@ impl CipherSuite for CipherSuiteAes128Ccm { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes128Ccm::PRF_MAC_LEN, - CipherSuiteAes128Ccm::PRF_KEY_LEN, - CipherSuiteAes128Ccm::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes128Ccm::PRF_MAC_LEN, + key: CipherSuiteAes128Ccm::PRF_KEY_LEN, + iv: CipherSuiteAes128Ccm::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.ccm = Some(CryptoCcm::new( + provider, &self.crypto_ccm_tag_len, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.ccm = Some(CryptoCcm::new( + provider, &self.crypto_ccm_tag_len, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let ccm = self.ccm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let ccm = self.ccm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; ccm.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let ccm = self.ccm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let ccm = self.ccm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; ccm.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs index 87b4fbe0..08500b25 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs @@ -2,7 +2,6 @@ use super::*; use crate::crypto::crypto_gcm::*; use crate::prf::*; -#[derive(Clone)] /// The shared AES-128-GCM with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128GcmSha256 { gcm: Option, @@ -59,49 +58,55 @@ impl CipherSuite for CipherSuiteAes128GcmSha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteAes128GcmSha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes128GcmSha256::PRF_MAC_LEN, + key: CipherSuiteAes128GcmSha256::PRF_KEY_LEN, + iv: CipherSuiteAes128GcmSha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.gcm = Some(CryptoGcm::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.gcm = Some(CryptoGcm::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs index 52601e2e..f6989646 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs @@ -2,7 +2,6 @@ use super::*; use crate::crypto::crypto_cbc::*; use crate::prf::*; -#[derive(Clone)] /// The shared AES-256-CBC with SHA-1 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes256CbcSha { cbc: Option, @@ -59,23 +58,28 @@ impl CipherSuite for CipherSuiteAes256CbcSha { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes256CbcSha::PRF_MAC_LEN, - CipherSuiteAes256CbcSha::PRF_KEY_LEN, - CipherSuiteAes256CbcSha::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes256CbcSha::PRF_MAC_LEN, + key: CipherSuiteAes256CbcSha::PRF_KEY_LEN, + iv: CipherSuiteAes256CbcSha::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.cbc = Some(CryptoCbc::new( + provider, &keys.client_write_key, &keys.client_mac_key, &keys.server_write_key, @@ -83,6 +87,7 @@ impl CipherSuite for CipherSuiteAes256CbcSha { )?); } else { self.cbc = Some(CryptoCbc::new( + provider, &keys.server_write_key, &keys.server_mac_key, &keys.client_write_key, @@ -93,15 +98,15 @@ impl CipherSuite for CipherSuiteAes256CbcSha { Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.cbc.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cbc.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.cbc.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.cbc.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs index 2c045ac5..624e7cd8 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs @@ -2,7 +2,6 @@ use super::*; use crate::crypto::crypto_chacha20::*; use crate::prf::*; -#[derive(Clone)] /// The shared ChaCha20-Poly1305 with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteChaCha20Poly1305Sha256 { rsa: bool, @@ -59,49 +58,55 @@ impl CipherSuite for CipherSuiteChaCha20Poly1305Sha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN, - CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN, - CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN, + key: CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN, + iv: CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.cipher = Some(CryptoChaCha20::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.cipher = Some(CryptoChaCha20::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.cipher.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cipher.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.cipher.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.cipher.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs index f0f31840..d3a0c286 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs @@ -2,7 +2,7 @@ use super::*; use crate::crypto::crypto_gcm::*; use crate::prf::*; -#[derive(Clone, Default)] +#[derive(Default)] /// The shared `TLS_PSK_WITH_AES_128_GCM_SHA256` implementation, parameterized over the key exchange and signature. pub struct CipherSuiteTlsPskWithAes128GcmSha256 { gcm: Option, @@ -41,49 +41,55 @@ impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, + key: CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, + iv: CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.gcm = Some(CryptoGcm::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.gcm = Some(CryptoGcm::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/mod.rs b/rtc-dtls/src/cipher_suite/mod.rs index 0183644d..4810d214 100644 --- a/rtc-dtls/src/cipher_suite/mod.rs +++ b/rtc-dtls/src/cipher_suite/mod.rs @@ -18,7 +18,12 @@ pub mod cipher_suite_tls_psk_with_aes_128_ccm8; /// `TLS_PSK_WITH_AES_128_GCM_SHA256`, for pre-shared-key handshakes. pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256; +use crypto::RTCCryptoProvider; +use crypto::{ + AeadAlgorithm, CbcAlgorithm, CryptoAlgorithm, HashAlgorithm, HmacAlgorithm, RTCCrypto, +}; use std::fmt; +use std::sync::Arc; use super::client_certificate_type::*; use super::record_layer::record_layer_header::*; @@ -174,6 +179,38 @@ impl From<&str> for CipherSuiteId { } } +impl CipherSuiteId { + pub(crate) fn supported_by(self, crypto: &dyn RTCCrypto) -> bool { + let record_algorithm = match self { + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm | Self::Tls_Psk_With_Aes_128_Ccm => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 | Self::Tls_Psk_With_Aes_128_Ccm_8 => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm8) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 + | Self::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + | Self::Tls_Psk_With_Aes_128_Gcm_Sha256 => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha + | Self::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { + CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + } + Self::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 + | Self::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + CryptoAlgorithm::Aead(AeadAlgorithm::ChaCha20Poly1305) + } + Self::Unsupported => return false, + }; + crypto.supports(CryptoAlgorithm::Hash(HashAlgorithm::Sha256)) + && crypto.supports(CryptoAlgorithm::Hmac(HmacAlgorithm::Sha256)) + && crypto.supports(record_algorithm) + && (!matches!(record_algorithm, CryptoAlgorithm::Cbc(_)) + || crypto.supports(CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1))) + } +} + #[derive(Copy, Clone, Debug)] /// The hash a suite uses in its PRF and `Finished` computation. #[non_exhaustive] @@ -192,7 +229,7 @@ impl CipherSuiteHash { /// A negotiated cipher suite: its identity, and the record encryption it performs once keys /// are installed. -pub trait CipherSuite: Send + Sync { +pub trait CipherSuite: Send { /// The suite's IANA name. fn to_string(&self) -> String; /// The suite's code point. @@ -214,6 +251,7 @@ pub trait CipherSuite: Send + Sync { /// Fails if the key or salt lengths do not match what this suite expects. fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], @@ -225,13 +263,13 @@ pub trait CipherSuite: Send + Sync { /// # Errors /// /// Fails if keys are not installed, or the cipher rejects the input. - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; /// Unprotects one record. /// /// # Errors /// /// Fails if authentication fails, or the record is malformed. - fn decrypt(&self, input: &[u8]) -> Result>; + fn decrypt(&mut self, input: &[u8]) -> Result>; } // Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index f3def5be..523a9350 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -19,10 +19,12 @@ mod config_test; use crate::cipher_suite::*; use crate::conn::{DEFAULT_REPLAY_PROTECTION_WINDOW, INITIAL_TICKER_INTERVAL}; use crate::crypto::*; +use crate::curve::named_curve::NamedCurve; use crate::extension::extension_use_srtp::SrtpProtectionProfile; use crate::signature_hash_algorithm::{ SignatureHashAlgorithm, SignatureScheme, parse_signature_schemes, }; +use crypto::RTCCryptoProvider; use log::warn; use shared::error::*; use std::collections::HashMap; @@ -46,17 +48,17 @@ use rustls::server::danger::ClientCertVerifier; /// /// If neither feature is enabled there is no provider to name, so fall back to whatever the /// application installed. -fn crypto_provider() -> Option> { - #[cfg(feature = "aws-lc-rs")] +fn rustls_crypto_provider() -> Option> { + #[cfg(feature = "ring")] + { + Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())) + } + #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] { Some(std::sync::Arc::new( rustls::crypto::aws_lc_rs::default_provider(), )) } - #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] - { - Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())) - } #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] { None @@ -71,7 +73,7 @@ fn crypto_provider() -> Option> { fn server_cert_verifier( roots: std::sync::Arc, ) -> Result> { - let builder = match crypto_provider() { + let builder = match rustls_crypto_provider() { Some(provider) => { rustls::client::WebPkiServerVerifier::builder_with_provider(roots, provider) } @@ -86,6 +88,7 @@ fn server_cert_verifier( /// After a Config is passed to a DTLS function it must not be modified. #[derive(Clone)] pub struct ConfigBuilder { + crypto_provider: Option>, certificates: Vec, cipher_suites: Vec, signature_schemes: Vec, @@ -109,6 +112,7 @@ pub struct ConfigBuilder { impl Default for ConfigBuilder { fn default() -> Self { Self { + crypto_provider: None, certificates: vec![], cipher_suites: vec![], signature_schemes: vec![], @@ -132,6 +136,15 @@ impl Default for ConfigBuilder { } impl ConfigBuilder { + /// Selects the cryptography and CSPRNG implementation for this DTLS association. + /// + /// The provider is resolved while building the handshake configuration and is reused for the + /// entire handshake and record lifetime. No global registration is required. + pub fn with_crypto_provider(mut self, provider: Arc) -> Self { + self.crypto_provider = Some(provider); + self + } + /// certificates contains certificate chain to present to the other side of the connection. /// Server MUST set this if psk is non-nil /// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil @@ -343,16 +356,6 @@ impl ConfigBuilder { return Err(Error::ErrIdentityNoPsk); } - // Gates future private key kinds from being automatically allowed. - for cert in &self.certificates { - match cert.private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => {} - CryptoPrivateKeyKind::Ecdsa256(_) => {} - CryptoPrivateKeyKind::Rsa256(_) => {} - CryptoPrivateKeyKind::Custom(_) => {} - } - } - parse_cipher_suites(&self.cipher_suites, self.psk.is_none(), self.psk.is_some())?; Ok(()) @@ -364,16 +367,77 @@ impl ConfigBuilder { is_client: bool, remote_addr: Option, ) -> Result { + let crypto_provider = match self.crypto_provider.take() { + Some(provider) => provider, + None => crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?, + }; self.validate(is_client)?; - let local_cipher_suites: Vec = + let mut local_cipher_suites: Vec = parse_cipher_suites(&self.cipher_suites, self.psk.is_none(), self.psk.is_some())? .iter() .map(|cs| cs.id()) + .filter(|id| id.supported_by(crypto_provider.crypto())) .collect(); + if local_cipher_suites.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } let sigs: Vec = self.signature_schemes.iter().map(|x| *x as u16).collect(); - let local_signature_schemes = parse_signature_schemes(&sigs, self.insecure_hashes)?; + let local_signature_schemes: Vec<_> = parse_signature_schemes(&sigs, self.insecure_hashes)? + .into_iter() + .filter(|algorithm| { + algorithm.crypto_scheme().is_ok_and(|scheme| { + crypto_provider + .crypto() + .supports(crypto::CryptoAlgorithm::Signature(scheme)) + }) + }) + .collect(); + if self.psk.is_none() && local_signature_schemes.is_empty() { + return Err(Error::ErrNoAvailableSignatureSchemes); + } + + let local_named_curves: Vec<_> = [NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384] + .into_iter() + .filter(|curve| { + curve.crypto_algorithm().is_ok_and(|algorithm| { + crypto_provider + .crypto() + .supports(crypto::CryptoAlgorithm::KeyExchange(algorithm)) + }) + }) + .collect(); + if self.psk.is_none() && local_named_curves.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } + + if !is_client && self.psk.is_none() { + let signing_key = &self.certificates[0].private_key.signing_key; + local_cipher_suites.retain(|id| { + local_signature_schemes.iter().any(|algorithm| { + let signature_family_matches = match id { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + | CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha + | CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + algorithm.signature + == crate::signature_hash_algorithm::SignatureAlgorithm::Rsa + } + _ => { + algorithm.signature + == crate::signature_hash_algorithm::SignatureAlgorithm::Ecdsa + } + }; + signature_family_matches + && algorithm + .crypto_scheme() + .is_ok_and(|scheme| signing_key.supports(scheme)) + }) + }); + if local_cipher_suites.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } + } let retransmit_interval = if self.flight_interval != Duration::from_secs(0) { self.flight_interval @@ -404,9 +468,11 @@ impl ConfigBuilder { } Ok(HandshakeConfig { + crypto_provider, local_psk_callback: self.psk.take(), local_psk_identity_hint: self.psk_identity_hint.take(), local_cipher_suites, + local_named_curves, local_signature_schemes, extended_master_secret: self.extended_master_secret, local_srtp_protection_profiles: self.srtp_protection_profiles, @@ -453,9 +519,11 @@ pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { #[derive(Clone)] /// The resolved configuration a handshake runs with, produced by [`ConfigBuilder::build`]. pub struct HandshakeConfig { + pub(crate) crypto_provider: Arc, pub(crate) local_psk_callback: Option, pub(crate) local_psk_identity_hint: Option>, pub(crate) local_cipher_suites: Vec, // Available CipherSuites + pub(crate) local_named_curves: Vec, pub(crate) local_signature_schemes: Vec, // Available signature schemes pub(crate) extended_master_secret: ExtendedMasterSecretType, // Policy for the Extended Master Support extension pub(crate) local_srtp_protection_profiles: Vec, // Available SRTPProtectionProfiles, if empty no SRTP support @@ -479,8 +547,10 @@ pub struct HandshakeConfig { impl fmt::Debug for HandshakeConfig { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("HandshakeConfig") + .field("crypto_provider", &self.crypto_provider.name()) .field("local_psk_identity_hint", &self.local_psk_identity_hint) .field("local_cipher_suites", &self.local_cipher_suites) + .field("local_named_curves", &self.local_named_curves) .field("local_signature_schemes", &self.local_signature_schemes) .field("extended_master_secret", &self.extended_master_secret) .field( @@ -506,9 +576,12 @@ impl fmt::Debug for HandshakeConfig { impl Default for HandshakeConfig { fn default() -> Self { HandshakeConfig { + crypto_provider: crypto::default_provider() + .expect("rtc-dtls requires an enabled default crypto provider"), local_psk_callback: None, local_psk_identity_hint: None, local_cipher_suites: vec![], + local_named_curves: vec![], local_signature_schemes: vec![], extended_master_secret: ExtendedMasterSecretType::Disable, local_srtp_protection_profiles: vec![], @@ -533,6 +606,10 @@ impl Default for HandshakeConfig { } impl HandshakeConfig { + pub(crate) fn provider(&self) -> &Arc { + &self.crypto_provider + } + pub(crate) fn get_certificate(&self, server_name: &str) -> Result { if self.local_certificates.is_empty() { return Err(Error::ErrNoCertificates); diff --git a/rtc-dtls/src/config/config_test.rs b/rtc-dtls/src/config/config_test.rs index e448f856..ec3d5bf7 100644 --- a/rtc-dtls/src/config/config_test.rs +++ b/rtc-dtls/src/config/config_test.rs @@ -1,6 +1,34 @@ use super::*; use shared::error::Result; +struct IncompleteProvider; + +impl crypto::RTCCryptoProvider for IncompleteProvider { + fn name(&self) -> &'static str { + "incomplete" + } + + fn crypto(&self) -> &dyn crypto::RTCCrypto { + self + } + + fn random(&self) -> &dyn crypto::RTCRandom { + self + } +} + +impl crypto::RTCCrypto for IncompleteProvider { + fn supports(&self, _algorithm: crypto::CryptoAlgorithm) -> bool { + false + } +} + +impl crypto::RTCRandom for IncompleteProvider { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), crypto::CryptoError> { + Err(crypto::CryptoError::RandomnessFailed) + } +} + #[derive(Debug)] struct MockSigner; @@ -18,20 +46,50 @@ impl CustomSigner for MockSigner { fn test_config_accepts_custom_signer() -> Result<()> { let cert = Certificate { certificate: vec![], - private_key: CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(Box::new(MockSigner)), - serialized_der: vec![], - }, + private_key: CryptoPrivateKey::from_custom_signer(Box::new(MockSigner)), }; let handshake = ConfigBuilder::default() .with_certificates(vec![cert]) .build(false, None)?; - assert!(matches!( - handshake.local_certificates[0].private_key.kind, - CryptoPrivateKeyKind::Custom(_) - )); + assert!( + handshake.local_certificates[0] + .private_key + .signing_key + .supports(crypto::SignatureScheme::EcdsaP256Sha256) + ); + + Ok(()) +} + +#[test] +fn test_config_rejects_incomplete_provider() { + let result = ConfigBuilder::default() + .with_crypto_provider(Arc::new(IncompleteProvider)) + .build(true, None); + + assert!(matches!(result, Err(Error::ErrNoAvailableCipherSuites))); +} + +#[cfg(feature = "ring")] +#[test] +fn test_config_accepts_ring_provider() -> Result<()> { + let handshake = ConfigBuilder::default() + .with_crypto_provider(Arc::new(crypto::providers::RingProvider::new())) + .build(true, None)?; + + assert_eq!(handshake.provider().name(), "ring"); + Ok(()) +} + +#[cfg(feature = "aws-lc-rs")] +#[test] +fn test_config_accepts_aws_lc_rs_provider() -> Result<()> { + let handshake = ConfigBuilder::default() + .with_crypto_provider(Arc::new(crypto::providers::AwsLcRsProvider::new())) + .build(true, None)?; + assert_eq!(handshake.provider().name(), "aws-lc-rs"); Ok(()) } diff --git a/rtc-dtls/src/conn/conn_test.rs b/rtc-dtls/src/conn/conn_test.rs index 960c0bb8..de9e09ed 100644 --- a/rtc-dtls/src/conn/conn_test.rs +++ b/rtc-dtls/src/conn/conn_test.rs @@ -2478,7 +2478,13 @@ fn test_handle_incoming_queued_packets_drains_when_cipher_ready() { // Now initialize the cipher suite (simulates ChangeCipherSpec having been processed). let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); // init() with dummy key material — will produce an initialized cipher suite. - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); assert!(cs.is_initialized()); conn.state.cipher_suite = Some(cs); @@ -2503,7 +2509,13 @@ fn test_handle_incoming_queued_packets_sets_handshake_rx() { // Initialize cipher suite. let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); conn.state.remote_epoch = 0; conn.handshake_rx = None; @@ -2575,7 +2587,13 @@ fn test_queued_packets_drained_when_cipher_ready() { // Initialize cipher suite (simulates ChangeCipherSpec having been processed). let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); conn.state.remote_epoch = 1; // ChangeCipherSpec has been processed @@ -2602,7 +2620,13 @@ fn test_handshake_rx_set_after_queue_processing() { // Initialize cipher suite so the queue-drain guard passes. let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); assert!(conn.handshake_rx.is_none()); diff --git a/rtc-dtls/src/conn/mod.rs b/rtc-dtls/src/conn/mod.rs index 695591a4..23d6f816 100644 --- a/rtc-dtls/src/conn/mod.rs +++ b/rtc-dtls/src/conn/mod.rs @@ -110,7 +110,8 @@ impl DTLSConn { is_client: bool, initial_state: Option, ) -> Self { - let (state, flight, initial_fsm_state) = if let Some(state) = initial_state { + let provider = Some(handshake_config.crypto_provider.clone()); + let (mut state, flight, initial_fsm_state) = if let Some(state) = initial_state { let flight = if is_client { Box::new(Flight5 {}) as Box } else { @@ -134,6 +135,7 @@ impl DTLSConn { HandshakeState::Preparing, ) }; + state.crypto_provider = provider; Self { is_client, @@ -348,7 +350,7 @@ impl DTLSConn { p.record.marshal(&mut raw_packet)?; if p.should_encrypt - && let Some(cipher_suite) = &self.state.cipher_suite + && let Some(cipher_suite) = &mut self.state.cipher_suite { raw_packet = cipher_suite.encrypt(&p.record.record_layer_header, &raw_packet)?; } @@ -397,7 +399,7 @@ impl DTLSConn { raw_packet.extend_from_slice(&record_layer_header_bytes); raw_packet.extend_from_slice(handshake_fragment); if p.should_encrypt - && let Some(cipher_suite) = &self.state.cipher_suite + && let Some(cipher_suite) = &mut self.state.cipher_suite { raw_packet = cipher_suite.encrypt(&record_layer_header, &raw_packet)?; } @@ -636,7 +638,7 @@ impl DTLSConn { return (false, None, None); } - if let Some(cipher_suite) = &self.state.cipher_suite { + if let Some(cipher_suite) = &mut self.state.cipher_suite { pkt = match cipher_suite.decrypt(&pkt) { Ok(pkt) => pkt, Err(err) => { diff --git a/rtc-dtls/src/crypto/crypto_cbc.rs b/rtc-dtls/src/crypto/crypto_cbc.rs index 6a958220..02d7fc21 100644 --- a/rtc-dtls/src/crypto/crypto_cbc.rs +++ b/rtc-dtls/src/crypto/crypto_cbc.rs @@ -6,28 +6,21 @@ // Removed in TLS 1.3 year 2018. // RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268 -// https://github.com/RustCrypto/block-ciphers - -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use p256::elliptic_curve::subtle::ConstantTimeEq; -use rand::RngExt; +use crypto::{CbcAlgorithm, CbcCipher, RTCCryptoProvider, constant_time_eq}; use std::io::Cursor; -use std::ops::Not; +use std::sync::Arc; -use super::padding::DtlsPadding; use crate::content::*; +use crate::crypto::{authentication_error, crypto_error}; use crate::prf::*; use crate::record_layer::record_layer_header::*; use shared::error::*; -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; -// State needed to handle encrypted input/output -#[derive(Clone)] /// AES-CBC encryption with a separate HMAC for DTLS records, holding the per-direction keys. pub struct CryptoCbc { - local_key: Vec, - remote_key: Vec, + provider: Arc, + local_cipher: Box, + remote_cipher: Box, write_mac: Vec, read_mac: Vec, } @@ -38,16 +31,25 @@ impl CryptoCbc { /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_mac: &[u8], remote_key: &[u8], remote_mac: &[u8], ) -> Result { + let local_cipher = provider + .crypto() + .new_cbc(CbcAlgorithm::Aes256Cbc, local_key) + .map_err(crypto_error)?; + let remote_cipher = provider + .crypto() + .new_cbc(CbcAlgorithm::Aes256Cbc, remote_key) + .map_err(crypto_error)?; Ok(CryptoCbc { - local_key: local_key.to_vec(), + provider, + local_cipher, write_mac: local_mac.to_vec(), - - remote_key: remote_key.to_vec(), + remote_cipher, read_mac: remote_mac.to_vec(), }) } @@ -57,7 +59,7 @@ impl CryptoCbc { /// # Errors /// /// Fails if the cipher rejects the input. - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec(); let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -65,6 +67,7 @@ impl CryptoCbc { let h = pkt_rlh; let mac = prf_mac( + self.provider.crypto(), h.epoch, h.sequence_number, h.content_type, @@ -74,17 +77,20 @@ impl CryptoCbc { )?; payload.extend_from_slice(&mac); - let mut iv: Vec = vec![0; Self::BLOCK_SIZE]; - rand::rng().fill(iv.as_mut_slice()); + let padding_len = Self::BLOCK_SIZE - (payload.len() % Self::BLOCK_SIZE); + payload.resize(payload.len() + padding_len, (padding_len - 1) as u8); - let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?; - let encrypted = write_cbc.encrypt_padded_vec_mut::(&payload); + let mut iv = [0; Self::BLOCK_SIZE]; + self.provider.random().fill(&mut iv).map_err(crypto_error)?; + self.local_cipher + .encrypt_blocks(&iv, &mut payload) + .map_err(crypto_error)?; // Prepend unencrypte header with encrypted payload let mut r = vec![]; r.extend_from_slice(raw); r.extend_from_slice(&iv); - r.extend_from_slice(&encrypted); + r.extend_from_slice(&payload); let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] @@ -98,7 +104,7 @@ impl CryptoCbc { /// # Errors /// /// Fails if authentication fails or the record is too short. - pub fn decrypt(&self, r: &[u8]) -> Result> { + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -118,11 +124,20 @@ impl CryptoCbc { return Err(Error::ErrInvalidPacketLength); } - let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?; + let mut decrypted = body.to_vec(); + self.remote_cipher + .decrypt_blocks(iv, &mut decrypted) + .map_err(authentication_error)?; - let decrypted = read_cbc - .decrypt_padded_vec_mut::(body) - .map_err(|_| Error::ErrInvalidPacketLength)?; + let padding_value = decrypted.last().copied().ok_or(Error::ErrInvalidMac)?; + let padding_len = padding_value as usize + 1; + if padding_len > decrypted.len() { + return Err(Error::ErrInvalidMac); + } + let padding_start = decrypted.len() - padding_len; + let expected_padding = vec![padding_value; padding_len]; + let padding_valid = constant_time_eq(&decrypted[padding_start..], &expected_padding); + decrypted.truncate(padding_start); if decrypted.len() < Self::MAC_SIZE { return Err(Error::ErrInvalidMac); @@ -131,6 +146,7 @@ impl CryptoCbc { let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..]; let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE]; let mac = prf_mac( + self.provider.crypto(), h.epoch, h.sequence_number, h.content_type, @@ -139,7 +155,7 @@ impl CryptoCbc { &self.read_mac, )?; - if recv_mac.ct_eq(&mac).not().into() { + if !padding_valid || !constant_time_eq(recv_mac, &mac) { return Err(Error::ErrInvalidMac); } @@ -150,3 +166,67 @@ impl CryptoCbc { Ok(d) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn cipher_pair() -> (CryptoCbc, CryptoCbc) { + let provider = crypto::default_provider().unwrap(); + let local_key = [0x11; 32]; + let remote_key = [0x22; 32]; + let local_mac = [0x33; 20]; + let remote_mac = [0x44; 20]; + let sender = CryptoCbc::new( + provider.clone(), + &local_key, + &local_mac, + &remote_key, + &remote_mac, + ) + .unwrap(); + let receiver = + CryptoCbc::new(provider, &remote_key, &remote_mac, &local_key, &local_mac).unwrap(); + (sender, receiver) + } + + fn record(payload: &[u8]) -> (RecordLayerHeader, Vec) { + let header = RecordLayerHeader { + content_type: ContentType::ApplicationData, + protocol_version: PROTOCOL_VERSION1_2, + epoch: 1, + sequence_number: 7, + content_len: payload.len() as u16, + }; + let mut raw = Vec::new(); + header.marshal(&mut raw).unwrap(); + raw.extend_from_slice(payload); + (header, raw) + } + + #[test] + fn roundtrip_and_authentication_failures() { + let (mut sender, mut receiver) = cipher_pair(); + let (header, raw) = record(b"CBC record payload"); + let encrypted = sender.encrypt(&header, &raw).unwrap(); + assert_eq!( + receiver.decrypt(&encrypted).unwrap()[RECORD_LAYER_HEADER_SIZE..], + raw[RECORD_LAYER_HEADER_SIZE..] + ); + + let mut wrong_mac = encrypted.clone(); + wrong_mac[RECORD_LAYER_HEADER_SIZE] ^= 1; + assert_eq!(receiver.decrypt(&wrong_mac), Err(Error::ErrInvalidMac)); + + let mut bad_padding = encrypted.clone(); + *bad_padding.last_mut().unwrap() ^= 1; + assert_eq!(receiver.decrypt(&bad_padding), Err(Error::ErrInvalidMac)); + + let mut truncated = encrypted; + truncated.pop(); + assert_eq!( + receiver.decrypt(&truncated), + Err(Error::ErrInvalidPacketLength) + ); + } +} diff --git a/rtc-dtls/src/crypto/crypto_ccm.rs b/rtc-dtls/src/crypto/crypto_ccm.rs index a8437584..3e9ee16f 100644 --- a/rtc-dtls/src/crypto/crypto_ccm.rs +++ b/rtc-dtls/src/crypto/crypto_ccm.rs @@ -9,27 +9,17 @@ // https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm? use std::io::Cursor; +use std::sync::Arc; -use aes::Aes128; -use ccm::Ccm; -use ccm::KeyInit; -use ccm::aead::AeadInPlace; -use ccm::aead::generic_array::GenericArray; -use ccm::consts::{U8, U12, U16}; -use rand::RngExt; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; use crate::record_layer::record_layer_header::*; use shared::error::*; -const CRYPTO_CCM_8_TAG_LENGTH: usize = 8; -const CRYPTO_CCM_TAG_LENGTH: usize = 16; const CRYPTO_CCM_NONCE_LENGTH: usize = 12; -type AesCcm8 = Ccm; -type AesCcm = Ccm; - #[derive(Clone)] /// The authentication tag length a CCM suite uses. pub enum CryptoCcmTagLen { @@ -39,73 +29,44 @@ pub enum CryptoCcmTagLen { CryptoCcmTagLength, } -enum CryptoCcmType { - CryptoCcm8(AesCcm8), - CryptoCcm(AesCcm), -} - -// State needed to handle encrypted input/output /// AES-CCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoCcm { - local_ccm: CryptoCcmType, - remote_ccm: CryptoCcmType, + provider: Arc, + local_ccm: Box, + remote_ccm: Box, local_write_iv: Vec, remote_write_iv: Vec, - // used by clone() - local_write_key: Vec, - remote_write_key: Vec, -} - -impl Clone for CryptoCcm { - fn clone(&self) -> Self { - match self.local_ccm { - CryptoCcmType::CryptoCcm(_) => Self::new( - &CryptoCcmTagLen::CryptoCcmTagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - CryptoCcmType::CryptoCcm8(_) => Self::new( - &CryptoCcmTagLen::CryptoCcm8TagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - } - } } impl CryptoCcm { /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, tag_len: &CryptoCcmTagLen, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), - }; - - let key = GenericArray::from_slice(remote_key); - let remote_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), + ) -> Result { + let algorithm = match tag_len { + CryptoCcmTagLen::CryptoCcmTagLength => AeadAlgorithm::Aes128Ccm, + CryptoCcmTagLen::CryptoCcm8TagLength => AeadAlgorithm::Aes128Ccm8, }; - - CryptoCcm { + let local_ccm = provider + .crypto() + .new_aead(algorithm, local_key) + .map_err(crypto_error)?; + let remote_ccm = provider + .crypto() + .new_aead(algorithm, remote_key) + .map_err(crypto_error)?; + Ok(CryptoCcm { + provider, local_ccm, - local_write_key: local_key.to_vec(), local_write_iv: local_write_iv.to_vec(), remote_ccm, - remote_write_key: remote_key.to_vec(), remote_write_iv: remote_write_iv.to_vec(), - } + }) } /// Protects one record, returning header plus ciphertext. @@ -113,36 +74,31 @@ impl CryptoCcm { /// # Errors /// /// Fails if the cipher rejects the input. - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH]; + let mut nonce = [0u8; CRYPTO_CCM_NONCE_LENGTH]; nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::rng().fill(&mut nonce[4..]); - let nonce = GenericArray::from_slice(&nonce); + self.provider + .random() + .fill(&mut nonce[4..]) + .map_err(crypto_error)?; let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(payload); - - match &self.local_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - ccm.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - } + let mut buffer = payload.to_vec(); + let mut tag = vec![0; self.local_ccm.tag_len()]; + self.local_ccm + .seal_in_place(&nonce, &additional_data, &mut buffer, &mut tag) + .map_err(crypto_error)?; - let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); + let mut r = Vec::with_capacity(raw.len() + 8 + buffer.len() + tag.len()); r.extend_from_slice(raw); r.extend_from_slice(&nonce[4..]); r.extend_from_slice(&buffer); + r.extend_from_slice(&tag); // Update recordLayer size to include explicit nonce let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; @@ -157,7 +113,7 @@ impl CryptoCcm { /// # Errors /// /// Fails if authentication fails or the record is too short. - pub fn decrypt(&self, r: &[u8]) -> Result> { + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -169,30 +125,22 @@ impl CryptoCcm { return Err(Error::ErrNotEnoughRoomForNonce); } - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..4]); - nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); - let nonce = GenericArray::from_slice(&nonce); + let mut nonce = [0; CRYPTO_CCM_NONCE_LENGTH]; + nonce[..4].copy_from_slice(&self.remote_write_iv[..4]); + nonce[4..].copy_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); - - match &self.remote_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH); - ccm.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH); - ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } + let tag_len = self.remote_ccm.tag_len(); + if out.len() < tag_len { + return Err(Error::ErrInvalidMac); } + let tag_start = out.len() - tag_len; + let additional_data = generate_aead_additional_data(&h, tag_start); + let mut buffer = out[..tag_start].to_vec(); + self.remote_ccm + .open_in_place(&nonce, &additional_data, &mut buffer, &out[tag_start..]) + .map_err(authentication_error)?; let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); diff --git a/rtc-dtls/src/crypto/crypto_chacha20.rs b/rtc-dtls/src/crypto/crypto_chacha20.rs index ca02f409..f00f4553 100644 --- a/rtc-dtls/src/crypto/crypto_chacha20.rs +++ b/rtc-dtls/src/crypto/crypto_chacha20.rs @@ -1,8 +1,7 @@ use std::io::Cursor; +use std::sync::Arc; -use chacha20poly1305::aead::AeadInPlace; -use chacha20poly1305::aead::generic_array::GenericArray; -use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; @@ -12,13 +11,10 @@ const CRYPTO_CHACHA20_TAG_LENGTH: usize = 16; const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12; // State needed to handle encrypted input/output -#[derive(Clone)] /// ChaCha20-Poly1305 authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoChaCha20 { - local_cc: ChaCha20Poly1305, - remote_cc: ChaCha20Poly1305, - local_key: Vec, - remote_key: Vec, + local_cc: Box, + remote_cc: Box, local_write_iv: Vec, remote_write_iv: Vec, } @@ -34,25 +30,26 @@ fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) { impl CryptoChaCha20 { /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_cc = ChaCha20Poly1305::new(key); - - let key = GenericArray::from_slice(remote_key); - let remote_cc = ChaCha20Poly1305::new(key); - - CryptoChaCha20 { + ) -> Result { + let local_cc = provider + .crypto() + .new_aead(AeadAlgorithm::ChaCha20Poly1305, local_key) + .map_err(crypto_error)?; + let remote_cc = provider + .crypto() + .new_aead(AeadAlgorithm::ChaCha20Poly1305, remote_key) + .map_err(crypto_error)?; + Ok(CryptoChaCha20 { local_cc, local_write_iv: local_write_iv.to_vec(), remote_cc, - local_key: local_key.to_vec(), - remote_key: remote_key.to_vec(), remote_write_iv: remote_write_iv.to_vec(), - } + }) } /// Protects one record, returning header plus ciphertext. @@ -60,26 +57,24 @@ impl CryptoChaCha20 { /// # Errors /// /// Fails if the cipher rejects the input. - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - let mut nonce = vec![0u8; CRYPTO_CHACHA20_NONCE_LENGTH]; + let mut nonce = [0u8; CRYPTO_CHACHA20_NONCE_LENGTH]; nonce[..CRYPTO_CHACHA20_NONCE_LENGTH] .copy_from_slice(&self.local_write_iv[..CRYPTO_CHACHA20_NONCE_LENGTH]); noncegen(&mut nonce[..], pkt_rlh.epoch, pkt_rlh.sequence_number); - let nonce = GenericArray::from_slice(&nonce); - let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); let mut buffer: Vec = Vec::new(); buffer.extend_from_slice(payload); - let tag = self - .local_cc - .encrypt_in_place_detached(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; + let mut tag = [0; CRYPTO_CHACHA20_TAG_LENGTH]; + self.local_cc + .seal_in_place(&nonce, &additional_data, &mut buffer, &mut tag) + .map_err(crypto_error)?; let mut r = Vec::with_capacity(raw.len() + buffer.len() + tag.len()); r.extend_from_slice(raw); @@ -99,7 +94,7 @@ impl CryptoChaCha20 { /// # Errors /// /// Fails if authentication fails or the record is too short. - pub fn decrypt(&self, r: &[u8]) -> Result> { + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -107,23 +102,24 @@ impl CryptoChaCha20 { return Ok(r.to_vec()); } - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..]); + let mut nonce = [0; CRYPTO_CHACHA20_NONCE_LENGTH]; + nonce.copy_from_slice(&self.remote_write_iv[..]); noncegen(&mut nonce[..], h.epoch, h.sequence_number); - let nonce = GenericArray::from_slice(&nonce); - let out = &r[RECORD_LAYER_HEADER_SIZE..]; + if out.len() < CRYPTO_CHACHA20_TAG_LENGTH { + return Err(Error::ErrInvalidMac); + } let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_CHACHA20_TAG_LENGTH); - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); + let tag_start = out.len() - CRYPTO_CHACHA20_TAG_LENGTH; + let mut buffer = out[..tag_start].to_vec(); self.remote_cc - .decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; + .open_in_place(&nonce, &additional_data, &mut buffer, &out[tag_start..]) + .map_err(authentication_error)?; let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); diff --git a/rtc-dtls/src/crypto/crypto_gcm.rs b/rtc-dtls/src/crypto/crypto_gcm.rs index 20e71d34..8f5828ae 100644 --- a/rtc-dtls/src/crypto/crypto_gcm.rs +++ b/rtc-dtls/src/crypto/crypto_gcm.rs @@ -4,11 +4,9 @@ // RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288 use std::io::Cursor; -#[cfg(feature = "aws-lc-rs")] use std::sync::Arc; -use rand::RngExt; -use ring::aead::{AES_128_GCM, Aad, LessSafeKey, Nonce, UnboundKey}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; @@ -18,26 +16,11 @@ use shared::error::*; const CRYPTO_GCM_TAG_LENGTH: usize = 16; const CRYPTO_GCM_NONCE_LENGTH: usize = 12; -// State needed to handle encrypted input/output. -// -// The AES-128-GCM AEAD runs on ring's hardware-accelerated single-pass assembly -// (AES-NI + CLMUL on x86_64, ARMv8 AES + PMULL on aarch64) instead of the -// pure-Rust RustCrypto `aes-gcm`. ring is already a dependency of this crate -// (handshake signatures / key generation). `LessSafeKey` is `Clone`, so -// `CryptoGcm` stays cloneable for the cipher-suite state that embeds it. -#[derive(Clone)] /// AES-GCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoGcm { - #[cfg(feature = "ring")] - local_gcm: LessSafeKey, - #[cfg(feature = "ring")] - remote_gcm: LessSafeKey, - // Arc is needed until `Clone` is implemented - // https://github.com/aws/aws-lc-rs/issues/1165 - #[cfg(feature = "aws-lc-rs")] - local_gcm: Arc, - #[cfg(feature = "aws-lc-rs")] - remote_gcm: Arc, + provider: Arc, + local_gcm: Box, + remote_gcm: Box, local_write_iv: Vec, remote_write_iv: Vec, } @@ -45,31 +28,27 @@ pub struct CryptoGcm { impl CryptoGcm { /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - // Keys are exactly AES_128_GCM.key_len() (16) bytes as derived by the - // handshake; a wrong length here is a programming error. - let local_gcm = LessSafeKey::new( - UnboundKey::new(&AES_128_GCM, local_key).expect("valid AES-128-GCM local key"), - ); - let remote_gcm = LessSafeKey::new( - UnboundKey::new(&AES_128_GCM, remote_key).expect("valid AES-128-GCM remote key"), - ); - - #[cfg(feature = "aws-lc-rs")] - let local_gcm = Arc::new(local_gcm); - #[cfg(feature = "aws-lc-rs")] - let remote_gcm = Arc::new(remote_gcm); - - CryptoGcm { + ) -> Result { + let local_gcm = provider + .crypto() + .new_aead(AeadAlgorithm::Aes128Gcm, local_key) + .map_err(crypto_error)?; + let remote_gcm = provider + .crypto() + .new_aead(AeadAlgorithm::Aes128Gcm, remote_key) + .map_err(crypto_error)?; + Ok(CryptoGcm { + provider, local_gcm, local_write_iv: local_write_iv.to_vec(), remote_gcm, remote_write_iv: remote_write_iv.to_vec(), - } + }) } /// Protects one record, returning header plus ciphertext. @@ -77,13 +56,16 @@ impl CryptoGcm { /// # Errors /// /// Fails if the cipher rejects the input. - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; let mut nonce = [0u8; CRYPTO_GCM_NONCE_LENGTH]; nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::rng().fill(&mut nonce[4..]); + self.provider + .random() + .fill(&mut nonce[4..]) + .map_err(crypto_error)?; let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); @@ -97,15 +79,16 @@ impl CryptoGcm { r.extend_from_slice(&nonce[4..]); r.extend_from_slice(payload); - let tag = self - .local_gcm - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(nonce), - Aad::from(&additional_data), + let mut tag = [0; CRYPTO_GCM_TAG_LENGTH]; + self.local_gcm + .seal_in_place( + &nonce, + &additional_data, &mut r[RECORD_LAYER_HEADER_SIZE + 8..], + &mut tag, ) - .map_err(|e| Error::Other(format!("DTLS AES-GCM seal failed: {e}")))?; - r.extend_from_slice(tag.as_ref()); + .map_err(crypto_error)?; + r.extend_from_slice(&tag); // Update recordLayer size to include explicit nonce let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; @@ -120,7 +103,7 @@ impl CryptoGcm { /// # Errors /// /// Fails if authentication fails or the record is too short. - pub fn decrypt(&self, r: &[u8]) -> Result> { + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -147,24 +130,17 @@ impl CryptoGcm { let additional_data = generate_aead_additional_data(&h, tag_start); - // Copy header + ciphertext||tag once and decrypt the ciphertext+tag - // region in place. ring's `open_in_place` wants ciphertext and tag - // contiguous (they already are on the wire) and returns the plaintext - // slice; drop the trailing tag afterwards. - let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + out.len()); + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + tag_start); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); - d.extend_from_slice(out); - - let plaintext_len = self - .remote_gcm + d.extend_from_slice(&out[..tag_start]); + self.remote_gcm .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&additional_data), + &nonce, + &additional_data, &mut d[RECORD_LAYER_HEADER_SIZE..], + &out[tag_start..], ) - .map_err(|e| Error::Other(format!("DTLS AES-GCM open failed: {e}")))? - .len(); - d.truncate(RECORD_LAYER_HEADER_SIZE + plaintext_len); + .map_err(authentication_error)?; Ok(d) } @@ -195,8 +171,17 @@ mod tests { let local_iv = [0x22u8; 4]; let remote_key = [0x33u8; 16]; let remote_iv = [0x44u8; 4]; - let sender = CryptoGcm::new(&local_key, &local_iv, &remote_key, &remote_iv); - let receiver = CryptoGcm::new(&remote_key, &remote_iv, &local_key, &local_iv); + let provider = crypto::default_provider().unwrap(); + let mut sender = CryptoGcm::new( + provider.clone(), + &local_key, + &local_iv, + &remote_key, + &remote_iv, + ) + .unwrap(); + let mut receiver = + CryptoGcm::new(provider, &remote_key, &remote_iv, &local_key, &local_iv).unwrap(); let payload = b"application data!"; let (header, raw) = make_record(payload); @@ -229,7 +214,8 @@ mod tests { fn test_crypto_gcm_decrypt_too_short_for_tag() { let key = [0x11u8; 16]; let iv = [0x22u8; 4]; - let cg = CryptoGcm::new(&key, &iv, &key, &iv); + let mut cg = + CryptoGcm::new(crypto::default_provider().unwrap(), &key, &iv, &key, &iv).unwrap(); let (_, mut raw) = make_record(&[0u8; 0]); // 8-byte explicit nonce plus 10 bytes: less than the 16-byte tag. @@ -243,7 +229,8 @@ mod tests { fn test_crypto_gcm_decrypt_rejects_tampering() { let key = [0x11u8; 16]; let iv = [0x22u8; 4]; - let cg = CryptoGcm::new(&key, &iv, &key, &iv); + let mut cg = + CryptoGcm::new(crypto::default_provider().unwrap(), &key, &iv, &key, &iv).unwrap(); let (header, raw) = make_record(b"payload"); let mut encrypted = cg.encrypt(&header, &raw).unwrap(); diff --git a/rtc-dtls/src/crypto/crypto_test.rs b/rtc-dtls/src/crypto/crypto_test.rs index ac65d6f5..3bb27d33 100644 --- a/rtc-dtls/src/crypto/crypto_test.rs +++ b/rtc-dtls/src/crypto/crypto_test.rs @@ -1,50 +1,18 @@ -use std::io::Cursor; - -use x509_parser::pem::Pem; - use super::crypto_ccm::*; use super::*; use crate::content::ContentType; use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE}; - -const RAW_PRIVATE_KEY: &str = " ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP -6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3 -bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1 -ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB -ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL -rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0 -N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra -ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3 -AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0 -xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD -8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl -pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22 -HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S -Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87 -mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin -mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71 -hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5 -lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH -BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz -uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ -srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq -eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O -UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr -nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L -JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/ ------END RSA PRIVATE KEY----- -"; +use crate::signature_hash_algorithm::HashAlgorithm; #[test] fn test_generate_key_signature() -> Result<()> { - let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes()); - let pem = match Pem::read(reader) { - Ok((pem, _)) => pem, - Err(_) => return Err(Error::Other("Pem::read error".to_owned())), - }; - //let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?; + let provider = crypto::default_provider().map_err(crypto_error)?; + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + let signing_key = provider + .crypto() + .generate_signing_key(scheme) + .map_err(crypto_error)?; + let private_key = CryptoPrivateKey::from_signing_key(signing_key.clone()); let client_random = vec![ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, @@ -61,45 +29,97 @@ fn test_generate_key_signature() -> Result<()> { 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, 0xb6, 0x15, ]; - let expected_signature = vec![ - 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, - 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, - 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, - 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, - 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, - 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, - 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, - 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, - 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, - 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, - 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, - 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, - 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, - 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, - 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, - 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, - 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, - 0x86, - ]; - let signature = generate_key_signature( &client_random, &server_random, &public_key, NamedCurve::X25519, - &CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_der(&pem.contents) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der: pem.contents.clone(), - }, //hashAlgorithmSHA256, + &SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + &private_key, )?; - assert_eq!( - signature, expected_signature, - "Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} " - ); + provider + .crypto() + .verify_signature( + scheme, + signing_key.public_key(), + &value_key_message( + &client_random, + &server_random, + &public_key, + NamedCurve::X25519, + ), + &signature, + ) + .map_err(crypto_error)?; + + Ok(()) +} + +#[test] +fn test_exported_signing_key_can_be_imported() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + let generated = provider + .crypto() + .generate_signing_key(scheme) + .map_err(crypto_error)?; + let pkcs8 = generated + .to_pkcs8_der() + .map_err(crypto_error)? + .expect("built-in generated keys are exportable"); + let imported = provider + .crypto() + .import_signing_key(scheme, pkcs8.as_ref()) + .map_err(crypto_error)?; + let signature = imported + .sign(scheme, b"imported DTLS key") + .map_err(crypto_error)?; + + provider + .crypto() + .verify_signature( + scheme, + imported.public_key(), + b"imported DTLS key", + &signature, + ) + .map_err(crypto_error) +} + +#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[test] +fn test_cross_provider_signature_verification() -> Result<()> { + let ring = crypto::providers::RingProvider::new(); + let aws = crypto::providers::AwsLcRsProvider::new(); + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + + for (signer, verifier) in [ + ( + ring.crypto() as &dyn crypto::RTCCrypto, + aws.crypto() as &dyn crypto::RTCCrypto, + ), + ( + aws.crypto() as &dyn crypto::RTCCrypto, + ring.crypto() as &dyn crypto::RTCCrypto, + ), + ] { + let key = signer.generate_signing_key(scheme).map_err(crypto_error)?; + let signature = key + .sign(scheme, b"cross-provider DTLS signature") + .map_err(crypto_error)?; + verifier + .verify_signature( + scheme, + key.public_key(), + b"cross-provider DTLS signature", + &signature, + ) + .map_err(crypto_error)?; + } Ok(()) } @@ -112,7 +132,14 @@ fn test_ccm_encryption_and_decryption() -> Result<()> { ]; let iv = vec![0x0e, 0xb2, 0x09, 0x06]; - let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv); + let mut ccm = CryptoCcm::new( + crypto::default_provider().map_err(crypto_error)?, + &CryptoCcmTagLen::CryptoCcmTagLength, + &key, + &iv, + &key, + &iv, + )?; let rlh = RecordLayerHeader { content_type: ContentType::ApplicationData, @@ -155,6 +182,7 @@ fn test_ccm_encryption_and_decryption() -> Result<()> { #[test] fn test_certificate_verify() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; let plain_text: Vec = vec![ 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, @@ -178,14 +206,19 @@ fn test_certificate_verify() -> Result<()> { //test ECDSA256 let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - let cert_verify_ecdsa256 = - generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?; + let ecdsa_algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }; + let cert_verify_ecdsa256 = generate_certificate_verify( + &plain_text, + &ecdsa_algorithm, + &certificate_ecdsa256.private_key, + )?; verify_certificate_verify( + provider.crypto(), &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, + &ecdsa_algorithm, &cert_verify_ecdsa256, &certificate_ecdsa256 .certificate @@ -200,14 +233,19 @@ fn test_certificate_verify() -> Result<()> { vec!["localhost".to_owned()], &rcgen::PKCS_ED25519, )?; - let cert_verify_ed25519 = - generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?; + let ed25519_algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ed25519, + }; + let cert_verify_ed25519 = generate_certificate_verify( + &plain_text, + &ed25519_algorithm, + &certificate_ed25519.private_key, + )?; verify_certificate_verify( + provider.crypto(), &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ed25519, - }, + &ed25519_algorithm, &cert_verify_ed25519, &certificate_ed25519 .certificate @@ -249,14 +287,18 @@ fn test_custom_signer_is_invoked_for_signing() -> Result<()> { let call_count = std::sync::Arc::new(std::sync::Mutex::new(0usize)); let last_message = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let private_key = CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(Box::new(MockSigner { - call_count: std::sync::Arc::clone(&call_count), - last_message: std::sync::Arc::clone(&last_message), - signature: expected_signature.clone(), - })), - serialized_der: vec![], - }; + let private_key = CryptoPrivateKey::from_custom_signer(Box::new(MockSigner { + call_count: std::sync::Arc::clone(&call_count), + last_message: std::sync::Arc::clone(&last_message), + signature: expected_signature.clone(), + })); + assert!( + private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .is_none() + ); let client_random = [0x01u8, 0x02, 0x03, 0x04]; let server_random = [0x05u8, 0x06, 0x07, 0x08]; @@ -264,12 +306,17 @@ fn test_custom_signer_is_invoked_for_signing() -> Result<()> { let named_curve = NamedCurve::X25519; let expected_key_message = value_key_message(&client_random, &server_random, &public_key, named_curve); + let algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }; let key_signature = generate_key_signature( &client_random, &server_random, &public_key, named_curve, + &algorithm, &private_key, )?; @@ -278,7 +325,7 @@ fn test_custom_signer_is_invoked_for_signing() -> Result<()> { assert_eq!(key_signature, expected_signature); let handshake_bodies = b"certificate-verify-handshake-bodies"; - let cert_verify = generate_certificate_verify(handshake_bodies, &private_key)?; + let cert_verify = generate_certificate_verify(handshake_bodies, &algorithm, &private_key)?; assert_eq!(*call_count.lock().unwrap(), 2); assert_eq!(&*last_message.lock().unwrap(), handshake_bodies); diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index 2642e3c5..ded04f38 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -18,8 +18,6 @@ pub mod crypto_ccm; pub mod crypto_chacha20; /// AES-GCM authenticated encryption. pub mod crypto_gcm; -/// Block-cipher padding for the CBC suites. -pub mod padding; use std::convert::TryFrom; use std::sync::Arc; @@ -31,15 +29,33 @@ use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::{CertificateDer, ServerName}; use rustls::server::danger::ClientCertVerifier; +use crypto::{ + PublicKey, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme as CryptoSignatureScheme, + SigningKey, +}; use rcgen::{CertifiedKey, KeyPair, generate_simple_self_signed}; -use ring::rand::SystemRandom; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; use crate::curve::named_curve::*; use crate::record_layer::record_layer_header::*; -use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm}; +use crate::signature_hash_algorithm::{SignatureAlgorithm, SignatureHashAlgorithm}; use shared::error::*; +pub(crate) fn crypto_error(error: crypto::CryptoError) -> Error { + Error::Crypto(error.to_string()) +} + +pub(crate) fn authentication_error(_: crypto::CryptoError) -> Error { + Error::ErrInvalidMac +} + +fn signature_verification_error(error: crypto::CryptoError) -> Error { + match error { + crypto::CryptoError::InvalidSignature => Error::ErrKeySignatureMismatch, + crypto::CryptoError::UnsupportedAlgorithm(_) => Error::ErrKeySignatureVerifyUnimplemented, + error => crypto_error(error), + } +} + /// A X.509 certificate(s) used to authenticate a DTLS connection. #[derive(Clone, PartialEq, Debug)] pub struct Certificate { @@ -54,10 +70,19 @@ impl Certificate { /// /// See [`rcgen::generate_simple_self_signed`]. pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::generate_self_signed_with_provider(subject_alt_names, provider) + } + + /// Generates a self-signed certificate and imports its key into `provider`. + pub fn generate_self_signed_with_provider( + subject_alt_names: impl Into>, + provider: Arc, + ) -> Result { let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names)?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&signing_key)?, + private_key: CryptoPrivateKey::from_key_pair_with_provider(&signing_key, provider)?, }) } @@ -68,19 +93,41 @@ impl Certificate { subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, ) -> Result { - let params = rcgen::CertificateParams::new(subject_alt_names).unwrap(); - let key_pair = rcgen::KeyPair::generate_for(alg).unwrap(); - let cert = params.self_signed(&key_pair).unwrap(); + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::generate_self_signed_with_alg_and_provider(subject_alt_names, alg, provider) + } + + /// Generates a self-signed certificate with `alg` and imports its key into `provider`. + pub fn generate_self_signed_with_alg_and_provider( + subject_alt_names: impl Into>, + alg: &'static rcgen::SignatureAlgorithm, + provider: Arc, + ) -> Result { + let params = rcgen::CertificateParams::new(subject_alt_names) + .map_err(|error| Error::Other(error.to_string()))?; + let key_pair = + rcgen::KeyPair::generate_for(alg).map_err(|error| Error::Other(error.to_string()))?; + let cert = params + .self_signed(&key_pair) + .map_err(|error| Error::Other(error.to_string()))?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&key_pair)?, + private_key: CryptoPrivateKey::from_key_pair_with_provider(&key_pair, provider)?, }) } /// Parses a certificate from the ASCII PEM format. - #[cfg(feature = "pem")] pub fn from_pem(pem_str: &str) -> Result { + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::from_pem_with_provider(pem_str, provider) + } + + /// Parses a PEM certificate and imports its PKCS#8 key into `provider`. + pub fn from_pem_with_provider( + pem_str: &str, + provider: Arc, + ) -> Result { let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; if pems.len() < 2 { return Err(Error::InvalidPEM(format!( @@ -111,16 +158,21 @@ impl Certificate { Ok(Certificate { certificate: rustls_certs, - private_key: CryptoPrivateKey::try_from(&keypair)?, + private_key: CryptoPrivateKey::from_key_pair_with_provider(&keypair, provider)?, }) } /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. - #[cfg(feature = "pem")] - pub fn serialize_pem(&self) -> String { + pub fn serialize_pem(&self) -> Result { + let private_key = self + .private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .ok_or_else(|| Error::Other("the certificate signing key is not exportable".into()))?; let mut data = vec![pem::Pem::new( "PRIVATE_KEY".to_string(), - self.private_key.serialized_der.clone(), + private_key.as_ref(), )]; for rustls_cert in &self.certificate { data.push(pem::Pem::new( @@ -128,7 +180,18 @@ impl Certificate { rustls_cert.as_ref(), )); } - pem::encode_many(&data) + Ok(pem::encode_many(&data)) + } + + /// Builds a certificate chain around an application-owned signing key, including HSM/KMS keys. + pub fn from_signing_key( + certificate: Vec>, + signing_key: Arc, + ) -> Self { + Self { + certificate, + private_key: CryptoPrivateKey::from_signing_key(signing_key), + } } } @@ -165,88 +228,32 @@ pub trait CustomSigner: Send + Sync + std::fmt::Debug { fn clone_box(&self) -> Box; } -/// Either ED25519, ECDSA, RSA keypair, or a custom external signer. -#[derive(Debug)] -#[non_exhaustive] -pub enum CryptoPrivateKeyKind { - /// An Ed25519 key pair. - Ed25519(Ed25519KeyPair), - /// An ECDSA key pair over NIST P-256. - Ecdsa256(EcdsaKeyPair), - /// An RSA key pair used with SHA-256. - Rsa256(ring::rsa::KeyPair), - /// Delegate signing to an external provider. The signer receives the raw - /// message bytes and must return a signature in the format expected by the - /// negotiated signature algorithm (e.g., ASN.1 DER for ECDSA). - Custom(Box), -} - -/// Private key. -#[derive(Debug)] +/// Provider-neutral DTLS signing key. +#[derive(Clone)] pub struct CryptoPrivateKey { - /// Keypair. - pub kind: CryptoPrivateKeyKind, - /// DER-encoded keypair. + /// Provider-owned signing key. It may be non-exportable. + pub signing_key: Arc, + /// DER-encoded keypair retained by the temporary rcgen compatibility adapter. pub serialized_der: Vec, } impl PartialEq for CryptoPrivateKey { fn eq(&self, other: &Self) -> bool { - if self.serialized_der != other.serialized_der { - return false; - } - - matches!( - (&self.kind, &other.kind), - ( - CryptoPrivateKeyKind::Rsa256(_), - CryptoPrivateKeyKind::Rsa256(_) - ) | ( - CryptoPrivateKeyKind::Ecdsa256(_), - CryptoPrivateKeyKind::Ecdsa256(_) - ) | ( - CryptoPrivateKeyKind::Ed25519(_), - CryptoPrivateKeyKind::Ed25519(_) - ) | ( - CryptoPrivateKeyKind::Custom(_), - CryptoPrivateKeyKind::Custom(_) - ) - ) + let left = self.signing_key.public_key(); + let right = other.signing_key.public_key(); + left.encoding == right.encoding && left.bytes == right.bytes } } -impl Clone for CryptoPrivateKey { - fn clone(&self) -> Self { - match self.kind { - CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &self.serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Custom(ref signer) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(signer.clone_box()), - serialized_der: self.serialized_der.clone(), - }, - } +impl std::fmt::Debug for CryptoPrivateKey { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let public_key = self.signing_key.public_key(); + formatter + .debug_struct("CryptoPrivateKey") + .field("public_key_encoding", &public_key.encoding) + .field("public_key_len", &public_key.bytes.len()) + .field("exportable", &(!self.serialized_der.is_empty())) + .finish() } } @@ -265,40 +272,74 @@ impl CryptoPrivateKey { /// /// Fails if the key type has no supported scheme. pub fn from_key_pair(key_pair: &KeyPair) -> Result { + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::from_key_pair_with_provider(key_pair, provider) + } + + /// Imports an rcgen key pair into an explicit provider. + pub fn from_key_pair_with_provider( + key_pair: &KeyPair, + provider: Arc, + ) -> Result { let serialized_der = key_pair.serialize_der(); - if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8_maybe_unchecked(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + let scheme = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { + CryptoSignatureScheme::Ed25519 } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + CryptoSignatureScheme::EcdsaP256Sha256 } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + CryptoSignatureScheme::RsaPkcs1Sha256 } else { - Err(Error::Other("Unsupported key_pair".to_owned())) + return Err(Error::Other("Unsupported key_pair".to_owned())); + }; + let signing_key = provider + .crypto() + .import_signing_key(scheme, &serialized_der) + .map_err(crypto_error)?; + Ok(Self { + signing_key, + serialized_der, + }) + } + + /// Wraps a provider-neutral, potentially non-exportable signing key. + pub fn from_signing_key(signing_key: Arc) -> Self { + Self { + signing_key, + serialized_der: Vec::new(), } } + + /// Adapts the legacy external signer API until that API is removed before 1.0. + pub fn from_custom_signer(signer: Box) -> Self { + Self::from_signing_key(Arc::new(CustomSigningKey { signer })) + } +} + +struct CustomSigningKey { + signer: Box, +} + +impl SigningKey for CustomSigningKey { + fn supports(&self, _scheme: CryptoSignatureScheme) -> bool { + true + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], + } + } + + fn sign( + &self, + _scheme: CryptoSignatureScheme, + message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { + self.signer + .sign(message) + .map_err(crypto::CryptoError::Provider) + } } // If the client provided a "signature_algorithms" extension, then all @@ -311,38 +352,14 @@ pub(crate) fn generate_key_signature( server_random: &[u8], public_key: &[u8], named_curve: NamedCurve, - private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/ + algorithm: &SignatureHashAlgorithm, + private_key: &CryptoPrivateKey, ) -> Result> { let msg = value_key_message(client_random, server_random, public_key, named_curve); - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, &msg) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - #[cfg(feature = "ring")] - let mut signature = vec![0; kp.public().modulus_len()]; - #[cfg(feature = "aws-lc-rs")] - let mut signature = vec![0; kp.public_modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - &msg, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - CryptoPrivateKeyKind::Custom(signer) => signer.sign(&msg).map_err(Error::Other)?, - }; - - Ok(signature) + private_key + .signing_key + .sign(algorithm.crypto_scheme()?, &msg) + .map_err(crypto_error) } // add OID_ED25519 which is not defined in x509_parser @@ -352,6 +369,7 @@ pub const OID_ED25519: Oid<'static> = oid!(1.3.101.112); pub const OID_ECDSA: Oid<'static> = oid!(1.2.840.10045.2.1); fn verify_signature( + crypto: &dyn crypto::RTCCrypto, message: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -365,56 +383,37 @@ fn verify_signature( let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0]) .map_err(|e| Error::Other(e.to_string()))?; - let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature { - SignatureAlgorithm::Ed25519 => &ring::signature::ED25519, - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => { - &ring::signature::ECDSA_P256_SHA256_ASN1 - } - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::ECDSA_P384_SHA384_ASN1 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => { - &ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY - } - SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA256 - } - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::RSA_PKCS1_2048_8192_SHA384 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA512 - } - } - _ => return Err(Error::ErrKeySignatureVerifyUnimplemented), + let encoding = match hash_algorithm.signature { + SignatureAlgorithm::Ed25519 => PublicKeyEncoding::Ed25519Raw, + SignatureAlgorithm::Ecdsa => PublicKeyEncoding::EcUncompressedPoint, + SignatureAlgorithm::Rsa => PublicKeyEncoding::RsaPkcs1Der, + SignatureAlgorithm::Unsupported => return Err(Error::ErrKeySignatureVerifyUnimplemented), }; - - log::trace!("Picked an algorithm {verify_alg:?}"); - - let public_key = ring::signature::UnparsedPublicKey::new( - verify_alg, - certificate - .tbs_certificate - .subject_pki - .subject_public_key - .data, - ); - - public_key - .verify(message, remote_key_signature) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) + if hash_algorithm.signature == SignatureAlgorithm::Rsa + && remote_key_signature.len() < 256 + && !insecure_verification + { + return Err(Error::ErrKeySignatureMismatch); + } + crypto + .verify_signature( + hash_algorithm.crypto_scheme()?, + PublicKey { + encoding, + bytes: &certificate + .tbs_certificate + .subject_pki + .subject_public_key + .data, + }, + message, + remote_key_signature, + ) + .map_err(signature_verification_error) } pub(crate) fn verify_key_signature( + crypto: &dyn crypto::RTCCrypto, message: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -422,6 +421,7 @@ pub(crate) fn verify_key_signature( insecure_verification: bool, ) -> Result<()> { verify_signature( + crypto, message, hash_algorithm, remote_key_signature, @@ -440,42 +440,17 @@ pub(crate) fn verify_key_signature( // https://tools.ietf.org/html/rfc5246#section-7.3 pub(crate) fn generate_certificate_verify( handshake_bodies: &[u8], - private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/ + algorithm: &SignatureHashAlgorithm, + private_key: &CryptoPrivateKey, ) -> Result> { - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, handshake_bodies) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - #[cfg(feature = "ring")] - let mut signature = vec![0; kp.public().modulus_len()]; - #[cfg(feature = "aws-lc-rs")] - let mut signature = vec![0; kp.public_modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - handshake_bodies, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - CryptoPrivateKeyKind::Custom(signer) => { - signer.sign(handshake_bodies).map_err(Error::Other)? - } - }; - - Ok(signature) + private_key + .signing_key + .sign(algorithm.crypto_scheme()?, handshake_bodies) + .map_err(crypto_error) } pub(crate) fn verify_certificate_verify( + crypto: &dyn crypto::RTCCrypto, handshake_bodies: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -483,6 +458,7 @@ pub(crate) fn verify_certificate_verify( insecure_verification: bool, ) -> Result<()> { verify_signature( + crypto, handshake_bodies, hash_algorithm, remote_key_signature, @@ -571,15 +547,13 @@ pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: #[cfg(test)] mod test { - #[cfg(feature = "pem")] use super::*; - #[cfg(feature = "pem")] #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?; - let pem = cert.serialize_pem(); + let pem = cert.serialize_pem()?; let loaded_cert = Certificate::from_pem(&pem)?; assert_eq!(loaded_cert, cert); diff --git a/rtc-dtls/src/crypto/padding.rs b/rtc-dtls/src/crypto/padding.rs deleted file mode 100644 index 0f1337ea..00000000 --- a/rtc-dtls/src/crypto/padding.rs +++ /dev/null @@ -1,125 +0,0 @@ -use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError}; -use core::panic; - -/// DTLS block-cipher padding, as a marker type for the padding scheme. -/// -/// Has no values — it exists to parameterize the CBC cipher over its padding. -pub enum DtlsPadding {} -/// Reference: RFC5246, 6.2.3.2 -impl RawPadding for DtlsPadding { - const TYPE: PadType = PadType::Reversible; - - fn raw_pad(block: &mut [u8], pos: usize) { - if pos >= block.len() { - panic!("`pos` is bigger or equal to block size"); - } - - let padding_length = block.len() - pos - 1; - if padding_length > 255 { - panic!("block size is too big for DTLS"); - } - - set(&mut block[pos..], padding_length as u8); - } - - fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> { - let padding_length = data.last().copied().unwrap_or(1) as usize; - if padding_length + 1 > data.len() { - return Err(UnpadError); - } - - let padding_begin = data.len() - padding_length - 1; - - if data[padding_begin..data.len() - 1] - .iter() - .any(|&byte| byte as usize != padding_length) - { - return Err(UnpadError); - } - - Ok(&data[0..padding_begin]) - } -} - -/// Sets all bytes in `dst` equal to `value` -#[inline(always)] -fn set(dst: &mut [u8], value: u8) { - // SAFETY: we overwrite valid memory behind `dst` - // note: loop is not used here because it produces - // unnecessary branch which tests for zero-length slices - unsafe { - core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len()); - } -} - -#[cfg(test)] -mod tests { - use rand::RngExt; - - use super::*; - - #[test] - fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> { - for original_length in 0..128 { - for padding_length in 0..(256 - original_length) { - let mut block = vec![0; original_length + padding_length + 1]; - rand::rng().fill(&mut block[0..original_length]); - let original = block[0..original_length].to_vec(); - DtlsPadding::raw_pad(&mut block, original_length); - - for byte in block[original_length..].iter() { - assert_eq!(*byte as usize, padding_length); - } - assert_eq!(block[0..original_length], original); - } - } - - Ok(()) - } - - #[test] - #[should_panic] - fn full_block_is_padding_error() { - for original_length in 0..256 { - let mut block = vec![0; original_length]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - #[should_panic] - fn padding_length_bigger_than_255_is_a_pad_error() { - let padding_length = 256; - for original_length in 0..128 { - let mut block = vec![0; original_length + padding_length + 1]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - fn empty_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[]); - assert!(r.is_err()); - } - - #[test] - fn padding_too_big_for_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[1]); - assert!(r.is_err()); - } - - #[test] - fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() { - for padding_length in 0..16 { - for invalid_byte in 0..padding_length { - let mut block = vec![0; padding_length + 1]; - DtlsPadding::raw_pad(&mut block, 0); - - assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..])); - block[invalid_byte] = (padding_length - 1) as u8; - let r = DtlsPadding::raw_unpad(&block); - assert!(r.is_err()); - } - } - } -} diff --git a/rtc-dtls/src/curve/named_curve.rs b/rtc-dtls/src/curve/named_curve.rs index 322476e6..dc3f0e2e 100644 --- a/rtc-dtls/src/curve/named_curve.rs +++ b/rtc-dtls/src/curve/named_curve.rs @@ -1,5 +1,4 @@ -use rand_core::OsRng; // requires 'getrandom' feature - +use crypto::{ActiveKeyExchange, KeyExchangeAlgorithm, RTCCrypto}; use shared::error::*; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 @@ -29,67 +28,59 @@ impl From for NamedCurve { } } -pub(crate) enum NamedCurvePrivateKey { - EphemeralSecretP256(p256::ecdh::EphemeralSecret), - EphemeralSecretP384(p384::ecdh::EphemeralSecret), - StaticSecretX25519(x25519_dalek::StaticSecret), -} - /// An ephemeral ECDHE key pair, with the curve it belongs to. pub struct NamedCurveKeypair { pub(crate) curve: NamedCurve, pub(crate) public_key: Vec, - pub(crate) private_key: NamedCurvePrivateKey, + active: Option>, } -fn elliptic_curve_keypair(curve: NamedCurve) -> Result { - let (public_key, private_key) = match curve { - NamedCurve::P256 => { - let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p256::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP256(secret_key), - ) - } - NamedCurve::P384 => { - let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p384::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP384(secret_key), - ) - } - NamedCurve::X25519 => { - let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng); - let public_key = x25519_dalek::PublicKey::from(&secret_key); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::StaticSecretX25519(secret_key), - ) - } - _ => return Err(Error::ErrInvalidNamedCurve), - }; - - Ok(NamedCurveKeypair { - curve, - public_key, - private_key, - }) +impl NamedCurveKeypair { + pub(crate) fn complete(&mut self, peer_public_key: &[u8]) -> Result> { + let active = self + .active + .take() + .ok_or(Error::ErrNamedCurveAndPrivateKeyMismatch)?; + active + .complete(peer_public_key) + .map(|secret| secret.into_bytes()) + .map_err(|error| Error::Crypto(error.to_string())) + } } impl NamedCurve { + pub(crate) const fn crypto_algorithm(self) -> Result { + match self { + Self::P256 => Ok(KeyExchangeAlgorithm::P256), + Self::P384 => Ok(KeyExchangeAlgorithm::P384), + Self::X25519 => Ok(KeyExchangeAlgorithm::X25519), + Self::Unsupported => Err(Error::ErrInvalidNamedCurve), + } + } + + pub(crate) fn generate_keypair_with_crypto( + self, + crypto: &dyn RTCCrypto, + ) -> Result { + let active = crypto + .start_key_exchange(self.crypto_algorithm()?) + .map_err(|error| Error::Crypto(error.to_string()))?; + let public_key = active.public_key().to_vec(); + Ok(NamedCurveKeypair { + curve: self, + public_key, + active: Some(active), + }) + } + /// Generates an ephemeral key pair on this curve. /// /// # Errors /// /// Fails if the curve is unsupported or key generation fails. pub fn generate_keypair(&self) -> Result { - match *self { - NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519), - NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256), - NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384), - _ => Err(Error::ErrInvalidNamedCurve), - } + let provider = + crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + self.generate_keypair_with_crypto(provider.crypto()) } } diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index a6b52adc..cfe8ede4 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -291,3 +291,205 @@ impl Endpoint { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::cipher_suite::CipherSuiteId; + use crate::config::ConfigBuilder; + use crate::crypto::Certificate; + use crypto::{CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; + + fn client_addr() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 4444)) + } + + fn server_addr() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 4445)) + } + + struct FailingRandom; + + impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } + } + + struct FailingRandomProvider { + crypto: Arc, + } + + impl RTCCryptoProvider for FailingRandomProvider { + fn name(&self) -> &'static str { + "failing-random" + } + + fn crypto(&self) -> &dyn RTCCrypto { + self.crypto.crypto() + } + + fn random(&self) -> &dyn RTCRandom { + &FailingRandom + } + } + + fn config( + provider: Arc, + is_client: bool, + suite: CipherSuiteId, + ) -> Result> { + let mut builder = ConfigBuilder::default() + .with_crypto_provider(provider.clone()) + .with_cipher_suites(vec![suite]) + .with_insecure_skip_verify(true); + let is_psk = matches!( + suite, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm + | CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 + | CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 + ); + if is_psk { + builder = builder.with_psk(Some(Arc::new(|_| Ok(vec![0xab, 0xcd, 0xef])))); + if is_client { + builder = builder.with_psk_identity_hint(Some(b"rtc-dtls-test".to_vec())); + } + } else if !is_client { + builder = + builder.with_certificates(vec![Certificate::generate_self_signed_with_provider( + vec!["localhost".to_owned()], + provider, + )?]); + } + Ok(Arc::new(builder.build(is_client, None)?)) + } + + fn transfer( + source: &mut Endpoint, + destination: &mut Endpoint, + source_addr: SocketAddr, + ) -> Result> { + let mut events = Vec::new(); + while let Some(transmit) = source.poll_transmit() { + events.extend(destination.read( + Instant::now(), + source_addr, + transmit.transport.ecn, + transmit.message, + )?); + } + Ok(events) + } + + fn handshake_and_exchange( + client_provider: Arc, + server_provider: Arc, + suite: CipherSuiteId, + ) -> Result<()> { + let client_config = config(client_provider, true, suite)?; + let server_config = config(server_provider, false, suite)?; + let mut client = Endpoint::new(client_addr(), TransportProtocol::UDP, None); + let mut server = Endpoint::new(server_addr(), TransportProtocol::UDP, Some(server_config)); + client.connect(server_addr(), client_config, None)?; + + let mut client_complete = false; + let mut server_complete = false; + for _ in 0..32 { + for event in transfer(&mut client, &mut server, client_addr())? { + server_complete |= matches!(event, EndpointEvent::HandshakeComplete); + } + for event in transfer(&mut server, &mut client, server_addr())? { + client_complete |= matches!(event, EndpointEvent::HandshakeComplete); + } + if client_complete && server_complete { + break; + } + } + assert!( + client_complete && server_complete, + "DTLS handshake did not complete" + ); + + client.write(server_addr(), b"provider-backed DTLS")?; + let transmit = client + .poll_transmit() + .expect("application write produces a DTLS record"); + let replay = transmit.message.clone(); + let events = server.read( + Instant::now(), + client_addr(), + transmit.transport.ecn, + transmit.message, + )?; + assert!(events.into_iter().any(|event| matches!( + event, + EndpointEvent::ApplicationData(data) if data.as_ref() == b"provider-backed DTLS" + ))); + assert!( + server + .read(Instant::now(), client_addr(), None, replay)? + .is_empty() + ); + Ok(()) + } + + #[cfg(feature = "ring")] + #[test] + fn ring_provider_completes_handshake_and_record_exchange() -> Result<()> { + let provider: Arc = Arc::new(crypto::providers::RingProvider::new()); + handshake_and_exchange( + provider.clone(), + provider, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ) + } + + #[cfg(feature = "aws-lc-rs")] + #[test] + fn aws_lc_rs_provider_completes_handshake_and_record_exchange() -> Result<()> { + let provider: Arc = + Arc::new(crypto::providers::AwsLcRsProvider::new()); + handshake_and_exchange( + provider.clone(), + provider, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ) + } + + #[cfg(all(feature = "ring", feature = "aws-lc-rs"))] + #[test] + fn ring_and_aws_lc_rs_complete_cross_provider_handshakes() -> Result<()> { + let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); + let aws: Arc = Arc::new(crypto::providers::AwsLcRsProvider::new()); + for suite in [ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, + ] { + handshake_and_exchange(ring.clone(), aws.clone(), suite)?; + handshake_and_exchange(aws.clone(), ring.clone(), suite)?; + } + Ok(()) + } + + #[test] + fn failing_random_provider_aborts_client_hello_cleanly() -> Result<()> { + let base = crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + let provider: Arc = Arc::new(FailingRandomProvider { crypto: base }); + let config = config( + provider, + true, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + )?; + let mut endpoint = Endpoint::new(client_addr(), TransportProtocol::UDP, None); + + let result = endpoint.connect(server_addr(), config, None); + assert!(matches!(result, Err(Error::Crypto(_)))); + Ok(()) + } +} diff --git a/rtc-dtls/src/flight/flight0.rs b/rtc-dtls/src/flight/flight0.rs index 7f8362d3..24c9d7ff 100644 --- a/rtc-dtls/src/flight/flight0.rs +++ b/rtc-dtls/src/flight/flight0.rs @@ -2,7 +2,6 @@ use super::flight2::*; use super::*; use crate::config::*; use crate::conn::*; -use crate::curve::named_curve::NamedCurve; use crate::extension::*; use crate::handshake::*; use crate::record_layer::record_layer_header::*; @@ -10,7 +9,6 @@ use crate::*; use shared::error::Error; use log::debug; -use rand::RngExt; use std::fmt; #[derive(Debug, PartialEq)] @@ -106,7 +104,7 @@ impl Flight for Flight0 { )); } for curve in e.elliptic_curves.iter() { - if curve != &NamedCurve::Unsupported { + if cfg.local_named_curves.contains(curve) { state.named_curve = *curve; break; } @@ -153,7 +151,10 @@ impl Flight for Flight0 { } if state.local_keypair.is_none() { - state.local_keypair = match state.named_curve.generate_keypair() { + state.local_keypair = match state + .named_curve + .generate_keypair_with_crypto(cfg.provider().crypto()) + { Ok(local_keypar) => Some(local_keypar), Err(err) => { return Err(( @@ -183,17 +184,24 @@ impl Flight for Flight0 { &self, state: &mut State, _cache: &HandshakeCache, - _cfg: &HandshakeConfig, + cfg: &HandshakeConfig, ) -> Result, (Option, Option)> { // Initialize state.cookie = vec![0; COOKIE_LENGTH]; - rand::rng().fill(state.cookie.as_mut_slice()); + if let Err(error) = cfg.provider().random().fill(state.cookie.as_mut_slice()) { + return Err((None, Some(Error::Crypto(error.to_string())))); + } state.local_epoch = 0; state.remote_epoch = 0; state.named_curve = DEFAULT_NAMED_CURVE; - state.local_random.populate(); + if cfg.local_psk_callback.is_none() { + state.named_curve = cfg.local_named_curves[0]; + } + if let Err(error) = state.local_random.populate(cfg.provider().random()) { + return Err((None, Some(error))); + } Ok(vec![]) } diff --git a/rtc-dtls/src/flight/flight1.rs b/rtc-dtls/src/flight/flight1.rs index 786c8e62..85333be6 100644 --- a/rtc-dtls/src/flight/flight1.rs +++ b/rtc-dtls/src/flight/flight1.rs @@ -4,7 +4,6 @@ use crate::compression_methods::*; use crate::config::*; use crate::conn::*; use crate::content::*; -use crate::curve::named_curve::*; use crate::extension::extension_server_name::*; use crate::extension::extension_supported_elliptic_curves::*; use crate::extension::extension_supported_point_formats::*; @@ -120,7 +119,9 @@ impl Flight for Flight1 { state.named_curve = DEFAULT_NAMED_CURVE; state.cookie = vec![]; - state.local_random.populate(); + if let Err(error) = state.local_random.populate(cfg.provider().random()) { + return Err((None, Some(error))); + } let mut extensions = vec![ Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { @@ -134,7 +135,7 @@ impl Flight for Flight1 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], diff --git a/rtc-dtls/src/flight/flight3.rs b/rtc-dtls/src/flight/flight3.rs index 27fc918d..83e89d45 100644 --- a/rtc-dtls/src/flight/flight3.rs +++ b/rtc-dtls/src/flight/flight3.rs @@ -3,7 +3,6 @@ use super::*; use crate::compression_methods::*; use crate::config::*; use crate::content::*; -use crate::curve::named_curve::*; use crate::extension::extension_server_name::*; use crate::extension::extension_supported_elliptic_curves::*; use crate::extension::extension_supported_point_formats::*; @@ -350,7 +349,7 @@ impl Flight for Flight3 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], @@ -422,7 +421,10 @@ pub(crate) fn handle_server_key_exchange( state.identity_hint.clone_from(&h.identity_hint); state.pre_master_secret = prf_psk_pre_master_secret(&psk); } else { - let local_keypair = match h.named_curve.generate_keypair() { + let mut local_keypair = match h + .named_curve + .generate_keypair_with_crypto(cfg.provider().crypto()) + { Ok(local_keypair) => local_keypair, Err(err) => { return Err(( @@ -435,22 +437,20 @@ pub(crate) fn handle_server_key_exchange( } }; - state.pre_master_secret = match prf_pre_master_secret( - &h.public_key, - &local_keypair.private_key, - local_keypair.curve, - ) { - Ok(pre_master_secret) => pre_master_secret, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - }; + let curve = local_keypair.curve; + state.pre_master_secret = + match prf_pre_master_secret(&h.public_key, &mut local_keypair, curve) { + Ok(pre_master_secret) => pre_master_secret, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + }; state.local_keypair = Some(local_keypair); } diff --git a/rtc-dtls/src/flight/flight4.rs b/rtc-dtls/src/flight/flight4.rs index edf22c09..858317ec 100644 --- a/rtc-dtls/src/flight/flight4.rs +++ b/rtc-dtls/src/flight/flight4.rs @@ -30,6 +30,8 @@ use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; use log::*; use std::fmt; use std::io::BufWriter; +#[cfg(test)] +use std::sync::Arc; #[derive(Debug, PartialEq)] pub(crate) struct Flight4; @@ -204,6 +206,7 @@ impl Flight for Flight4 { } if let Err(err) = verify_certificate_verify( + cfg.provider().crypto(), &plain_text, &h.algorithm, &h.signature, @@ -305,11 +308,12 @@ impl Flight for Flight4 { .identity_hint .clone_from(&client_key_exchange.identity_hint); pre_master_secret = prf_psk_pre_master_secret(&psk); - } else if let Some(local_keypair) = &state.local_keypair { + } else if let Some(local_keypair) = &mut state.local_keypair { + let curve = local_keypair.curve; pre_master_secret = match prf_pre_master_secret( &client_key_exchange.public_key, - &local_keypair.private_key, - local_keypair.curve, + local_keypair, + curve, ) { Ok(pre_master_secret) => pre_master_secret, Err(err) => { @@ -326,7 +330,12 @@ impl Flight for Flight4 { if state.extended_master_secret { let hf = cipher_suite_hash_func; - let session_hash = match cache.session_hash(hf, cfg.initial_epoch, &[]) { + let session_hash = match cache.session_hash( + cfg.provider().crypto(), + hf, + cfg.initial_epoch, + &[], + ) { Ok(s) => s, Err(err) => { return Err(( @@ -340,6 +349,7 @@ impl Flight for Flight4 { }; state.master_secret = match prf_extended_master_secret( + cfg.provider().crypto(), &pre_master_secret, &session_hash, cipher_suite_hash_func, @@ -357,6 +367,7 @@ impl Flight for Flight4 { }; } else { state.master_secret = match prf_master_secret( + cfg.provider().crypto(), &pre_master_secret, &client_random, &server_random, @@ -377,6 +388,7 @@ impl Flight for Flight4 { if let Some(cipher_suite) = &mut state.cipher_suite && let Err(err) = cipher_suite.init( + cfg.provider().clone(), &state.master_secret, &client_random, &server_random, @@ -520,7 +532,7 @@ impl Flight for Flight4 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], @@ -618,7 +630,8 @@ impl Flight for Flight4 { &server_random, &local_keypair.public_key, state.named_curve, - &certificate.private_key, /*, signature_hash_algo.hash*/ + &signature_hash_algo, + &certificate.private_key, ) { Ok(s) => s, Err(err) => { @@ -751,6 +764,7 @@ mod tests { // Generate the internal encryption state fn init( &mut self, + _provider: Arc, _master_secret: &[u8], _client_random: &[u8], _server_random: &[u8], @@ -759,10 +773,10 @@ mod tests { unimplemented!(); } - fn encrypt(&self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { + fn encrypt(&mut self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { unimplemented!(); } - fn decrypt(&self, _input: &[u8]) -> Result> { + fn decrypt(&mut self, _input: &[u8]) -> Result> { unimplemented!(); } } diff --git a/rtc-dtls/src/flight/flight5.rs b/rtc-dtls/src/flight/flight5.rs index c738cd28..fbdf8170 100644 --- a/rtc-dtls/src/flight/flight5.rs +++ b/rtc-dtls/src/flight/flight5.rs @@ -134,6 +134,7 @@ impl Flight for Flight5 { { if let Some(cipher_suite) = &state.cipher_suite { let expected_verify_data = match prf_verify_data_server( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), @@ -414,7 +415,8 @@ impl Flight for Flight5 { let cert_verify = match generate_certificate_verify( &plain_text, - &certificate.as_ref().unwrap().private_key, /*, signature_hash_algo.hash*/ + &signature_hash_algo, + &certificate.as_ref().unwrap().private_key, ) { Ok(cert) => cert, Err(err) => { @@ -556,6 +558,7 @@ impl Flight for Flight5 { if let Some(cipher_suite) = &state.cipher_suite { state.local_verify_data = match prf_verify_data_client( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), @@ -624,6 +627,7 @@ fn initalize_cipher_suite( if let Some((_, cipher_suite_hash_func)) = cipher_suite { if state.extended_master_secret { let session_hash = match cache.session_hash( + cfg.provider().crypto(), cipher_suite_hash_func, cfg.initial_epoch, sending_plain_text, @@ -641,6 +645,7 @@ fn initalize_cipher_suite( }; state.master_secret = match prf_extended_master_secret( + cfg.provider().crypto(), &state.pre_master_secret, &session_hash, cipher_suite_hash_func, @@ -658,6 +663,7 @@ fn initalize_cipher_suite( }; } else { state.master_secret = match prf_master_secret( + cfg.provider().crypto(), &state.pre_master_secret, &client_random, &server_random, @@ -699,6 +705,7 @@ fn initalize_cipher_suite( let expected_msg = value_key_message(&client_random, &server_random, &h.public_key, h.named_curve); if let Err(err) = verify_key_signature( + cfg.provider().crypto(), &expected_msg, &h.algorithm, &h.signature, @@ -747,8 +754,13 @@ fn initalize_cipher_suite( } if let Some(cipher_suite) = &mut state.cipher_suite - && let Err(err) = - cipher_suite.init(&state.master_secret, &client_random, &server_random, true) + && let Err(err) = cipher_suite.init( + cfg.provider().clone(), + &state.master_secret, + &client_random, + &server_random, + true, + ) { return Err(( Some(Alert { diff --git a/rtc-dtls/src/flight/flight6.rs b/rtc-dtls/src/flight/flight6.rs index c4bc65ec..f7efa2d8 100644 --- a/rtc-dtls/src/flight/flight6.rs +++ b/rtc-dtls/src/flight/flight6.rs @@ -145,6 +145,7 @@ impl Flight for Flight6 { if let Some(cipher_suite) = &state.cipher_suite { state.local_verify_data = match prf_verify_data_server( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), diff --git a/rtc-dtls/src/handshake/handshake_cache.rs b/rtc-dtls/src/handshake/handshake_cache.rs index b38636a3..18d83060 100644 --- a/rtc-dtls/src/handshake/handshake_cache.rs +++ b/rtc-dtls/src/handshake/handshake_cache.rs @@ -7,8 +7,6 @@ use crate::handshake::*; use std::collections::HashMap; use std::io::BufReader; -use sha2::{Digest, Sha256}; - #[derive(Clone, Debug)] pub(crate) struct HandshakeCacheItem { typ: HandshakeType, @@ -154,6 +152,7 @@ impl HandshakeCache { // https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4 pub(crate) fn session_hash( &self, + crypto: &dyn crypto::RTCCrypto, hf: CipherSuiteHash, epoch: u16, additional: &[u8], @@ -218,12 +217,10 @@ impl HandshakeCache { merged.extend_from_slice(additional); - let mut hasher = match hf { - CipherSuiteHash::Sha256 => Sha256::new(), - }; - hasher.update(&merged); - let result = hasher.finalize(); - - Ok(result.as_slice().to_vec()) + match hf { + CipherSuiteHash::Sha256 => crypto + .hash(crypto::HashAlgorithm::Sha256, &merged) + .map_err(|error| Error::Crypto(error.to_string())), + } } } diff --git a/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs b/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs index 24a67608..c8667364 100644 --- a/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs +++ b/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs @@ -644,7 +644,8 @@ fn test_handshake_cache_session_hash() -> Result<()> { h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client); } - let verify_data = h.session_hash(CipherSuiteHash::Sha256, 0, &[])?; + let provider = crypto::default_provider().map_err(|e| Error::Crypto(e.to_string()))?; + let verify_data = h.session_hash(provider.crypto(), CipherSuiteHash::Sha256, 0, &[])?; assert_eq!( verify_data, expected, diff --git a/rtc-dtls/src/handshake/handshake_random.rs b/rtc-dtls/src/handshake/handshake_random.rs index 6ea22848..bb2bd927 100644 --- a/rtc-dtls/src/handshake/handshake_random.rs +++ b/rtc-dtls/src/handshake/handshake_random.rs @@ -1,5 +1,3 @@ -use rand::RngExt; - use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{self, Read, Write}; use std::time::{Duration, SystemTime}; @@ -80,8 +78,11 @@ impl HandshakeRandom { // populate fills the HandshakeRandom with random values // may be called multiple times /// Fills in the current time and fresh random bytes. - pub fn populate(&mut self) { + pub fn populate(&mut self, random: &dyn crypto::RTCRandom) -> shared::error::Result<()> { self.gmt_unix_time = SystemTime::now(); - rand::rng().fill(&mut self.random_bytes); + random + .fill(&mut self.random_bytes) + .map_err(|error| shared::error::Error::Crypto(error.to_string()))?; + Ok(()) } } diff --git a/rtc-dtls/src/lib.rs b/rtc-dtls/src/lib.rs index da906ace..e613c2b1 100644 --- a/rtc-dtls/src/lib.rs +++ b/rtc-dtls/src/lib.rs @@ -99,13 +99,6 @@ pub mod state; use cipher_suite::*; use extension::extension_use_srtp::SrtpProtectionProfile; -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; - pub(crate) fn find_matching_srtp_profile( a: &[SrtpProtectionProfile], b: &[SrtpProtectionProfile], diff --git a/rtc-dtls/src/prf/mod.rs b/rtc-dtls/src/prf/mod.rs index aebe02e2..52a39994 100644 --- a/rtc-dtls/src/prf/mod.rs +++ b/rtc-dtls/src/prf/mod.rs @@ -1,16 +1,9 @@ #[cfg(test)] mod prf_test; -use std::convert::TryInto; use std::fmt; -use hmac::{Hmac, Mac}; -use sha1::Sha1; -use sha2::Digest; -use sha2::Sha256; - -type HmacSha256 = Hmac; -type HmacSha1 = Hmac; +use crypto::{HashAlgorithm as CryptoHashAlgorithm, HmacAlgorithm, RTCCrypto}; use crate::cipher_suite::CipherSuiteHash; use crate::content::ContentType; @@ -71,50 +64,13 @@ pub(crate) fn prf_psk_pre_master_secret(psk: &[u8]) -> Vec { pub(crate) fn prf_pre_master_secret( public_key: &[u8], - private_key: &NamedCurvePrivateKey, - curve: NamedCurve, -) -> Result> { - match curve { - NamedCurve::P256 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::P384 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::X25519 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - _ => Err(Error::ErrInvalidNamedCurve), - } -} - -fn elliptic_curve_pre_master_secret( - public_key: &[u8], - private_key: &NamedCurvePrivateKey, + keypair: &mut NamedCurveKeypair, curve: NamedCurve, ) -> Result> { - match curve { - NamedCurve::P256 => { - let pub_key = p256::EncodedPoint::from_bytes(public_key)?; - let public = p256::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP256(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::P384 => { - let pub_key = p384::EncodedPoint::from_bytes(public_key)?; - let public = p384::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP384(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::X25519 => { - if public_key.len() != 32 { - return Err(Error::Other("Public key is not 32 len".into())); - } - let pub_key: [u8; 32] = public_key.try_into().unwrap(); - let public = x25519_dalek::PublicKey::from(pub_key); - if let NamedCurvePrivateKey::StaticSecretX25519(secret) = private_key { - return Ok(secret.diffie_hellman(&public).as_bytes().to_vec()); - } - } - _ => return Err(Error::ErrInvalidNamedCurve), + if keypair.curve != curve { + return Err(Error::ErrNamedCurveAndPrivateKeyMismatch); } - Err(Error::ErrNamedCurveAndPrivateKeyMismatch) + keypair.complete(public_key) } // This PRF with the SHA-256 hash function is used for all cipher suites @@ -140,19 +96,24 @@ fn elliptic_curve_pre_master_secret( // output data. // // https://tools.ietf.org/html/rfc4346w -fn hmac_sha(h: CipherSuiteHash, key: &[u8], data: &[u8]) -> Result> { - let mut mac = match h { - CipherSuiteHash::Sha256 => { - HmacSha256::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))? - } +fn hmac_sha( + crypto: &dyn RTCCrypto, + h: CipherSuiteHash, + key: &[u8], + input: &[&[u8]], +) -> Result> { + let algorithm = match h { + CipherSuiteHash::Sha256 => HmacAlgorithm::Sha256, }; - mac.update(data); - let result = mac.finalize(); - let code_bytes = result.into_bytes(); - Ok(code_bytes.to_vec()) + let mut output = vec![0; algorithm.output_len()]; + crypto + .hmac(algorithm, key, input, &mut output) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(output) } pub(crate) fn prf_p_hash( + crypto: &dyn RTCCrypto, secret: &[u8], seed: &[u8], requested_length: usize, @@ -163,11 +124,8 @@ pub(crate) fn prf_p_hash( let iterations = ((requested_length as f64) / (h.size() as f64)).ceil() as usize; for _ in 0..iterations { - last_round = hmac_sha(h, secret, &last_round)?; - - let mut last_round_seed = last_round.clone(); - last_round_seed.extend_from_slice(seed); - let with_secret = hmac_sha(h, secret, &last_round_seed)?; + last_round = hmac_sha(crypto, h, secret, &[&last_round])?; + let with_secret = hmac_sha(crypto, h, secret, &[&last_round, seed])?; out.extend_from_slice(&with_secret); } @@ -176,16 +134,18 @@ pub(crate) fn prf_p_hash( } pub(crate) fn prf_extended_master_secret( + crypto: &dyn RTCCrypto, pre_master_secret: &[u8], session_hash: &[u8], h: CipherSuiteHash, ) -> Result> { let mut seed = PRF_EXTENDED_MASTER_SECRET_LABEL.as_bytes().to_vec(); seed.extend_from_slice(session_hash); - prf_p_hash(pre_master_secret, &seed, 48, h) + prf_p_hash(crypto, pre_master_secret, &seed, 48, h) } pub(crate) fn prf_master_secret( + crypto: &dyn RTCCrypto, pre_master_secret: &[u8], client_random: &[u8], server_random: &[u8], @@ -194,16 +154,21 @@ pub(crate) fn prf_master_secret( let mut seed = PRF_MASTER_SECRET_LABEL.as_bytes().to_vec(); seed.extend_from_slice(client_random); seed.extend_from_slice(server_random); - prf_p_hash(pre_master_secret, &seed, 48, h) + prf_p_hash(crypto, pre_master_secret, &seed, 48, h) +} + +pub(crate) struct EncryptionKeyLengths { + pub(crate) mac: usize, + pub(crate) key: usize, + pub(crate) iv: usize, } pub(crate) fn prf_encryption_keys( + crypto: &dyn RTCCrypto, master_secret: &[u8], client_random: &[u8], server_random: &[u8], - prf_mac_len: usize, - prf_key_len: usize, - prf_iv_len: usize, + lengths: EncryptionKeyLengths, h: CipherSuiteHash, ) -> Result { let mut seed = PRF_KEY_EXPANSION_LABEL.as_bytes().to_vec(); @@ -211,29 +176,30 @@ pub(crate) fn prf_encryption_keys( seed.extend_from_slice(client_random); let material = prf_p_hash( + crypto, master_secret, &seed, - (2 * prf_mac_len) + (2 * prf_key_len) + (2 * prf_iv_len), + (2 * lengths.mac) + (2 * lengths.key) + (2 * lengths.iv), h, )?; let mut key_material = &material[..]; - let client_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; + let client_mac_key = key_material[..lengths.mac].to_vec(); + key_material = &key_material[lengths.mac..]; - let server_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; + let server_mac_key = key_material[..lengths.mac].to_vec(); + key_material = &key_material[lengths.mac..]; - let client_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; + let client_write_key = key_material[..lengths.key].to_vec(); + key_material = &key_material[lengths.key..]; - let server_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; + let server_write_key = key_material[..lengths.key].to_vec(); + key_material = &key_material[lengths.key..]; - let client_write_iv = key_material[..prf_iv_len].to_vec(); - key_material = &key_material[prf_iv_len..]; + let client_write_iv = key_material[..lengths.iv].to_vec(); + key_material = &key_material[lengths.iv..]; - let server_write_iv = key_material[..prf_iv_len].to_vec(); + let server_write_iv = key_material[..lengths.iv].to_vec(); Ok(EncryptionKeys { master_secret: master_secret.to_vec(), @@ -247,28 +213,31 @@ pub(crate) fn prf_encryption_keys( } pub(crate) fn prf_verify_data( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], label: &str, h: CipherSuiteHash, ) -> Result> { - let mut hasher = match h { - CipherSuiteHash::Sha256 => Sha256::new(), + let result = match h { + CipherSuiteHash::Sha256 => crypto + .hash(CryptoHashAlgorithm::Sha256, handshake_bodies) + .map_err(|error| Error::Crypto(error.to_string()))?, }; - hasher.update(handshake_bodies); - let result = hasher.finalize(); let mut seed = label.as_bytes().to_vec(); seed.extend_from_slice(&result); - prf_p_hash(master_secret, &seed, 12, h) + prf_p_hash(crypto, master_secret, &seed, 12, h) } pub(crate) fn prf_verify_data_client( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], h: CipherSuiteHash, ) -> Result> { prf_verify_data( + crypto, master_secret, handshake_bodies, PRF_VERIFY_DATA_CLIENT_LABEL, @@ -277,11 +246,13 @@ pub(crate) fn prf_verify_data_client( } pub(crate) fn prf_verify_data_server( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], h: CipherSuiteHash, ) -> Result> { prf_verify_data( + crypto, master_secret, handshake_bodies, PRF_VERIFY_DATA_SERVER_LABEL, @@ -291,6 +262,7 @@ pub(crate) fn prf_verify_data_server( // compute the MAC using HMAC-SHA1 pub(crate) fn prf_mac( + crypto: &dyn RTCCrypto, epoch: u16, sequence_number: u64, content_type: ContentType, @@ -298,8 +270,6 @@ pub(crate) fn prf_mac( payload: &[u8], key: &[u8], ) -> Result> { - let mut hmac = HmacSha1::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))?; - let mut msg = vec![0u8; 13]; msg[..2].copy_from_slice(&epoch.to_be_bytes()); msg[2..8].copy_from_slice(&sequence_number.to_be_bytes()[2..]); @@ -308,9 +278,9 @@ pub(crate) fn prf_mac( msg[10] = protocol_version.minor; msg[11..].copy_from_slice(&(payload.len() as u16).to_be_bytes()); - hmac.update(&msg); - hmac.update(payload); - let result = hmac.finalize(); - - Ok(result.into_bytes().to_vec()) + let mut output = vec![0; HmacAlgorithm::Sha1.output_len()]; + crypto + .hmac(HmacAlgorithm::Sha1, key, &[&msg, payload], &mut output) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(output) } diff --git a/rtc-dtls/src/prf/prf_test.rs b/rtc-dtls/src/prf/prf_test.rs index 33d5241f..3f648d40 100644 --- a/rtc-dtls/src/prf/prf_test.rs +++ b/rtc-dtls/src/prf/prf_test.rs @@ -1,33 +1,44 @@ use super::*; use crate::cipher_suite::CipherSuiteHash; +#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +use crypto::RTCCryptoProvider; #[test] fn test_pre_master_secret() -> Result<()> { - let private_key: [u8; 32] = [ - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, - 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, - 0x3e, 0x3f, - ]; - let private_key = - NamedCurvePrivateKey::StaticSecretX25519(x25519_dalek::StaticSecret::from(private_key)); - let public_key = [ - 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, - 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, - 0xb6, 0x15, - ]; + let provider = crypto::default_provider().unwrap(); + for curve in [NamedCurve::P256, NamedCurve::P384, NamedCurve::X25519] { + let mut left = curve.generate_keypair_with_crypto(provider.crypto())?; + let mut right = curve.generate_keypair_with_crypto(provider.crypto())?; + let left_public = left.public_key.clone(); + let right_public = right.public_key.clone(); + let left_secret = prf_pre_master_secret(&right_public, &mut left, curve)?; + let right_secret = prf_pre_master_secret(&left_public, &mut right, curve)?; + assert_eq!(left_secret, right_secret); + assert!(!left_secret.is_empty()); + assert!(matches!( + left.complete(&right_public), + Err(Error::ErrNamedCurveAndPrivateKeyMismatch) + )); + } - let expected_pre_master_secret = vec![ - 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, - 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, - 0x76, 0x24, - ]; + Ok(()) +} - let pre_master_secret = prf_pre_master_secret(&public_key, &private_key, NamedCurve::X25519)?; +#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[test] +fn test_cross_provider_pre_master_secret() -> Result<()> { + let ring = crypto::providers::RingProvider::new(); + let aws = crypto::providers::AwsLcRsProvider::new(); - assert_eq!( - expected_pre_master_secret, pre_master_secret, - "PremasterSecret exp: {expected_pre_master_secret:?} actual: {pre_master_secret:?}" - ); + for curve in [NamedCurve::P256, NamedCurve::P384, NamedCurve::X25519] { + let mut left = curve.generate_keypair_with_crypto(ring.crypto())?; + let mut right = curve.generate_keypair_with_crypto(aws.crypto())?; + let left_public = left.public_key.clone(); + let right_public = right.public_key.clone(); + let left_secret = prf_pre_master_secret(&right_public, &mut left, curve)?; + let right_secret = prf_pre_master_secret(&left_public, &mut right, curve)?; + assert_eq!(left_secret, right_secret); + } Ok(()) } @@ -57,6 +68,7 @@ fn test_master_secret() -> Result<()> { ]; let master_secret = prf_master_secret( + crypto::default_provider().unwrap().crypto(), &pre_master_secret, &client_random, &server_random, @@ -107,12 +119,15 @@ fn test_encryption_keys() -> Result<()> { }; let keys = prf_encryption_keys( + crypto::default_provider().unwrap().crypto(), &master_secret, &client_random, &server_random, - 0, - 16, - 4, + EncryptionKeyLengths { + mac: 0, + key: 16, + iv: 4, + }, CipherSuiteHash::Sha256, )?; @@ -250,7 +265,12 @@ fn test_verify_data() -> Result<()> { 0xcf, 0x91, 0x96, 0x26, 0xf1, 0x36, 0x0c, 0x53, 0x6a, 0xaa, 0xd7, 0x3a, ]; - let verify_data = prf_verify_data_client(&master_secret, &final_msg, CipherSuiteHash::Sha256)?; + let verify_data = prf_verify_data_client( + crypto::default_provider().unwrap().crypto(), + &master_secret, + &final_msg, + CipherSuiteHash::Sha256, + )?; assert_eq!( expected_verify_data, verify_data, diff --git a/rtc-dtls/src/signature_hash_algorithm/mod.rs b/rtc-dtls/src/signature_hash_algorithm/mod.rs index 1fc13565..e1006411 100644 --- a/rtc-dtls/src/signature_hash_algorithm/mod.rs +++ b/rtc-dtls/src/signature_hash_algorithm/mod.rs @@ -114,14 +114,35 @@ pub struct SignatureHashAlgorithm { } impl SignatureHashAlgorithm { + pub(crate) fn crypto_scheme(&self) -> Result { + match (self.signature, self.hash) { + (SignatureAlgorithm::Ed25519, _) => Ok(crypto::SignatureScheme::Ed25519), + (SignatureAlgorithm::Ecdsa, HashAlgorithm::Sha256) => { + Ok(crypto::SignatureScheme::EcdsaP256Sha256) + } + (SignatureAlgorithm::Ecdsa, HashAlgorithm::Sha384) => { + Ok(crypto::SignatureScheme::EcdsaP384Sha384) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha1) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha1) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha256) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha256) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha384) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha384) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha512) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha512) + } + _ => Err(Error::ErrKeySignatureVerifyUnimplemented), + } + } + // is_compatible checks that given private key is compatible with the signature scheme. pub(crate) fn is_compatible(&self, private_key: &CryptoPrivateKey) -> bool { - match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => self.signature == SignatureAlgorithm::Ed25519, - CryptoPrivateKeyKind::Ecdsa256(_) => self.signature == SignatureAlgorithm::Ecdsa, - CryptoPrivateKeyKind::Rsa256(_) => self.signature == SignatureAlgorithm::Rsa, - CryptoPrivateKeyKind::Custom(_) => true, - } + self.crypto_scheme() + .is_ok_and(|scheme| private_key.signing_key.supports(scheme)) } } diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index 6924a142..78e8643d 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -8,11 +8,13 @@ use rkyv::{Archive, Deserialize, Serialize}; use shared::crypto::KeyingMaterialExporter; use shared::error::*; use std::io::{BufWriter, Cursor}; +use std::sync::Arc; // State holds the dtls connection state and implements both encoding.BinaryMarshaler and encoding.BinaryUnmarshaler /// The negotiated connection state: keys, sequence numbers, peer identity and the active /// cipher suite. pub struct State { + pub(crate) crypto_provider: Option>, pub(crate) local_epoch: u16, pub(crate) remote_epoch: u16, pub(crate) local_sequence_number: Vec, // uint48 @@ -66,6 +68,7 @@ struct SerializedState { impl Default for State { fn default() -> Self { State { + crypto_provider: crypto::default_provider().ok(), local_epoch: 0, remote_epoch: 0, local_sequence_number: vec![], @@ -203,9 +206,25 @@ impl State { } if self.is_client { - cipher_suite.init(&self.master_secret, &local_random, &remote_random, true) + cipher_suite.init( + self.crypto_provider.clone().ok_or_else(|| { + Error::Crypto("DTLS crypto provider is not configured".into()) + })?, + &self.master_secret, + &local_random, + &remote_random, + true, + ) } else { - cipher_suite.init(&self.master_secret, &remote_random, &local_random, false) + cipher_suite.init( + self.crypto_provider.clone().ok_or_else(|| { + Error::Crypto("DTLS crypto provider is not configured".into()) + })?, + &self.master_secret, + &remote_random, + &local_random, + false, + ) } } else { Err(Error::ErrCipherSuiteUnset) @@ -303,7 +322,17 @@ impl KeyingMaterialExporter for State { } if let Some(cipher_suite) = &self.cipher_suite { - match prf_p_hash(&self.master_secret, &seed, length, cipher_suite.hash_func()) { + let provider = self + .crypto_provider + .as_ref() + .ok_or_else(|| Error::Crypto("DTLS crypto provider is not configured".into()))?; + match prf_p_hash( + provider.crypto(), + &self.master_secret, + &seed, + length, + cipher_suite.hash_func(), + ) { Ok(v) => Ok(v), Err(err) => Err(Error::Hash(err.to_string())), } diff --git a/rtc-stun/Cargo.toml b/rtc-stun/Cargo.toml index 2c6030d4..04154c90 100644 --- a/rtc-stun/Cargo.toml +++ b/rtc-stun/Cargo.toml @@ -13,9 +13,9 @@ categories.workspace = true [features] default = ["ring"] -bench = [] ring = ["crypto/ring"] aws-lc-rs = ["crypto/aws-lc-rs"] +bench = [] [dependencies] shared = { workspace = true, default-features = false, features = [] } diff --git a/rtc-turn/Cargo.toml b/rtc-turn/Cargo.toml index 1ee294ca..5f73e9bc 100644 --- a/rtc-turn/Cargo.toml +++ b/rtc-turn/Cargo.toml @@ -11,6 +11,12 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[features] +default = ["ring"] +ring = ["crypto/ring", "stun/ring"] +aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] +metrics = [] + [dependencies] shared = { workspace = true, default-features = false, features = [] } stun.workspace = true @@ -29,12 +35,6 @@ criterion.workspace = true crossbeam-channel = "0.5" ctrlc.workspace = true -[features] -default = ["ring"] -metrics = [] -ring = ["crypto/ring", "stun/ring"] -aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] - [[bench]] name = "bench" harness = false diff --git a/src/lib.rs b/src/lib.rs index 23cfaba0..16f1340a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -714,5 +714,3 @@ pub mod statistics; compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); #[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index 8c3de511..f7cfc66b 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -71,7 +71,6 @@ //! ## Persist Certificate Across Sessions //! //! ```no_run -//! # #[cfg(feature = "pem")] //! # fn example() -> Result<(), Box> { //! use rtc::peer_connection::certificate::RTCCertificate; //! use rcgen::KeyPair; @@ -186,12 +185,8 @@ use std::ops::Add; use std::time::{Duration, SystemTime}; -use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind}; +use dtls::crypto::CryptoPrivateKey; use rcgen::{CertificateParams, KeyPair}; -#[cfg(feature = "ring")] -use ring::rand::SystemRandom; -use ring::rsa; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; use sha2::{Digest, Sha256}; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; @@ -256,7 +251,6 @@ use shared::util::math_rand_alpha; /// ## Persisting and loading certificates /// /// ``` -/// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; /// # use rcgen::KeyPair; @@ -342,41 +336,10 @@ impl RTCCertificate { fn from_params(params: CertificateParams, key_pair: KeyPair) -> Result { let not_after = params.not_after; - let x509_cert = params.self_signed(&key_pair).unwrap(); - let serialized_der = key_pair.serialize_der(); - - let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else { - return Err(Error::Other("Unsupported key_pair".to_owned())); - }; + let x509_cert = params + .self_signed(&key_pair) + .map_err(|error| Error::Other(error.to_string()))?; + let private_key = CryptoPrivateKey::from_key_pair(&key_pair)?; let expires = if cfg!(target_arch = "arm") { // Workaround for issue overflow when adding duration to instant on armv7 @@ -467,7 +430,6 @@ impl RTCCertificate { /// # Examples /// /// ``` - /// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; /// # use rcgen::KeyPair; @@ -483,7 +445,6 @@ impl RTCCertificate { /// # Ok(()) /// # } /// ``` - #[cfg(feature = "pem")] pub fn from_pem(pem_str: &str) -> Result { let mut pem_blocks = pem_str.split("\n\n"); let first_block = if let Some(b) = pem_blocks.next() { @@ -576,7 +537,6 @@ impl RTCCertificate { /// # Examples /// /// ``` - /// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; /// # use rcgen::KeyPair; @@ -594,7 +554,6 @@ impl RTCCertificate { /// # Ok(()) /// # } /// ``` - #[cfg(any(doc, feature = "pem"))] pub fn serialize_pem(&self) -> String { // Encode `expires` as a PEM block. // @@ -611,7 +570,9 @@ impl RTCCertificate { format!( "{}\n{}", pem::encode(&expires_pem), - self.dtls_certificate.serialize_pem() + self.dtls_certificate + .serialize_pem() + .expect("RTCCertificate keys are exportable") ) } @@ -727,7 +688,6 @@ mod test { Ok(()) } - #[cfg(feature = "pem")] #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; From 927b27dd9d65210b63dd3fcb8e917de24d3c59eb Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Sun, 2 Aug 2026 22:08:09 -0700 Subject: [PATCH 35/40] =?UTF-8?q?P6=20=E2=80=94=20Top-level=20RTC=20and=20?= =?UTF-8?q?certificate=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 86 ++- rtc-crypto/Cargo.toml | 10 +- rtc-dtls/Cargo.toml | 20 +- rtc-dtls/src/config.rs | 151 +++-- rtc-dtls/src/conn/conn_test.rs | 1 - rtc-dtls/src/crypto/mod.rs | 36 +- rtc-dtls/src/flight/flight5.rs | 39 +- rtc-dtls/src/state.rs | 32 +- rtc-ice/Cargo.toml | 20 +- rtc-interceptor-derive/Cargo.toml | 6 +- rtc-mdns/Cargo.toml | 2 +- rtc-media/Cargo.toml | 2 +- rtc-rtp/Cargo.toml | 2 +- rtc-sctp/Cargo.toml | 10 +- rtc-sdp/Cargo.toml | 2 +- rtc-shared/Cargo.toml | 16 +- rtc-shared/src/crypto/mod.rs | 19 - rtc-shared/src/lib.rs | 4 - rtc-srtp/Cargo.toml | 4 +- rtc-srtp/src/config.rs | 101 ++- rtc-stun/Cargo.toml | 8 +- rtc-turn/Cargo.toml | 4 +- src/lib.rs | 9 +- src/peer_connection/certificate/mod.rs | 358 +++++++++-- src/peer_connection/configuration/mod.rs | 7 +- .../configuration/setting_engine.rs | 11 + src/peer_connection/handler/dtls.rs | 34 +- src/peer_connection/internal.rs | 37 +- src/peer_connection/mod.rs | 2 +- src/peer_connection/transport/dtls/mod.rs | 213 ++++++- src/peer_connection/transport/ice/mod.rs | 8 +- tests/crypto_provider_peer_connections.rs | 574 ++++++++++++++++++ tests/no_builtin_crypto_provider.rs | 20 + 33 files changed, 1554 insertions(+), 294 deletions(-) delete mode 100644 rtc-shared/src/crypto/mod.rs create mode 100644 tests/crypto_provider_peer_connections.rs create mode 100644 tests/no_builtin_crypto_provider.rs diff --git a/Cargo.toml b/Cargo.toml index 64218ddf..6b1af104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ shared = { version = "0.21.0", path = "rtc-shared", package = "rtc-shared", defa srtp = { version = "0.21.0", path = "rtc-srtp", package = "rtc-srtp", default-features = false } stun = { version = "0.21.0", path = "rtc-stun", package = "rtc-stun", default-features = false } turn = { version = "0.21.0", path = "rtc-turn", package = "rtc-turn", default-features = false } +signal = { version = "0.21.0", path = "examples/signal", package = "rtc-signal" } # common dependencies sansio = "1" @@ -60,9 +61,36 @@ ring = "0.17.14" aws-lc-rs = { version = "1.17.3", features = ["aws-lc-sys"], default-features = false } rand = "0.10.1" serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.114" thiserror = "2.0.18" zeroize = "1.8.2" pem = "3.0.3" +aes = "0.8.4" +aes-gcm = { version = "0.10.3", features = ["std"] } +sec1 = { version = "0.7.3", features = ["std"] } +p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } +ccm = "0.5.0" +md-5 = "0.10.6" +subtle = "2.6.1" +x509-parser = "0.16.0" +der-parser = "9.0.0" +rustls = { version = "0.23.27", default-features = false, features = ["std"] } +rkyv = "0.8.17" +bytecheck = "0.8" +url = "2.5.0" +base64 = "0.22.1" +crc = "3.0.1" +slab = "0.4.9" +crc32c = "0.6" +rustc-hash = "2" +memchr = "2.1.1" +socket2 = { version = "0.6", features = ["all"] } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "parsing", "extra-traits"] } +uuid = { version = "1", features = ["v4"] } +unicase = "2.8" +substring = "1.4.5" # dev dependencies env_logger = "0.11.11" @@ -71,6 +99,22 @@ clap = { version = "4.6.1", features = ["derive"] } ctrlc = "3.5.2" criterion = "0.8.2" tokio = { version = "1.52.3", features = ["full"] } +tokio-util = { version = "0.7.17", features = ["codec"] } +tokio-tungstenite = "0.28.0" +hyper = { version = "0.14.28", features = ["full"] } +lazy_static = "1.4.0" +hex = "0.4.3" +crossbeam-channel = "0.5" +assert_matches = "1.5.0" +nearly_eq = "0.2" +regex = "1.10.3" +ipnet = "2.9.0" +waitgroup = "0.1.2" +futures = "0.3.30" +futures-util = "0.3" +local-sync = "0.1.1" +core_affinity = "0.8.1" +anyhow = "1.0.80" [package] name = "rtc" @@ -88,16 +132,18 @@ readme = "README.md" [features] default = ["ring"] -ring = ["dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] -aws-lc-rs = ["dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] +ring = ["crypto/ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] +aws-lc-rs = ["crypto/aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } +shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } +crypto.workspace = true sansio.workspace = true datachannel.workspace = true dtls.workspace = true ice.workspace = true interceptor.workspace = true +interceptor-derive.workspace = true mdns.workspace = true media.workspace = true rtcp.workspace = true @@ -110,39 +156,37 @@ turn.workspace = true bytes.workspace = true log.workspace = true -serde = "1" -serde_json = { version = "1", features = [] } +serde.workspace = true +serde_json.workspace = true rcgen.workspace = true -sha2 = "0.10" -rustls = { version = "0.23.35", default-features = false, features = ["std"] } -url = { version = "2", features = [] } -hex = { version = "0.4", features = [] } +rustls.workspace = true +url.workspace = true +hex.workspace = true pem.workspace = true -unicase = "2.8" +x509-parser.workspace = true +unicase.workspace = true rand.workspace = true [dev-dependencies] sansio.workspace = true shared.workspace = true ice.workspace = true -webrtc = "0.14.0" -signal = { version = "0.21.0", path = "examples/signal", package = "rtc-signal" } +signal.workspace = true tokio.workspace = true env_logger.workspace = true -anyhow = "1" +anyhow.workspace = true clap.workspace = true -hyper = { version = "0.14.32", features = ["full"] } -tokio-util = { version = "0.7.17", features = ["codec"] } +hyper.workspace = true +tokio-util.workspace = true chrono.workspace = true log.workspace = true -serde.workspace = true -serde_json = "1.0.145" -bytes.workspace = true -rand.workspace = true ctrlc.workspace = true -tokio-tungstenite = "0.28.0" -futures-util = "0.3" +tokio-tungstenite.workspace = true +futures-util.workspace = true + +# integration test with old webrtc crate +webrtc = "0.14.0" [[example]] name = "broadcast" diff --git a/rtc-crypto/Cargo.toml b/rtc-crypto/Cargo.toml index 5b1474cc..b4c4f4d8 100644 --- a/rtc-crypto/Cargo.toml +++ b/rtc-crypto/Cargo.toml @@ -19,14 +19,14 @@ aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ccm", "dep:md-5"] test-support = [] [dependencies] -aes = { version = "0.8.4", optional = true } -ccm = { version = "0.5.0", optional = true } -md-5 = { version = "0.10.6", optional = true } -subtle = "2.6.1" +aes = { workspace = true, optional = true } +ccm = { workspace = true, optional = true } +md-5 = { workspace = true, optional = true } +subtle.workspace = true thiserror.workspace = true zeroize.workspace = true ring = { workspace = true, optional = true } aws-lc-rs = { workspace = true, optional = true } [dev-dependencies] -pem = "3.0.3" +pem.workspace = true diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index f9d5ec02..3719441e 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -17,29 +17,29 @@ ring = ["crypto/ring", "rustls/ring", "rcgen/ring"] aws-lc-rs = ["crypto/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "replay"] } +shared = { workspace = true, default-features = false, features = ["replay"] } crypto.workspace = true bytes.workspace = true byteorder.workspace = true -x509-parser = "0.16.0" -der-parser = "9.0.0" +x509-parser.workspace = true +der-parser.workspace = true rcgen.workspace = true -rustls = { version = "0.23.27", default-features = false, features = ["std"] } -rkyv = "0.8.17" -bytecheck = "0.8" +rustls.workspace = true +rkyv.workspace = true +bytecheck.workspace = true log.workspace = true pem.workspace = true [dev-dependencies] -local-sync = "0.1.1" -core_affinity = "0.8.1" +local-sync.workspace = true +core_affinity.workspace = true chrono.workspace = true env_logger.workspace = true clap.workspace = true -anyhow = "1.0.80" +anyhow.workspace = true ctrlc.workspace = true -futures = "0.3.30" +futures.workspace = true #[[example]] #name = "dtls_chat_server" diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 523a9350..3713f8a6 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -37,27 +37,46 @@ use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::CertificateDer; use rustls::server::danger::ClientCertVerifier; -/// The rustls [`CryptoProvider`](rustls::crypto::CryptoProvider) this crate was built with. +/// Explicit rustls/webpki backend used only for CA-chain and hostname verification. /// -/// rustls can infer a process-wide default from its own crate features, but only when exactly -/// one of `ring`/`aws-lc-rs` is enabled — and it panics otherwise. Feature unification makes -/// that easy to violate: any other crate in the graph that asks rustls for a different provider -/// enables both, which is what happens as soon as `rtc`'s `webrtc` interop dev-dependency joins -/// the build. Since our own `ring`/`aws-lc-rs` features already decide the answer, pass it -/// explicitly and never consult the global default. -/// -/// If neither feature is enabled there is no provider to name, so fall back to whatever the -/// application installed. -fn rustls_crypto_provider() -> Option> { +/// This policy adapter is separate from [`RTCCryptoProvider`]. Applications authenticating with +/// SDP fingerprints do not need it, while applications enabling WebPKI validation can select its +/// backend without changing their primitive RTC provider. +#[derive(Clone)] +pub struct RustlsVerifierAdapter { + provider: Arc, +} + +impl RustlsVerifierAdapter { + /// Wraps a rustls crypto provider for WebPKI verification. + #[must_use] + pub fn new(provider: Arc) -> Self { + Self { provider } + } + + /// Uses rustls's ring verification backend. + #[cfg(feature = "ring")] + #[must_use] + pub fn ring() -> Self { + Self::new(Arc::new(rustls::crypto::ring::default_provider())) + } + + /// Uses rustls's AWS-LC-RS verification backend. + #[cfg(feature = "aws-lc-rs")] + #[must_use] + pub fn aws_lc_rs() -> Self { + Self::new(Arc::new(rustls::crypto::aws_lc_rs::default_provider())) + } +} + +fn default_verifier_adapter() -> Option { #[cfg(feature = "ring")] { - Some(std::sync::Arc::new(rustls::crypto::ring::default_provider())) + Some(RustlsVerifierAdapter::ring()) } #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] { - Some(std::sync::Arc::new( - rustls::crypto::aws_lc_rs::default_provider(), - )) + Some(RustlsVerifierAdapter::aws_lc_rs()) } #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] { @@ -72,16 +91,15 @@ fn rustls_crypto_provider() -> Option, -) -> Result> { - let builder = match rustls_crypto_provider() { - Some(provider) => { - rustls::client::WebPkiServerVerifier::builder_with_provider(roots, provider) - } - None => rustls::client::WebPkiServerVerifier::builder(roots), - }; - builder - .build() - .map_err(|err| Error::Other(format!("rustls server cert verifier: {err}"))) + adapter: &RustlsVerifierAdapter, +) -> Result> { + let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider( + roots, + adapter.provider.clone(), + ) + .build() + .map_err(|err| Error::Other(format!("rustls server cert verifier: {err}")))?; + Ok(verifier) } /// Config is used to configure a DTLS client or server. @@ -104,6 +122,7 @@ pub struct ConfigBuilder { verify_peer_certificate: Option, roots_cas: rustls::RootCertStore, client_cas: rustls::RootCertStore, + verifier_adapter: Option, server_name: String, mtu: usize, replay_protection_window: usize, @@ -128,6 +147,7 @@ impl Default for ConfigBuilder { verify_peer_certificate: None, roots_cas: rustls::RootCertStore::empty(), client_cas: rustls::RootCertStore::empty(), + verifier_adapter: default_verifier_adapter(), server_name: String::default(), mtu: 0, replay_protection_window: 0, @@ -276,6 +296,12 @@ impl ConfigBuilder { self } + /// Selects the rustls/webpki adapter used for optional CA-chain and hostname verification. + pub fn with_rustls_verifier_adapter(mut self, adapter: RustlsVerifierAdapter) -> Self { + self.verifier_adapter = Some(adapter); + self + } + /// server_name is used to verify the hostname on the returned /// certificates unless insecure_skip_verify is given. pub fn with_server_name(mut self, server_name: String) -> Self { @@ -467,6 +493,41 @@ impl ConfigBuilder { } } + let server_cert_verifier = if self.insecure_skip_verify { + None + } else { + let adapter = self.verifier_adapter.as_ref().ok_or_else(|| { + Error::Crypto("CA-chain verification requires a RustlsVerifierAdapter".to_owned()) + })?; + let roots = if self.roots_cas.is_empty() { + gen_self_signed_root_cert() + } else { + self.roots_cas.clone() + }; + Some(server_cert_verifier(Arc::new(roots), adapter)?) + }; + + let client_cert_verifier = if self.client_auth as u8 + >= ClientAuthType::VerifyClientCertIfGiven as u8 + { + let adapter = self.verifier_adapter.as_ref().ok_or_else(|| { + Error::Crypto( + "client-certificate verification requires a RustlsVerifierAdapter".to_owned(), + ) + })?; + Some( + rustls::server::WebPkiClientVerifier::builder_with_provider( + Arc::new(self.client_cas.clone()), + adapter.provider.clone(), + ) + .build() + .map_err(|err| Error::Other(format!("rustls client cert verifier: {err}")))? + as Arc, + ) + } else { + None + }; + Ok(HandshakeConfig { crypto_provider, local_psk_callback: self.psk.take(), @@ -483,8 +544,8 @@ impl ConfigBuilder { insecure_verification: self.insecure_verification, verify_peer_certificate: self.verify_peer_certificate.take(), roots_cas: self.roots_cas, - server_cert_verifier: server_cert_verifier(Arc::new(gen_self_signed_root_cert()))?, - client_cert_verifier: None, + server_cert_verifier, + client_cert_verifier, retransmit_interval, initial_epoch: 0, maximum_transmission_unit, @@ -503,17 +564,24 @@ pub type VerifyPeerCertificateFn = /// Generates a self-signed certificate, as WebRTC endpoints use. pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { - let mut certs = rustls::RootCertStore::empty(); - certs - .add( - rcgen::generate_simple_self_signed(vec![]) - .unwrap() - .cert - .der() - .to_owned(), - ) - .unwrap(); - certs + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + { + let mut certs = rustls::RootCertStore::empty(); + certs + .add( + rcgen::generate_simple_self_signed(vec![]) + .unwrap() + .cert + .der() + .to_owned(), + ) + .unwrap(); + certs + } + #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + { + rustls::RootCertStore::empty() + } } #[derive(Clone)] @@ -535,7 +603,7 @@ pub struct HandshakeConfig { pub(crate) insecure_verification: bool, pub(crate) verify_peer_certificate: Option, pub(crate) roots_cas: rustls::RootCertStore, - pub(crate) server_cert_verifier: Arc, + pub(crate) server_cert_verifier: Option>, pub(crate) client_cert_verifier: Option>, pub(crate) retransmit_interval: std::time::Duration, pub(crate) initial_epoch: u16, @@ -593,8 +661,9 @@ impl Default for HandshakeConfig { insecure_verification: false, verify_peer_certificate: None, roots_cas: rustls::RootCertStore::empty(), - server_cert_verifier: server_cert_verifier(Arc::new(gen_self_signed_root_cert())) - .expect("the built-in self-signed root is always a valid trust anchor"), + server_cert_verifier: default_verifier_adapter().and_then(|adapter| { + server_cert_verifier(Arc::new(gen_self_signed_root_cert()), &adapter).ok() + }), client_cert_verifier: None, retransmit_interval: std::time::Duration::from_secs(0), initial_epoch: 0, diff --git a/rtc-dtls/src/conn/conn_test.rs b/rtc-dtls/src/conn/conn_test.rs index de9e09ed..cb3d8607 100644 --- a/rtc-dtls/src/conn/conn_test.rs +++ b/rtc-dtls/src/conn/conn_test.rs @@ -20,7 +20,6 @@ use shared::error::*;*/ //use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; //use rand::Rng; -//use shared::crypto::KeyingMaterialExporter; const ERR_TEST_PSK_INVALID_IDENTITY: &str = "TestPSK: Server got invalid identity"; const ERR_PSK_REJECTED: &str = "PSK Rejected"; diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index ded04f38..f2728fbf 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -33,6 +33,7 @@ use crypto::{ PublicKey, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme as CryptoSignatureScheme, SigningKey, }; +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] use rcgen::{CertifiedKey, KeyPair, generate_simple_self_signed}; use crate::curve::named_curve::*; @@ -69,12 +70,14 @@ impl Certificate { /// Generate a self-signed certificate. /// /// See [`rcgen::generate_simple_self_signed`]. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { let provider = crypto::default_provider().map_err(crypto_error)?; Self::generate_self_signed_with_provider(subject_alt_names, provider) } /// Generates a self-signed certificate and imports its key into `provider`. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn generate_self_signed_with_provider( subject_alt_names: impl Into>, provider: Arc, @@ -89,6 +92,7 @@ impl Certificate { /// Generate a self-signed certificate with the given algorithm. /// /// See `rcgen::Certificate::self_signed`. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn generate_self_signed_with_alg( subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, @@ -98,6 +102,7 @@ impl Certificate { } /// Generates a self-signed certificate with `alg` and imports its key into `provider`. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn generate_self_signed_with_alg_and_provider( subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, @@ -142,8 +147,7 @@ impl Certificate { ))); } - let keypair = KeyPair::try_from(pems[0].contents()) - .map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?; + let private_key_der = pems[0].contents().to_vec(); let mut rustls_certs = Vec::new(); for p in pems.drain(1..) { @@ -156,10 +160,27 @@ impl Certificate { rustls_certs.push(CertificateDer::from(p.contents().to_vec())); } - Ok(Certificate { - certificate: rustls_certs, - private_key: CryptoPrivateKey::from_key_pair_with_provider(&keypair, provider)?, - }) + let schemes = [ + CryptoSignatureScheme::Ed25519, + CryptoSignatureScheme::EcdsaP256Sha256, + CryptoSignatureScheme::RsaPkcs1Sha256, + ]; + let signing_key = schemes + .into_iter() + .filter(|scheme| { + provider + .crypto() + .supports(crypto::CryptoAlgorithm::SigningKeyImport(*scheme)) + }) + .find_map(|scheme| { + provider + .crypto() + .import_signing_key(scheme, &private_key_der) + .ok() + }) + .ok_or_else(|| Error::InvalidPEM("can't decode PKCS#8 signing key".into()))?; + + Ok(Certificate::from_signing_key(rustls_certs, signing_key)) } /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. @@ -257,6 +278,7 @@ impl std::fmt::Debug for CryptoPrivateKey { } } +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] impl TryFrom<&KeyPair> for CryptoPrivateKey { type Error = Error; @@ -271,12 +293,14 @@ impl CryptoPrivateKey { /// # Errors /// /// Fails if the key type has no supported scheme. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn from_key_pair(key_pair: &KeyPair) -> Result { let provider = crypto::default_provider().map_err(crypto_error)?; Self::from_key_pair_with_provider(key_pair, provider) } /// Imports an rcgen key pair into an explicit provider. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn from_key_pair_with_provider( key_pair: &KeyPair, provider: Arc, diff --git a/rtc-dtls/src/flight/flight5.rs b/rtc-dtls/src/flight/flight5.rs index fbdf8170..daf25650 100644 --- a/rtc-dtls/src/flight/flight5.rs +++ b/rtc-dtls/src/flight/flight5.rs @@ -723,22 +723,31 @@ fn initalize_cipher_suite( let mut chains = vec![]; if !cfg.insecure_skip_verify { - chains = match verify_server_cert( - &state.peer_certificates, - &cfg.server_cert_verifier, - &cfg.server_name, - ) { - Ok(chains) => chains, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); + let cert_verifier = cfg.server_cert_verifier.as_ref().ok_or_else(|| { + ( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(Error::Crypto( + "CA-chain verification has no configured verifier adapter".to_owned(), + )), + ) + })?; + chains = + match verify_server_cert(&state.peer_certificates, cert_verifier, &cfg.server_name) + { + Ok(chains) => chains, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } } - } } if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate && let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index 78e8643d..a72fd655 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -4,8 +4,8 @@ use super::curve::named_curve::*; use super::extension::extension_use_srtp::SrtpProtectionProfile; use super::handshake::handshake_random::*; use super::prf::*; +use crypto::SecretVec; use rkyv::{Archive, Deserialize, Serialize}; -use shared::crypto::KeyingMaterialExporter; use shared::error::*; use std::io::{BufWriter, Cursor}; use std::sync::Arc; @@ -280,19 +280,31 @@ impl State { pub fn cipher_suite(&self) -> Option<&dyn CipherSuite> { self.cipher_suite.as_deref() } -} -impl KeyingMaterialExporter for State { - /// export_keying_material returns length bytes of exported key material in a new - /// slice as defined in RFC 5705. - /// This allows protocols to use DTLS for key establishment, but - /// then use some of the keying material for their own purposes - fn export_keying_material( + /// Returns the provider selected for this DTLS session. + /// + /// # Errors + /// + /// Returns an error when the state has not been attached to a configured session. + pub fn crypto_provider(&self) -> Result> { + self.crypto_provider + .clone() + .ok_or_else(|| Error::Crypto("DTLS crypto provider is not configured".into())) + } + + /// Exports `length` bytes of keying material from an established session, as defined in + /// RFC 5705. + /// + /// # Errors + /// + /// Returns an error before the handshake completes, when `context` is non-empty, when the + /// label is reserved by TLS, or when the negotiated cipher suite is unavailable. + pub fn export_keying_material( &self, label: &str, context: &[u8], length: usize, - ) -> shared::error::Result> { + ) -> Result { if self.local_epoch == 0 { return Err(Error::HandshakeInProgress); } else if !context.is_empty() { @@ -333,7 +345,7 @@ impl KeyingMaterialExporter for State { length, cipher_suite.hash_func(), ) { - Ok(v) => Ok(v), + Ok(v) => Ok(SecretVec::new(v)), Err(err) => Err(Error::Hash(err.to_string())), } } else { diff --git a/rtc-ice/Cargo.toml b/rtc-ice/Cargo.toml index 051eb004..86cf2fce 100644 --- a/rtc-ice/Cargo.toml +++ b/rtc-ice/Cargo.toml @@ -23,25 +23,25 @@ stun.workspace = true mdns.workspace = true crypto.workspace = true -crc = "3.0.1" +crc.workspace = true log.workspace = true serde.workspace = true -url = "2.5.0" -uuid = { version = "1", features = ["v4"] } +url.workspace = true +uuid.workspace = true bytes.workspace = true [dev-dependencies] -regex = "1.10.3" +regex.workspace = true env_logger.workspace = true chrono.workspace = true -ipnet = "2.9.0" +ipnet.workspace = true clap.workspace = true -lazy_static = "1.4.0" -hyper = { version = "0.14.28", features = ["full"] } -waitgroup = "0.1.2" -serde_json = "1.0.114" +lazy_static.workspace = true +hyper.workspace = true +waitgroup.workspace = true +serde_json.workspace = true tokio.workspace = true -futures = "0.3.30" +futures.workspace = true ctrlc.workspace = true [[example]] diff --git a/rtc-interceptor-derive/Cargo.toml b/rtc-interceptor-derive/Cargo.toml index 901584e8..d421d8d2 100644 --- a/rtc-interceptor-derive/Cargo.toml +++ b/rtc-interceptor-derive/Cargo.toml @@ -15,6 +15,6 @@ categories.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full", "parsing", "extra-traits"] } +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/rtc-mdns/Cargo.toml b/rtc-mdns/Cargo.toml index b9999160..ba5def4e 100644 --- a/rtc-mdns/Cargo.toml +++ b/rtc-mdns/Cargo.toml @@ -17,7 +17,7 @@ sansio.workspace = true bytes.workspace = true log.workspace = true -socket2 = { version = "0.6", features = ["all"] } +socket2.workspace = true [dev-dependencies] env_logger.workspace = true diff --git a/rtc-media/Cargo.toml b/rtc-media/Cargo.toml index e5ba0b10..ea8cef99 100644 --- a/rtc-media/Cargo.toml +++ b/rtc-media/Cargo.toml @@ -22,7 +22,7 @@ thiserror.workspace = true [dev-dependencies] criterion.workspace = true -nearly_eq = "0.2" +nearly_eq.workspace = true [[bench]] name = "bench" diff --git a/rtc-rtp/Cargo.toml b/rtc-rtp/Cargo.toml index de3473f2..5e01ccb8 100644 --- a/rtc-rtp/Cargo.toml +++ b/rtc-rtp/Cargo.toml @@ -17,7 +17,7 @@ shared = { workspace = true, default-features = false, features = ["marshal"] } bytes.workspace = true rand.workspace = true serde.workspace = true -memchr = "2.1.1" +memchr.workspace = true [dev-dependencies] chrono.workspace = true diff --git a/rtc-sctp/Cargo.toml b/rtc-sctp/Cargo.toml index 819227ad..d022be2d 100644 --- a/rtc-sctp/Cargo.toml +++ b/rtc-sctp/Cargo.toml @@ -23,14 +23,14 @@ shared = { workspace = true, default-features = false, features = [] } bytes.workspace = true rand.workspace = true thiserror.workspace = true -slab = "0.4.9" +slab.workspace = true log.workspace = true -crc32c = "0.6" -rustc-hash = "2" +crc32c.workspace = true +rustc-hash.workspace = true [dev-dependencies] -assert_matches = "1.5.0" -lazy_static = "1.4.0" +assert_matches.workspace = true +lazy_static.workspace = true [[example]] name = "sctp_micro" diff --git a/rtc-sdp/Cargo.toml b/rtc-sdp/Cargo.toml index 9e32093b..47d45e75 100644 --- a/rtc-sdp/Cargo.toml +++ b/rtc-sdp/Cargo.toml @@ -14,7 +14,7 @@ categories.workspace = true [dependencies] shared = { workspace = true, default-features = false, features = [] } -url = "2.5.0" +url.workspace = true rand.workspace = true [dev-dependencies] diff --git a/rtc-shared/Cargo.toml b/rtc-shared/Cargo.toml index c29db68a..f6f8e3e6 100644 --- a/rtc-shared/Cargo.toml +++ b/rtc-shared/Cargo.toml @@ -12,22 +12,21 @@ keywords.workspace = true categories.workspace = true [features] -default = ["crypto", "ifaces", "marshal", "replay"] -crypto = [] +default = ["ifaces", "marshal", "replay"] ifaces = [] marshal = [] replay = [] [dependencies] thiserror.workspace = true -substring = "1.4.5" +substring.workspace = true bytes.workspace = true -aes-gcm = { version = "0.10.3", features = ["std"] } -url = "2.5.0" +url.workspace = true rcgen.workspace = true -sec1 = { version = "0.7.3", features = ["std"] } -p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } -aes = "0.8.4" +sec1.workspace = true +p256.workspace = true +aes.workspace = true +aes-gcm.workspace = true rand.workspace = true serde.workspace = true @@ -43,4 +42,3 @@ winapi = { version = "0.3.9", features = [ "winerror", "ws2ipdef", ] } - diff --git a/rtc-shared/src/crypto/mod.rs b/rtc-shared/src/crypto/mod.rs deleted file mode 100644 index 0084e95a..00000000 --- a/rtc-shared/src/crypto/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::error::Result; - -/// KeyingMaterialExporter to extract keying material. -/// -/// This trait sits here to avoid getting a direct dependency between -/// the dtls and srtp crates. -pub trait KeyingMaterialExporter { - /// Derives keying material from the established session, per RFC 5705. - /// - /// `label` and `context` bind the derived key to a purpose — DTLS-SRTP uses the - /// `EXTRACTOR-dtls_srtp` label to obtain SRTP master keys and salts from a completed - /// DTLS handshake. - /// - /// # Errors - /// - /// Fails if the session has not completed its handshake, so no secret is available yet. - fn export_keying_material(&self, label: &str, context: &[u8], length: usize) - -> Result>; -} diff --git a/rtc-shared/src/lib.rs b/rtc-shared/src/lib.rs index d1f9526a..ec624af6 100644 --- a/rtc-shared/src/lib.rs +++ b/rtc-shared/src/lib.rs @@ -57,10 +57,6 @@ #[macro_use] extern crate bitflags; -#[cfg(feature = "crypto")] -/// Cryptographic primitives shared by DTLS and SRTP, including DTLS-SRTP keying-material export. -pub mod crypto; - #[cfg(feature = "ifaces")] /// Local network interface enumeration, used to gather ICE host candidates. pub mod ifaces; diff --git a/rtc-srtp/Cargo.toml b/rtc-srtp/Cargo.toml index d3ac550f..fecc7b54 100644 --- a/rtc-srtp/Cargo.toml +++ b/rtc-srtp/Cargo.toml @@ -17,7 +17,7 @@ ring = ["crypto/ring"] aws-lc-rs = ["crypto/aws-lc-rs"] [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } +shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } rtp.workspace = true rtcp.workspace = true crypto.workspace = true @@ -27,7 +27,7 @@ bytes.workspace = true [dev-dependencies] criterion.workspace = true -lazy_static = "1.4.0" +lazy_static.workspace = true [[bench]] name = "bench" diff --git a/rtc-srtp/src/config.rs b/rtc-srtp/src/config.rs index fb058ea6..a4c5c7d1 100644 --- a/rtc-srtp/src/config.rs +++ b/rtc-srtp/src/config.rs @@ -1,7 +1,8 @@ use crate::{option::*, protection_profile::*}; -use shared::{crypto::KeyingMaterialExporter, error::Result}; +use shared::error::{Error, Result}; -const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; +/// RFC 5764 exporter label used to derive SRTP master keys and salts from DTLS. +pub const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; /// SessionKeys bundles the keys required to setup an SRTP session #[derive(Default, Debug, Clone)] @@ -17,8 +18,8 @@ pub struct SessionKeys { } /// Config is used to configure a session. -/// You can provide either a KeyingMaterialExporter to export keys -/// or directly pass the keys themselves. +/// The top-level integration exports keying material from DTLS and installs it here, or callers +/// can directly pass the keys themselves. /// After a Config is passed to a session it must not be modified. #[derive(Default)] pub struct Config { @@ -41,23 +42,36 @@ pub struct Config { } impl Config { - /// ExtractSessionKeysFromDTLS allows setting the Config SessionKeys by - /// extracting them from DTLS. This behavior is defined in RFC5764: - /// - pub fn extract_session_keys_from_dtls( + /// Returns the exact number of DTLS exporter bytes required by this profile. + #[must_use] + pub fn keying_material_len(&self) -> usize { + let key_len = self.profile.key_len(); + let salt_len = self.profile.salt_len(); + (key_len * 2) + (salt_len * 2) + } + + /// Splits DTLS-SRTP exporter output into local and remote master keys and salts according to + /// RFC 5764. + /// + /// # Errors + /// + /// Returns an error unless `keying_material` has exactly [`Self::keying_material_len`] bytes. + pub fn set_session_keys_from_keying_material( &mut self, - exporter: &impl KeyingMaterialExporter, + keying_material: &[u8], is_client: bool, ) -> Result<()> { + let expected = self.keying_material_len(); + if keying_material.len() != expected { + return Err(Error::Other(format!( + "invalid DTLS-SRTP keying material length: expected {expected}, got {}", + keying_material.len() + ))); + } + let key_len = self.profile.key_len(); let salt_len = self.profile.salt_len(); - let keying_material = exporter.export_keying_material( - LABEL_EXTRACTOR_DTLS_SRTP, - &[], - (key_len * 2) + (salt_len * 2), - )?; - let mut offset = 0; let client_write_key = keying_material[offset..offset + key_len].to_vec(); offset += key_len; @@ -85,3 +99,60 @@ impl Config { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn material(len: usize) -> Vec { + (0..len).map(|value| value as u8).collect() + } + + #[test] + fn rejects_keying_material_with_the_wrong_length() { + let mut config = Config { + profile: ProtectionProfile::Aes128CmHmacSha1_80, + ..Default::default() + }; + let expected = config.keying_material_len(); + + for actual in [expected - 1, expected + 1] { + let error = config + .set_session_keys_from_keying_material(&material(actual), true) + .unwrap_err(); + assert!( + error + .to_string() + .contains(&format!("expected {expected}, got {actual}")) + ); + } + } + + #[test] + fn assigns_client_and_server_material_by_role() -> Result<()> { + let mut client = Config { + profile: ProtectionProfile::Aes128CmHmacSha1_80, + ..Default::default() + }; + let bytes = material(client.keying_material_len()); + client.set_session_keys_from_keying_material(&bytes, true)?; + + let mut server = Config { + profile: client.profile, + ..Default::default() + }; + server.set_session_keys_from_keying_material(&bytes, false)?; + + assert_eq!(client.keys.local_master_key, server.keys.remote_master_key); + assert_eq!( + client.keys.local_master_salt, + server.keys.remote_master_salt + ); + assert_eq!(client.keys.remote_master_key, server.keys.local_master_key); + assert_eq!( + client.keys.remote_master_salt, + server.keys.local_master_salt + ); + Ok(()) + } +} diff --git a/rtc-stun/Cargo.toml b/rtc-stun/Cargo.toml index 04154c90..51a48b6a 100644 --- a/rtc-stun/Cargo.toml +++ b/rtc-stun/Cargo.toml @@ -23,11 +23,11 @@ sansio.workspace = true crypto.workspace = true bytes.workspace = true -lazy_static = "1.4.0" -url = "2.5.0" +lazy_static.workspace = true +url.workspace = true rand.workspace = true -base64 = "0.22.1" -crc = "3.0.1" +base64.workspace = true +crc.workspace = true [dev-dependencies] clap.workspace = true diff --git a/rtc-turn/Cargo.toml b/rtc-turn/Cargo.toml index 5f73e9bc..b744853a 100644 --- a/rtc-turn/Cargo.toml +++ b/rtc-turn/Cargo.toml @@ -29,10 +29,10 @@ log.workspace = true [dev-dependencies] env_logger.workspace = true chrono.workspace = true -hex = "0.4.3" +hex.workspace = true clap.workspace = true criterion.workspace = true -crossbeam-channel = "0.5" +crossbeam-channel.workspace = true ctrlc.workspace = true [[bench]] diff --git a/src/lib.rs b/src/lib.rs index 16f1340a..11c7185f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -700,8 +700,8 @@ #![allow(dead_code)] pub use { - datachannel, dtls, ice, interceptor, mdns, media, rtcp, rtp, sansio, sctp, sdp, shared, srtp, - stun, turn, + crypto, datachannel, dtls, ice, interceptor, interceptor_derive, mdns, media, rtcp, rtp, + sansio, sctp, sdp, shared, srtp, stun, turn, }; pub mod data_channel; @@ -709,8 +709,3 @@ pub mod media_stream; pub mod peer_connection; pub mod rtp_transceiver; pub mod statistics; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index f7cfc66b..c31f84e3 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -79,7 +79,7 @@ //! // First run: Generate and save certificate //! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; //! let certificate = RTCCertificate::from_key_pair(key_pair)?; -//! let pem_data = certificate.serialize_pem(); +//! let pem_data = certificate.serialize_pem()?; //! fs::write("my_cert.pem", pem_data)?; //! //! // Later runs: Load existing certificate @@ -171,10 +171,6 @@ //! - Mismatched fingerprints indicate MITM attack - abort connection //! - Use out-of-band verification for high-security scenarios //! -//! # Feature Flags -//! -//! - `pem` - Enable PEM serialization/deserialization (enabled by default) -//! //! # Specifications //! //! * [W3C RTCCertificate](https://w3c.github.io/webrtc-pc/#dom-rtccertificate) @@ -183,14 +179,20 @@ //! * [RFC 8122 - WebRTC Security Architecture](https://tools.ietf.org/html/rfc8122) use std::ops::Add; +use std::sync::Arc; use std::time::{Duration, SystemTime}; +use crypto::{HashAlgorithm, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme, SigningKey}; +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] use dtls::crypto::CryptoPrivateKey; -use rcgen::{CertificateParams, KeyPair}; -use sha2::{Digest, Sha256}; +use rcgen::CertificateParams; +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] +use rcgen::KeyPair; +use rustls::pki_types::CertificateDer; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; use shared::error::{Error, Result}; +#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] use shared::util::math_rand_alpha; /// X.509 certificate used to authenticate WebRTC peer-to-peer communications. @@ -257,7 +259,7 @@ use shared::util::math_rand_alpha; /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; /// # let certificate = RTCCertificate::from_key_pair(key_pair)?; /// // Serialize certificate to PEM format (includes private key) -/// let pem_string = certificate.serialize_pem(); +/// let pem_string = certificate.serialize_pem()?; /// /// // Save to file or database... /// // std::fs::write("cert.pem", &pem_string)?; @@ -313,6 +315,75 @@ impl PartialEq for RTCCertificate { } impl RTCCertificate { + /// Generates a self-signed certificate with a provider-owned signing key. + /// + /// `params` controls X.509 formatting and validity while `provider` owns key generation and + /// signing. This keeps certificate formatting independent from the primitive backend. + pub fn generate( + provider: Arc, + scheme: SignatureScheme, + params: CertificateParams, + ) -> Result { + let signing_key = provider + .crypto() + .generate_signing_key(scheme) + .map_err(crypto_error)?; + Self::generate_from_signing_key(params, scheme, signing_key) + } + + /// Imports a PKCS#8 private key through `provider` and associates it with an existing chain. + pub fn from_pkcs8( + provider: Arc, + scheme: SignatureScheme, + certificate_chain: Vec>, + private_key_der: &[u8], + expires: SystemTime, + ) -> Result { + let signing_key = provider + .crypto() + .import_signing_key(scheme, private_key_der) + .map_err(crypto_error)?; + Ok(Self::from_signing_key( + certificate_chain, + signing_key, + expires, + )) + } + + /// Builds a certificate around an application-owned signing key, including HSM/KMS keys. + #[must_use] + pub fn from_signing_key( + certificate_chain: Vec>, + signing_key: Arc, + expires: SystemTime, + ) -> Self { + Self { + dtls_certificate: dtls::crypto::Certificate::from_signing_key( + certificate_chain, + signing_key, + ), + expires, + } + } + + fn generate_from_signing_key( + params: CertificateParams, + scheme: SignatureScheme, + signing_key: Arc, + ) -> Result { + let not_after = params.not_after; + let adapter = RcgenSigningKey::new(scheme, signing_key.clone())?; + let x509_cert = params + .self_signed(&adapter) + .map_err(|error| Error::Other(error.to_string()))?; + let expires = certificate_expiration(not_after); + Ok(Self::from_signing_key( + vec![x509_cert.der().to_owned()], + signing_key, + expires, + )) + } + /// Generates a new certificate from custom parameters. /// /// This is an internal method used to create certificates with specific configuration. @@ -333,21 +404,19 @@ impl RTCCertificate { /// /// On ARM architectures, certificate expiration is capped at 48 hours due to /// overflow issues with SystemTime arithmetic. - fn from_params(params: CertificateParams, key_pair: KeyPair) -> Result { + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + fn from_params( + params: CertificateParams, + key_pair: KeyPair, + provider: Arc, + ) -> Result { let not_after = params.not_after; let x509_cert = params .self_signed(&key_pair) .map_err(|error| Error::Other(error.to_string()))?; - let private_key = CryptoPrivateKey::from_key_pair(&key_pair)?; - - let expires = if cfg!(target_arch = "arm") { - // Workaround for issue overflow when adding duration to instant on armv7 - // https://github.com/webrtc-rs/examples/issues/5 https://github.com/chronotope/chrono/issues/343 - SystemTime::now().add(Duration::from_secs(172800)) //60*60*48 or 2 days - } else { - not_after.into() - }; + let private_key = CryptoPrivateKey::from_key_pair_with_provider(&key_pair, provider)?; + let expires = certificate_expiration(not_after); Ok(Self { dtls_certificate: dtls::crypto::Certificate { @@ -391,7 +460,18 @@ impl RTCCertificate { /// # Ok(()) /// # } /// ``` + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] pub fn from_key_pair(key_pair: KeyPair) -> Result { + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::from_key_pair_with_provider(key_pair, provider) + } + + /// Imports an `rcgen` key pair through an explicit provider compatibility adapter. + #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + pub fn from_key_pair_with_provider( + key_pair: KeyPair, + provider: Arc, + ) -> Result { if !(key_pair.is_compatible(&rcgen::PKCS_ED25519) || key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) || key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256)) @@ -402,6 +482,7 @@ impl RTCCertificate { RTCCertificate::from_params( CertificateParams::new(vec![math_rand_alpha(16)]).unwrap(), key_pair, + provider, ) } @@ -436,7 +517,7 @@ impl RTCCertificate { /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; /// # let original = RTCCertificate::from_key_pair(key_pair)?; /// // Load certificate from PEM string - /// # let pem_str = original.serialize_pem(); + /// # let pem_str = original.serialize_pem()?; /// let certificate = RTCCertificate::from_pem(&pem_str)?; /// /// // Certificate is ready to use @@ -446,6 +527,15 @@ impl RTCCertificate { /// # } /// ``` pub fn from_pem(pem_str: &str) -> Result { + let provider = crypto::default_provider().map_err(crypto_error)?; + Self::from_pem_with_provider(pem_str, provider) + } + + /// Parses PEM and imports its private key through an explicit provider. + pub fn from_pem_with_provider( + pem_str: &str, + provider: Arc, + ) -> Result { let mut pem_blocks = pem_str.split("\n\n"); let first_block = if let Some(b) = pem_blocks.next() { b @@ -469,8 +559,10 @@ impl RTCCertificate { } else { return Err(Error::InvalidPEM("failed to calculate SystemTime".into())); }; - let dtls_certificate = - dtls::crypto::Certificate::from_pem(&pem_blocks.collect::>().join("\n\n"))?; + let dtls_certificate = dtls::crypto::Certificate::from_pem_with_provider( + &pem_blocks.collect::>().join("\n\n"), + provider, + )?; Ok(RTCCertificate::from_existing(dtls_certificate, expires)) } @@ -520,7 +612,7 @@ impl RTCCertificate { /// /// Produces a PEM-encoded string containing both the certificate and its private /// key in PKCS#8 format. The output can be safely stored and later loaded with - /// `from_pem` (requires the `pem` feature). + /// `from_pem`. /// /// # Security Warning /// @@ -543,7 +635,7 @@ impl RTCCertificate { /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; /// # let certificate = RTCCertificate::from_key_pair(key_pair)?; /// // Serialize for storage - /// let pem_string = certificate.serialize_pem(); + /// let pem_string = certificate.serialize_pem()?; /// /// // Save to secure storage /// // std::fs::write("private/cert.pem", &pem_string)?; @@ -554,7 +646,7 @@ impl RTCCertificate { /// # Ok(()) /// # } /// ``` - pub fn serialize_pem(&self) -> String { + pub fn serialize_pem(&self) -> Result { // Encode `expires` as a PEM block. // // TODO: serialize as nanos when https://github.com/rust-lang/rust/issues/103332 is fixed. @@ -567,13 +659,11 @@ impl RTCCertificate { .to_le_bytes() .to_vec(), ); - format!( + Ok(format!( "{}\n{}", pem::encode(&expires_pem), - self.dtls_certificate - .serialize_pem() - .expect("RTCCertificate keys are exportable") - ) + self.dtls_certificate.serialize_pem()? + )) } /// Returns SHA-256 fingerprints of the certificate chain. @@ -615,12 +705,23 @@ impl RTCCertificate { /// # } /// ``` pub fn get_fingerprints(&self) -> Vec { + let provider = crypto::default_provider().expect("a default crypto provider is required"); + self.get_fingerprints_with_provider(provider) + .expect("the default crypto provider supports SHA-256") + } + + /// Returns SHA-256 fingerprints computed by an explicit crypto provider. + pub fn get_fingerprints_with_provider( + &self, + provider: Arc, + ) -> Result> { let mut fingerprints = Vec::new(); for c in &self.dtls_certificate.certificate { - let mut h = Sha256::new(); - h.update(c.as_ref()); - let hashed = h.finalize(); + let hashed = provider + .crypto() + .hash(HashAlgorithm::Sha256, c.as_ref()) + .map_err(crypto_error)?; let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); fingerprints.push(RTCDtlsFingerprint { @@ -629,21 +730,142 @@ impl RTCCertificate { }); } - fingerprints + Ok(fingerprints) + } +} + +fn crypto_error(error: crypto::CryptoError) -> Error { + Error::Crypto(error.to_string()) +} + +fn certificate_expiration(not_after: impl Into) -> SystemTime { + if cfg!(target_arch = "arm") { + // Workaround for issue overflow when adding duration to instant on armv7. + SystemTime::now().add(Duration::from_secs(172800)) + } else { + not_after.into() } } -#[cfg(test)] +struct RcgenSigningKey { + scheme: SignatureScheme, + algorithm: &'static rcgen::SignatureAlgorithm, + signing_key: Arc, + public_key: Vec, +} + +impl RcgenSigningKey { + fn new(scheme: SignatureScheme, signing_key: Arc) -> Result { + let algorithm = match scheme { + SignatureScheme::Ed25519 => &rcgen::PKCS_ED25519, + SignatureScheme::EcdsaP256Sha256 => &rcgen::PKCS_ECDSA_P256_SHA256, + SignatureScheme::EcdsaP384Sha384 => &rcgen::PKCS_ECDSA_P384_SHA384, + SignatureScheme::RsaPkcs1Sha256 => &rcgen::PKCS_RSA_SHA256, + SignatureScheme::RsaPkcs1Sha384 => &rcgen::PKCS_RSA_SHA384, + SignatureScheme::RsaPkcs1Sha512 => &rcgen::PKCS_RSA_SHA512, + _ => { + return Err(Error::Crypto(format!( + "certificate generation does not support {scheme:?}" + ))); + } + }; + if !signing_key.supports(scheme) { + return Err(Error::Crypto(format!( + "signing key does not support {scheme:?}" + ))); + } + let public_key = signing_key.public_key(); + let public_key = match public_key.encoding { + PublicKeyEncoding::SubjectPublicKeyInfoDer => { + use x509_parser::prelude::FromDer; + let (remaining, subject_public_key_info) = + x509_parser::x509::SubjectPublicKeyInfo::from_der(public_key.bytes) + .map_err(|error| Error::Other(error.to_string()))?; + if !remaining.is_empty() { + return Err(Error::Other( + "trailing bytes in SubjectPublicKeyInfo".to_owned(), + )); + } + subject_public_key_info.subject_public_key.data.to_vec() + } + PublicKeyEncoding::EcUncompressedPoint + | PublicKeyEncoding::Ed25519Raw + | PublicKeyEncoding::RsaPkcs1Der => public_key.bytes.to_vec(), + _ => { + return Err(Error::Crypto(format!( + "certificate generation does not support public-key encoding {:?}", + public_key.encoding + ))); + } + }; + Ok(Self { + scheme, + algorithm, + signing_key, + public_key, + }) + } +} + +impl rcgen::PublicKeyData for RcgenSigningKey { + fn der_bytes(&self) -> &[u8] { + &self.public_key + } + + fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm { + self.algorithm + } +} + +impl rcgen::SigningKey for RcgenSigningKey { + fn sign(&self, message: &[u8]) -> std::result::Result, rcgen::Error> { + self.signing_key + .sign(self.scheme, message) + .map_err(|_| rcgen::Error::RemoteKeyError) + } +} + +#[cfg(all(test, any(feature = "ring", feature = "aws-lc-rs")))] mod test { use super::*; + struct NonExportableSigningKey(Arc); + + impl SigningKey for NonExportableSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.0.supports(scheme) + } + + fn public_key(&self) -> crypto::PublicKey<'_> { + self.0.public_key() + } + + fn sign( + &self, + scheme: SignatureScheme, + message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { + self.0.sign(scheme, message) + } + } + + fn provider_certificate(provider: Arc) -> Result { + RTCCertificate::generate( + provider, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()])?, + ) + } + #[test] fn test_generate_certificate_rsa() -> Result<()> { - let key_pair = KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256); - #[cfg(feature = "ring")] - assert!(key_pair.is_err(), "RcgenError::KeyGenerationUnavailable"); - #[cfg(feature = "aws-lc-rs")] - let _cert = RTCCertificate::from_key_pair(key_pair?)?; + match KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256) { + Ok(key_pair) => { + let _certificate = RTCCertificate::from_key_pair(key_pair)?; + } + Err(rcgen::Error::KeyGenerationUnavailable) => {} + Err(error) => return Err(Error::Other(error.to_string())), + } Ok(()) } @@ -693,11 +915,67 @@ mod test { let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; let cert = RTCCertificate::from_key_pair(kp)?; - let pem = cert.serialize_pem(); + let pem = cert.serialize_pem()?; let loaded_cert = RTCCertificate::from_pem(&pem)?; assert_eq!(loaded_cert, cert); Ok(()) } + + #[cfg(feature = "ring")] + #[test] + fn ring_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { + provider_certificate_round_trip(Arc::new(crypto::providers::RingProvider::new())) + } + + #[cfg(feature = "aws-lc-rs")] + #[test] + fn aws_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { + provider_certificate_round_trip(Arc::new(crypto::providers::AwsLcRsProvider::new())) + } + + fn provider_certificate_round_trip(provider: Arc) -> Result<()> { + let certificate = provider_certificate(provider.clone())?; + let fingerprints = certificate.get_fingerprints_with_provider(provider.clone())?; + assert_eq!(fingerprints.len(), 1); + assert_eq!(fingerprints[0].algorithm, "sha-256"); + + let pem = certificate.serialize_pem()?; + let imported = RTCCertificate::from_pem_with_provider(&pem, provider.clone())?; + assert_eq!(imported, certificate); + + let private_key = certificate + .dtls_certificate + .private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .expect("built-in generated keys are exportable"); + let imported = RTCCertificate::from_pkcs8( + provider, + SignatureScheme::EcdsaP256Sha256, + certificate.dtls_certificate.certificate.clone(), + private_key.as_ref(), + certificate.expires, + )?; + assert_eq!(imported, certificate); + Ok(()) + } + + #[test] + fn non_exportable_signing_key_returns_an_explicit_pem_error() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; + let certificate = provider_certificate(provider)?; + let signing_key = certificate.dtls_certificate.private_key.signing_key.clone(); + let certificate = RTCCertificate::from_signing_key( + certificate.dtls_certificate.certificate, + Arc::new(NonExportableSigningKey(signing_key)), + certificate.expires, + ); + + let error = certificate.serialize_pem().unwrap_err(); + assert!(error.to_string().contains("not exportable")); + Ok(()) + } } diff --git a/src/peer_connection/configuration/mod.rs b/src/peer_connection/configuration/mod.rs index 0c2b6ac2..9ba6dd02 100644 --- a/src/peer_connection/configuration/mod.rs +++ b/src/peer_connection/configuration/mod.rs @@ -205,7 +205,6 @@ use crate::peer_connection::certificate::RTCCertificate; pub use crate::peer_connection::transport::ice::server::RTCIceServer; -use rcgen::KeyPair; use shared::error::{Error, Result}; use std::time::SystemTime; @@ -389,11 +388,7 @@ impl RTCConfiguration { .duration_since(now) .map_err(|_| Error::ErrCertificateExpired)?; } - } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - self.certificates = vec![cert]; - }; + } Ok(()) } diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 6cdfc80e..484e03c8 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -108,6 +108,7 @@ use std::net::IpAddr; use std::sync::Arc; +use crypto::RTCCryptoProvider; use dtls::cipher_suite::CipherSuiteId; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; //TODO: use ice::agent::agent_config::{InterfaceFilterFn, IpFilterFn}; @@ -332,6 +333,7 @@ impl Default for SctpMaxMessageSize { /// - [RFC 8445 - ICE](https://datatracker.ietf.org/doc/html/rfc8445) #[derive(Default, Clone)] pub struct SettingEngine { + pub(crate) crypto_provider: Option>, pub(crate) timeout: Timeout, pub(crate) candidates: Candidates, pub(crate) multicast_dns: MulticastDNS, @@ -361,6 +363,15 @@ pub struct SettingEngine { } impl SettingEngine { + /// Selects the cryptographic provider used by peer connections built with this setting engine. + /// + /// The provider is resolved once at peer-connection construction and shared with ICE, DTLS, + /// SRTP, certificate, and fingerprint operations. Different peer connections may select + /// different providers in the same process. + pub fn set_crypto_provider(&mut self, provider: Arc) { + self.crypto_provider = Some(provider); + } + /// Returns the configured receive MTU, or the default if not set. pub(crate) fn get_receive_mtu(&self) -> usize { if self.receive_mtu != 0 { diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index 560bb22d..63736317 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -12,7 +12,6 @@ use dtls::endpoint::EndpointEvent; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; use dtls::state::State; use log::{debug, warn}; -use sha2::{Digest, Sha256}; use shared::TransportContext; use shared::error::{Error, Result}; use srtp::option::{srtcp_replay_protection, srtp_replay_protection}; @@ -62,7 +61,7 @@ impl<'a> DtlsHandler<'a> { srtp_profile: SrtpProtectionProfile, peer_certificates: &[Vec], dtls_cipher: Option, - ) { + ) -> Result<()> { // Update transport DTLS state self.stats .transport @@ -91,7 +90,8 @@ impl<'a> DtlsHandler<'a> { // Register local certificate and set local_certificate_id if let Some(local_cert) = self.ctx.dtls_transport.certificates.first() { - let fingerprints = local_cert.get_fingerprints(); + let fingerprints = local_cert + .get_fingerprints_with_provider(self.ctx.dtls_transport.crypto_provider.clone())?; if let Some(fp) = fingerprints.first() { // Register certificate in accumulator // Use hex encoding for certificate (base64 would need additional dependency) @@ -115,9 +115,13 @@ impl<'a> DtlsHandler<'a> { // Register remote certificate and set remote_certificate_id if let Some(peer_cert_der) = peer_certificates.first() { // Compute fingerprint from peer certificate - let mut hasher = Sha256::new(); - hasher.update(peer_cert_der); - let hash = hasher.finalize(); + let hash = self + .ctx + .dtls_transport + .crypto_provider + .crypto() + .hash(crypto::HashAlgorithm::Sha256, peer_cert_der) + .map_err(|error| Error::Crypto(error.to_string()))?; let fingerprint: String = hash .iter() .map(|b| format!("{:02x}", b)) @@ -140,6 +144,7 @@ impl<'a> DtlsHandler<'a> { // Set remote certificate ID in transport stats self.stats.transport.remote_certificate_id = cert_id; } + Ok(()) } } @@ -239,7 +244,7 @@ impl<'a> sansio::Protocol DtlsHandler<'a> { )); } - srtp_config.extract_session_keys_from_dtls(state, state.is_client())?; + let keying_material = state.export_keying_material( + srtp::config::LABEL_EXTRACTOR_DTLS_SRTP, + &[], + srtp_config.keying_material_len(), + )?; + srtp_config + .set_session_keys_from_keying_material(keying_material.as_ref(), state.is_client())?; + let crypto_provider = state.crypto_provider()?; - let local_context = srtp::context::Context::new( + let local_context = srtp::context::Context::new_with_provider( &srtp_config.keys.local_master_key, &srtp_config.keys.local_master_salt, srtp_config.profile, srtp_config.local_rtp_options, srtp_config.local_rtcp_options, + crypto_provider.clone(), )?; - let remote_context = srtp::context::Context::new( + let remote_context = srtp::context::Context::new_with_provider( &srtp_config.keys.remote_master_key, &srtp_config.keys.remote_master_salt, srtp_config.profile, @@ -426,6 +439,7 @@ impl<'a> DtlsHandler<'a> { } else { srtp_config.remote_rtcp_options }, + crypto_provider, )?; Ok((local_context, remote_context)) diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index 8cd50fd6..9d266fde 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -29,11 +29,21 @@ where pub(super) fn new( mut configuration: RTCConfiguration, media_engine: MediaEngine, - setting_engine: SettingEngine, + mut setting_engine: SettingEngine, interceptor: I, ) -> Result { configuration.validate()?; + let crypto_provider = match setting_engine.crypto_provider.clone() { + Some(provider) => provider, + None => crypto::default_provider().map_err(|error| { + Error::Crypto(format!( + "peer connection requires a crypto provider: {error}; configure one with SettingEngine::set_crypto_provider" + )) + })?, + }; + setting_engine.crypto_provider = Some(crypto_provider.clone()); + let mut candidate_types = vec![]; if setting_engine.candidates.ice_lite { candidate_types.push(ice::candidate::CandidateType::Host); @@ -83,19 +93,22 @@ where }; // Create the ICE transport - let ice_transport = RTCIceTransport::new(agent_config)?; + let ice_transport = RTCIceTransport::new(agent_config, crypto_provider.clone())?; // Create the DTLS transport let certificates = configuration.certificates.drain(..).collect(); - let dtls_transport = RTCDtlsTransport::new( + let dtls_transport = RTCDtlsTransport::new(RTCDtlsTransportConfig { certificates, - setting_engine.answering_dtls_role, - setting_engine.srtp_protection_profiles.clone(), - setting_engine.dtls_cipher_suites.clone(), - setting_engine.allow_insecure_verification_algorithm, - setting_engine.disable_certificate_fingerprint_verification, - setting_engine.replay_protection, - )?; + answering_dtls_role: setting_engine.answering_dtls_role, + srtp_protection_profiles: setting_engine.srtp_protection_profiles.clone(), + dtls_cipher_suites: setting_engine.dtls_cipher_suites.clone(), + allow_insecure_verification_algorithm: setting_engine + .allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification: setting_engine + .disable_certificate_fingerprint_verification, + replay_protection: setting_engine.replay_protection, + crypto_provider, + })?; // Create the SCTP transport let sctp_transport = RTCSctpTransport::new( @@ -177,7 +190,7 @@ where } let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints() + cert.get_fingerprints_with_provider(self.dtls_transport().crypto_provider.clone())? } else { return Err(Error::ErrNonCertificate); }; @@ -322,7 +335,7 @@ where }; let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints() + cert.get_fingerprints_with_provider(self.dtls_transport().crypto_provider.clone())? } else { return Err(Error::ErrNonCertificate); }; diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index f6615fdf..f0f1b6b8 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -275,12 +275,12 @@ use crate::peer_connection::state::peer_connection_state::{ NegotiationNeededState, RTCPeerConnectionState, }; use crate::peer_connection::state::signaling_state::{RTCSignalingState, StateChangeOp}; -use crate::peer_connection::transport::dtls::RTCDtlsTransport; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters; use crate::peer_connection::transport::dtls::role::{ DEFAULT_DTLS_ROLE_ANSWER, DEFAULT_DTLS_ROLE_OFFER, RTCDtlsRole, }; +use crate::peer_connection::transport::dtls::{RTCDtlsTransport, RTCDtlsTransportConfig}; use crate::peer_connection::transport::ice::RTCIceTransport; use crate::peer_connection::transport::ice::candidate::RTCIceCandidateInit; use crate::peer_connection::transport::ice::parameters::RTCIceParameters; diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 51081036..5383297b 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -4,12 +4,12 @@ use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters; use crate::peer_connection::transport::dtls::role::{DEFAULT_DTLS_ROLE_ANSWER, RTCDtlsRole}; use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState; use crate::peer_connection::transport::ice::role::RTCIceRole; +use crypto::{HashAlgorithm, RTCCryptoProvider}; use dtls::cipher_suite::CipherSuiteId; use dtls::config::{ClientAuthType, VerifyPeerCertificateFn}; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; -use rcgen::KeyPair; +use rcgen::CertificateParams; use rustls::pki_types::CertificateDer; -use sha2::{Digest, Sha256}; use shared::error::{Error, Result}; use shared::{TransportContext, TransportProtocol}; use std::sync::Arc; @@ -33,8 +33,8 @@ pub(crate) fn default_srtp_protection_profiles() -> Vec { /// transport over which RTP and RTCP packets are sent and received by /// RTPSender and RTPReceiver, as well other data such as SCTP packets sent /// and received by data channels. -#[derive(Default)] pub(crate) struct RTCDtlsTransport { + pub(crate) crypto_provider: Arc, pub(crate) dtls_role: RTCDtlsRole, pub(crate) dtls_handshake_config: Option>, pub(crate) dtls_endpoint: Option<::dtls::endpoint::Endpoint>, @@ -53,16 +53,49 @@ pub(crate) struct RTCDtlsTransport { pub(crate) replay_protection: ReplayProtection, } +pub(crate) struct RTCDtlsTransportConfig { + pub(crate) certificates: Vec, + pub(crate) answering_dtls_role: RTCDtlsRole, + pub(crate) srtp_protection_profiles: Vec, + pub(crate) dtls_cipher_suites: Vec, + pub(crate) allow_insecure_verification_algorithm: bool, + pub(crate) disable_certificate_fingerprint_verification: bool, + pub(crate) replay_protection: ReplayProtection, + pub(crate) crypto_provider: Arc, +} + +impl Default for RTCDtlsTransport { + fn default() -> Self { + Self { + crypto_provider: crypto::default_provider() + .expect("a default crypto provider is required for RTCDtlsTransport::default"), + dtls_role: RTCDtlsRole::default(), + dtls_handshake_config: None, + dtls_endpoint: None, + state: RTCDtlsTransportState::default(), + certificates: Vec::new(), + answering_dtls_role: RTCDtlsRole::default(), + srtp_protection_profiles: Vec::new(), + dtls_cipher_suites: Vec::new(), + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: ReplayProtection::default(), + } + } +} + impl RTCDtlsTransport { - pub(crate) fn new( - mut certificates: Vec, - answering_dtls_role: RTCDtlsRole, - srtp_protection_profiles: Vec, - dtls_cipher_suites: Vec, - allow_insecure_verification_algorithm: bool, - disable_certificate_fingerprint_verification: bool, - replay_protection: ReplayProtection, - ) -> Result { + pub(crate) fn new(config: RTCDtlsTransportConfig) -> Result { + let RTCDtlsTransportConfig { + mut certificates, + answering_dtls_role, + srtp_protection_profiles, + dtls_cipher_suites, + allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification, + replay_protection, + crypto_provider, + } = config; if !certificates.is_empty() { let now = SystemTime::now(); for cert in &certificates { @@ -71,8 +104,13 @@ impl RTCDtlsTransport { .map_err(|_| Error::ErrCertificateExpired)?; } } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let params = CertificateParams::new(vec![shared::util::math_rand_alpha(16)]) + .map_err(|error| Error::Other(error.to_string()))?; + let cert = RTCCertificate::generate( + crypto_provider.clone(), + crypto::SignatureScheme::EcdsaP256Sha256, + params, + )?; certificates = vec![cert]; }; @@ -89,6 +127,7 @@ impl RTCDtlsTransport { allow_insecure_verification_algorithm, disable_certificate_fingerprint_verification, replay_protection, + crypto_provider, }) } @@ -141,6 +180,7 @@ impl RTCDtlsTransport { // need this. libp2p's WebRTC-Direct is the canonical case: the server synthesizes the // client's offer locally with a placeholder fingerprint and authenticates the peer // afterwards with a Noise handshake over the data channel. + let fingerprint_crypto = self.crypto_provider.clone(); let verify_peer_certificate: Option = if !self.disable_certificate_fingerprint_verification { Some(Arc::new( @@ -154,9 +194,10 @@ impl RTCDtlsTransport { return Err(Error::ErrUnsupportedFingerprintAlgorithm); } - let mut h = Sha256::new(); - h.update(&certs[0]); - let hashed = h.finalize(); + let hashed = fingerprint_crypto + .crypto() + .hash(HashAlgorithm::Sha256, &certs[0]) + .map_err(|error| Error::Crypto(error.to_string()))?; let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); let remote_value = values.join(":").to_lowercase(); @@ -180,14 +221,13 @@ impl RTCDtlsTransport { }; self.state_change(RTCDtlsTransportState::Connecting); + let profiles = self.supported_srtp_protection_profiles()?; + Ok(Arc::new( ::dtls::config::ConfigBuilder::default() + .with_crypto_provider(self.crypto_provider.clone()) .with_certificates(vec![certificate]) - .with_srtp_protection_profiles(if !self.srtp_protection_profiles.is_empty() { - self.srtp_protection_profiles.clone() - } else { - default_srtp_protection_profiles() - }) + .with_srtp_protection_profiles(profiles) // Empty leaves `dtls`'s default set in place; a non-empty list replaces it. .with_cipher_suites(self.dtls_cipher_suites.clone()) .with_client_auth(ClientAuthType::RequireAnyClientCert) @@ -200,6 +240,31 @@ impl RTCDtlsTransport { )) } + fn supported_srtp_protection_profiles(&self) -> Result> { + let configured = if self.srtp_protection_profiles.is_empty() { + default_srtp_protection_profiles() + } else { + self.srtp_protection_profiles.clone() + }; + let supported: Vec<_> = configured + .into_iter() + .filter(|profile| { + srtp_profile(*profile).is_some_and(|profile| { + profile + .ensure_crypto_supported(self.crypto_provider.crypto()) + .is_ok() + }) + }) + .collect(); + if supported.is_empty() { + return Err(Error::Crypto(format!( + "crypto provider {} supports none of the configured SRTP protection profiles", + self.crypto_provider.name() + ))); + } + Ok(supported) + } + pub(crate) fn role(&self) -> RTCDtlsRole { self.dtls_role } @@ -236,6 +301,23 @@ impl RTCDtlsTransport { } } +fn srtp_profile( + profile: SrtpProtectionProfile, +) -> Option { + use srtp::protection_profile::ProtectionProfile; + match profile { + SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm => Some(ProtectionProfile::AeadAes128Gcm), + SrtpProtectionProfile::Srtp_Aead_Aes_256_Gcm => Some(ProtectionProfile::AeadAes256Gcm), + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80 => { + Some(ProtectionProfile::Aes128CmHmacSha1_80) + } + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32 => { + Some(ProtectionProfile::Aes128CmHmacSha1_32) + } + _ => None, + } +} + #[cfg(test)] mod tests { //! Cipher-suite plumbing for issue #808. @@ -246,17 +328,46 @@ mod tests { use super::*; use crate::peer_connection::configuration::setting_engine::ReplayProtection; + use crypto::{AeadAlgorithm, CryptoAlgorithm, RTCCrypto, RTCRandom}; + + struct MissingAes128Gcm; + + impl RTCCrypto for MissingAes128Gcm { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + algorithm != CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm) + } + } + + struct ProfileFilteringProvider { + random_provider: Arc, + crypto: MissingAes128Gcm, + } + + impl RTCCryptoProvider for ProfileFilteringProvider { + fn name(&self) -> &'static str { + "missing-aes-128-gcm" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + self.random_provider.random() + } + } fn transport(dtls_cipher_suites: Vec) -> RTCDtlsTransport { - RTCDtlsTransport::new( - vec![], - DEFAULT_DTLS_ROLE_ANSWER, - vec![], + RTCDtlsTransport::new(RTCDtlsTransportConfig { + certificates: vec![], + answering_dtls_role: DEFAULT_DTLS_ROLE_ANSWER, + srtp_protection_profiles: vec![], dtls_cipher_suites, - false, - false, - ReplayProtection::default(), - ) + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: ReplayProtection::default(), + crypto_provider: crypto::default_provider().expect("test crypto provider"), + }) .expect("a self-signed ECDSA certificate is generated when none is supplied") } @@ -305,4 +416,46 @@ mod tests { "expected a cipher-suite error, got: {err}" ); } + + #[test] + fn filters_srtp_profiles_by_provider_capabilities() -> Result<()> { + let default_provider = crypto::default_provider().expect("test crypto provider"); + let certificate = RTCCertificate::generate( + default_provider.clone(), + crypto::SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()])?, + )?; + let provider: Arc = Arc::new(ProfileFilteringProvider { + random_provider: default_provider, + crypto: MissingAes128Gcm, + }); + + let build = |profiles| { + RTCDtlsTransport::new(RTCDtlsTransportConfig { + certificates: vec![certificate.clone()], + answering_dtls_role: DEFAULT_DTLS_ROLE_ANSWER, + srtp_protection_profiles: profiles, + dtls_cipher_suites: vec![], + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: ReplayProtection::default(), + crypto_provider: provider.clone(), + }) + }; + + let error = build(vec![SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm])? + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .expect_err("the provider's unsupported profile must not be advertised"); + assert!(error.to_string().contains("supports none")); + + assert!( + build(vec![ + SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + ])? + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + Ok(()) + } } diff --git a/src/peer_connection/transport/ice/mod.rs b/src/peer_connection/transport/ice/mod.rs index 5a9c29e0..0d0b4830 100644 --- a/src/peer_connection/transport/ice/mod.rs +++ b/src/peer_connection/transport/ice/mod.rs @@ -4,6 +4,7 @@ use crate::peer_connection::transport::ice::candidate::RTCIceCandidate; use crate::peer_connection::transport::ice::parameters::RTCIceParameters; use crate::peer_connection::transport::ice::role::RTCIceRole; use crate::peer_connection::transport::ice::state::RTCIceTransportState; +use crypto::RTCCryptoProvider; use ice::candidate::Candidate; use ice::tcp_type::TcpType; use ice::{Agent, AgentConfig}; @@ -31,8 +32,11 @@ pub(crate) struct RTCIceTransport { impl RTCIceTransport { /// creates a new RTCIceTransport - pub(crate) fn new(agent_config: AgentConfig) -> Result { - let agent = Agent::new(Arc::new(agent_config))?; + pub(crate) fn new( + agent_config: AgentConfig, + crypto_provider: Arc, + ) -> Result { + let agent = Agent::new_with_provider(Arc::new(agent_config), crypto_provider)?; Ok(RTCIceTransport { agent, diff --git a/tests/crypto_provider_peer_connections.rs b/tests/crypto_provider_peer_connections.rs new file mode 100644 index 00000000..d87e9a96 --- /dev/null +++ b/tests/crypto_provider_peer_connections.rs @@ -0,0 +1,574 @@ +#![cfg(all(feature = "ring", feature = "aws-lc-rs"))] + +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{Duration, Instant}; + +use anyhow::{Result, ensure}; +use bytes::{Bytes, BytesMut}; +use rtc::crypto::providers::{AwsLcRsProvider, RingProvider}; +use rtc::crypto::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, + RTCCrypto, RTCCryptoProvider, RTCRandom, SignatureScheme, SigningKey, StreamCipher, + StreamCipherAlgorithm, +}; +use rtc::media_stream::MediaStreamTrack; +use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine}; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::state::RTCPeerConnectionState; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; +use rtc::rtp; +use rtc::rtp_transceiver::rtp_sender::{ + RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters, + RtpCodecKind, +}; +use rtc::rtp_transceiver::{RTCRtpReceiverId, RTCRtpSenderId}; +use rtc::sansio::Protocol; +use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use tokio::net::UdpSocket; + +const SESSION_TIMEOUT: Duration = Duration::from_secs(10); +const MEDIA_SSRC: u32 = 0x1122_3344; + +#[derive(Default)] +struct Calls { + random: AtomicUsize, + hash: AtomicUsize, + hmac: AtomicUsize, + aead: AtomicUsize, + aead_seal: AtomicUsize, + aead_open: AtomicUsize, + key_exchange: AtomicUsize, + signing: AtomicUsize, + verification: AtomicUsize, +} + +struct RecordingProvider { + inner: Arc, + calls: Arc, +} + +impl RecordingProvider { + fn wrap(inner: impl RTCCryptoProvider + 'static) -> (Arc, Arc) { + let calls = Arc::new(Calls::default()); + let provider: Arc = Arc::new(Self { + inner: Arc::new(inner), + calls: calls.clone(), + }); + (provider, calls) + } +} + +impl RTCCryptoProvider for RecordingProvider { + fn name(&self) -> &'static str { + self.inner.name() + } + + fn crypto(&self) -> &dyn RTCCrypto { + self + } + + fn random(&self) -> &dyn RTCRandom { + self + } +} + +impl RTCRandom for RecordingProvider { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + self.calls.random.fetch_add(1, Ordering::Relaxed); + self.inner.random().fill(output) + } +} + +impl RTCCrypto for RecordingProvider { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.inner.crypto().supports(algorithm) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + self.calls.hash.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().hash(algorithm, data) + } + + fn hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + output: &mut [u8], + ) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().hmac(algorithm, key, input, output) + } + + fn verify_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + input: &[&[u8]], + expected: &[u8], + ) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner + .crypto() + .verify_hmac(algorithm, key, input, expected) + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + self.inner.crypto().block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.inner.crypto().new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.calls.aead.fetch_add(1, Ordering::Relaxed); + Ok(Box::new(RecordingAead { + inner: self.inner.crypto().new_aead(algorithm, key)?, + calls: self.calls.clone(), + })) + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.inner.crypto().new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + self.calls.key_exchange.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().start_key_exchange(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + self.calls.signing.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().generate_signing_key(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + self.calls.signing.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().import_signing_key(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + self.calls.verification.fetch_add(1, Ordering::Relaxed); + self.inner + .crypto() + .verify_signature(scheme, public_key, message, signature) + } +} + +struct RecordingAead { + inner: Box, + calls: Arc, +} + +impl AeadCipher for RecordingAead { + fn tag_len(&self) -> usize { + self.inner.tag_len() + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + self.calls.aead_seal.fetch_add(1, Ordering::Relaxed); + self.inner + .seal_in_place(nonce, aad, plaintext_and_ciphertext, tag_out) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + self.calls.aead_open.fetch_add(1, Ordering::Relaxed); + self.inner + .open_in_place(nonce, aad, ciphertext_and_plaintext, tag) + } +} + +struct Peer { + pc: RTCPeerConnection, + socket: UdpSocket, + local_addr: SocketAddr, +} + +impl Peer { + async fn new( + provider: Arc, + send_media: bool, + ) -> Result<(Self, Option)> { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + let codec = RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90_000, + ..Default::default() + }, + payload_type: 96, + }; + let mut media_engine = MediaEngine::default(); + media_engine.register_codec(codec.clone(), RtpCodecKind::Video)?; + let mut setting_engine = SettingEngine::default(); + setting_engine.set_crypto_provider(provider); + let mut pc = RTCPeerConnectionBuilder::new() + .with_setting_engine(setting_engine) + .with_media_engine(media_engine) + .build()?; + + let sender_id = if send_media { + Some(pc.add_track(MediaStreamTrack::new( + "provider-test-stream".to_owned(), + "provider-test-video".to_owned(), + "provider test video".to_owned(), + RtpCodecKind::Video, + vec![RTCRtpEncodingParameters { + rtp_coding_parameters: RTCRtpCodingParameters { + ssrc: Some(MEDIA_SSRC), + ..Default::default() + }, + codec: codec.rtp_codec, + ..Default::default() + }], + ))?) + } else { + None + }; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + Ok(( + Self { + pc, + socket, + local_addr, + }, + sender_id, + )) + } + + async fn send_pending(&mut self) -> Result<()> { + while let Some(message) = self.pc.poll_write() { + self.socket + .send_to(&message.message, message.transport.peer_addr) + .await?; + } + Ok(()) + } + + fn receive(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + Ok(self.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?) + } +} + +async fn exercise_pair( + offer_provider: Arc, + offer_calls: Arc, + answer_provider: Arc, + answer_calls: Arc, +) -> Result<()> { + let (mut offer, sender_id) = Peer::new(offer_provider, true).await?; + let sender_id = sender_id.expect("offer peer has a media sender"); + let (mut answer, _) = Peer::new(answer_provider, false).await?; + + let description = offer.pc.create_offer(None)?; + offer.pc.set_local_description(description.clone())?; + answer.pc.set_remote_description(description)?; + let description = answer.pc.create_answer(None)?; + answer.pc.set_local_description(description.clone())?; + offer.pc.set_remote_description(description)?; + + let mut offer_connected = false; + let mut answer_connected = false; + let mut receiver_id: Option = None; + let mut sent_rtp = 0_u16; + let mut received_rtp = false; + let mut sent_rtcp = false; + let mut received_rtcp = false; + let mut rtcp_aead_baseline = None; + let mut sequence_number = 0; + let mut offer_buffer = vec![0; 2048]; + let mut answer_buffer = vec![0; 2048]; + let started = Instant::now(); + + while started.elapsed() < SESSION_TIMEOUT && !received_rtcp { + offer.send_pending().await?; + answer.send_pending().await?; + + while let Some(event) = offer.pc.poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + offer_connected = true; + } + } + while let Some(event) = answer.pc.poll_event() { + match event { + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected, + ) => answer_connected = true, + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + receiver_id = Some(init.receiver_id) + } + _ => {} + } + } + + while let Some(message) = answer.pc.poll_read() { + if matches!(message, RTCMessage::RtpPacket(_, _)) { + received_rtp = true; + } + } + while let Some(message) = offer.pc.poll_read() { + if matches!(message, RTCMessage::RtcpPacket(_, _)) { + received_rtcp = true; + } + } + if let Some((offer_open, answer_seal)) = rtcp_aead_baseline { + received_rtcp = offer_calls.aead_open.load(Ordering::Relaxed) > offer_open + && answer_calls.aead_seal.load(Ordering::Relaxed) > answer_seal; + } + + if offer_connected && answer_connected && !received_rtp && sent_rtp < 30 { + sequence_number += 1; + let payload_type = offer + .pc + .rtp_sender(sender_id) + .and_then(|mut sender| { + sender + .get_parameters() + .rtp_parameters + .codecs + .first() + .map(|codec| codec.payload_type) + }) + .unwrap_or(96); + offer + .pc + .rtp_sender(sender_id) + .expect("media sender exists") + .write_rtp(rtp::packet::Packet { + header: rtp::header::Header { + version: 2, + marker: true, + payload_type, + sequence_number, + timestamp: 90_000, + ssrc: MEDIA_SSRC, + ..Default::default() + }, + payload: Bytes::from_static(b"provider-isolation"), + })?; + sent_rtp += 1; + } + if received_rtp && !sent_rtcp { + rtcp_aead_baseline = Some(( + offer_calls.aead_open.load(Ordering::Relaxed), + answer_calls.aead_seal.load(Ordering::Relaxed), + )); + answer + .pc + .rtp_receiver(receiver_id.expect("receiver opened before RTP arrived")) + .expect("media receiver exists") + .write_rtcp(vec![Box::new(PictureLossIndication { + sender_ssrc: 0, + media_ssrc: MEDIA_SSRC, + })])?; + sent_rtcp = true; + } + + let next_timeout = offer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + SESSION_TIMEOUT) + .min( + answer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + SESSION_TIMEOUT), + ); + let delay = next_timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + if delay.is_zero() { + offer.pc.handle_timeout(Instant::now())?; + answer.pc.handle_timeout(Instant::now())?; + continue; + } + + tokio::select! { + _ = tokio::time::sleep(delay) => { + offer.pc.handle_timeout(Instant::now())?; + answer.pc.handle_timeout(Instant::now())?; + } + result = offer.socket.recv_from(&mut offer_buffer) => { + let (length, peer_addr) = result?; + offer.receive(&offer_buffer[..length], peer_addr)?; + } + result = answer.socket.recv_from(&mut answer_buffer) => { + let (length, peer_addr) = result?; + answer.receive(&answer_buffer[..length], peer_addr)?; + } + } + } + + ensure!( + offer_connected && answer_connected, + "peer connection did not reach Connected" + ); + ensure!( + received_rtp, + "answerer did not receive provider-encrypted SRTP" + ); + ensure!( + received_rtcp, + "offerer did not receive provider-encrypted SRTCP" + ); + offer.pc.close()?; + answer.pc.close()?; + Ok(()) +} + +fn assert_calls(calls: &Calls) { + assert!( + calls.random.load(Ordering::Relaxed) > 0, + "provider randomness was bypassed" + ); + assert!( + calls.hash.load(Ordering::Relaxed) > 0, + "provider fingerprint hashing was bypassed" + ); + assert!( + calls.hmac.load(Ordering::Relaxed) > 0, + "provider STUN HMAC was bypassed" + ); + assert!( + calls.aead.load(Ordering::Relaxed) > 0, + "provider DTLS/SRTP AEAD was bypassed" + ); + assert!( + calls.key_exchange.load(Ordering::Relaxed) > 0, + "provider key exchange was bypassed" + ); + assert!( + calls.signing.load(Ordering::Relaxed) > 0, + "provider signing-key generation was bypassed" + ); + assert!( + calls.verification.load(Ordering::Relaxed) > 0, + "provider signature verification was bypassed" + ); +} + +#[tokio::test] +async fn isolates_crypto_providers_across_simultaneous_peer_connections() -> Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + let (ring_a, ring_a_calls) = RecordingProvider::wrap(RingProvider::new()); + let (ring_b, ring_b_calls) = RecordingProvider::wrap(RingProvider::new()); + let (aws_a, aws_a_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (aws_b, aws_b_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + exercise_pair(ring_a, ring_a_calls.clone(), ring_b, ring_b_calls.clone()).await?; + exercise_pair(aws_a, aws_a_calls.clone(), aws_b, aws_b_calls.clone()).await?; + + let (ring_offer, ring_offer_calls) = RecordingProvider::wrap(RingProvider::new()); + let (aws_answer, aws_answer_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (aws_offer, aws_offer_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (ring_answer, ring_answer_calls) = RecordingProvider::wrap(RingProvider::new()); + let (ring_to_aws, aws_to_ring) = tokio::join!( + exercise_pair( + ring_offer, + ring_offer_calls.clone(), + aws_answer, + aws_answer_calls.clone(), + ), + exercise_pair( + aws_offer, + aws_offer_calls.clone(), + ring_answer, + ring_answer_calls.clone(), + ), + ); + ring_to_aws?; + aws_to_ring?; + + for calls in [ + ring_a_calls, + ring_b_calls, + aws_a_calls, + aws_b_calls, + ring_offer_calls, + aws_answer_calls, + aws_offer_calls, + ring_answer_calls, + ] { + assert_calls(&calls); + } + Ok(()) +} diff --git a/tests/no_builtin_crypto_provider.rs b/tests/no_builtin_crypto_provider.rs new file mode 100644 index 00000000..1ad9503a --- /dev/null +++ b/tests/no_builtin_crypto_provider.rs @@ -0,0 +1,20 @@ +#![cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + +use rtc::peer_connection::RTCPeerConnectionBuilder; + +#[test] +fn peer_connection_without_a_provider_returns_actionable_error() { + let error = match RTCPeerConnectionBuilder::new().build() { + Ok(_) => panic!("a no-built-in build must require an application crypto provider"), + Err(error) => error, + }; + let message = error.to_string(); + assert!( + message.contains("crypto provider"), + "unexpected error: {message}" + ); + assert!( + message.contains("SettingEngine::set_crypto_provider"), + "error must explain how to configure a provider: {message}" + ); +} From b3062a136c71ed00a1a9d8d7b5efc91af3b2b1cb Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Mon, 3 Aug 2026 15:06:23 -0700 Subject: [PATCH 36/40] =?UTF-8?q?P7=20=E2=80=94=20Cleanup,=20validation,?= =?UTF-8?q?=20performance,=20and=20migration=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cargo.yml | 122 +++++ CHANGELOG.md | 118 ++++- Cargo.toml | 6 +- docs/benchmarking-crypto-migration.md | 267 +++++++++++ docs/crypto-provider-migration.md | 424 +++++++++++++++++- .../trickle-ice-relay/trickle-ice-relay.rs | 5 +- examples/trickle-ice/trickle-ice.rs | 3 +- rtc-crypto/Cargo.toml | 8 +- rtc-crypto/src/common.rs | 160 +++++-- rtc-crypto/src/conformance.rs | 35 +- rtc-crypto/src/lib.rs | 1 + rtc-crypto/src/providers/aws_lc_rs.rs | 85 ++-- rtc-crypto/src/providers/ring.rs | 89 ++-- rtc-crypto/src/traits.rs | 62 +-- rtc-dtls/Cargo.toml | 5 + rtc-dtls/benches/README.md | 104 +++++ rtc-dtls/benches/record_protection.rs | 144 ++++++ rtc-dtls/src/config.rs | 30 +- rtc-dtls/src/config/config_test.rs | 26 +- rtc-dtls/src/conn/conn_test.rs | 24 +- rtc-dtls/src/conn/mod.rs | 9 +- rtc-dtls/src/crypto/crypto_cbc.rs | 27 +- rtc-dtls/src/crypto/crypto_test.rs | 37 +- rtc-dtls/src/crypto/mod.rs | 135 +----- rtc-dtls/src/curve/named_curve.rs | 11 - rtc-dtls/src/endpoint.rs | 9 +- rtc-dtls/src/flight/flight4.rs | 8 +- rtc-dtls/src/lib.rs | 8 + rtc-dtls/src/prf/mod.rs | 17 +- rtc-dtls/src/state.rs | 52 +-- rtc-ice/examples/ping_pong.rs | 14 +- rtc-ice/src/agent/agent_test.rs | 135 +++--- rtc-ice/src/agent/mod.rs | 57 +-- rtc-ice/src/lib.rs | 7 + rtc-shared/Cargo.toml | 5 - rtc-shared/src/error.rs | 44 -- rtc-srtp/benches/README.md | 223 +++++---- rtc-srtp/benches/bench.rs | 150 +++++++ rtc-srtp/examples/srtp_micro.rs | 9 + .../src/cipher/cipher_aes_cm_hmac_sha1.rs | 46 +- rtc-srtp/src/context/context_test.rs | 35 +- rtc-srtp/src/context/mod.rs | 24 +- rtc-srtp/src/context/srtcp_test.rs | 21 +- rtc-srtp/src/context/srtp_test.rs | 8 + rtc-srtp/src/lib.rs | 10 +- rtc-srtp/tests/provider_profiles.rs | 41 +- rtc-stun/benches/README.md | 38 +- rtc-stun/benches/bench.rs | 30 +- rtc-stun/src/integrity.rs | 68 +-- rtc-stun/src/integrity/integrity_test.rs | 82 ++-- rtc-stun/src/lib.rs | 7 + rtc-stun/src/message/message_test.rs | 10 +- rtc-turn/examples/turn_client_udp.rs | 4 +- rtc-turn/src/client/client_test.rs | 55 ++- rtc-turn/src/client/mod.rs | 16 +- rtc-turn/src/lib.rs | 7 + scripts/check-crypto-boundary.py | 101 +++++ src/peer_connection/certificate/mod.rs | 388 ++++++++-------- src/peer_connection/configuration/mod.rs | 40 +- src/peer_connection/handler/dtls.rs | 28 +- src/peer_connection/handler/ice.rs | 1 - src/peer_connection/handler/mod.rs | 1 - src/peer_connection/internal.rs | 33 +- src/peer_connection/transport/dtls/mod.rs | 23 +- src/peer_connection/transport/ice/mod.rs | 5 +- tests/crypto_provider_peer_connections.rs | 49 +- tests/dtls_rsa_certificate.rs | 29 +- 67 files changed, 2768 insertions(+), 1107 deletions(-) create mode 100644 docs/benchmarking-crypto-migration.md create mode 100644 rtc-dtls/benches/README.md create mode 100644 rtc-dtls/benches/record_protection.rs create mode 100755 scripts/check-crypto-boundary.py diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 7f40f246..0941d593 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -92,6 +92,128 @@ jobs: - name: Verify publishable package run: cargo package --package rtc-crypto --no-default-features + workspace_provider_matrix: + name: Workspace (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + args: "" + - name: ring only + args: --no-default-features --features ring + - name: aws-lc-rs only + args: --no-default-features --features aws-lc-rs + # Both backends in one graph. This was a `compile_error!` before G3; it is now the + # regression guard for Cargo feature additivity, since feature unification can enable + # both from unrelated dependencies. + - name: ring + aws-lc-rs + args: --features ring,aws-lc-rs + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Compiles the `assert_dyn_compatible` guard in rtc-crypto/src/lib.rs under this feature + # set. `Arc` is load-bearing for the whole design, and object + # safety is lost silently by adding one generic method — possibly behind a feature flag. + - name: Build workspace and dyn-compatibility guard + run: cargo build --workspace --all-targets ${{ matrix.args }} --verbose + - name: Test workspace + run: cargo test --workspace --no-fail-fast ${{ matrix.args }} --verbose + - name: Doctests + run: cargo test --workspace --doc ${{ matrix.args }} --verbose + + standalone_crate_checks: + name: Standalone crates (${{ matrix.backend }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + backend: [ ring, aws-lc-rs ] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Crates that forward provider features must build on their own, not only as part of the + # workspace, since standalone protocol users are a supported audience. + - name: Check provider-forwarding crates + run: | + cargo check --package rtc-crypto --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-dtls --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-srtp --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-stun --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-ice --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-turn --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + # These perform no cryptography and must not have acquired a provider dependency. + - name: Check crates that must stay crypto-free + run: | + cargo check --package rtc-shared --all-targets --verbose + cargo check --package rtc-sctp --all-targets --verbose + cargo check --package rtc-rtp --all-targets --verbose + cargo check --package rtc-rtcp --all-targets --verbose + cargo check --package rtc-sdp --all-targets --verbose + cargo check --package rtc-media --all-targets --verbose + + crypto_dependency_audit: + name: Crypto dependency boundary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Enforces the G3 invariant: rtc-crypto is the only crate naming a crypto implementation. + # The script parses dependency sections so that provider-feature forwarding + # (`ring = ["crypto/ring"]`) and the root `[workspace.dependencies]` table are not + # mistaken for real dependencies. + - name: No crypto implementation outside rtc-crypto + run: python3 scripts/check-crypto-boundary.py + - name: No mutually exclusive backend guards + run: | + if grep -rn 'At most one of the features' --include='*.rs' . ; then + echo "A mutually exclusive backend compile_error! guard is still present." + exit 1 + fi + - name: No backend-name aliasing + run: | + if grep -rn 'extern crate aws_lc_rs as ring' --include='*.rs' . ; then + echo "The aws-lc-rs-as-ring alias is still present." + exit 1 + fi + + package_publication_order: + name: cargo package (publication order) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # rtc-crypto is new in this release and has no intra-workspace dependencies, so it is the + # first crate in publication order and the only one whose packaging can be verified from + # a clean checkout today. + - name: Package rtc-crypto first + run: cargo package --package rtc-crypto --allow-dirty + - name: Package crates with no rtc-crypto dependency + run: cargo package --package rtc-shared --allow-dirty + # Crates depending on rtc-crypto cannot be packaged until rtc-crypto 0.21.0 exists on + # crates.io: `cargo package` resolves the dependency graph against the registry and the + # path dependency is stripped from the published manifest. This is expected for a newly + # introduced crate, not a regression. This step asserts the failure is exactly that + # missing-version resolution error and nothing else, so a different packaging break is + # still caught. + - name: Confirm dependents block only on rtc-crypto publication + run: | + if cargo package --package rtc-stun --allow-dirty 2>package.log; then + echo "rtc-stun packaged; rtc-crypto appears to be published." + echo "Replace this step with a normal packaging check." + exit 0 + fi + if grep -q 'failed to select a version for the requirement `rtc-crypto' package.log; then + echo "Expected: rtc-stun awaits the first rtc-crypto publish." + exit 0 + fi + echo "rtc-stun packaging failed for an unexpected reason:" + cat package.log + exit 1 + rustfmt_and_clippy: name: Check rustfmt style && run clippy runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc32402..138eb57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- +- `RTCCrypto::new_hmac` returns a keyed `Mac` whose key schedule is derived once, mirroring the + existing keyed cipher factories. It replaces the removed one-shot `hmac`/`verify_hmac`. +- **New `rtc-crypto` crate: a provider-neutral cryptographic API** + ([webrtc#839](https://github.com/webrtc-rs/webrtc/issues/839), + [rtc#128](https://github.com/webrtc-rs/rtc/issues/128)). `RTCCryptoProvider` bundles an + `RTCCrypto` operations trait and an `RTCRandom` CSPRNG. Two built-in providers ship, + `RingProvider` and `AwsLcRsProvider`, and applications can supply their own — for OpenSSL, a + FIPS-validated module, an HSM, or a platform backend — by implementing the public traits. A + reusable conformance suite (`rtc_crypto::conformance::assert_provider`, behind the + `test-support` feature) validates any implementation against the same RFC vectors the built-ins + pass. +- `SettingEngine::set_crypto_provider` selects the provider per peer connection. Two peer + connections in one process may use different providers. There is no process-global provider to + install. +- Explicit provider constructors on the standalone protocol crates: + `rtc_dtls::ConfigBuilder::with_crypto_provider`, `rtc_srtp::Context::new_with_provider`, and + `MessageIntegrity::{new_raw,new_short_term,new_long_term}_integrity_with_provider`. +- Provider-neutral certificate construction: `RTCCertificate::generate`, + `RTCCertificate::generate_from_signing_key`, and `RTCCertificate::from_pkcs8`. + `rcgen::CertificateParams` is re-exported from `rtc::peer_connection::certificate`, so callers + no longer need a direct `rcgen` dependency to name it. +- DTLS record-protection benchmarks (`cargo bench --package rtc-dtls --bench record_protection`) + and SRTP AEAD plus context-construction benchmarks, all reporting each enabled provider under + identical inputs and separating one-time key-schedule cost from per-packet cost. ### Changed +- **No library code resolves a default crypto provider.** `crypto::default_provider()` is now + called in exactly one place — peer-connection construction — where the application either + supplied one via `SettingEngine::set_crypto_provider` or gets the feature-selected built-in. + Every constructor below that takes an `Arc` from its caller, so a + `--no-default-features` build fails at configuration time instead of deep inside a handshake. + The `*_with_provider` constructor pairs are collapsed into single provider-taking constructors + (`Context::new`, `Client::new`, `Agent::new`, `Certificate::generate_self_signed`, + `Certificate::from_pem`, `RTCCertificate::from_pem`, `RTCCertificate::get_fingerprints`, the + `MessageIntegrity` constructors). `Default` impls that resolved a provider — `HandshakeConfig`, + `rtc_dtls::State`, `Agent`, `RTCDtlsTransport` — are removed in favour of those constructors, + and `ConfigBuilder::build` errors when no provider was configured rather than inventing one. +- Each protocol crate re-exports the crypto API (`rtc_srtp::crypto`, `rtc_stun::crypto`, + `rtc_ice::crypto`, `rtc_turn::crypto`, `rtc_dtls::crypto_provider`) so standalone users can name + `Arc` without a direct `rtc-crypto` dependency. +- **`ring` and `aws-lc-rs` Cargo features are now additive.** Enabling both builds successfully + and is covered in CI. Previously each of `rtc`, `rtc-dtls`, `rtc-srtp`, and `rtc-stun` carried a + `compile_error!` rejecting the combination, which made an otherwise valid build fail whenever + Cargo feature unification pulled in both. `rtc_crypto::default_provider()` still prefers `ring` + when both are enabled, so default behaviour is unchanged. +- DTLS, SRTP, STUN, ICE, TURN, and the top-level `rtc` crate perform all cryptography through the + configured provider. Protocol composition is unchanged and stays in its own crate: the TLS 1.2 + PRF in DTLS, SRTP key derivation and packet layout in SRTP, STUN integrity framing in STUN. + Default algorithm selection and wire behaviour are unchanged. +- `rtc_dtls::State::export_keying_material` is now an inherent method returning `SecretVec`, and + `rtc_srtp::Config::set_session_keys_from_keying_material` consumes the exported bytes. The + top-level crate performs the handoff, so `rtc-srtp` and `rtc-dtls` remain independent of each + other. `rtc_srtp::config::LABEL_EXTRACTOR_DTLS_SRTP` is now public for standalone callers. +- `rtc_stun::MessageIntegrity` changed from a public tuple struct to named fields and now holds + its provider, because `Setter::add_to` and `check` have no parameter through which to receive + one. Its key is stored as a `SecretVec`. - **`MediaEngine::register_default_codecs` no longer registers `video/ulpfec`** ([#837](https://github.com/webrtc-rs/webrtc/issues/837)). The receive path does not recover media from ULPFEC packets, so offering the codec invited peers to send repair packets that @@ -27,10 +80,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -- Removed the partial `openssl` and `vendored-openssl` Cargo features from `rtc-srtp` and `rtc`; SRTP cryptography now uses the selected `rtc-crypto` provider for every protection profile. +These are deliberate pre-1.0 API removals, not behaviour changes. Every entry has a replacement; +see `docs/crypto-provider-migration.md` for before/after examples. + +- Removed the partial `openssl` and `vendored-openssl` Cargo features from `rtc-srtp` and `rtc`; SRTP cryptography now uses the selected `rtc-crypto` provider for every protection profile. They selected only an alternate AES-CTR path and never implemented the full provider contract. An OpenSSL backend can return as a complete downstream `RTCCryptoProvider`. +- Removed `rtc_shared::crypto::KeyingMaterialExporter`. `rtc-shared` performs no cryptography and + no longer carries a crypto bridge. +- Removed `rtc_srtp::Config::extract_session_keys_from_dtls`; use + `set_session_keys_from_keying_material` with material exported from the DTLS session. +- Removed the default-resolving `MessageIntegrity::{new_raw_integrity, new_short_term_integrity, + new_long_term_integrity}` constructors and the derived `Default` impl. `Default` produced a + credential with an empty key, which is never valid; use the `_with_provider` constructors. +- Removed `rtc_dtls::crypto::CustomSigner` and `CryptoPrivateKey::from_custom_signer`. Implement + `rtc_crypto::SigningKey` and use `CryptoPrivateKey::from_signing_key`. This is a superset of the + old capability: non-exportable HSM and KMS keys are supported, and `to_pkcs8_der()` returns + `Ok(None)` for them instead of fabricating key bytes. +- Removed `CryptoPrivateKey::from_key_pair`, its `TryFrom<&rcgen::KeyPair>` impl, and the + `serialized_der` field. +- Removed `RTCCertificate::from_key_pair` and `from_key_pair_with_provider`; use + `RTCCertificate::generate` or `generate_from_signing_key`. +- Removed the `Sec1`, `P256`, `RcGen`, `AesGcm`, and `Aes` variants from `rtc_shared::Error`, + along with the `sec1`, `p256`, `rcgen`, `aes`, and `aes-gcm` dependencies of `rtc-shared`. These + put crypto-crate types in the public API of every crate in the workspace and pinned their major + versions there. Crypto failures now cross crate boundaries as `Error::Crypto(String)`. +- Removed `RTCCrypto::hmac` and `RTCCrypto::verify_hmac`. They are exactly + `new_hmac(..)?.sign(..)` and `new_hmac(..)?.verify(..)`, and retaining them preserved a path + that re-derives the HMAC key schedule on every call. +- Removed the four duplicated `compile_error!` backend guards and the four + `extern crate aws_lc_rs as ring;` aliases. ### Fixed +- `rtc-crypto`'s AES-CTR keystream now uses a batched implementation instead of one + `encrypt_block` call per 16-byte block, which defeated AES-NI / ARMv8 instruction pipelining. + Roughly 9-10% faster on a 1200-byte SRTP payload. +- **SRTP no longer derives the HMAC key schedule on every packet.** `RTCCrypto::new_hmac` returns + a keyed `Mac`, and `rtc-srtp` keys its SRTP and SRTCP MACs once per context instead of passing + raw key bytes per packet. Roughly 40% faster per RTCP packet and 7% per 1200-byte RTP packet. + A counting provider in `rtc-srtp/tests/provider_profiles.rs` guards the invariant. +- **DTLS CBC no longer derives its record MAC key on every record.** `CryptoCbc` holds two keyed + `Mac` objects instead of passing raw key bytes to `prf_mac` per record: ~4% faster on encrypt and + ~7% on decrypt, with the key schedule moving to epoch setup. +- STUN `MESSAGE-INTEGRITY` was measured against its pre-migration baseline and shows no regression + (see `rtc-stun/benches/README.md`); it already used `ring`'s HMAC-SHA1 before G3. +- **The built-in `RTCRandom` implementations no longer read the operating system on every call.** + DTLS generates a GCM explicit nonce and a CBC record IV per record; routing those through the + backend's `SystemRandom` cost ~829 ns on `ring` and ~2196 ns on `aws-lc-rs`, against ~8 ns for + the thread-local CSPRNG the pre-provider code used. They now use an OS-seeded, periodically + reseeded thread-local CSPRNG, as BoringSSL and OpenSSL do internally. `SystemRandom` is still + used where the backend owns the operation — keypair generation and signing. DTLS GCM encryption + went from 1.015 µs to 262 ns per record. A deployment needing validated entropy everywhere + supplies its own `RTCRandom`. +- **The `ring` provider composes RustCrypto's HMAC-SHA1.** `ring` exposes SHA-1 only as + `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 instructions: 4469 ns against + RustCrypto's 1373 ns over a 1212-byte message. The built-in providers are already composite — + AES-CTR, CCM, CBC and MD5 come from RustCrypto — so HMAC-SHA1 is composed the same way. This + closes the gap without making `aws-lc-rs` the default, which would impose the `aws-lc-sys` C + toolchain on every downstream build. SHA-256 stays on `ring`; `aws-lc-rs` keeps its own SHA-1. + +**Net effect: every per-packet and per-record benchmark is at parity with or faster than the +pre-migration baseline on the default provider.** SRTP AES-CM/HMAC RTP encryption 1.725 µs before +and 1.723 µs after; DTLS GCM record encryption 270.5 ns before and 270.1 ns after; DTLS CBC 6-8% +faster than before. Context and epoch setup costs rose, which is the intended trade — key +schedules moved off the per-packet path. See the three `benches/README.md` files for the full +tables and methodology. + - Add RSA as an allowed private key kind in rtc_dtls::ConfigBuilder:: validate- [PR #141](https://github.com/webrtc-rs/rtc/pull/141) diff --git a/Cargo.toml b/Cargo.toml index 6b1af104..eae706ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,11 +66,11 @@ thiserror = "2.0.18" zeroize = "1.8.2" pem = "3.0.3" aes = "0.8.4" -aes-gcm = { version = "0.10.3", features = ["std"] } -sec1 = { version = "0.7.3", features = ["std"] } -p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } +ctr = "0.9.2" ccm = "0.5.0" md-5 = "0.10.6" +hmac = "0.12.1" +sha1 = "0.10.6" subtle = "2.6.1" x509-parser = "0.16.0" der-parser = "9.0.0" diff --git a/docs/benchmarking-crypto-migration.md b/docs/benchmarking-crypto-migration.md new file mode 100644 index 00000000..264494b7 --- /dev/null +++ b/docs/benchmarking-crypto-migration.md @@ -0,0 +1,267 @@ +# Benchmarking against a pre-migration baseline + +How the before/after figures in `rtc-srtp/benches/README.md`, `rtc-dtls/benches/README.md`, and +`rtc-stun/benches/README.md` were produced, and how to reproduce or extend them. + +The G3 crypto migration moved every cryptographic operation behind `rtc-crypto`. That is a +performance-sensitive change on paths that run per packet, so each affected crate was measured +against the last commit before *its own* migration. Three real regressions were found this way and +none of them were visible from code review. + +## Contents + +- [The rule that makes or breaks this](#the-rule-that-makes-or-breaks-this) +- [Baseline commits](#baseline-commits) +- [Procedure A — a benchmark already exists at the baseline](#procedure-a--a-benchmark-already-exists-at-the-baseline) +- [Procedure B — no benchmark exists at the baseline](#procedure-b--no-benchmark-exists-at-the-baseline) +- [Procedure C — micro-benchmarks for diagnosis](#procedure-c--micro-benchmarks-for-diagnosis) +- [Interpreting the results](#interpreting-the-results) +- [Pitfalls hit during G3](#pitfalls-hit-during-g3) +- [Reporting](#reporting) + +## The rule that makes or breaks this + +**Both sides must be measured on the same machine, in the same session, with the same criterion +settings.** Everything else in this document is detail. + +This is not a formality. `rtc-srtp/benches/README.md` previously carried results from a MacBook +Air M3 showing `Encrypt/RTP` at 5.69 µs. The post-migration figure on an M1 Max was 4.95 µs. +Comparing those two numbers says performance *improved*. Measuring the actual baseline on the same +M1 Max gave 1.73 µs — a 2.9x regression. The stale cross-machine figure did not merely add noise, +it inverted the conclusion. + +Delete or clearly quarantine historical numbers from other machines before comparing. + +## Baseline commits + +Use the last commit **before the crate in question was migrated**, not a single global baseline — +each crate moved in a different phase, and an earlier commit drags in unrelated changes. + +| Crate | Migrated in | Baseline commit | Baseline is | +|---|---|---|---| +| `rtc-stun` | P2 `f8298ce` | `b8bb313` | P1 — rtc-crypto exists, STUN not yet using it | +| `rtc-srtp` | P4 `fd81f68` | `425494c` | P3 | +| `rtc-dtls` | P5 `219788a` | `fd81f68` | P4 | + +Verify a baseline before trusting it: + +```bash +# The crate must not yet depend on rtc-crypto at the baseline. +for c in 425494c HEAD; do + echo "${c}: $(git show "${c}:rtc-srtp/Cargo.toml" | grep -cE '^crypto[ .=]')" +done +# 425494c: 0 <- baseline, not yet migrated +# HEAD: 1 +``` + +Match the dependency line, not the word. A bare `grep -c crypto` returns 1 even at the baseline, +because `rtc-shared` is pulled in with `features = ["crypto", …]` — that is `rtc-shared`'s own +feature gate, not the `rtc-crypto` crate. Anchoring to `^crypto[ .=]` matches only the dependency +entry. + +In zsh, write `"${c}:path"` and not `"$c:path"`: `$c:r` is parsed as a history modifier and +silently mangles the revision. + +## Procedure A — a benchmark already exists at the baseline + +This is the strong case: the same benchmark source runs on both sides, so the only variable is the +code under test. Used for **SRTP** and **STUN**. + +**1. Confirm the benchmark exists and is unchanged.** + +```bash +git cat-file -e 425494c:rtc-srtp/benches/bench.rs && echo present + +# Byte-compare the region you intend to compare. For SRTP the first 151 lines — the four +# original Encrypt/Decrypt RTP/RTCP benchmarks and their key constants — are identical between +# the baseline and today; everything added later sits after them. +git show 425494c:rtc-srtp/benches/bench.rs > /tmp/base_bench.rs +diff <(sed -n '1,151p' /tmp/base_bench.rs) <(sed -n '1,151p' rtc-srtp/benches/bench.rs) +``` + +If that diff is empty, the comparison is sound. If the benchmark changed for reasons unrelated to +the migration, compare only the benchmarks that did not, or fall back to Procedure B. + +Where the benchmark *had* to change because the API under test changed — STUN's +`new_short_term_integrity` becoming `new_short_term_integrity_with_provider` — that is +unavoidable and is exactly the migration being measured. Keep every other input identical. + +**2. Create a worktree and run it.** + +```bash +git worktree add /tmp/rtc-srtp-base 425494c +cd /tmp/rtc-srtp-base +cargo bench -p rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 +``` + +A worktree gets its own `target/`, so the two builds do not share artifacts. Do not set a shared +`CARGO_TARGET_DIR`. + +**3. Run the current tree with identical arguments.** + +```bash +cd /path/to/rtc +cargo bench -p rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 +``` + +**4. Repeat the baseline and quote a representative run, not the fastest.** Three independent runs +of the SRTP baseline gave 1.725, 1.766, and 1.780 µs for `Encrypt/RTP` — a spread of about ±3%, +which is the noise floor on this machine. Reporting the minimum from one side and a typical value +from the other manufactures a difference of that size out of nothing. Take the median, or quote +the range, and treat anything inside ±3% here as parity. + +**5. Remove the worktree.** + +```bash +cd /path/to/rtc && git worktree remove /tmp/rtc-srtp-base --force +``` + +## Procedure B — no benchmark exists at the baseline + +The weaker case: you write the baseline harness yourself, so a mistake in the port silently +corrupts the comparison. Used for **DTLS**. Say so explicitly when reporting. + +**1. Write the benchmark for the current tree first**, and get it passing. That file is the +specification the baseline port must match. + +**2. Port it back to the pre-migration API.** For DTLS the deltas were: + +| | Baseline (`fd81f68`) | Current | +|---|---|---| +| `CryptoGcm::new` | `(local_key, local_iv, remote_key, remote_iv) -> Self` | `(provider, …) -> Result` | +| `CryptoCbc::new` | `(local_key, local_mac, remote_key, remote_mac) -> Result` | `(provider, …) -> Result` | +| `encrypt` / `decrypt` | `&self` | `&mut self` | + +So the port drops the provider argument, drops `?`/`.unwrap()` where the old constructor returned +`Self`, and drops `mut` on the cipher bindings. Nothing else may change. + +**3. Hold every input identical** — payload length, key and IV constants, record construction, +criterion settings. Copy them literally rather than retyping. In the DTLS bench the header must be +marshalled rather than zero-filled, because `decrypt` re-parses it from the ciphertext buffer; +getting that wrong produces `ErrUnsupportedProtocolVersion` rather than a wrong number, which is +the good failure mode. + +**4. Add the harness plumbing to the worktree**, since it will not be there: + +```toml +# rtc-dtls/Cargo.toml in the worktree +[dev-dependencies] +criterion.workspace = true + +[[bench]] +name = "record_protection" +harness = false +``` + +**5. Sanity-check the port before trusting it.** A ported harness that produces plausible-looking +numbers can still be measuring the wrong thing. Two cheap checks: + +- Does an unrelated benchmark in the same file match between the two sides? If the port is sound, + operations the migration did not touch should be within noise. +- Does the result *predict* something? The DTLS port showed encryption regressing while decryption + did not. That asymmetry pointed at randomness, an independent micro-benchmark (Procedure C) + confirmed the RNG cost, and removing the RNG moved the figure to 270.1 ns against a predicted + 270.5 ns. A broken harness is unlikely to survive that. + +## Procedure C — micro-benchmarks for diagnosis + +Once a regression is confirmed, a throwaway crate isolates the cause far faster than profiling the +whole path. These are **diagnostic only** — never quote them as headline results, and never commit +them. + +```bash +mkdir -p /tmp/rngbench/src && cd /tmp/rngbench +cat > Cargo.toml <<'EOF' +[package] +name = "rngbench" +version = "0.0.0" +edition = "2021" +[dependencies] +ring = "0.17.14" +rand = "0.10.1" +EOF +cat > src/main.rs <<'EOF' +use ring::rand::{SecureRandom, SystemRandom}; +use std::time::Instant; + +fn main() { + const N: u32 = 200_000; + let mut buf = [0u8; 8]; + + let t = Instant::now(); + for _ in 0..N { SystemRandom::new().fill(&mut buf).unwrap(); } + println!("ring SystemRandom : {:>7.1} ns", t.elapsed().as_nanos() as f64 / N as f64); + + let t = Instant::now(); + for _ in 0..N { rand::fill(&mut buf); } + println!("rand thread-local : {:>7.1} ns", t.elapsed().as_nanos() as f64 / N as f64); +} +EOF +cargo run --release -q +``` + +Rules that keep these honest: build with `--release`, use `std::hint::black_box` around anything +the optimiser could elide, run enough iterations that per-call cost dwarfs timer overhead, and +compare candidates **in the same binary** so the environment is shared. + +This is how the two dominant causes were found — an 8-byte OS read at 829 ns versus 8.3 ns, and +`ring`'s HMAC-SHA1 at 4469 ns versus RustCrypto's 1373 ns over the same 1212 bytes. + +## Interpreting the results + +**Separate setup from per-operation cost.** Benchmark them as different groups. The migration +deliberately moves work from the per-packet path to context construction — keyed cipher and MAC +objects are built once. A `Setup/*` figure rising while `Encrypt/*` falls is the design working, +not a regression. Reporting only a combined number hides both directions. + +**Measure every enabled provider under identical inputs.** The benchmarks loop over the built-in +providers, so `--features ring,aws-lc-rs` reports both side by side. This is what showed that +`aws-lc-rs` was *worse* on DTLS encrypt while faster everywhere else, which localised the cause to +the encrypt-only RNG call. + +**Asymmetries are the most useful signal.** Encrypt regressed and decrypt did not; large payloads +improved and small ones did not. Each asymmetry excludes whole classes of explanation before any +profiling. + +**A backend difference is not an architecture problem.** `ring`'s slow SHA-1 was a property of the +backend, not of the provider abstraction. It was fixed by composing RustCrypto's HMAC-SHA1 into +the `ring` provider — the built-ins are already composite — rather than by changing the default +provider, which would have imposed a C toolchain on every downstream build. + +## Pitfalls hit during G3 + +**Cross-machine comparison inverted a conclusion.** Covered above. The single most dangerous item +here. + +**Sequential runs of the same command are cached.** Running `cargo clippy` twice and grepping each +output separately gives real diagnostics from the first and an empty second. Capture one run to a +file and derive every count from that file. + +**zsh eats `:r` in a revision string.** `git show "$c:rtc-srtp/Cargo.toml"` parses `$c:r` as a +history modifier and looks up `425494ctc-srtp/Cargo.toml`. Use `"${c}:path"`. + +**zsh does not word-split unquoted variables.** `F="--no-default-features --features ring"; cargo build $F` passes one bogus argument. This produced four false "failures" in a feature-matrix loop. +Write the invocations out, or use an array. + +**Bisecting can misattribute.** An early bisect blamed the AES-CTR rewrite for a set of DTLS test +failures. The real cause was a text-slice deletion that had removed a provider method along with +its neighbours; the CTR change was innocent. Confirm a bisect result by reverting *only* the +suspected hunk, not a whole file. + +**Build-only verification is not verification.** After the last code edit before a commit, only +build and clippy were re-run — both clean — while the tests were not. Nine DTLS tests were broken +at that commit. Re-run the full suite after the final edit, not the second-to-last. + +## Reporting + +Each crate's `benches/README.md` carries its own table. State, every time: + +- the machine and OS; +- the baseline commit and what it precedes; +- the exact criterion arguments, identical on both sides; +- whether the baseline benchmark was pre-existing (Procedure A) or reconstructed (Procedure B); +- for reconstructed harnesses, that the comparison rests on the port being faithful. + +Quarantine or delete numbers from other machines. If a table mixes measurements taken at different +points — for example a "before fix / after fix" column pair captured as work progressed rather +than in one sweep — label it, or re-run everything at HEAD in a single session. diff --git a/docs/crypto-provider-migration.md b/docs/crypto-provider-migration.md index 40d77865..354a969b 100644 --- a/docs/crypto-provider-migration.md +++ b/docs/crypto-provider-migration.md @@ -1,11 +1,425 @@ # Crypto provider migration -## SRTP in 0.21 +`rtc` 0.21 routes every cryptographic operation through `rtc-crypto`. Protocol crates no longer +depend on `ring`, `aws-lc-rs`, or RustCrypto primitive crates; they take an +`Arc` and call provider traits. -`rtc-srtp::context::Context::new_with_provider` is the provider-selecting constructor. It accepts an `Arc`, validates the selected protection profile against `RTCCrypto::supports`, derives the SRTP and SRTCP session material through that provider, and creates reusable keyed cipher objects once per one-way context. Packet indexes, rollover counters, replay windows, IVs, AAD, authentication-tag truncation, and wire layout remain owned by `rtc-srtp`. +This guide covers what changed for callers. For *why*, see `docs/crypto-provider-decisions.md`. -`Context::new` remains available as a compatibility constructor and resolves `rtc_crypto::default_provider()`. Applications that need deterministic provider selection, use both built-ins in one process, or supply their own implementation should migrate to `Context::new_with_provider`. +## Contents -The `ring` and `aws-lc-rs` features on `rtc-srtp` now forward additively to `rtc-crypto`. Ring remains the default when enabled, including builds that enable both features. A no-built-in build is supported when the application supplies its own provider. +- [Choosing a provider](#choosing-a-provider) +- [Cargo features are now additive](#cargo-features-are-now-additive) +- [Removed and changed public items](#removed-and-changed-public-items) +- [Top-level `rtc`](#top-level-rtc) +- [Certificates](#certificates) +- [Standalone DTLS](#standalone-dtls) +- [Standalone SRTP](#standalone-srtp) +- [Standalone STUN, ICE, and TURN](#standalone-stun-ice-and-turn) +- [Writing your own provider](#writing-your-own-provider) +- [OpenSSL](#openssl) -The former `openssl` and `vendored-openssl` features were removed. They selected only an alternate AES-CTR path inside SRTP and did not implement the complete `RTCCryptoProvider` contract, so retaining them would create a misleading partial-provider surface. An OpenSSL integration can be added in the future as a complete application or crate-provided `RTCCryptoProvider` that passes the public conformance suite. +## Choosing a provider + +```rust +use std::sync::Arc; +use rtc_crypto::{RTCCryptoProvider, default_provider}; +use rtc_crypto::providers::{AwsLcRsProvider, RingProvider}; + +// The feature-selected default: ring when enabled, otherwise aws-lc-rs. +// Returns Err(CryptoError::NoDefaultProvider) when neither feature is on. +let provider: Arc = default_provider()?; + +// Or name one explicitly. Both can be compiled in at once. +let ring: Arc = Arc::new(RingProvider::new()); +let aws: Arc = Arc::new(AwsLcRsProvider::new()); +``` + +There is no process-global provider and nothing to install at startup. A provider is an ordinary +value passed through configuration, so two peer connections in one process can use different +providers. + +## The provider is always passed in + +**No library code resolves a default provider.** `crypto::default_provider()` is called in exactly +one place in the workspace — peer-connection construction, where the application either supplied +one via `SettingEngine::set_crypto_provider` or gets the feature-selected built-in. Everything +below that receives an `Arc` from its caller. + +This is why the `*_with_provider` constructor pairs are gone. Each had a default-resolving sibling +that hid the choice — and, in a `--no-default-features` build, panicked or failed deep inside a +call rather than at configuration time. There is now one constructor per type and it takes the +provider: + +| Before | Now | +|---|---| +| `Context::new(key, salt, profile, a, b)` | `Context::new(key, salt, profile, a, b, provider)` | +| `Context::new_with_provider(…)` | (folded into `new`) | +| `Client::new(config)` | `Client::new(config, provider)` | +| `Agent::new(config)` | `Agent::new(config, provider)` | +| `Certificate::generate_self_signed(names)` | `Certificate::generate_self_signed(names, provider)` | +| `Certificate::from_pem(pem)` | `Certificate::from_pem(pem, provider)` | +| `RTCCertificate::from_pem(pem)` | `RTCCertificate::from_pem(pem, provider)` | +| `RTCCertificate::get_fingerprints()` | `get_fingerprints(provider) -> Result<…>` | +| `MessageIntegrity::new_short_term_integrity(pw)` | `…(pw, provider)` | + +`Default` impls that resolved a provider are removed rather than reworked, because `Default` has +nowhere to accept one. `HandshakeConfig`, `rtc_dtls::State`, `Agent`, and `RTCDtlsTransport` are +now built through constructors that take the provider. `ConfigBuilder::build` returns an error +instead of inventing a provider when none was configured. + +Each protocol crate re-exports the crypto API — `rtc_srtp::crypto`, `rtc_stun::crypto`, +`rtc_ice::crypto`, `rtc_turn::crypto`, and `rtc_dtls::crypto_provider` (named differently because +`rtc-dtls` already has its own `crypto` module) — so a standalone user can name +`Arc` without adding and version-matching a direct `rtc-crypto` dependency. + +Tests, examples, and benchmarks are the outside caller and may call `default_provider()` directly. + +## Cargo features are now additive + +Before 0.21, `rtc`, `rtc-dtls`, `rtc-srtp`, and `rtc-stun` each carried: + +```rust +#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] +compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); +``` + +Enabling both is now supported and tested in CI. This matters because Cargo unifies features: a +transitive dependency enabling the other backend used to break an otherwise valid build, with no +fix available to the person hitting it. + +| Build | Result | +|---|---| +| `--features ring` | ring only | +| `--features aws-lc-rs` | aws-lc-rs only | +| `--features ring,aws-lc-rs` | both compiled; `default_provider()` returns ring | +| `--no-default-features` | no built-in provider; supply your own | + +`default_provider()` prefers ring when both are enabled, matching the previous precedence. +Enabling `aws-lc-rs` alongside the default never silently switches the default. + +## Removed and changed public items + +| Item | Status | Replacement | +|---|---|---| +| `rtc_shared::crypto::KeyingMaterialExporter` | removed | inherent `rtc_dtls::State::export_keying_material` | +| `rtc_srtp::Config::extract_session_keys_from_dtls` | removed | `set_session_keys_from_keying_material` | +| `rtc_stun::MessageIntegrity` (tuple struct) | changed | named fields; `key` is a `SecretVec` | +| `MessageIntegrity::new_short_term_integrity` | changed | `new_short_term_integrity(password, provider)` | +| `MessageIntegrity::new_long_term_integrity` | changed | `new_long_term_integrity(user, realm, pass, provider)` | +| `MessageIntegrity::new_raw_integrity` | changed | `new_raw_integrity(key, provider)` | +| `MessageIntegrity::default()` | removed | construct with an explicit key and provider | +| `rtc_dtls::crypto::CustomSigner` | removed | implement `rtc_crypto::SigningKey` | +| `CryptoPrivateKey::from_custom_signer` | removed | `CryptoPrivateKey::from_signing_key` | +| `CryptoPrivateKey::from_key_pair(&KeyPair)` | changed | now takes a provider: `from_key_pair(&KeyPair, provider)` | +| `CryptoPrivateKey::serialized_der` field | removed | `SigningKey::to_pkcs8_der()` | +| `rtc_dtls::crypto::CryptoPrivateKeyKind` | removed | opaque `Arc` | +| `RTCCertificate::from_key_pair` | removed | `RTCCertificate::generate` | +| `RTCCertificate::from_key_pair_with_provider` | removed | `RTCCertificate::generate_from_signing_key` | +| `rtc_shared::Error::{Sec1, P256, RcGen, AesGcm, Aes}` | removed | `Error::Crypto(String)` | +| `RTCCrypto::hmac` | removed | `new_hmac(alg, key)?.sign(input, out)` | +| `RTCCrypto::verify_hmac` | removed | `new_hmac(alg, key)?.verify(input, tag)` | +| `rtc_srtp` `openssl` / `vendored-openssl` features | removed | see [OpenSSL](#openssl) | + +`MessageIntegrity::default()` produced a credential with an empty key, which was never useful. No +in-tree caller existed; downstream code that relied on it must supply a real key. + +### Keyed MACs + +`RTCCrypto::hmac` and `verify_hmac` are gone. They were exactly `new_hmac(..)?.sign(..)` and +`new_hmac(..)?.verify(..)`, and keeping them preserved a path that derives the HMAC key schedule +on every call — which cost SRTP roughly 40% of its per-RTCP-packet time. There is now one way to +compute an HMAC, and it makes the keying cost visible at the call site. + +Before: + +```rust +let mut tag = [0u8; 20]; +crypto.hmac(HmacAlgorithm::Sha1, &key, &[header, payload], &mut tag)?; +crypto.verify_hmac(HmacAlgorithm::Sha1, &key, &[header, payload], &tag)?; +``` + +After — one-shot: + +```rust +let mut tag = [0u8; 20]; +let mut mac = crypto.new_hmac(HmacAlgorithm::Sha1, &key)?; +mac.sign(&[header, payload], &mut tag)?; +mac.verify(&[header, payload], &tag)?; +``` + +After — repeated authentication with one key, which is the point: hold the `Mac` in your state and +key it once. + +```rust +struct MyContext { + auth: Box, // keyed once at construction +} + +impl MyContext { + fn new(crypto: &dyn RTCCrypto, key: &[u8]) -> Result { + Ok(Self { auth: crypto.new_hmac(HmacAlgorithm::Sha1, key)? }) + } + + // `&mut self`, because Mac methods take &mut self like the cipher traits. + fn tag(&mut self, message: &[&[u8]]) -> Result<[u8; 20], CryptoError> { + let mut tag = [0u8; 20]; + self.auth.sign(message, &mut tag)?; + Ok(tag) + } +} +``` + +`Mac` is `Send` and its methods take `&mut self`, matching `StreamCipher`, `AeadCipher`, and +`CbcCipher`. A caller whose own signature is fixed to `&self` can still build a local `Mac` per +message and use it mutably. + +## Top-level `rtc` + +`SettingEngine` carries the provider. It is resolved once during peer-connection construction and +cloned into DTLS, SRTP, STUN, certificate, and fingerprint state. + +```rust +use std::sync::Arc; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc_crypto::providers::AwsLcRsProvider; + +let mut setting_engine = SettingEngine::default(); +setting_engine.set_crypto_provider(Arc::new(AwsLcRsProvider::new())); +``` + +Omitting the call keeps the feature-selected default, so existing code needs no change. + +## Certificates + +`rcgen::KeyPair`-based construction is gone. Certificate *formatting* still uses `rcgen` — +X.509 is deliberately not a provider concern — but key generation and signing go through the +provider. `CertificateParams` is re-exported so callers need no direct `rcgen` dependency. + +Before: + +```rust +use rcgen::KeyPair; + +let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; +let certificate = RTCCertificate::from_key_pair(key_pair)?; +``` + +After: + +```rust +use rtc::crypto::{self, SignatureScheme}; +use rtc::peer_connection::certificate::{CertificateParams, RTCCertificate}; + +let certificate = RTCCertificate::generate( + crypto::default_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["localhost".to_owned()])?, +)?; +``` + +Three construction paths are available: + +| Need | Use | +|---|---| +| Provider generates the key | `RTCCertificate::generate(provider, scheme, params)` | +| Key already exists (imported PKCS#8, HSM, KMS) | `RTCCertificate::generate_from_signing_key(params, scheme, signing_key)` | +| Certificate chain already exists | `RTCCertificate::from_pkcs8(provider, scheme, chain, der, expires)` | + +### External and non-exportable signing keys + +`CustomSigner` is replaced by `rtc_crypto::SigningKey`, which is also what the built-in providers +implement. An HSM or KMS key returns `Ok(None)` from `to_pkcs8_der()`; PEM serialization of such a +key returns an explicit error rather than fabricating bytes. + +```rust +use std::sync::Arc; +use rtc_crypto::{CryptoError, PublicKey, PublicKeyEncoding, SignatureScheme, SigningKey}; + +#[derive(Debug)] +struct KmsKey { /* handle, cached SPKI */ } + +impl SigningKey for KmsKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + scheme == SignatureScheme::EcdsaP256Sha256 + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], // cached SPKI DER + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + let _ = (scheme, message); + todo!("call the KMS") + } +} + +let key: Arc = Arc::new(KmsKey { /* … */ }); +``` + +Pass it to `RTCCertificate::generate_from_signing_key`, or to +`rtc_dtls::crypto::CryptoPrivateKey::from_signing_key` for standalone DTLS. + +## Standalone DTLS + +```rust +use rtc_dtls::config::ConfigBuilder; + +let config = ConfigBuilder::default() + .with_crypto_provider(provider.clone()) + .with_certificates(vec![certificate]) + .build(true, None)?; +``` + +`Certificate::generate_self_signed`, `generate_self_signed_with_alg`, and `from_pem` all take the +provider as their last argument. The default-resolving overloads are gone, as is +`ConfigBuilder::build`'s silent fallback — it now returns an error if no provider was configured. + +### DTLS-SRTP keying material + +`KeyingMaterialExporter` is gone. Export is an inherent method on the DTLS session, and SRTP takes +the resulting bytes, so `rtc-srtp` and `rtc-dtls` stay independent of each other. + +Before: + +```rust +srtp_config.extract_session_keys_from_dtls(dtls_state, is_client)?; +``` + +After: + +```rust +use rtc_srtp::config::LABEL_EXTRACTOR_DTLS_SRTP; + +let material = dtls_state.export_keying_material( + LABEL_EXTRACTOR_DTLS_SRTP, + &[], + srtp_config.keying_material_len(), +)?; +srtp_config.set_session_keys_from_keying_material(&material, dtls_state.is_client())?; +``` + +## Standalone SRTP + +`Context::new` takes the provider. It validates the protection +profile against `RTCCrypto::supports`, derives session material through the provider, and builds +keyed cipher objects **once per one-way context**. Packet indexes, rollover counters, replay +windows, IVs, AAD, tag truncation, and wire layout stay in `rtc-srtp`. + +```rust +let context = Context::new( + &master_key, + &master_salt, + ProtectionProfile::Aes128CmHmacSha1_80, + None, + None, + provider.clone(), +)?; +``` + +There is no default-resolving overload. `rtc-srtp` never calls `default_provider()`; an +application that wants the feature-selected built-in passes `crypto::default_provider()?`. + +## Standalone STUN, ICE, and TURN + +`MessageIntegrity` now holds its provider, because `Setter::add_to` and `check` have no place to +receive one. The struct changed from a public tuple to named fields, and the default-resolving +constructors were removed. + +Before: + +```rust +let integrity = MessageIntegrity::new_short_term_integrity(password); +``` + +After: + +```rust +let integrity = MessageIntegrity::new_short_term_integrity(password, provider.clone()); + +let integrity = MessageIntegrity::new_long_term_integrity( + username, realm, password, provider.clone(), +)?; // returns Result: MD5 must be available +``` + +ICE and TURN reach crypto only through these values; they hold one provider handle and clone it +into the integrity attributes they build. + +## Writing your own provider + +Implement `RTCCryptoProvider` plus the component traits. Nothing needs to be registered — pass the +value in through configuration. + +```rust +use std::sync::Arc; +use rtc_crypto::{RTCCrypto, RTCCryptoProvider, RTCRandom}; + +#[derive(Debug)] +struct MyProvider { crypto: MyCrypto, random: MyRandom } + +impl RTCCryptoProvider for MyProvider { + fn name(&self) -> &'static str { "my-provider" } + fn crypto(&self) -> &dyn RTCCrypto { &self.crypto } + fn random(&self) -> &dyn RTCRandom { &self.random } +} + +setting_engine.set_crypto_provider(Arc::new(MyProvider { /* … */ })); +``` + +Validate it against the same conformance suite the built-ins use: + +```rust +// Cargo.toml: rtc-crypto = { version = "0.21", features = ["test-support"] } +#[test] +fn my_provider_conforms() { + rtc_crypto::conformance::assert_provider(&MyProvider::new()); +} +``` + +`assert_provider` runs the whole suite. Individual sections are also public +(`assert_hashes_and_hmac`, `assert_aead`, `assert_cbc`, `assert_block_and_stream_ciphers`, +`assert_key_exchange`, `assert_signatures`, `assert_random`) for a provider that implements only +part of the surface. + +The suite covers RFC known-answer vectors, round trips, tag-length and nonce-length validation, +and unsupported-algorithm reporting. `cargo test --package rtc-crypto --no-default-features +--features test-support --test custom_provider` shows a complete downstream-style provider built +with no built-in enabled. + +Report unsupported algorithms honestly through `RTCCrypto::supports` — negotiation intersects +protocol support, provider capability, and application configuration before advertising anything, +so an accurate answer turns an unusable combination into a construction-time error instead of a +stalled handshake. + +## Provider performance + +The built-in providers are **composite**: each uses its primary backend where that backend is +strong and RustCrypto elsewhere. `ring` supplies SHA-256, AEAD, key exchange and signatures; +AES-CTR, CCM, CBC, MD5 and HMAC-SHA1 come from RustCrypto because `ring`'s SHA-1 does not use the +ARMv8 instructions. `aws-lc-rs` keeps its own SHA-1, which is faster than both. + +Randomness for protocol values — DTLS randoms and record IVs, cookies, transaction IDs — comes +from an OS-seeded, periodically reseeded thread-local CSPRNG rather than a per-call read of the +operating system, which measured 100-250x slower and showed up directly on the DTLS record path. +Keypair generation and signing still use the backend's own RNG. + +After these choices, SRTP and DTLS per-packet throughput is at parity with, or better than, +pre-`rtc-crypto` releases on the default provider. `aws-lc-rs` is faster on the SRTP AES-CM/HMAC +and DTLS CBC paths if you can accept the `aws-lc-sys` C toolchain in your build. Measurements and +methodology are in `rtc-srtp/benches/README.md`, `rtc-dtls/benches/README.md`, and +`rtc-stun/benches/README.md`. + +A custom provider is free to make different choices; the conformance suite checks correctness, not +speed. + +## OpenSSL + +The `openssl` and `vendored-openssl` features on `rtc-srtp` were removed. They selected an +alternate AES-CTR path inside SRTP only and never implemented the full provider contract, so +keeping the names would imply a completeness that did not exist. + +An OpenSSL backend can return as a complete `RTCCryptoProvider` — in an application or a separate +crate — that passes the public conformance suite. No `rtc` change is required to enable that. diff --git a/examples/trickle-ice-relay/trickle-ice-relay.rs b/examples/trickle-ice-relay/trickle-ice-relay.rs index 08867c97..1e9a81d1 100644 --- a/examples/trickle-ice-relay/trickle-ice-relay.rs +++ b/examples/trickle-ice-relay/trickle-ice-relay.rs @@ -14,6 +14,7 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Response, Server, StatusCode}; use ice::candidate::candidate_relay::CandidateRelayConfig; use log::{error, info, trace}; +use rtc::crypto; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::configuration::setting_engine::SettingEngine; use rtc::peer_connection::event::RTCDataChannelEvent; @@ -188,7 +189,9 @@ async fn run_main_loop( rto_in_ms: 0, }; - let mut turn_client = Client::new(cfg)?; + // An application is the outside caller, so it selects the crypto provider explicitly. + // Library code never resolves a default. + let mut turn_client = Client::new(cfg, crypto::default_provider()?)?; let allocate_tid = turn_client.allocate()?; let mut relay_addr: Option = None; let relay_local_addr = local_addr; diff --git a/examples/trickle-ice/trickle-ice.rs b/examples/trickle-ice/trickle-ice.rs index 877fcec0..1825ee17 100644 --- a/examples/trickle-ice/trickle-ice.rs +++ b/examples/trickle-ice/trickle-ice.rs @@ -21,6 +21,7 @@ use ice::candidate::candidate_host::CandidateHostConfig; use ice::candidate::candidate_relay::CandidateRelayConfig; use ice::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; use log::{error, info, trace, warn}; +use rtc::crypto; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::configuration::setting_engine::SettingEngine; use rtc::peer_connection::event::RTCDataChannelEvent; @@ -281,7 +282,7 @@ async fn run_main_loop(cli: Cli) -> Result<()> { rto_in_ms: 0, }; - let mut client = TurnClient::new(cfg)?; + let mut client = TurnClient::new(cfg, crypto::default_provider()?)?; let tid = client.allocate()?; allocate_tid = Some(tid); turn_client = Some(client); diff --git a/rtc-crypto/Cargo.toml b/rtc-crypto/Cargo.toml index b4c4f4d8..1b1cf9fe 100644 --- a/rtc-crypto/Cargo.toml +++ b/rtc-crypto/Cargo.toml @@ -14,14 +14,18 @@ readme = "README.md" [features] default = ["ring"] -ring = ["dep:ring", "dep:aes", "dep:ccm", "dep:md-5"] -aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ccm", "dep:md-5"] +ring = ["dep:ring", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] +aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] test-support = [] [dependencies] aes = { workspace = true, optional = true } +ctr = { workspace = true, optional = true } +rand.workspace = true ccm = { workspace = true, optional = true } md-5 = { workspace = true, optional = true } +hmac = { workspace = true, optional = true } +sha1 = { workspace = true, optional = true } subtle.workspace = true thiserror.workspace = true zeroize.workspace = true diff --git a/rtc-crypto/src/common.rs b/rtc-crypto/src/common.rs index 2745198d..6c848134 100644 --- a/rtc-crypto/src/common.rs +++ b/rtc-crypto/src/common.rs @@ -1,14 +1,17 @@ use aes::cipher::generic_array::GenericArray; -use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, KeyIvInit}; use aes::{Aes128, Aes256}; use ccm::Ccm; use ccm::aead::AeadInPlace; use ccm::consts::{U8, U12, U16}; +use ctr::cipher::StreamCipher as CtrStreamCipher; +use hmac::{Hmac, Mac as RustCryptoMac}; use md5::{Digest, Md5}; +use sha1::Sha1; use crate::{ AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoError, - StreamCipher, StreamCipherAlgorithm, + Mac as StreamMac, SecretVec, StreamCipher, StreamCipherAlgorithm, }; const AES_BLOCK_LEN: usize = 16; @@ -17,6 +20,78 @@ const CCM_NONCE_LEN: usize = 12; type Aes128Ccm = Ccm; type Aes128Ccm8 = Ccm; +// SRTP counter mode (RFC 3711 section 4.1.1) is AES-CTR with a big-endian 128-bit counter. +type Aes128Ctr = ctr::Ctr128BE; +type Aes256Ctr = ctr::Ctr128BE; + +/// Fills `output` from the thread-local CSPRNG shared by the built-in providers. +/// +/// This is a ChaCha-based generator seeded from the operating system and periodically reseeded, +/// not a per-call OS read. `ring::rand::SystemRandom` and `aws_lc_rs::rand::SystemRandom` reach +/// the OS on every call — measured at ~829 ns and ~2196 ns for an 8-byte fill, against ~8 ns +/// here. DTLS generates a GCM explicit nonce and a CBC record IV *per record*, so the difference +/// showed up as a 3-8x regression on DTLS encryption; see `rtc-dtls/benches/README.md`. +/// +/// This restores what the pre-provider code did (`rand::rng()`), and matches how BoringSSL and +/// OpenSSL buffer internally. A deployment that requires every byte of entropy to come from a +/// validated module supplies its own [`RTCRandom`](crate::RTCRandom) implementation; that is what +/// the trait is for. +pub(crate) fn fill_random(output: &mut [u8]) -> Result<(), CryptoError> { + rand::fill(output); + Ok(()) +} + +type HmacSha1 = Hmac; + +/// HMAC-SHA1 backed by RustCrypto, keyed once. +/// +/// `ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 +/// instructions, measuring 4469 ns against RustCrypto's 1373 ns over a 1212-byte SRTP packet — +/// 3.3x, and the whole of the SRTP AES-CM/HMAC regression. The built-in providers are already +/// composite (AES-CTR, CCM, CBC and MD5 come from RustCrypto too), so HMAC-SHA1 is composed the +/// same way rather than making the slower backend the default. `aws-lc-rs` has fast SHA-1 and +/// keeps its own. +pub(crate) struct RustCryptoHmacSha1 { + keyed: HmacSha1, +} + +impl RustCryptoHmacSha1 { + pub(crate) fn new(key: &[u8]) -> Self { + Self { + // HMAC accepts any key length: it hashes longer keys and zero-pads shorter ones. + keyed: ::new_from_slice(key) + .expect("HMAC accepts keys of any length"), + } + } +} + +impl StreamMac for RustCryptoHmacSha1 { + fn output_len(&self) -> usize { + 20 + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + check_tag_len(20, output.len())?; + let mut mac = self.keyed.clone(); + for part in input { + mac.update(part); + } + output.copy_from_slice(&mac.finalize().into_bytes()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + check_tag_len(20, expected.len())?; + let mut actual = [0u8; 20]; + self.sign(input, &mut actual)?; + if crate::constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + pub(crate) fn md5(data: &[u8]) -> Vec { Md5::digest(data).to_vec() } @@ -48,11 +123,20 @@ pub(crate) fn new_stream_cipher( algorithm: StreamCipherAlgorithm, key: &[u8], ) -> Result, CryptoError> { - let key = match algorithm { - StreamCipherAlgorithm::Aes128Ctr => ExpandedAesKey::new_128(key)?, - StreamCipherAlgorithm::Aes256Ctr => ExpandedAesKey::new_256(key)?, + let bits = match algorithm { + StreamCipherAlgorithm::Aes128Ctr => { + check_key_len(16, key.len())?; + AesKeyBits::Aes128 + } + StreamCipherAlgorithm::Aes256Ctr => { + check_key_len(32, key.len())?; + AesKeyBits::Aes256 + } }; - Ok(Box::new(AesCtr { key })) + Ok(Box::new(AesCtr { + key: SecretVec::new(key.to_vec()), + bits, + })) } pub(crate) fn new_cbc( @@ -87,22 +171,15 @@ pub(crate) fn new_ccm( Ok(Box::new(cipher)) } -// Both variants store an expanded key inside an already boxed cipher object. Keeping them inline -// avoids another allocation in every constructed state object. +// Stores an expanded key inside an already boxed cipher object, avoiding another allocation in +// every constructed state object. Only AES-256 is needed: `CbcAlgorithm` has a single variant, +// and CTR now goes through the `ctr` crate. #[allow(clippy::large_enum_variant)] enum ExpandedAesKey { - Aes128(Aes128), Aes256(Aes256), } impl ExpandedAesKey { - fn new_128(key: &[u8]) -> Result { - check_key_len(16, key.len())?; - Ok(Self::Aes128( - Aes128::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?, - )) - } - fn new_256(key: &[u8]) -> Result { check_key_len(32, key.len())?; Ok(Self::Aes256( @@ -112,37 +189,52 @@ impl ExpandedAesKey { fn encrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { match self { - Self::Aes128(cipher) => cipher.encrypt_block(GenericArray::from_mut_slice(block)), Self::Aes256(cipher) => cipher.encrypt_block(GenericArray::from_mut_slice(block)), } } fn decrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { match self { - Self::Aes128(cipher) => cipher.decrypt_block(GenericArray::from_mut_slice(block)), Self::Aes256(cipher) => cipher.decrypt_block(GenericArray::from_mut_slice(block)), } } } +/// AES counter mode. +/// +/// Delegates to the `ctr` crate rather than driving `encrypt_block` once per 16-byte block, which +/// defeats the batching that lets AES-NI / ARMv8 crypto instructions pipeline. Measured ~9-10% +/// faster on a 1200-byte SRTP payload and ~8-10% slower on a two-block RTCP packet, where the +/// per-call setup dominates; RTP traffic dominates in practice. See +/// `rtc-srtp/benches/README.md`. +/// +/// The key is retained rather than pre-expanded because `ctr::Ctr128BE` owns its own cipher +/// state and is constructed per call. It is held in a [`SecretVec`] so it is zeroized on drop. struct AesCtr { - key: ExpandedAesKey, + key: SecretVec, + bits: AesKeyBits, +} + +#[derive(Clone, Copy)] +enum AesKeyBits { + Aes128, + Aes256, } impl StreamCipher for AesCtr { fn apply_keystream(&mut self, iv: &[u8], data: &mut [u8]) -> Result<(), CryptoError> { check_nonce_len(AES_BLOCK_LEN, iv.len())?; - let mut counter: [u8; AES_BLOCK_LEN] = iv - .try_into() - .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; - - for chunk in data.chunks_mut(AES_BLOCK_LEN) { - let mut stream_block = counter; - self.key.encrypt(&mut stream_block); - for (byte, mask) in chunk.iter_mut().zip(stream_block) { - *byte ^= mask; + let nonce = GenericArray::from_slice(iv); + let key = self.key.as_ref(); + match self.bits { + AesKeyBits::Aes128 => { + let mut stream = Aes128Ctr::new(GenericArray::from_slice(key), nonce); + stream.apply_keystream(data); + } + AesKeyBits::Aes256 => { + let mut stream = Aes256Ctr::new(GenericArray::from_slice(key), nonce); + stream.apply_keystream(data); } - increment_be(&mut counter); } Ok(()) } @@ -272,16 +364,6 @@ impl AeadCipher for CommonCcm { } } -fn increment_be(counter: &mut [u8; AES_BLOCK_LEN]) { - for byte in counter.iter_mut().rev() { - let (next, overflow) = byte.overflowing_add(1); - *byte = next; - if !overflow { - break; - } - } -} - fn check_blocks(blocks: &[u8]) -> Result<(), CryptoError> { if blocks.is_empty() || !blocks.len().is_multiple_of(AES_BLOCK_LEN) { return Err(CryptoError::OutputTooSmall { diff --git a/rtc-crypto/src/conformance.rs b/rtc-crypto/src/conformance.rs index e9236010..3612e188 100644 --- a/rtc-crypto/src/conformance.rs +++ b/rtc-crypto/src/conformance.rs @@ -92,38 +92,41 @@ pub fn assert_hashes_and_hmac(crypto: &dyn RTCCrypto) { let key = [0x0b; 20]; let mut sha1 = [0; 20]; - crypto - .hmac(HmacAlgorithm::Sha1, &key, &[b"Hi ", b"There"], &mut sha1) - .unwrap(); + let mut sha1_mac = crypto.new_hmac(HmacAlgorithm::Sha1, &key).unwrap(); + assert_eq!(sha1_mac.output_len(), 20); + sha1_mac.sign(&[b"Hi ", b"There"], &mut sha1).unwrap(); assert_eq!( sha1.as_slice(), bytes("b617318655057264e28bc0b6fb378c8ef146be00") ); + // A keyed MAC is reusable: the second message must not be affected by the first. + let mut repeat = [0; 20]; + sha1_mac.sign(&[b"Hi ", b"There"], &mut repeat).unwrap(); + assert_eq!(repeat, sha1, "a Mac must produce the same tag when reused"); + let mut sha256 = [0; 32]; - crypto - .hmac( - HmacAlgorithm::Sha256, - &key, - &[b"Hi ", b"There"], - &mut sha256, - ) - .unwrap(); + let mut sha256_mac = crypto.new_hmac(HmacAlgorithm::Sha256, &key).unwrap(); + sha256_mac.sign(&[b"Hi ", b"There"], &mut sha256).unwrap(); assert_eq!( sha256.as_slice(), bytes("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") ); - crypto - .verify_hmac(HmacAlgorithm::Sha256, &key, &[b"Hi There"], &sha256) - .unwrap(); + + // Splitting the input across slices must not change the tag. + let mut joined = [0; 32]; + sha256_mac.sign(&[b"Hi There"], &mut joined).unwrap(); + assert_eq!(joined, sha256, "slice boundaries must not affect the tag"); + + sha256_mac.verify(&[b"Hi There"], &sha256).unwrap(); let mut bad_tag = sha256; bad_tag[0] ^= 1; assert_eq!( - crypto.verify_hmac(HmacAlgorithm::Sha256, &key, &[b"Hi There"], &bad_tag), + sha256_mac.verify(&[b"Hi There"], &bad_tag), Err(CryptoError::AuthenticationFailed) ); assert!(matches!( - crypto.hmac(HmacAlgorithm::Sha256, &key, &[b"x"], &mut [0; 31]), + sha256_mac.sign(&[b"x"], &mut [0; 31]), Err(CryptoError::InvalidTagLength { .. }) )); } diff --git a/rtc-crypto/src/lib.rs b/rtc-crypto/src/lib.rs index 8536e4b3..84bbb32b 100644 --- a/rtc-crypto/src/lib.rs +++ b/rtc-crypto/src/lib.rs @@ -37,6 +37,7 @@ const _: () = { _provider: &dyn RTCCryptoProvider, _crypto: &dyn RTCCrypto, _random: &dyn RTCRandom, + _mac: &dyn Mac, _stream: &dyn StreamCipher, _aead: &dyn AeadCipher, _cbc: &dyn CbcCipher, diff --git a/rtc-crypto/src/providers/aws_lc_rs.rs b/rtc-crypto/src/providers/aws_lc_rs.rs index 4ab4a4d4..1988f2b2 100644 --- a/rtc-crypto/src/providers/aws_lc_rs.rs +++ b/rtc-crypto/src/providers/aws_lc_rs.rs @@ -1,15 +1,15 @@ use std::sync::Arc; -use aws_lc_rs::rand::{SecureRandom, SystemRandom}; +use aws_lc_rs::rand::SystemRandom; use aws_lc_rs::signature::{self, KeyPair}; use aws_lc_rs::{aead, agreement, digest, hmac}; use crate::common; use crate::{ ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, - CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, - PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, SignatureScheme, - SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, Mac, + PublicKey, PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, + SignatureScheme, SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, }; /// The built-in AWS-LC-RS provider bundle. @@ -55,9 +55,7 @@ pub struct AwsLcRsRandom; impl RTCRandom for AwsLcRsRandom { fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { - SystemRandom::new() - .fill(output) - .map_err(|_| CryptoError::RandomnessFailed) + common::fill_random(output) } } @@ -113,38 +111,11 @@ impl RTCCrypto for AwsLcRsCrypto { } } - fn hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - output: &mut [u8], - ) -> Result<(), CryptoError> { - common::check_tag_len(algorithm.output_len(), output.len())?; - let key = hmac::Key::new(hmac_algorithm(algorithm), key); - let mut context = hmac::Context::with_key(&key); - for part in input { - context.update(part); - } - output.copy_from_slice(context.sign().as_ref()); - Ok(()) - } - - fn verify_hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - expected: &[u8], - ) -> Result<(), CryptoError> { - common::check_tag_len(algorithm.output_len(), expected.len())?; - let mut actual = vec![0; algorithm.output_len()]; - self.hmac(algorithm, key, input, &mut actual)?; - if constant_time_eq(&actual, expected) { - Ok(()) - } else { - Err(CryptoError::AuthenticationFailed) - } + fn new_hmac(&self, algorithm: HmacAlgorithm, key: &[u8]) -> Result, CryptoError> { + Ok(Box::new(AwsLcRsHmac { + key: hmac::Key::new(hmac_algorithm(algorithm), key), + output_len: algorithm.output_len(), + })) } fn block_encrypt( @@ -221,6 +192,42 @@ impl RTCCrypto for AwsLcRsCrypto { } } +/// A keyed HMAC holding an `aws_lc_rs::hmac::Key`. +/// +/// `Key::new` performs the ipad/opad derivation; `Context::with_key` only clones the resulting +/// state. Keeping the key here is what moves that derivation off the per-packet path. +struct AwsLcRsHmac { + key: hmac::Key, + output_len: usize, +} + +impl Mac for AwsLcRsHmac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, output.len())?; + let mut context = hmac::Context::with_key(&self.key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, expected.len())?; + let mut actual = vec![0; self.output_len]; + self.sign(input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { match algorithm { HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, diff --git a/rtc-crypto/src/providers/ring.rs b/rtc-crypto/src/providers/ring.rs index d53ce229..75b9c2b1 100644 --- a/rtc-crypto/src/providers/ring.rs +++ b/rtc-crypto/src/providers/ring.rs @@ -4,15 +4,15 @@ use ring::aead; use ring::agreement; use ring::digest; use ring::hmac; -use ring::rand::{SecureRandom, SystemRandom}; +use ring::rand::SystemRandom; use ring::signature::{self, KeyPair}; use crate::common; use crate::{ ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, - CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, - PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, SignatureScheme, - SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, Mac, + PublicKey, PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, + SignatureScheme, SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, }; /// The built-in Ring provider bundle. @@ -58,9 +58,7 @@ pub struct RingRandom; impl RTCRandom for RingRandom { fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { - SystemRandom::new() - .fill(output) - .map_err(|_| CryptoError::RandomnessFailed) + common::fill_random(output) } } @@ -116,37 +114,16 @@ impl RTCCrypto for RingCrypto { } } - fn hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - output: &mut [u8], - ) -> Result<(), CryptoError> { - common::check_tag_len(algorithm.output_len(), output.len())?; - let key = hmac::Key::new(hmac_algorithm(algorithm), key); - let mut context = hmac::Context::with_key(&key); - for part in input { - context.update(part); - } - output.copy_from_slice(context.sign().as_ref()); - Ok(()) - } - - fn verify_hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - expected: &[u8], - ) -> Result<(), CryptoError> { - common::check_tag_len(algorithm.output_len(), expected.len())?; - let mut actual = vec![0; algorithm.output_len()]; - self.hmac(algorithm, key, input, &mut actual)?; - if constant_time_eq(&actual, expected) { - Ok(()) - } else { - Err(CryptoError::AuthenticationFailed) + fn new_hmac(&self, algorithm: HmacAlgorithm, key: &[u8]) -> Result, CryptoError> { + match algorithm { + // ring's SHA-1 is a software implementation and measures 3.3x slower than + // RustCrypto's; see common::RustCryptoHmacSha1. SHA-256 stays on ring, which uses + // the hardware instructions. + HmacAlgorithm::Sha1 => Ok(Box::new(common::RustCryptoHmacSha1::new(key))), + HmacAlgorithm::Sha256 => Ok(Box::new(RingHmac { + key: hmac::Key::new(hmac_algorithm(algorithm), key), + output_len: algorithm.output_len(), + })), } } @@ -224,6 +201,42 @@ impl RTCCrypto for RingCrypto { } } +/// A keyed HMAC holding a `ring::hmac::Key`. +/// +/// `Key::new` performs the ipad/opad derivation; `Context::with_key` only clones the resulting +/// state. Keeping the key here is what moves that derivation off the per-packet path. +struct RingHmac { + key: hmac::Key, + output_len: usize, +} + +impl Mac for RingHmac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, output.len())?; + let mut context = hmac::Context::with_key(&self.key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, expected.len())?; + let mut actual = vec![0; self.output_len]; + self.sign(input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { match algorithm { HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, diff --git a/rtc-crypto/src/traits.rs b/rtc-crypto/src/traits.rs index 17f8e686..bcab4f19 100644 --- a/rtc-crypto/src/traits.rs +++ b/rtc-crypto/src/traits.rs @@ -36,32 +36,6 @@ pub trait RTCCrypto: Send + Sync { ))) } - /// Computes a native-length HMAC into `output`. - fn hmac( - &self, - algorithm: HmacAlgorithm, - _key: &[u8], - _input: &[&[u8]], - _output: &mut [u8], - ) -> Result<(), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( - algorithm, - ))) - } - - /// Verifies a complete native-length HMAC tag. - fn verify_hmac( - &self, - algorithm: HmacAlgorithm, - _key: &[u8], - _input: &[&[u8]], - _expected: &[u8], - ) -> Result<(), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( - algorithm, - ))) - } - /// Encrypts exactly one block in place. fn block_encrypt( &self, @@ -74,6 +48,18 @@ pub trait RTCCrypto: Send + Sync { )) } + /// Creates a keyed MAC. + /// + /// This is the only HMAC entry point. Deriving the key schedule is the expensive part, so it + /// happens here rather than per message; a caller that authenticates many messages with one + /// key holds the returned [`Mac`]. One-shot callers simply drop it after a single `sign` or + /// `verify`. + fn new_hmac(&self, algorithm: HmacAlgorithm, _key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))) + } + /// Creates a keyed stream cipher. fn new_stream_cipher( &self, @@ -152,6 +138,30 @@ pub trait RTCCrypto: Send + Sync { } } +/// A keyed message authentication code with a reusable key schedule. +/// +/// Created once per key by [`RTCCrypto::new_hmac`] and used for every message authenticated with +/// that key, so the ipad/opad derivation is paid once rather than per packet. +/// +/// `Send` and mutable, like the keyed cipher traits, and for the same reason: `&mut self` lets an +/// implementation carry per-message state — a reused streaming context, a hardware session +/// handle — without interior mutability, and does not impose `Sync` on implementors that cannot +/// offer it. A caller whose own signature is fixed to `&self`, such as STUN's `Setter::add_to`, +/// can still create a local `Mac` per message and use it mutably. +pub trait Mac: Send { + /// Returns the untruncated tag length in bytes. + fn output_len(&self) -> usize; + + /// Writes the tag over the concatenation of `input` into `output`. + /// + /// `output` must be exactly [`output_len`](Self::output_len) bytes. Protocols that transmit a + /// truncated tag — SRTP sends 80 or 32 bits of an SHA-1 tag — truncate the result themselves. + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError>; + + /// Verifies a complete untruncated tag in constant time. + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError>; +} + /// A keyed stream cipher with a reusable expanded key. pub trait StreamCipher: Send { /// Applies the keystream in place with a fresh IV. diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index 3719441e..ff2032c6 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -32,6 +32,7 @@ log.workspace = true pem.workspace = true [dev-dependencies] +criterion.workspace = true local-sync.workspace = true core_affinity.workspace = true chrono.workspace = true @@ -86,3 +87,7 @@ futures.workspace = true #name = "listen_verify" #path = "examples/listen/verify/listen_verify.rs" #bench = false + +[[bench]] +name = "record_protection" +harness = false diff --git a/rtc-dtls/benches/README.md b/rtc-dtls/benches/README.md new file mode 100644 index 00000000..8b956c38 --- /dev/null +++ b/rtc-dtls/benches/README.md @@ -0,0 +1,104 @@ +# DTLS record-protection benchmarks + +```bash +cargo bench --package rtc-dtls --bench record_protection +cargo bench --package rtc-dtls --bench record_protection --no-default-features --features aws-lc-rs +``` + +`Setup/*` constructs a cipher — provider dispatch, key import, key schedule, and (after G3) keying +the record MAC. Paid once per DTLS epoch. `Encrypt/*` and `Decrypt/*` protect one 1200-byte record +on an already-keyed cipher; that is the hot path. + +## G3 crypto-provider migration: measured impact + +One machine (Apple M1 Max, macOS 26.5.2), identical criterion settings, comparing a worktree at +`fd81f68` (P4 — the last commit before DTLS moved to `rtc-crypto`) against the current tree. + +| Benchmark | Pre-migration | ring (default) | aws-lc-rs | +|---|---|---|---| +| `Setup/AES-128-GCM` | 274.2 ns | 326.9 ns | 296.6 ns | +| `Encrypt/AES-128-GCM` | 270.5 ns | 270.1 ns | 245.8 ns | +| `Decrypt/AES-128-GCM` | 280.6 ns | 274.8 ns | 225.6 ns | +| `Setup/AES-256-CBC` | 86.5 ns | 755.9 ns | 818.4 ns | +| `Encrypt/AES-256-CBC` | 3.419 µs | 3.204 µs | 2.498 µs | +| `Decrypt/AES-256-CBC` | 2.036 µs | 1.882 µs | 1.194 µs | + +**Every per-record path is at parity or better than before the migration**, on both providers. + +`Setup/*` rose, which is the intended trade: the record MAC key schedule moved there from the +per-record path, so it is paid once per epoch instead of once per record. + +Getting here took three fixes, each found by an initial measurement that showed encryption +3-8x slower. They are documented below in the order they were diagnosed. + +## Fixed: per-record `SystemRandom` + +DTLS generates fresh randomness per record — the GCM explicit nonce (`crypto_gcm.rs`, RFC 5288 +§3) and the CBC record IV (`crypto_cbc.rs`, RFC 5246 §6.2.3.2). Decryption does not, which is why +only encryption had regressed, and why `aws-lc-rs` — faster everywhere else — was the worse of the +two. + +Before G3 that randomness came from `rand::rng()`, a thread-local ChaCha CSPRNG. After it, it went +through `RTCRandom`, whose built-ins called the backend's `SystemRandom`, which reaches the +operating system on every call: + +| 8-byte fill | ns/call | +|---|---| +| `rand::fill` (thread-local) | **8.3** | +| `ring::rand::SystemRandom` | 829.1 | +| `aws_lc_rs::rand::SystemRandom` | 2196.5 | + +That accounted for the deltas almost exactly. Caching one `SystemRandom` instead of constructing +per call recovers only ~9% — the OS round trip dominates, so the handle was never the problem. + +The built-in `RTCRandom` implementations now use an OS-seeded, periodically reseeded thread-local +CSPRNG (`common::fill_random`), restoring what the pre-provider code did and what BoringSSL and +OpenSSL do internally. `SystemRandom` is still used where the backend owns the operation — +keypair generation and signing. + +| Benchmark (ring) | Backend `SystemRandom` | Thread-local CSPRNG | +|---|---|---| +| `Encrypt/AES-128-GCM` | 1.015 µs | **262.4 ns** | +| `Encrypt/AES-256-CBC` | 6.928 µs | 6.003 µs | + +A deployment that requires every byte of entropy to come from a validated module supplies its own +`RTCRandom`; that is what the trait is for. + +## Fixed: per-record HMAC key setup in CBC + +`CryptoCbc` passed raw key bytes to `prf_mac` on every record, so the HMAC key schedule was +re-derived per record — the same defect found in SRTP. It now holds two keyed `Mac` objects, keyed +once per epoch, and `prf_mac` takes `&mut dyn Mac` rather than a crypto handle plus a key. + +| Benchmark (ring) | Per-record keying | Pre-keyed `Mac` | +|---|---|---| +| `Encrypt/AES-256-CBC` | 7.240 µs | 6.928 µs | +| `Decrypt/AES-256-CBC` | 5.127 µs | 4.749 µs | + +`Setup/AES-256-CBC` rises correspondingly: two MACs are keyed there instead of on every record. + +## Fixed: `ring`'s software SHA-1 + +CBC authenticates every record with HMAC-SHA1, and `ring` exposes SHA-1 only as +`HMAC_SHA1_FOR_LEGACY_USE_ONLY`, without the ARMv8 SHA-1 instructions — 4469 ns against +RustCrypto's 1373 ns over 1212 bytes. The `ring` provider now composes RustCrypto's HMAC-SHA1, as +it already composes RustCrypto for AES-CTR, CCM, CBC and MD5. See `rtc-srtp/benches/README.md`. + +| Benchmark (ring) | `ring` SHA-1 | RustCrypto SHA-1 | +|---|---|---| +| `Encrypt/AES-256-CBC` | 6.003 µs | **3.204 µs** | +| `Decrypt/AES-256-CBC` | 4.702 µs | **1.882 µs** | + +## Reproducing + +```bash +# Current +cargo bench --package rtc-dtls --bench record_protection -- --warm-up-time 2 --measurement-time 4 + +# Pre-migration baseline: the bench does not exist at fd81f68, so port this file to the +# pre-G3 API — CryptoGcm::new / CryptoCbc::new took keys directly, without a provider. +git worktree add /tmp/rtc-dtls-base fd81f68 +``` + +The methodology, including why cross-machine numbers must not be compared, is in +`docs/benchmarking-crypto-migration.md`. diff --git a/rtc-dtls/benches/record_protection.rs b/rtc-dtls/benches/record_protection.rs new file mode 100644 index 00000000..dd8e540e --- /dev/null +++ b/rtc-dtls/benches/record_protection.rs @@ -0,0 +1,144 @@ +//! DTLS record-protection benchmarks. +//! +//! Measures the per-record cost of each record cipher after the G3 crypto-provider migration, +//! and separates it from one-time key-schedule setup: +//! +//! * `Setup/*` constructs a cipher, so it covers the provider lookup, key import, and key +//! schedule. This happens once per DTLS epoch. +//! * `Encrypt/*` and `Decrypt/*` protect and unprotect a single record on an already-keyed +//! cipher. This is the hot path and the number that matters for throughput. +//! +//! The split exists because the provider indirection is deliberately concentrated in setup: a +//! keyed cipher object is obtained once and then used per record with no further provider +//! dispatch. A regression in `Encrypt/*` would mean that property broke. +//! +//! Every cipher runs against each enabled built-in provider under identical inputs, so the two +//! backends are directly comparable. +//! +//! Run with: +//! +//! ```text +//! cargo bench --package rtc-dtls --bench record_protection +//! cargo bench --package rtc-dtls --bench record_protection --no-default-features --features aws-lc-rs +//! ``` + +use std::sync::Arc; + +use criterion::measurement::WallTime; +use criterion::{BenchmarkGroup, Criterion, criterion_main}; + +use crypto::RTCCryptoProvider; +use rtc_dtls::content::ContentType; +use rtc_dtls::crypto::crypto_cbc::CryptoCbc; +use rtc_dtls::crypto::crypto_gcm::CryptoGcm; +use rtc_dtls::record_layer::record_layer_header::{ + PROTOCOL_VERSION1_2, RECORD_LAYER_HEADER_SIZE, RecordLayerHeader, +}; + +/// A 1200-byte record, sized to a typical WebRTC MTU-bound payload. +const PAYLOAD_LEN: usize = 1200; + +const KEY_128: &[u8] = &[ + 0x60, 0xb4, 0x1f, 0x04, 0x77, 0x89, 0x80, 0xfc, 0x4b, 0xc2, 0xfc, 0x2c, 0x3f, 0x38, 0x3d, 0x37, +]; +const KEY_256: &[u8] = &[ + 0x60, 0xb4, 0x1f, 0x04, 0x77, 0x89, 0x80, 0xfc, 0x4b, 0xc2, 0xfc, 0x2c, 0x3f, 0x38, 0x3d, 0x37, + 0xf7, 0x1a, 0x31, 0x5e, 0x63, 0x1d, 0x4f, 0x5e, 0x05, 0x6f, 0xfc, 0xd8, 0x3e, 0xc3, 0x11, 0x22, +]; +const IV_GCM: &[u8] = &[0xf7, 0x1a, 0x31, 0x5e]; +const MAC_SHA1: &[u8] = &[ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, +]; + +/// A DTLS 1.2 application-data record: marshalled header followed by the payload. +/// +/// The header must be marshalled rather than zero-filled, because `decrypt` re-parses it from +/// the ciphertext buffer. +fn record() -> (RecordLayerHeader, Vec) { + let header = RecordLayerHeader { + content_type: ContentType::ApplicationData, + protocol_version: PROTOCOL_VERSION1_2, + epoch: 1, + sequence_number: 1, + content_len: PAYLOAD_LEN as u16, + }; + let mut raw = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + PAYLOAD_LEN); + header.marshal(&mut raw).unwrap(); + raw.extend((0..PAYLOAD_LEN).map(|index| index as u8)); + (header, raw) +} + +/// The built-in providers compiled into this benchmark. +fn providers() -> Vec<(&'static str, Arc)> { + let mut providers: Vec<(&'static str, Arc)> = Vec::new(); + #[cfg(feature = "ring")] + providers.push(("ring", Arc::new(crypto::providers::RingProvider::default()))); + #[cfg(feature = "aws-lc-rs")] + providers.push(( + "aws-lc-rs", + Arc::new(crypto::providers::AwsLcRsProvider::default()), + )); + assert!( + !providers.is_empty(), + "enable `ring` or `aws-lc-rs` to run these benchmarks" + ); + providers +} + +fn benchmark_gcm(group: &mut BenchmarkGroup) { + for (name, provider) in providers() { + // One-time cost: provider dispatch, key import, key schedule. + group.bench_function(format!("Setup/AES-128-GCM/{name}"), |b| { + b.iter(|| { + CryptoGcm::new(Arc::clone(&provider), KEY_128, IV_GCM, KEY_128, IV_GCM).unwrap() + }); + }); + + let mut cipher = + CryptoGcm::new(Arc::clone(&provider), KEY_128, IV_GCM, KEY_128, IV_GCM).unwrap(); + let (header, raw) = record(); + + group.bench_function(format!("Encrypt/AES-128-GCM/{name}"), |b| { + b.iter(|| cipher.encrypt(&header, &raw).unwrap()); + }); + + let encrypted = cipher.encrypt(&header, &raw).unwrap(); + group.bench_function(format!("Decrypt/AES-128-GCM/{name}"), |b| { + b.iter(|| cipher.decrypt(&encrypted).unwrap()); + }); + } +} + +fn benchmark_cbc(group: &mut BenchmarkGroup) { + for (name, provider) in providers() { + group.bench_function(format!("Setup/AES-256-CBC/{name}"), |b| { + b.iter(|| { + CryptoCbc::new(Arc::clone(&provider), KEY_256, MAC_SHA1, KEY_256, MAC_SHA1).unwrap() + }); + }); + + let mut cipher = + CryptoCbc::new(Arc::clone(&provider), KEY_256, MAC_SHA1, KEY_256, MAC_SHA1).unwrap(); + let (header, raw) = record(); + + group.bench_function(format!("Encrypt/AES-256-CBC/{name}"), |b| { + b.iter(|| cipher.encrypt(&header, &raw).unwrap()); + }); + + let encrypted = cipher.encrypt(&header, &raw).unwrap(); + group.bench_function(format!("Decrypt/AES-256-CBC/{name}"), |b| { + b.iter(|| cipher.decrypt(&encrypted).unwrap()); + }); + } +} + +fn benches() { + let mut criterion = Criterion::default().configure_from_args(); + let mut group = criterion.benchmark_group("DTLS"); + benchmark_gcm(&mut group); + benchmark_cbc(&mut group); + group.finish(); +} + +criterion_main!(benches); diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 3713f8a6..9d902bcc 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -393,10 +393,14 @@ impl ConfigBuilder { is_client: bool, remote_addr: Option, ) -> Result { - let crypto_provider = match self.crypto_provider.take() { - Some(provider) => provider, - None => crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?, - }; + // The caller supplies the provider; this crate never resolves a default. An application + // that wants the feature-selected built-in passes `crypto::default_provider()?`. + let crypto_provider = self.crypto_provider.take().ok_or_else(|| { + Error::Crypto( + "no crypto provider configured: call ConfigBuilder::with_crypto_provider" + .to_owned(), + ) + })?; self.validate(is_client)?; let mut local_cipher_suites: Vec = @@ -528,6 +532,8 @@ impl ConfigBuilder { None }; + // Fields not derived from the builder come from `HandshakeConfig::new`, which also + // supplies `crypto_provider`. Ok(HandshakeConfig { crypto_provider, local_psk_callback: self.psk.take(), @@ -540,6 +546,7 @@ impl ConfigBuilder { server_name, client_auth: self.client_auth, local_certificates: self.certificates, + name_to_certificate: Default::default(), insecure_skip_verify: self.insecure_skip_verify, insecure_verification: self.insecure_verification, verify_peer_certificate: self.verify_peer_certificate.take(), @@ -549,8 +556,8 @@ impl ConfigBuilder { retransmit_interval, initial_epoch: 0, maximum_transmission_unit, + maximum_retransmit_number: 0, replay_protection_window, - ..Default::default() }) } } @@ -605,7 +612,7 @@ pub struct HandshakeConfig { pub(crate) roots_cas: rustls::RootCertStore, pub(crate) server_cert_verifier: Option>, pub(crate) client_cert_verifier: Option>, - pub(crate) retransmit_interval: std::time::Duration, + pub(crate) retransmit_interval: Duration, pub(crate) initial_epoch: u16, pub(crate) maximum_transmission_unit: usize, pub(crate) maximum_retransmit_number: usize, @@ -641,11 +648,10 @@ impl fmt::Debug for HandshakeConfig { } } -impl Default for HandshakeConfig { - fn default() -> Self { - HandshakeConfig { - crypto_provider: crypto::default_provider() - .expect("rtc-dtls requires an enabled default crypto provider"), +impl HandshakeConfig { + pub(crate) fn new(crypto_provider: Arc) -> Self { + Self { + crypto_provider, local_psk_callback: None, local_psk_identity_hint: None, local_cipher_suites: vec![], @@ -672,9 +678,7 @@ impl Default for HandshakeConfig { replay_protection_window: DEFAULT_REPLAY_PROTECTION_WINDOW, } } -} -impl HandshakeConfig { pub(crate) fn provider(&self) -> &Arc { &self.crypto_provider } diff --git a/rtc-dtls/src/config/config_test.rs b/rtc-dtls/src/config/config_test.rs index ec3d5bf7..0c94dafd 100644 --- a/rtc-dtls/src/config/config_test.rs +++ b/rtc-dtls/src/config/config_test.rs @@ -32,24 +32,36 @@ impl crypto::RTCRandom for IncompleteProvider { #[derive(Debug)] struct MockSigner; -impl CustomSigner for MockSigner { - fn sign(&self, _message: &[u8]) -> std::result::Result, String> { - Ok(vec![]) +impl crypto::SigningKey for MockSigner { + fn supports(&self, _scheme: crypto::SignatureScheme) -> bool { + true + } + + fn public_key(&self) -> crypto::PublicKey<'_> { + crypto::PublicKey { + encoding: crypto::PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], + } } - fn clone_box(&self) -> Box { - Box::new(MockSigner) + fn sign( + &self, + _scheme: crypto::SignatureScheme, + _message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { + Ok(vec![]) } } #[test] -fn test_config_accepts_custom_signer() -> Result<()> { +fn test_config_accepts_external_signing_key() -> Result<()> { let cert = Certificate { certificate: vec![], - private_key: CryptoPrivateKey::from_custom_signer(Box::new(MockSigner)), + private_key: CryptoPrivateKey::from_signing_key(std::sync::Arc::new(MockSigner)), }; let handshake = ConfigBuilder::default() + .with_crypto_provider(crypto::default_provider().map_err(crypto_error)?) .with_certificates(vec![cert]) .build(false, None)?; diff --git a/rtc-dtls/src/conn/conn_test.rs b/rtc-dtls/src/conn/conn_test.rs index cb3d8607..dbbfcbb4 100644 --- a/rtc-dtls/src/conn/conn_test.rs +++ b/rtc-dtls/src/conn/conn_test.rs @@ -308,7 +308,9 @@ async fn test_export_keying_material() -> shared::error::Result<()> { closed: AtomicBool::new(false), current_flight: Box::new(Flight0 {}) as Box, flights: None, - cfg: HandshakeConfig::default(), + cfg: HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + ), retransmit: false, handshake_rx: None, @@ -2383,7 +2385,9 @@ async fn test_renegotation_info() -> Result<()> { /// is completed (RFC 6347: buffer only until Finished is received). #[test] fn test_read_does_not_enqueue_after_handshake_completed() { - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); // Mark handshake as completed. @@ -2414,7 +2418,9 @@ fn test_read_does_not_enqueue_after_handshake_completed() { /// is in progress (needed to handle Finished arriving before ChangeCipherSpec). #[test] fn test_read_enqueues_during_handshake() { - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); assert!(!conn.is_handshake_completed()); @@ -2447,7 +2453,9 @@ fn test_handle_incoming_queued_packets_drains_when_cipher_ready() { use crate::cipher_suite::CipherSuite; use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::CipherSuiteAes128GcmSha256; - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); assert!(!conn.is_handshake_completed()); @@ -2503,7 +2511,9 @@ fn test_handle_incoming_queued_packets_sets_handshake_rx() { use crate::cipher_suite::CipherSuite; use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::CipherSuiteAes128GcmSha256; - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); // Initialize cipher suite. @@ -2548,7 +2558,9 @@ fn test_handle_incoming_queued_packets_sets_handshake_rx() { } fn setup_dtls_conn_server_handshake_in_progress() -> DTLSConn { - let handshake_config = Arc::new(HandshakeConfig::default()); + let handshake_config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); // is_client=false (server), no initial_state → handshake not completed DTLSConn::new(handshake_config, false, None) } diff --git a/rtc-dtls/src/conn/mod.rs b/rtc-dtls/src/conn/mod.rs index 23d6f816..a2c3fa8d 100644 --- a/rtc-dtls/src/conn/mod.rs +++ b/rtc-dtls/src/conn/mod.rs @@ -110,8 +110,7 @@ impl DTLSConn { is_client: bool, initial_state: Option, ) -> Self { - let provider = Some(handshake_config.crypto_provider.clone()); - let (mut state, flight, initial_fsm_state) = if let Some(state) = initial_state { + let (state, flight, initial_fsm_state) = if let Some(state) = initial_state { let flight = if is_client { Box::new(Flight5 {}) as Box } else { @@ -127,15 +126,11 @@ impl DTLSConn { }; ( - State { - is_client, - ..Default::default() - }, + State::new(handshake_config.crypto_provider.clone(), is_client), flight, HandshakeState::Preparing, ) }; - state.crypto_provider = provider; Self { is_client, diff --git a/rtc-dtls/src/crypto/crypto_cbc.rs b/rtc-dtls/src/crypto/crypto_cbc.rs index 02d7fc21..02695f85 100644 --- a/rtc-dtls/src/crypto/crypto_cbc.rs +++ b/rtc-dtls/src/crypto/crypto_cbc.rs @@ -6,7 +6,7 @@ // Removed in TLS 1.3 year 2018. // RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268 -use crypto::{CbcAlgorithm, CbcCipher, RTCCryptoProvider, constant_time_eq}; +use crypto::{CbcAlgorithm, CbcCipher, HmacAlgorithm, Mac, RTCCryptoProvider, constant_time_eq}; use std::io::Cursor; use std::sync::Arc; @@ -21,8 +21,10 @@ pub struct CryptoCbc { provider: Arc, local_cipher: Box, remote_cipher: Box, - write_mac: Vec, - read_mac: Vec, + /// Keyed once per epoch. Re-deriving the HMAC key schedule per record measured ~2x + /// slower on this path; see `rtc-srtp/benches/README.md` for the equivalent SRTP data. + write_mac: Box, + read_mac: Box, } impl CryptoCbc { @@ -45,12 +47,21 @@ impl CryptoCbc { .crypto() .new_cbc(CbcAlgorithm::Aes256Cbc, remote_key) .map_err(crypto_error)?; + // Key the record MACs once per epoch, alongside the ciphers. + let write_mac = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, local_mac) + .map_err(crypto_error)?; + let read_mac = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, remote_mac) + .map_err(crypto_error)?; Ok(CryptoCbc { provider, local_cipher, - write_mac: local_mac.to_vec(), + write_mac, remote_cipher, - read_mac: remote_mac.to_vec(), + read_mac, }) } @@ -67,13 +78,12 @@ impl CryptoCbc { let h = pkt_rlh; let mac = prf_mac( - self.provider.crypto(), + self.write_mac.as_mut(), h.epoch, h.sequence_number, h.content_type, h.protocol_version, &payload, - &self.write_mac, )?; payload.extend_from_slice(&mac); @@ -146,13 +156,12 @@ impl CryptoCbc { let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..]; let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE]; let mac = prf_mac( - self.provider.crypto(), + self.read_mac.as_mut(), h.epoch, h.sequence_number, h.content_type, h.protocol_version, decrypted, - &self.read_mac, )?; if !padding_valid || !constant_time_eq(recv_mac, &mac) { diff --git a/rtc-dtls/src/crypto/crypto_test.rs b/rtc-dtls/src/crypto/crypto_test.rs index 3bb27d33..a3b943da 100644 --- a/rtc-dtls/src/crypto/crypto_test.rs +++ b/rtc-dtls/src/crypto/crypto_test.rs @@ -205,7 +205,10 @@ fn test_certificate_verify() -> Result<()> { ]; //test ECDSA256 - let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; + let certificate_ecdsa256 = Certificate::generate_self_signed( + vec!["localhost".to_owned()], + crypto::default_provider().map_err(crypto_error)?, + )?; let ecdsa_algorithm = SignatureHashAlgorithm { hash: HashAlgorithm::Sha256, signature: SignatureAlgorithm::Ecdsa, @@ -232,6 +235,7 @@ fn test_certificate_verify() -> Result<()> { let certificate_ed25519 = Certificate::generate_self_signed_with_alg( vec!["localhost".to_owned()], &rcgen::PKCS_ED25519, + crypto::default_provider().map_err(crypto_error)?, )?; let ed25519_algorithm = SignatureHashAlgorithm { hash: HashAlgorithm::Sha256, @@ -265,29 +269,36 @@ struct MockSigner { signature: Vec, } -impl CustomSigner for MockSigner { - fn sign(&self, message: &[u8]) -> std::result::Result, String> { +impl SigningKey for MockSigner { + fn supports(&self, _scheme: CryptoSignatureScheme) -> bool { + true + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], + } + } + + fn sign( + &self, + _scheme: CryptoSignatureScheme, + message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { *self.call_count.lock().unwrap() += 1; *self.last_message.lock().unwrap() = message.to_vec(); Ok(self.signature.clone()) } - - fn clone_box(&self) -> Box { - Box::new(MockSigner { - call_count: std::sync::Arc::clone(&self.call_count), - last_message: std::sync::Arc::clone(&self.last_message), - signature: self.signature.clone(), - }) - } } #[test] -fn test_custom_signer_is_invoked_for_signing() -> Result<()> { +fn test_external_signing_key_is_invoked_for_signing() -> Result<()> { let expected_signature = vec![0xca, 0xfe, 0xba, 0xbe]; let call_count = std::sync::Arc::new(std::sync::Mutex::new(0usize)); let last_message = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let private_key = CryptoPrivateKey::from_custom_signer(Box::new(MockSigner { + let private_key = CryptoPrivateKey::from_signing_key(std::sync::Arc::new(MockSigner { call_count: std::sync::Arc::clone(&call_count), last_message: std::sync::Arc::clone(&last_message), signature: expected_signature.clone(), diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index f2728fbf..518931d8 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -67,45 +67,25 @@ pub struct Certificate { } impl Certificate { - /// Generate a self-signed certificate. - /// - /// See [`rcgen::generate_simple_self_signed`]. #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::generate_self_signed_with_provider(subject_alt_names, provider) - } - - /// Generates a self-signed certificate and imports its key into `provider`. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn generate_self_signed_with_provider( + /// Generates a self-signed certificate, importing its key into `provider`. + pub fn generate_self_signed( subject_alt_names: impl Into>, provider: Arc, ) -> Result { - let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names)?; + let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names) + .map_err(|error| Error::Other(error.to_string()))?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::from_key_pair_with_provider(&signing_key, provider)?, + private_key: CryptoPrivateKey::from_key_pair(&signing_key, provider)?, }) } - /// Generate a self-signed certificate with the given algorithm. - /// - /// See `rcgen::Certificate::self_signed`. #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + /// Generates a self-signed certificate with `alg`, importing its key into `provider`. pub fn generate_self_signed_with_alg( subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, - ) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::generate_self_signed_with_alg_and_provider(subject_alt_names, alg, provider) - } - - /// Generates a self-signed certificate with `alg` and imports its key into `provider`. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn generate_self_signed_with_alg_and_provider( - subject_alt_names: impl Into>, - alg: &'static rcgen::SignatureAlgorithm, provider: Arc, ) -> Result { let params = rcgen::CertificateParams::new(subject_alt_names) @@ -118,21 +98,13 @@ impl Certificate { Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::from_key_pair_with_provider(&key_pair, provider)?, + private_key: CryptoPrivateKey::from_key_pair(&key_pair, provider)?, }) } - /// Parses a certificate from the ASCII PEM format. - pub fn from_pem(pem_str: &str) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::from_pem_with_provider(pem_str, provider) - } - /// Parses a PEM certificate and imports its PKCS#8 key into `provider`. - pub fn from_pem_with_provider( - pem_str: &str, - provider: Arc, - ) -> Result { + /// Parses a PEM certificate and imports its PKCS#8 key into `provider`. + pub fn from_pem(pem_str: &str, provider: Arc) -> Result { let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; if pems.len() < 2 { return Err(Error::InvalidPEM(format!( @@ -236,26 +208,11 @@ pub(crate) fn value_key_message( plaintext } -/// Trait for delegating signing to an external service (e.g., HSM, TPM, cloud KMS). -/// -/// Implementations must be thread-safe and cloneable. Each DTLS handshake may -/// clone the signer, so `clone_box` must return a fresh instance that signs -/// with the same key. -pub trait CustomSigner: Send + Sync + std::fmt::Debug { - /// Sign the given message and return the raw signature bytes. - fn sign(&self, message: &[u8]) -> std::result::Result, String>; - - /// Clone this signer into a new boxed instance. - fn clone_box(&self) -> Box; -} - /// Provider-neutral DTLS signing key. #[derive(Clone)] pub struct CryptoPrivateKey { /// Provider-owned signing key. It may be non-exportable. pub signing_key: Arc, - /// DER-encoded keypair retained by the temporary rcgen compatibility adapter. - pub serialized_der: Vec, } impl PartialEq for CryptoPrivateKey { @@ -273,38 +230,14 @@ impl std::fmt::Debug for CryptoPrivateKey { .debug_struct("CryptoPrivateKey") .field("public_key_encoding", &public_key.encoding) .field("public_key_len", &public_key.bytes.len()) - .field("exportable", &(!self.serialized_der.is_empty())) .finish() } } -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -impl TryFrom<&KeyPair> for CryptoPrivateKey { - type Error = Error; - - fn try_from(key_pair: &KeyPair) -> Result { - Self::from_key_pair(key_pair) - } -} - impl CryptoPrivateKey { - /// Derives the signature scheme that matches `key_pair`. - /// - /// # Errors - /// - /// Fails if the key type has no supported scheme. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn from_key_pair(key_pair: &KeyPair) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::from_key_pair_with_provider(key_pair, provider) - } - /// Imports an rcgen key pair into an explicit provider. #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn from_key_pair_with_provider( - key_pair: &KeyPair, - provider: Arc, - ) -> Result { + pub fn from_key_pair(key_pair: &KeyPair, provider: Arc) -> Result { let serialized_der = key_pair.serialize_der(); let scheme = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { CryptoSignatureScheme::Ed25519 @@ -319,50 +252,12 @@ impl CryptoPrivateKey { .crypto() .import_signing_key(scheme, &serialized_der) .map_err(crypto_error)?; - Ok(Self { - signing_key, - serialized_der, - }) + Ok(Self { signing_key }) } /// Wraps a provider-neutral, potentially non-exportable signing key. pub fn from_signing_key(signing_key: Arc) -> Self { - Self { - signing_key, - serialized_der: Vec::new(), - } - } - - /// Adapts the legacy external signer API until that API is removed before 1.0. - pub fn from_custom_signer(signer: Box) -> Self { - Self::from_signing_key(Arc::new(CustomSigningKey { signer })) - } -} - -struct CustomSigningKey { - signer: Box, -} - -impl SigningKey for CustomSigningKey { - fn supports(&self, _scheme: CryptoSignatureScheme) -> bool { - true - } - - fn public_key(&self) -> PublicKey<'_> { - PublicKey { - encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, - bytes: &[], - } - } - - fn sign( - &self, - _scheme: CryptoSignatureScheme, - message: &[u8], - ) -> std::result::Result, crypto::CryptoError> { - self.signer - .sign(message) - .map_err(crypto::CryptoError::Provider) + Self { signing_key } } } @@ -575,10 +470,12 @@ mod test { #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { - let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?; + let provider = crypto::default_provider().map_err(crypto_error)?; + let cert = + Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()], provider.clone())?; let pem = cert.serialize_pem()?; - let loaded_cert = Certificate::from_pem(&pem)?; + let loaded_cert = Certificate::from_pem(&pem, provider)?; assert_eq!(loaded_cert, cert); diff --git a/rtc-dtls/src/curve/named_curve.rs b/rtc-dtls/src/curve/named_curve.rs index dc3f0e2e..6843bedc 100644 --- a/rtc-dtls/src/curve/named_curve.rs +++ b/rtc-dtls/src/curve/named_curve.rs @@ -72,15 +72,4 @@ impl NamedCurve { active: Some(active), }) } - - /// Generates an ephemeral key pair on this curve. - /// - /// # Errors - /// - /// Fails if the curve is unsupported or key generation fails. - pub fn generate_keypair(&self) -> Result { - let provider = - crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; - self.generate_keypair_with_crypto(provider.crypto()) - } } diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index cfe8ede4..56775249 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -355,11 +355,10 @@ mod tests { builder = builder.with_psk_identity_hint(Some(b"rtc-dtls-test".to_vec())); } } else if !is_client { - builder = - builder.with_certificates(vec![Certificate::generate_self_signed_with_provider( - vec!["localhost".to_owned()], - provider, - )?]); + builder = builder.with_certificates(vec![Certificate::generate_self_signed( + vec!["localhost".to_owned()], + provider, + )?]); } Ok(Arc::new(builder.build(is_client, None)?)) } diff --git a/rtc-dtls/src/flight/flight4.rs b/rtc-dtls/src/flight/flight4.rs index 858317ec..3aa3e411 100644 --- a/rtc-dtls/src/flight/flight4.rs +++ b/rtc-dtls/src/flight/flight4.rs @@ -786,9 +786,11 @@ mod tests { // is missing. #[test] fn test_flight4_process_certificateverify() { + let provider = + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"); let mut state = State { cipher_suite: Some(Box::new(MockCipherSuite {})), - ..Default::default() + ..State::new(provider, false) }; let raw_certificate = vec![ @@ -842,7 +844,9 @@ mod tests { true, ); - let cfg = HandshakeConfig::default(); + let cfg = HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + ); let f = Flight4 {}; let res = f.parse(&mut state, &cache, &cfg); diff --git a/rtc-dtls/src/lib.rs b/rtc-dtls/src/lib.rs index e613c2b1..45bd9c0e 100644 --- a/rtc-dtls/src/lib.rs +++ b/rtc-dtls/src/lib.rs @@ -52,6 +52,14 @@ //! [RFC 4492]: https://datatracker.ietf.org/doc/html/rfc4492 //! [RFC 5289]: https://datatracker.ietf.org/doc/html/rfc5289 +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an `Arc`, +/// which a caller must be able to name without adding — and version-matching — a direct +/// `rtc-crypto` dependency. Named `crypto_provider` here because this crate already has its own +/// [`crypto`] module for DTLS record ciphers and certificates. +pub use ::crypto as crypto_provider; + /// Alert records: fatal errors and the orderly `close_notify`. pub mod alert; /// Application data records — the payload DTLS carries once the handshake completes. diff --git a/rtc-dtls/src/prf/mod.rs b/rtc-dtls/src/prf/mod.rs index 52a39994..74ec2346 100644 --- a/rtc-dtls/src/prf/mod.rs +++ b/rtc-dtls/src/prf/mod.rs @@ -3,7 +3,7 @@ mod prf_test; use std::fmt; -use crypto::{HashAlgorithm as CryptoHashAlgorithm, HmacAlgorithm, RTCCrypto}; +use crypto::{HashAlgorithm as CryptoHashAlgorithm, HmacAlgorithm, Mac, RTCCrypto}; use crate::cipher_suite::CipherSuiteHash; use crate::content::ContentType; @@ -107,7 +107,8 @@ fn hmac_sha( }; let mut output = vec![0; algorithm.output_len()]; crypto - .hmac(algorithm, key, input, &mut output) + .new_hmac(algorithm, key) + .and_then(|mut mac| mac.sign(input, &mut output)) .map_err(|error| Error::Crypto(error.to_string()))?; Ok(output) } @@ -261,14 +262,17 @@ pub(crate) fn prf_verify_data_server( } // compute the MAC using HMAC-SHA1 +/// Computes the TLS 1.2 record MAC (RFC 5246 section 6.2.3.1) with an already-keyed MAC. +/// +/// Takes `&mut dyn Mac` rather than a crypto handle plus raw key bytes so the caller keys once +/// per epoch. Re-deriving the HMAC key schedule per record measured ~2x on the CBC record path. pub(crate) fn prf_mac( - crypto: &dyn RTCCrypto, + mac: &mut dyn Mac, epoch: u16, sequence_number: u64, content_type: ContentType, protocol_version: ProtocolVersion, payload: &[u8], - key: &[u8], ) -> Result> { let mut msg = vec![0u8; 13]; msg[..2].copy_from_slice(&epoch.to_be_bytes()); @@ -278,9 +282,8 @@ pub(crate) fn prf_mac( msg[10] = protocol_version.minor; msg[11..].copy_from_slice(&(payload.len() as u16).to_be_bytes()); - let mut output = vec![0; HmacAlgorithm::Sha1.output_len()]; - crypto - .hmac(HmacAlgorithm::Sha1, key, &[&msg, payload], &mut output) + let mut output = vec![0; mac.output_len()]; + mac.sign(&[&msg, payload], &mut output) .map_err(|error| Error::Crypto(error.to_string()))?; Ok(output) } diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index a72fd655..f5b481b0 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -4,6 +4,7 @@ use super::curve::named_curve::*; use super::extension::extension_use_srtp::SrtpProtectionProfile; use super::handshake::handshake_random::*; use super::prf::*; +use crypto::RTCCryptoProvider; use crypto::SecretVec; use rkyv::{Archive, Deserialize, Serialize}; use shared::error::*; @@ -14,7 +15,7 @@ use std::sync::Arc; /// The negotiated connection state: keys, sequence numbers, peer identity and the active /// cipher suite. pub struct State { - pub(crate) crypto_provider: Option>, + pub(crate) crypto_provider: Arc, pub(crate) local_epoch: u16, pub(crate) remote_epoch: u16, pub(crate) local_sequence_number: Vec, // uint48 @@ -65,23 +66,28 @@ struct SerializedState { is_client: bool, } -impl Default for State { - fn default() -> Self { +impl State { + /// Creates the initial handshake state for a connection. + /// + /// The crypto provider comes from the caller — `rtc-dtls` never resolves a default. This + /// replaces the former `Default` impl, which resolved one implicitly only for `DTLSConn::new` + /// to overwrite it a few lines later. + pub fn new(crypto_provider: Arc, is_client: bool) -> Self { State { - crypto_provider: crypto::default_provider().ok(), + crypto_provider, local_epoch: 0, remote_epoch: 0, local_sequence_number: vec![], local_random: HandshakeRandom::default(), remote_random: HandshakeRandom::default(), master_secret: vec![], - cipher_suite: None, // nil if a cipher_suite hasn't been chosen + cipher_suite: None, - srtp_protection_profile: SrtpProtectionProfile::Unsupported, // Negotiated srtp_protection_profile + srtp_protection_profile: SrtpProtectionProfile::Unsupported, peer_certificates: vec![], identity_hint: vec![], - is_client: false, + is_client, pre_master_secret: vec![], extended_master_secret: false, @@ -92,12 +98,11 @@ impl Default for State { handshake_send_sequence: 0, handshake_recv_sequence: 0, server_name: "".to_string(), - remote_requested_certificate: false, // Did we get a CertificateRequest - local_certificates_verify: vec![], // cache CertificateVerify - local_verify_data: vec![], // cached VerifyData - local_key_signature: vec![], // cached keySignature + remote_requested_certificate: false, + local_certificates_verify: vec![], + local_verify_data: vec![], + local_key_signature: vec![], peer_certificates_verified: false, - //replay_detector: vec![], } } } @@ -207,9 +212,7 @@ impl State { if self.is_client { cipher_suite.init( - self.crypto_provider.clone().ok_or_else(|| { - Error::Crypto("DTLS crypto provider is not configured".into()) - })?, + self.crypto_provider.clone(), &self.master_secret, &local_random, &remote_random, @@ -217,9 +220,7 @@ impl State { ) } else { cipher_suite.init( - self.crypto_provider.clone().ok_or_else(|| { - Error::Crypto("DTLS crypto provider is not configured".into()) - })?, + self.crypto_provider.clone(), &self.master_secret, &remote_random, &local_random, @@ -282,14 +283,8 @@ impl State { } /// Returns the provider selected for this DTLS session. - /// - /// # Errors - /// - /// Returns an error when the state has not been attached to a configured session. - pub fn crypto_provider(&self) -> Result> { - self.crypto_provider - .clone() - .ok_or_else(|| Error::Crypto("DTLS crypto provider is not configured".into())) + pub fn crypto_provider(&self) -> Arc { + self.crypto_provider.clone() } /// Exports `length` bytes of keying material from an established session, as defined in @@ -334,10 +329,7 @@ impl State { } if let Some(cipher_suite) = &self.cipher_suite { - let provider = self - .crypto_provider - .as_ref() - .ok_or_else(|| Error::Crypto("DTLS crypto provider is not configured".into()))?; + let provider = self.crypto_provider.as_ref(); match prf_p_hash( provider.crypto(), &self.master_secret, diff --git a/rtc-ice/examples/ping_pong.rs b/rtc-ice/examples/ping_pong.rs index 55a642cf..8e3dc419 100644 --- a/rtc-ice/examples/ping_pong.rs +++ b/rtc-ice/examples/ping_pong.rs @@ -7,6 +7,7 @@ use rtc_ice::agent::Agent; use rtc_ice::agent::agent_config::AgentConfig; use rtc_ice::candidate::candidate_host::CandidateHostConfig; use rtc_ice::candidate::*; +use rtc_ice::crypto; use rtc_ice::state::*; use rtc_ice::{Credentials, Event}; use sansio::Protocol; @@ -163,11 +164,14 @@ async fn main() -> Result<(), Error> { let port = if cli.controlling { 4000 } else { 4001 }; let udp_socket = UdpSocket::bind(("0.0.0.0", port)).await?; - let mut ice_agent = Agent::new(Arc::new(AgentConfig { - disconnected_timeout: Some(Duration::from_secs(5)), - failed_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }))?; + let mut ice_agent = Agent::new( + Arc::new(AgentConfig { + disconnected_timeout: Some(Duration::from_secs(5)), + failed_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }), + crypto::default_provider().map_err(|e| Error::Other(e.to_string()))?, + )?; let client = Arc::new(Client::new()); diff --git a/rtc-ice/src/agent/agent_test.rs b/rtc-ice/src/agent/agent_test.rs index 6c3bc15f..49183252 100644 --- a/rtc-ice/src/agent/agent_test.rs +++ b/rtc-ice/src/agent/agent_test.rs @@ -12,10 +12,16 @@ use crate::candidate::candidate_relay::CandidateRelayConfig; use crate::candidate::candidate_server_reflexive::*; use crate::candidate::*; +/// Explicit provider for tests. The default-resolving STUN constructors were removed before 1.0, +/// so every `MessageIntegrity` now names its provider. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + #[test] fn test_pair_search() -> Result<()> { let config = Arc::new(AgentConfig::default()); - let mut a = Agent::new(config)?; + let mut a = Agent::new(config, test_crypto_provider())?; assert!( a.candidate_pairs.is_empty(), @@ -32,7 +38,7 @@ fn test_pair_search() -> Result<()> { #[test] fn test_pair_priority() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -154,7 +160,7 @@ fn pipe( }; cfg0.urls = vec![]; - let a_agent = Agent::new(Arc::new(cfg0))?; + let a_agent = Agent::new(Arc::new(cfg0), test_crypto_provider())?; let mut cfg1 = if let Some(cfg) = default_config1 { cfg @@ -163,14 +169,14 @@ fn pipe( }; cfg1.urls = vec![]; - let b_agent = Agent::new(Arc::new(cfg1))?; + let b_agent = Agent::new(Arc::new(cfg1), test_crypto_provider())?; Ok((a_agent, b_agent)) } #[test] fn test_on_selected_candidate_pair_change() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -221,7 +227,7 @@ fn test_on_selected_candidate_pair_change() -> Result<()> { #[test] fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -262,7 +268,10 @@ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { Box::new(UseCandidateAttr::new()), Box::new(AttrControlling(tie_breaker)), Box::new(PriorityAttr(local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -304,7 +313,7 @@ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { #[test] fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let mut tid = TransactionId::default(); tid.0[..3].copy_from_slice("ABC".as_bytes()); @@ -343,7 +352,10 @@ fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { msg.build(&[ Box::new(BINDING_SUCCESS), Box::new(tid), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + remote_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -422,7 +434,7 @@ fn test_connectivity_on_startup() -> Result<()> { ..Default::default() }; - let mut a_agent = Agent::new(cfg0)?; + let mut a_agent = Agent::new(cfg0, test_crypto_provider())?; let cfg1 = AgentConfig { keepalive_interval, @@ -430,7 +442,7 @@ fn test_connectivity_on_startup() -> Result<()> { ..Default::default() }; - let mut b_agent = Agent::new(cfg1)?; + let mut b_agent = Agent::new(cfg1, test_crypto_provider())?; // Manual signaling let (a_ufrag, a_pwd) = a_agent.get_local_user_credentials(); @@ -493,7 +505,7 @@ fn test_connectivity_lite() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); a_agent.on_connection_state_change(a_notifier); let cfg1 = AgentConfig { @@ -505,7 +517,7 @@ fn test_connectivity_lite() -> Result<()> { ..Default::default() }; - let b_agent = Arc::new(Agent::new(cfg1)?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); b_agent.on_connection_state_change(b_notifier); let _ = connect_with_vnet(&a_agent, &b_agent)?; @@ -563,7 +575,7 @@ fn build_msg(c: MessageClass, username: String, key: String) -> Result Box::new(MessageType::new(METHOD_BINDING, c)), Box::new(TransactionId::new()), Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(key)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(key, test_crypto_provider())), Box::new(FINGERPRINT), ])?; Ok(msg) @@ -745,7 +757,7 @@ fn test_inbound_validity() -> Result<()> { Box::new(BINDING_REQUEST), Box::new(TransactionId::new()), Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(local_pwd, test_crypto_provider())), ])?; a.internal.handle_inbound(&mut msg, &local, remote); @@ -779,7 +791,7 @@ fn test_inbound_validity() -> Result<()> { msg.build(&[ Box::new(BINDING_SUCCESS), Box::new(t_id), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(remote_pwd, test_crypto_provider())), Box::new(FINGERPRINT), ])?; @@ -885,8 +897,8 @@ fn test_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_checking_tx, mut is_checking_rx) = mpsc::channel::<()>(1); let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); @@ -1478,8 +1490,8 @@ fn test_connection_state_failed_delete_all_candidates() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1); let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx))); @@ -1532,8 +1544,8 @@ fn test_connection_state_connecting_to_failed() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let is_failed = WaitGroup::new(); let is_checking = WaitGroup::new(); @@ -1792,8 +1804,8 @@ fn test_close_in_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1); let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); @@ -1849,8 +1861,8 @@ fn test_run_task_in_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1); let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx))); @@ -1901,8 +1913,8 @@ fn test_run_task_in_selected_candidate_pair_change_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_tested_tx, mut is_tested_rx) = mpsc::channel::<()>(1); let is_tested_tx = Arc::new(Mutex::new(Some(is_tested_tx))); @@ -2016,7 +2028,7 @@ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<( // Create agent with controlling role let mut config = AgentConfig::default(); config.is_controlling = true; - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a specific tiebreaker value agent.tie_breaker = 100; @@ -2069,7 +2081,10 @@ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<( Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -2134,7 +2149,7 @@ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> { // Create agent with controlling role let mut config = AgentConfig::default(); config.is_controlling = true; - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a larger tiebreaker value agent.tie_breaker = 500; @@ -2187,7 +2202,10 @@ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -2227,7 +2245,7 @@ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> // Create agent with controlled role let mut config = AgentConfig::default(); config.is_controlling = false; // Controlled - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a larger tiebreaker value agent.tie_breaker = 500; @@ -2280,7 +2298,10 @@ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -2326,7 +2347,7 @@ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> { // Create agent with controlled role let mut config = AgentConfig::default(); config.is_controlling = false; // Controlled - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a smaller tiebreaker value agent.tie_breaker = 100; @@ -2379,7 +2400,10 @@ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -2417,7 +2441,7 @@ fn test_candidate_type_filtering() -> Result<()> { candidate_types: vec![CandidateType::Relay], ..Default::default() }); - let mut agent = Agent::new(config)?; + let mut agent = Agent::new(config, test_crypto_provider())?; // Host local candidate should be rejected let host_local = CandidateHostConfig { @@ -2491,7 +2515,7 @@ fn test_candidate_type_filtering() -> Result<()> { // recently updated the candidate timestamps (RFC 7675). #[test] fn test_keepalive_sent_during_media_flow() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; // Set up a selected pair let host_local = CandidateHostConfig { @@ -2562,7 +2586,7 @@ fn test_keepalive_sent_during_media_flow() -> Result<()> { #[test] fn test_pair_network_type_mismatch() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; // UDP: IPv4 local should not pair with IPv6 remote. let local_v4 = CandidateHostConfig { @@ -2684,7 +2708,7 @@ fn test_pair_network_type_mismatch() -> Result<()> { // thread '...' panicked at 'index out of bounds: the len is 0 but the index is 0' #[test] fn test_transition_to_failed_clears_stale_candidate_pairs() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let local = CandidateHostConfig { base_config: CandidateConfig { @@ -2758,7 +2782,7 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> failed_timeout: Some(Duration::from_secs(0)), ..Default::default() }; - let mut a = Agent::new(Arc::new(cfg))?; + let mut a = Agent::new(Arc::new(cfg), test_crypto_provider())?; let local_candidate = CandidateHostConfig { base_config: CandidateConfig { @@ -2800,7 +2824,10 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> Box::new(UseCandidateAttr::new()), Box::new(AttrControlling(tie_breaker)), Box::new(PriorityAttr(local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider(), + )), Box::new(FINGERPRINT), ])?; @@ -2848,10 +2875,13 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { "1114572465 1 udp 2113939711 61b445d2-6503-41ac-96ce-ee3edac00e9f.local 61163 typ host"; // QueryOnly: adding the candidate issues an mDNS query (not a drop). - let mut agent = Agent::new(Arc::new(AgentConfig { - multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly, - ..Default::default() - }))?; + let mut agent = Agent::new( + Arc::new(AgentConfig { + multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly, + ..Default::default() + }), + test_crypto_provider(), + )?; let added = agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?; assert!( !added, @@ -2867,10 +2897,13 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { ); // Disabled: the same candidate is silently dropped -- no mDNS query. - let mut agent = Agent::new(Arc::new(AgentConfig { - multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled, - ..Default::default() - }))?; + let mut agent = Agent::new( + Arc::new(AgentConfig { + multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled, + ..Default::default() + }), + test_crypto_provider(), + )?; agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?; assert!( agent.poll_write().is_none(), @@ -2887,7 +2920,7 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { /// and drop the packet. #[test] fn test_send_stun_from_srflx_uses_base_addr() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let srflx_local = CandidateServerReflexiveConfig { base_config: CandidateConfig { diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index d31e34d2..9ebabcbb 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -191,62 +191,11 @@ pub struct Agent { pub(crate) event_outs: VecDeque, } -impl Default for Agent { - fn default() -> Self { - Self { - crypto_provider: crypto::default_provider() - .expect("a default crypto provider is required"), - tie_breaker: 0, - is_controlling: false, - lite: false, - start_time: Instant::now(), - connection_state: Default::default(), - last_connection_state: Default::default(), - ufrag_pwd: Default::default(), - local_candidates: vec![], - remote_candidates: vec![], - candidate_pairs: vec![], - nominated_pair: None, - selected_pair: None, - pending_binding_requests: vec![], - insecure_skip_verify: false, - max_binding_requests: 0, - host_acceptance_min_wait: Default::default(), - srflx_acceptance_min_wait: Default::default(), - prflx_acceptance_min_wait: Default::default(), - relay_acceptance_min_wait: Default::default(), - disconnected_timeout: Default::default(), - failed_timeout: Default::default(), - keepalive_interval: Default::default(), - last_consent_sent: Instant::now(), - check_interval: Default::default(), - checking_duration: Instant::now(), - last_checking_time: Instant::now(), - force_candidate_contact: false, - mdns_mode: MulticastDnsMode::Disabled, - mdns_local_name: "".to_owned(), - mdns_local_ip: None, - mdns_queries: HashMap::new(), - mdns: None, - candidate_types: vec![], - network_types: vec![], - urls: vec![], - write_outs: Default::default(), - event_outs: Default::default(), - } - } -} - impl Agent { /// Creates a new Agent. - pub fn new(config: Arc) -> Result { - let provider = - crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; - Self::new_with_provider(config, provider) - } - - /// Creates a new Agent using an explicitly selected crypto provider. - pub fn new_with_provider( + /// + /// The crypto provider is supplied by the caller; this crate never resolves a default. + pub fn new( config: Arc, crypto_provider: Arc, ) -> Result { diff --git a/rtc-ice/src/lib.rs b/rtc-ice/src/lib.rs index 70f3cefd..808fdf97 100644 --- a/rtc-ice/src/lib.rs +++ b/rtc-ice/src/lib.rs @@ -51,6 +51,13 @@ //! [RFC 7675]: https://datatracker.ietf.org/doc/html/rfc7675 /// The Sans-I/O ICE agent: candidate pairing, connectivity checks, and nomination. +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + pub mod agent; /// The ICE-specific STUN attributes carried in connectivity checks. pub mod attributes; diff --git a/rtc-shared/Cargo.toml b/rtc-shared/Cargo.toml index f6f8e3e6..1ff1c19b 100644 --- a/rtc-shared/Cargo.toml +++ b/rtc-shared/Cargo.toml @@ -22,11 +22,6 @@ thiserror.workspace = true substring.workspace = true bytes.workspace = true url.workspace = true -rcgen.workspace = true -sec1.workspace = true -p256.workspace = true -aes.workspace = true -aes-gcm.workspace = true rand.workspace = true serde.workspace = true diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index d62973e5..6fc89ecd 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -2246,21 +2246,9 @@ pub enum Error { }, //Third Party Error - /// An error from the `sec1` crate while handling EC key encodings. - #[error("{0}")] - Sec1(#[source] sec1::Error), - /// An error from the `p256` crate during NIST P-256 elliptic-curve operations. - #[error("{0}")] - P256(#[source] P256Error), - /// An error from the `rcgen` crate while generating a self-signed certificate. - #[error("{0}")] - RcGen(#[from] rcgen::Error), /// Invalid PEM. #[error("invalid PEM: {0}")] InvalidPEM(String), - /// AES GCM. - #[error("aes gcm: {0}")] - AesGcm(#[from] aes_gcm::Error), /// Parse ip. #[error("parse ip: {0}")] ParseIp(#[from] net::AddrParseError), @@ -2279,10 +2267,6 @@ pub enum Error { /// An error from the standard library or another boxed source. #[error("{0}")] Std(#[source] StdError), - /// An error from the `aes` crate during block-cipher setup. - #[error("{0}")] - Aes(#[from] aes::cipher::InvalidLength), - //Other Errors /// Other RTCP Err. #[error("Other RTCP Err: {0}")] @@ -2404,34 +2388,6 @@ impl From> for Error { } } -impl From for Error { - fn from(e: sec1::Error) -> Self { - Error::Sec1(e) - } -} - -/// Wrapper around [`p256::elliptic_curve::Error`] that implements [`PartialEq`]. -/// -/// `p256::elliptic_curve::Error` does not implement `PartialEq`, which is -/// required by the top-level [`enum@Error`] enum. This newtype always returns -/// `false` for equality comparisons, which is the safe conservative choice -/// for opaque cryptographic errors. -#[derive(Debug, Error)] -#[error("{0}")] -pub struct P256Error(#[source] p256::elliptic_curve::Error); - -impl PartialEq for P256Error { - fn eq(&self, _: &Self) -> bool { - false - } -} - -impl From for Error { - fn from(e: p256::elliptic_curve::Error) -> Self { - Error::P256(P256Error(e)) - } -} - impl From for Error { fn from(e: SystemTimeError) -> Self { Error::Other(e.to_string()) diff --git a/rtc-srtp/benches/README.md b/rtc-srtp/benches/README.md index c6923d4b..5f698641 100644 --- a/rtc-srtp/benches/README.md +++ b/rtc-srtp/benches/README.md @@ -1,89 +1,150 @@ -### Benchmark Results +# SRTP benchmarks -MacBook Air M3 24 GB MacOS 26.2 - -``` +```bash cargo bench --package rtc-srtp --bench bench -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6858 µs 5.7234 µs 5.7694 µs] -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) high severe -SRTP/Decrypt/RTP time: [5.6254 µs 5.6385 µs 5.6549 µs] -Found 3 outliers among 100 measurements (3.00%) - 2 (2.00%) high mild - 1 (1.00%) high severe -SRTP/Encrypt/RTCP time: [641.42 ns 643.53 ns 645.92 ns] -Found 7 outliers among 100 measurements (7.00%) - 3 (3.00%) high mild - 4 (4.00%) high severe -SRTP/Decrypt/RTCP time: [631.40 ns 633.15 ns 635.43 ns] -Found 8 outliers among 100 measurements (8.00%) - 5 (5.00%) high mild - 3 (3.00%) high severe ``` -``` - Finished `bench` profile [optimized] target(s) in 0.12s - Running benches/bench.rs (target/release/deps/bench-5824b5a56534ac1c) -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6013 µs 5.6042 µs 5.6073 µs] - change: [−1.5372% −1.1158% −0.7427%] (p = 0.00 < 0.05) - Change within noise threshold. -Found 13 outliers among 100 measurements (13.00%) - 5 (5.00%) low severe - 3 (3.00%) high mild - 5 (5.00%) high severe -SRTP/Decrypt/RTP time: [5.5840 µs 5.5879 µs 5.5937 µs] - change: [−1.8765% −1.4457% −1.0644%] (p = 0.00 < 0.05) - Performance has improved. -Found 3 outliers among 100 measurements (3.00%) - 1 (1.00%) high mild - 2 (2.00%) high severe -SRTP/Encrypt/RTCP time: [635.38 ns 638.62 ns 644.16 ns] - change: [−1.3894% −0.8625% −0.2766%] (p = 0.00 < 0.05) - Change within noise threshold. -Found 4 outliers among 100 measurements (4.00%) - 1 (1.00%) high mild - 3 (3.00%) high severe -SRTP/Decrypt/RTCP time: [627.27 ns 627.68 ns 628.08 ns] - change: [−1.6764% −1.3366% −1.0230%] (p = 0.00 < 0.05) - Performance has improved. -Found 4 outliers among 100 measurements (4.00%) - 2 (2.00%) high mild - 2 (2.00%) high severe +Benchmarks run against every enabled built-in provider, so `--features ring,aws-lc-rs` reports +both backends side by side under identical inputs. + +The groups are: + +* `Encrypt/*`, `Decrypt/*` — per-packet cost on an already-keyed context. This is the hot path. +* `Setup/*` — context construction: provider dispatch, RFC 3711 §4.3 key derivation, and the + cipher key schedule. Paid **once per one-way context**, not per packet. +The split is deliberate. The crypto-provider design concentrates provider indirection in setup: a +keyed cipher object is obtained once and used per packet with no further dispatch. If `Encrypt/*` +ever regresses while `Setup/*` holds steady, that property has broken. + +--- + +## G3 crypto-provider migration: measured impact + +All figures below were taken on **one machine** (Apple M1 Max, macOS 26.5.2) with identical +criterion settings, comparing a git worktree at `425494c` (P3 — the last commit before SRTP moved +to `rtc-crypto`) against the current tree. + +```bash +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 ``` +### AES-128-CM + HMAC-SHA1-80, 1200-byte payload + +| Benchmark | Pre-migration (`425494c`) | ring (default) | aws-lc-rs | +|---|---|---|---| +| `Encrypt/RTP` | 1.725-1.780 µs | 1.723 µs | 1.018 µs | +| `Decrypt/RTP` | 1.731-1.751 µs | 1.714 µs | 1.019 µs | +| `Encrypt/RTCP` | 295.0-295.6 ns | 296.0 ns | 347.2 ns | +| `Decrypt/RTCP` | 303.1-311.6 ns | 312.4 ns | 353.0 ns | + +**At parity with the pre-migration baseline** on the default provider, after the three fixes +below. `aws-lc-rs` is faster still on the RTP path. + +The baseline column gives the range over three independent runs; run-to-run spread on this machine +is about ±3%, so treat anything inside that as parity rather than a difference. See +`docs/benchmarking-crypto-migration.md` for the procedure. + +### AEAD-AES-128-GCM, 1200-byte payload + +| Benchmark | Current | +|---|---| +| `Encrypt/RTP/AEAD-AES-128-GCM/ring` | 326.8 ns | +| `Decrypt/RTP/AEAD-AES-128-GCM/ring` | 333.6 ns | + +No pre-migration equivalent exists — the old bench covered only the CM/HMAC profile. Worth noting +it is roughly **15× faster** than AES-CTR + HMAC-SHA1 on the same payload, consistent with making +one keyed AEAD call instead of a keystream pass plus a separately keyed HMAC. + +### Construction cost + +| Benchmark | Current | +|---|---| +| `Setup/AES-128-CM-HMAC-SHA1-80/ring` | 1.198 µs | +| `Setup/AEAD-AES-128-GCM/ring` | 925.5 ns | + +## Fixed: block-at-a-time AES-CTR + +`rtc-crypto`'s `AesCtr::apply_keystream` originally drove `encrypt_block` once per 16-byte block — +75 serialized AES calls for a 1200-byte payload, defeating the batching that lets AES-NI / ARMv8 +crypto instructions pipeline. It now delegates to the `ctr` crate, matching what the pre-migration +SRTP cipher used. + +This accounts for only a small part of the regression; the cipher was not the bottleneck. + +The effect is size-dependent and honestly mixed: + +| Benchmark | Manual per-block loop | `ctr` crate | Change | +|---|---|---|---| +| `Encrypt/RTP` (1200 B) | 5.462 µs | 4.949 µs | 9% faster | +| `Decrypt/RTP` (1200 B) | 5.478 µs | 4.945 µs | 10% faster | +| `Encrypt/RTCP` (24 B) | 956.8 ns | 1.034 µs | 8% slower | +| `Decrypt/RTCP` (24 B) | 960.0 ns | 1.057 µs | 10% slower | + +For a two-block RTCP packet the `ctr` crate's per-call setup exceeds the batching benefit. It is +retained because 1200-byte RTP dominates real media traffic and because it restores the +pre-migration implementation choice. Bit-exactness is covered by the RFC 3711 known-answer tests +in `rtc-srtp` and the `rtc-crypto` conformance suite, both passing unchanged. + +## Fixed: per-packet HMAC key setup + +Pre-migration, `CipherAesCmHmacSha1` held a pre-keyed `Hmac` built once during context +construction, so the ipad/opad key schedule was computed once. After the migration the auth tag +went through a stateless provider call that rebuilt the key on every packet — the same +anti-pattern the design rejects for ciphers in §15.8, applied to AEAD, stream, and CBC ciphers but +originally not to MACs. + +`RTCCrypto::new_hmac` now returns a keyed [`Mac`] object, mirroring the cipher factories, and +`rtc-srtp` keys its SRTP and SRTCP MACs once per context. The one-shot `hmac()` and `verify_hmac()` +methods were removed from the trait: they are exactly `new_hmac(..)?.sign(..)` and +`new_hmac(..)?.verify(..)`, and keeping them would preserve the path that invites per-packet +keying. + +| Benchmark | Per-packet keying | Pre-keyed `Mac` | Change | +|---|---|---|---| +| `Encrypt/RTP` (1200 B) | 4.949 µs | 4.606 µs | 7% faster | +| `Encrypt/RTCP` (24 B) | 1.034 µs | 642.0 ns | **38% faster** | +| `Decrypt/RTCP` (24 B) | 1.057 µs | 628.7 ns | **41% faster** | + +The gain is largest for small packets, where key setup dominated the message pass. Context setup +correspondingly rises (1.198 µs → ~2.09 µs) because two MACs are now keyed there — the intended +trade: once per context instead of once per packet. + +`rtc-srtp/tests/provider_profiles.rs` guards this with a counting provider asserting the MAC is +constructed exactly twice per context and not per packet. + +## Fixed: ring's software SHA-1 + +`ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 +instructions. Measured directly over a 1212-byte message: + +| HMAC-SHA1, 1212 B | ns/call | +|---|---| +| `ring` | 4468.6 | +| RustCrypto `hmac` + `sha1` (pre-migration) | 1373.0 | + +3.3x, and the whole of the residual SRTP gap. The built-in providers are **composite** by design +(§2.4 — AES-CTR, CCM, CBC and MD5 already come from RustCrypto), so the `ring` provider now +composes RustCrypto's HMAC-SHA1 as well. SHA-256 stays on `ring`, which does use the hardware +instructions. `aws-lc-rs` keeps its own SHA-1, which is faster than both. + +This closes the gap without making `aws-lc-rs` the default, which would have imposed the +`aws-lc-sys` C toolchain on every downstream build. + +## Reproducing + +```bash +# Current numbers +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 + +# Same-machine pre-migration baseline +git worktree add /tmp/rtc-baseline 425494c +cd /tmp/rtc-baseline +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 + +# Both backends, identical inputs +cargo bench --package rtc-srtp --bench bench --features ring,aws-lc-rs ``` - Finished `bench` profile [optimized] target(s) in 0.13s - Running benches/bench.rs (target/release/deps/bench-5824b5a56534ac1c) -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6134 µs 5.6378 µs 5.6736 µs] - change: [+0.8457% +3.4526% +7.1365%] (p = 0.01 < 0.05) - Change within noise threshold. -Found 15 outliers among 100 measurements (15.00%) - 1 (1.00%) low severe - 1 (1.00%) low mild - 2 (2.00%) high mild - 11 (11.00%) high severe -SRTP/Decrypt/RTP time: [5.5814 µs 5.5867 µs 5.5940 µs] - change: [−0.2377% −0.1258% +0.0067%] (p = 0.04 < 0.05) - Change within noise threshold. -Found 7 outliers among 100 measurements (7.00%) - 1 (1.00%) low mild - 2 (2.00%) high mild - 4 (4.00%) high severe -SRTP/Encrypt/RTCP time: [639.54 ns 642.79 ns 646.92 ns] - change: [−0.1150% +0.4636% +0.9981%] (p = 0.11 > 0.05) - No change in performance detected. -Found 8 outliers among 100 measurements (8.00%) - 1 (1.00%) high mild - 7 (7.00%) high severe -SRTP/Decrypt/RTCP time: [629.80 ns 630.61 ns 631.60 ns] - change: [+0.5928% +1.5843% +3.3894%] (p = 0.01 < 0.05) - Change within noise threshold. -Found 7 outliers among 100 measurements (7.00%) - 1 (1.00%) high mild - 6 (6.00%) high severe - -``` \ No newline at end of file + +Cross-machine comparison is not meaningful. Earlier revisions of this file recorded results from a +MacBook Air M3; those predate P3 and are not comparable to anything above. diff --git a/rtc-srtp/benches/bench.rs b/rtc-srtp/benches/bench.rs index 8a24a950..d85496d1 100644 --- a/rtc-srtp/benches/bench.rs +++ b/rtc-srtp/benches/bench.rs @@ -4,6 +4,12 @@ use criterion::{BenchmarkGroup, Criterion, criterion_main}; use rtc_srtp::{context::Context, protection_profile::ProtectionProfile}; use shared::marshal::Marshal; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + const MASTER_KEY: &[u8] = &[ 96, 180, 31, 4, 119, 137, 128, 252, 75, 194, 252, 44, 63, 56, 61, 55, ]; @@ -20,6 +26,7 @@ fn benchmark_encrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap(); @@ -63,6 +70,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap(); @@ -72,6 +80,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap(); @@ -113,6 +122,7 @@ fn benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap(); @@ -130,6 +140,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap() .encrypt_rtcp(RAW_RTCP) @@ -141,6 +152,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap(); @@ -149,6 +161,142 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) }); } +/// The built-in providers compiled into this benchmark, so the two backends can be compared +/// under identical inputs. +fn providers() -> Vec<(&'static str, std::sync::Arc)> { + let mut providers: Vec<(&'static str, std::sync::Arc)> = + Vec::new(); + #[cfg(feature = "ring")] + providers.push(( + "ring", + std::sync::Arc::new(crypto::providers::RingProvider::default()), + )); + #[cfg(feature = "aws-lc-rs")] + providers.push(( + "aws-lc-rs", + std::sync::Arc::new(crypto::providers::AwsLcRsProvider::default()), + )); + assert!( + !providers.is_empty(), + "enable `ring` or `aws-lc-rs` to run these benchmarks" + ); + providers +} + +/// Context construction: provider dispatch, SRTP key derivation (RFC 3711 section 4.3), and the +/// cipher key schedule. Paid once per one-way context, not per packet — the counterpart to the +/// `Encrypt/*` and `Decrypt/*` hot-path measurements above. +fn benchmark_context_setup(g: &mut BenchmarkGroup) { + for (name, provider) in providers() { + for (label, profile) in [ + ( + "AES-128-CM-HMAC-SHA1-80", + ProtectionProfile::Aes128CmHmacSha1_80, + ), + ("AEAD-AES-128-GCM", ProtectionProfile::AeadAes128Gcm), + ] { + g.bench_function(format!("Setup/{label}/{name}"), |b| { + b.iter(|| { + Context::new( + MASTER_KEY, + &master_salt_for(profile), + profile, + None, + None, + std::sync::Arc::clone(&provider), + ) + .unwrap() + }); + }); + } + } +} + +/// AEAD-AES-128-GCM packet path, per provider. +fn benchmark_aead_aes_128_gcm(g: &mut BenchmarkGroup) { + for (name, provider) in providers() { + let profile = ProtectionProfile::AeadAes128Gcm; + let salt = master_salt_for(profile); + let mut encrypt_ctx = Context::new( + MASTER_KEY, + &salt, + profile, + None, + None, + std::sync::Arc::clone(&provider), + ) + .unwrap(); + let mut decrypt_ctx = Context::new( + MASTER_KEY, + &salt, + profile, + None, + None, + std::sync::Arc::clone(&provider), + ) + .unwrap(); + + let mut pld = BytesMut::new(); + for i in 0..1200 { + pld.extend_from_slice(&[i as u8]); + } + + g.bench_function(format!("Encrypt/RTP/AEAD-AES-128-GCM/{name}"), |b| { + let mut seq = 1; + b.iter_batched( + || { + let pkt = rtp::packet::Packet { + header: rtp::header::Header { + sequence_number: seq, + timestamp: seq.into(), + ssrc: 0xcafebabe, + ..Default::default() + }, + payload: pld.clone().freeze(), + }; + seq = seq.wrapping_add(1); + pkt.marshal().unwrap() + }, + |raw| encrypt_ctx.encrypt_rtp(&raw).unwrap(), + criterion::BatchSize::SmallInput, + ); + }); + + // Pre-encrypt a run of packets so decryption never replays an index, which the replay + // detector would reject. + let mut encrypted = Vec::new(); + for seq in 1..=1024u16 { + let pkt = rtp::packet::Packet { + header: rtp::header::Header { + sequence_number: seq, + timestamp: seq.into(), + ssrc: 0xcafebabe, + ..Default::default() + }, + payload: pld.clone().freeze(), + }; + let raw = pkt.marshal().unwrap(); + encrypted.push(encrypt_ctx.encrypt_rtp(&raw).unwrap()); + } + + let mut index = 0usize; + g.bench_function(format!("Decrypt/RTP/AEAD-AES-128-GCM/{name}"), |b| { + b.iter(|| { + let packet = &encrypted[index % encrypted.len()]; + index += 1; + let _ = decrypt_ctx.decrypt_rtp(packet); + }); + }); + } +} + +/// The master salt length differs per protection profile. +fn master_salt_for(profile: ProtectionProfile) -> Vec { + let mut salt = MASTER_SALT.to_vec(); + salt.resize(profile.salt_len(), 0x5a); + salt +} + fn benches() { let mut c = Criterion::default().configure_from_args(); let mut g = c.benchmark_group("SRTP"); @@ -157,6 +305,8 @@ fn benches() { benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(&mut g); benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(&mut g); benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(&mut g); + benchmark_aead_aes_128_gcm(&mut g); + benchmark_context_setup(&mut g); g.finish(); } diff --git a/rtc-srtp/examples/srtp_micro.rs b/rtc-srtp/examples/srtp_micro.rs index a537d4c6..79d87e21 100644 --- a/rtc-srtp/examples/srtp_micro.rs +++ b/rtc-srtp/examples/srtp_micro.rs @@ -10,6 +10,12 @@ use std::hint::black_box; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::BytesMut; use rtc_srtp::option::srtp_replay_protection; use rtc_srtp::{context::Context, protection_profile::ProtectionProfile}; @@ -27,6 +33,7 @@ fn new_ctx() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) .unwrap() } @@ -41,6 +48,7 @@ fn new_gcm_ctx() -> Context { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider(), ) .unwrap() } @@ -54,6 +62,7 @@ fn new_ctx_replay() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, Some(srtp_replay_protection(128)), None, + test_crypto_provider(), ) .unwrap() } diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs index 8c568ff0..050afb9d 100644 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs +++ b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs @@ -1,7 +1,7 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, BytesMut}; use crypto::{ - HmacAlgorithm, RTCCryptoProvider, SecretVec, StreamCipher, StreamCipherAlgorithm, + HmacAlgorithm, Mac, RTCCryptoProvider, SecretVec, StreamCipher, StreamCipherAlgorithm, constant_time_eq, }; use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; @@ -18,9 +18,11 @@ pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; pub(crate) struct CipherAesCmHmacSha1 { profile: ProtectionProfile, srtp_session_salt: Vec, - srtp_session_auth: SecretVec, + /// Pre-keyed HMAC-SHA1, built once here rather than per packet. Deriving the key schedule on + /// every packet measured ~3x slower on the SRTP path; see `benches/README.md`. + srtp_session_auth: Box, srtcp_session_salt: Vec, - srtcp_session_auth: SecretVec, + srtcp_session_auth: Box, provider: Arc, srtp_cipher: Box, srtcp_cipher: Box, @@ -105,6 +107,17 @@ impl CipherAesCmHmacSha1 { auth_key_len, )?); + // Key the MACs once per context. Everything above is per-context setup; the auth tag on + // each packet then costs only the message pass. + let srtp_session_auth = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, srtp_session_auth.as_ref()) + .map_err(crypto_error)?; + let srtcp_session_auth = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, srtcp_session_auth.as_ref()) + .map_err(crypto_error)?; + Ok(Self { profile, srtp_session_salt, @@ -118,31 +131,22 @@ impl CipherAesCmHmacSha1 { } /// Generate the SRTP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. - fn generate_srtp_auth_tag(&self, buf: &[u8], roc: u32) -> Result<[u8; 20]> { + /// + /// Takes `&mut self` so the pre-keyed [`Mac`] can be used directly; the key schedule was + /// derived once in [`new`](Self::new). + fn generate_srtp_auth_tag(&mut self, buf: &[u8], roc: u32) -> Result<[u8; 20]> { let mut tag = [0; 20]; - self.provider - .crypto() - .hmac( - HmacAlgorithm::Sha1, - self.srtp_session_auth.as_ref(), - &[buf, &roc.to_be_bytes()], - &mut tag, - ) + self.srtp_session_auth + .sign(&[buf, &roc.to_be_bytes()], &mut tag) .map_err(crypto_error)?; Ok(tag) } /// Generate the SRTCP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. - fn generate_srtcp_auth_tag(&self, buf: &[u8]) -> Result<[u8; 20]> { + fn generate_srtcp_auth_tag(&mut self, buf: &[u8]) -> Result<[u8; 20]> { let mut tag = [0; 20]; - self.provider - .crypto() - .hmac( - HmacAlgorithm::Sha1, - self.srtcp_session_auth.as_ref(), - &[buf], - &mut tag, - ) + self.srtcp_session_auth + .sign(&[buf], &mut tag) .map_err(crypto_error)?; Ok(tag) } diff --git a/rtc-srtp/src/context/context_test.rs b/rtc-srtp/src/context/context_test.rs index 88abc4b0..53796152 100644 --- a/rtc-srtp/src/context/context_test.rs +++ b/rtc-srtp/src/context/context_test.rs @@ -1,6 +1,12 @@ use super::*; use crate::key_derivation::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::Bytes; use lazy_static::lazy_static; @@ -18,6 +24,7 @@ fn test_context_roc() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider(), )?; let roc = c.get_roc(123); @@ -45,6 +52,7 @@ fn test_context_index() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider(), )?; let index = c.get_index(123); @@ -66,10 +74,24 @@ fn test_key_len() -> Result<()> { let key_len = CIPHER_CONTEXT_ALGO.key_len(); let salt_len = CIPHER_CONTEXT_ALGO.salt_len(); - let result = Context::new(&[], &vec![0; salt_len], CIPHER_CONTEXT_ALGO, None, None); + let result = Context::new( + &[], + &vec![0; salt_len], + CIPHER_CONTEXT_ALGO, + None, + None, + test_crypto_provider(), + ); assert!(result.is_err(), "CreateContext accepted a 0 length key"); - let result = Context::new(&vec![0; key_len], &[], CIPHER_CONTEXT_ALGO, None, None); + let result = Context::new( + &vec![0; key_len], + &[], + CIPHER_CONTEXT_ALGO, + None, + None, + test_crypto_provider(), + ); assert!(result.is_err(), "CreateContext accepted a 0 length salt"); let result = Context::new( @@ -78,6 +100,7 @@ fn test_key_len() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider(), ); assert!( result.is_ok(), @@ -343,6 +366,7 @@ fn test_encrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -364,6 +388,7 @@ fn test_decrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -382,6 +407,7 @@ fn test_encrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -403,6 +429,7 @@ fn test_decrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -421,6 +448,7 @@ fn test_encrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -442,6 +470,7 @@ fn test_decrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -460,6 +489,7 @@ fn test_encrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); @@ -481,6 +511,7 @@ fn test_decrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider(), ) .expect("Error creating srtp context"); diff --git a/rtc-srtp/src/context/mod.rs b/rtc-srtp/src/context/mod.rs index 70f97ba9..029ad33c 100644 --- a/rtc-srtp/src/context/mod.rs +++ b/rtc-srtp/src/context/mod.rs @@ -105,35 +105,15 @@ pub struct Context { } impl Context { - /// Creates an SRTP context with the built-in default crypto provider. + /// Creates an SRTP context. /// - /// Applications that select or implement a provider should use [`Self::new_with_provider`]. + /// The crypto provider is supplied by the caller; this crate never resolves a default. pub fn new( master_key: &[u8], master_salt: &[u8], profile: ProtectionProfile, srtp_ctx_opt: Option, srtcp_ctx_opt: Option, - ) -> Result { - let provider = - crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; - Self::new_with_provider( - master_key, - master_salt, - profile, - srtp_ctx_opt, - srtcp_ctx_opt, - provider, - ) - } - - /// Creates an SRTP context with an explicit crypto provider. - pub fn new_with_provider( - master_key: &[u8], - master_salt: &[u8], - profile: ProtectionProfile, - srtp_ctx_opt: Option, - srtcp_ctx_opt: Option, provider: Arc, ) -> Result { let key_len = profile.key_len(); diff --git a/rtc-srtp/src/context/srtcp_test.rs b/rtc-srtp/src/context/srtcp_test.rs index 08dd00d5..58d25441 100644 --- a/rtc-srtp/src/context/srtcp_test.rs +++ b/rtc-srtp/src/context/srtcp_test.rs @@ -1,6 +1,12 @@ use super::*; use crate::key_derivation::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::{Buf, Bytes, BytesMut}; use lazy_static::lazy_static; @@ -101,6 +107,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), )?; let mut decrypt_context = Context::new( &RTCP_TEST_MASTER_KEY, @@ -108,6 +115,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), )?; for test_case in &*RTCP_TEST_CASES { @@ -138,6 +146,7 @@ fn test_rtcp_invalid_auth_tag() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), )?; let decrypt_result = decrypt_context.decrypt_rtcp(&RTCP_TEST_CASES[0].encrypted)?; @@ -169,6 +178,7 @@ fn test_rtcp_replay_detector_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), + test_crypto_provider(), )?; let rtcp_packet1 = RTCP_TEST_CASES[0].encrypted.clone(); @@ -215,6 +225,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), )?; let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.rtcp_auth_tag_len(); @@ -225,6 +236,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), + test_crypto_provider(), )?; let inputs = vec![ @@ -277,7 +289,14 @@ fn test_rtcp_short_packet_errors() -> Result<()> { ]; for (profile, salt) in cases { - let mut ctx = Context::new(&RTCP_TEST_MASTER_KEY, salt, profile, None, None)?; + let mut ctx = Context::new( + &RTCP_TEST_MASTER_KEY, + salt, + profile, + None, + None, + test_crypto_provider(), + )?; // Slices of a real packet (its first 4 bytes are a valid RTCP header, so // the header parse succeeds and pre-fix execution reached the diff --git a/rtc-srtp/src/context/srtp_test.rs b/rtc-srtp/src/context/srtp_test.rs index e6e7ef6f..24117f4d 100644 --- a/rtc-srtp/src/context/srtp_test.rs +++ b/rtc-srtp/src/context/srtp_test.rs @@ -1,6 +1,12 @@ use super::*; use shared::marshal::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::Bytes; use lazy_static::lazy_static; @@ -72,6 +78,7 @@ fn build_test_context() -> Result { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), ) } @@ -92,6 +99,7 @@ fn test_rtp_invalid_auth() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider(), )?; for test_case in &*RTP_TEST_CASES { diff --git a/rtc-srtp/src/lib.rs b/rtc-srtp/src/lib.rs index 1b289411..5c0d6eb0 100644 --- a/rtc-srtp/src/lib.rs +++ b/rtc-srtp/src/lib.rs @@ -38,13 +38,21 @@ //! [`rtc`](https://docs.rs/rtc) crate creates the contexts from the DTLS handshake and //! applies them to media as one layer of the peer-connection pipeline. //! Applications constructing contexts directly can select cryptography explicitly with -//! [`context::Context::new_with_provider`]; [`context::Context::new`] retains default-provider +//! [`context::Context::new`]; [`context::Context::new`] retains default-provider //! compatibility. //! //! [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 //! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 mod cipher; + +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + /// Session configuration: keys, protection profile, and replay-protection options. pub mod config; /// The encrypt/decrypt state for one SRTP/SRTCP session. diff --git a/rtc-srtp/tests/provider_profiles.rs b/rtc-srtp/tests/provider_profiles.rs index baa8d744..698e0f24 100644 --- a/rtc-srtp/tests/provider_profiles.rs +++ b/rtc-srtp/tests/provider_profiles.rs @@ -76,7 +76,7 @@ impl RTCCryptoProvider for IncompleteProvider { fn explicit_incomplete_provider_returns_actionable_capability_error() { let profile = ProtectionProfile::Aes128CmHmacSha1_80; let (key, salt) = key_material(profile); - let error = Context::new_with_provider( + let error = Context::new( &key, &salt, profile, @@ -109,7 +109,7 @@ fn context( replay: bool, ) -> Result { let (key, salt) = key_material(profile); - Context::new_with_provider( + Context::new( &key, &salt, profile, @@ -271,6 +271,7 @@ struct CountingCrypto { inner: crypto::providers::RingCrypto, stream_constructions: AtomicUsize, aead_constructions: AtomicUsize, + mac_constructions: AtomicUsize, } #[cfg(feature = "ring")] @@ -279,16 +280,6 @@ impl RTCCrypto for CountingCrypto { self.inner.supports(algorithm) } - fn hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - output: &mut [u8], - ) -> std::result::Result<(), CryptoError> { - self.inner.hmac(algorithm, key, input, output) - } - fn block_encrypt( &self, algorithm: BlockCipherAlgorithm, @@ -315,6 +306,15 @@ impl RTCCrypto for CountingCrypto { self.aead_constructions.fetch_add(1, Ordering::Relaxed); self.inner.new_aead(algorithm, key) } + + fn new_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.mac_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_hmac(algorithm, key) + } } #[cfg(feature = "ring")] @@ -331,6 +331,7 @@ impl CountingProvider { inner: crypto::providers::RingCrypto, stream_constructions: AtomicUsize::new(0), aead_constructions: AtomicUsize::new(0), + mac_constructions: AtomicUsize::new(0), }, random: crypto::providers::RingRandom, } @@ -368,6 +369,14 @@ fn keyed_ciphers_are_constructed_once_per_context_not_per_packet() -> Result<()> .load(Ordering::Relaxed), 2 ); + // Two MACs (SRTP and SRTCP), keyed once alongside the ciphers. + assert_eq!( + stream_provider + .crypto + .mac_constructions + .load(Ordering::Relaxed), + 2 + ); for sequence_number in 0..4 { stream_context.encrypt_rtp(&rtp_packet(sequence_number)?)?; } @@ -378,6 +387,14 @@ fn keyed_ciphers_are_constructed_once_per_context_not_per_packet() -> Result<()> .load(Ordering::Relaxed), 2 ); + assert_eq!( + stream_provider + .crypto + .mac_constructions + .load(Ordering::Relaxed), + 2, + "MACs must be keyed once per context, not per packet" + ); let aead_provider = Arc::new(CountingProvider::new()); let mut aead_context = context( diff --git a/rtc-stun/benches/README.md b/rtc-stun/benches/README.md index db8aaa5c..177d508d 100644 --- a/rtc-stun/benches/README.md +++ b/rtc-stun/benches/README.md @@ -784,4 +784,40 @@ BenchmarkXORMappedAddress_GetFrom Found 13 outliers among 100 measurements (13.00%) 2 (2.00%) high mild 11 (11.00%) high severe -``` \ No newline at end of file +``` + +--- + +## G3 crypto-provider migration: measured impact + +`MESSAGE-INTEGRITY` is the only cryptographic operation on the STUN path, so these two benchmarks +cover it. One machine (Apple M1 Max, macOS 26.5.2), identical criterion settings, comparing a +worktree at `b8bb313` (P1 — the last commit before STUN moved to `rtc-crypto`) against the current +tree. The historical results above were taken on a different machine and are not comparable. + +| Benchmark | Pre-migration | Current | Change | +|---|---|---|---| +| `BenchmarkMessageIntegrity_AddTo` | 934.5 ns | 937.5 ns | none | +| `BenchmarkMessageIntegrity_Check` | 944.5 ns | 956.6 ns | none (within noise) | + +**No regression.** This is the expected result, and it corroborates the diagnosis in +`rtc-srtp/benches/README.md`: STUN already computed HMAC-SHA1 through `ring::hmac` before the +migration, so routing it through `RTCCrypto` did not change the underlying implementation. SRTP +regressed because it moved from the RustCrypto `sha1` crate — which uses ARMv8 SHA-1 instructions +— onto `ring`'s software SHA-1. + +`MessageIntegrity` keys its MAC per message rather than holding a keyed `Mac`, because +`Setter::add_to` and `check` take `&self`. That is deliberate: a STUN message is authenticated +once, and ICE exchanges them at connectivity-check rates rather than per media packet. The figures +above show the cost is not material at that rate. The keyed-object pattern is used where it does +matter, on the SRTP and DTLS record paths. + +Reproduce: + +```bash +cargo bench --package rtc-stun --bench bench -- --warm-up-time 2 --measurement-time 4 MessageIntegrity +git worktree add /tmp/rtc-stun-base b8bb313 # same command in the worktree +``` + +The methodology, including why cross-machine numbers must not be compared, is in +`docs/benchmarking-crypto-migration.md`. diff --git a/rtc-stun/benches/bench.rs b/rtc-stun/benches/bench.rs index ba8ce4e2..2252ac78 100644 --- a/rtc-stun/benches/bench.rs +++ b/rtc-stun/benches/bench.rs @@ -26,6 +26,12 @@ use base64::prelude::*; // sufficient to make function zero-alloc in most cases. // const AGENT_COLLECT_CAP: usize = 100; +/// The built-in provider used by these benchmarks. Provider selection is explicit since the +/// default-resolving constructors were removed before 1.0. +fn bench_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled") +} + fn benchmark_addr(c: &mut Criterion) { let mut m = Message::new(); @@ -234,7 +240,10 @@ fn benchmark_message_build_overhead(c: &mut Criterion) { fn benchmark_message_integrity(c: &mut Criterion) { { let mut m = Message::new(); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let integrity = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + bench_provider(), + ); m.write_header(); c.bench_function("BenchmarkMessageIntegrity_AddTo", |b| { b.iter(|| { @@ -250,7 +259,10 @@ fn benchmark_message_integrity(c: &mut Criterion) { m.raw = Vec::with_capacity(1024); let software = Software::new(ATTR_SOFTWARE, "software".to_owned()); let _ = software.add_to(&mut m); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let integrity = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + bench_provider(), + ); m.write_header(); integrity.add_to(&mut m).unwrap(); m.write_header(); @@ -449,11 +461,15 @@ fn benchmark_message(c: &mut Criterion) { Box::new(BINDING_REQUEST), Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2])), Box::new(Software::new(ATTR_SOFTWARE, "webrtc-rs/stun".to_owned())), - Box::new(MessageIntegrity::new_long_term_integrity( - "username".to_owned(), - "realm".to_owned(), - "password".to_owned(), - )), + Box::new( + MessageIntegrity::new_long_term_integrity_with_provider( + "username".to_owned(), + "realm".to_owned(), + "password".to_owned(), + bench_provider(), + ) + .unwrap(), + ), Box::new(FINGERPRINT), ]) .unwrap(); diff --git a/rtc-stun/src/integrity.rs b/rtc-stun/src/integrity.rs index fa881a6d..734003cf 100644 --- a/rtc-stun/src/integrity.rs +++ b/rtc-stun/src/integrity.rs @@ -61,12 +61,13 @@ impl Setter for MessageIntegrity { m.length += (MESSAGE_INTEGRITY_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; m.write_length(); // writing length to m.Raw let mut value = [0_u8; MESSAGE_INTEGRITY_SIZE]; - let result = self.provider.crypto().hmac( - HmacAlgorithm::Sha1, - self.key.as_ref(), - &[&m.raw], - &mut value, - ); + // A STUN message is authenticated once, so the MAC is keyed here rather than held. On a + // per-packet path the keyed object belongs in the surrounding state instead. + let result = self + .provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, self.key.as_ref()) + .and_then(|mut mac| mac.sign(&[&m.raw], &mut value)); m.length = length; // changing m.Length back m.write_length(); result.map_err(crypto_error)?; @@ -122,46 +123,6 @@ impl MessageIntegrity { Ok(Self::new_raw_integrity_with_provider(key, provider)) } - /// Creates a raw-key integrity attribute using the built-in default provider. - /// - /// This compatibility adapter resolves the default once during construction and panics when - /// no built-in provider is enabled. New code should use - /// [`Self::new_raw_integrity_with_provider`]. - #[must_use] - pub fn new_raw_integrity(key: impl Into>) -> Self { - Self::new_raw_integrity_with_provider( - key, - crypto::default_provider().expect("a default crypto provider is required"), - ) - } - - /// Creates a long-term integrity attribute using the built-in default provider. - /// - /// Password, username, and realm must be SASL-prepared. This compatibility adapter resolves - /// the default once during construction and panics when no built-in provider is enabled. New - /// code should use [`Self::new_long_term_integrity_with_provider`]. - pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { - Self::new_long_term_integrity_with_provider( - username, - realm, - password, - crypto::default_provider().expect("a default crypto provider is required"), - ) - .expect("the default crypto provider must support STUN long-term credentials") - } - - /// Creates a short-term integrity attribute using the built-in default provider. - /// - /// Password must be SASL-prepared. This compatibility adapter resolves the default once during - /// construction and panics when no built-in provider is enabled. New code should use - /// [`Self::new_short_term_integrity_with_provider`]. - pub fn new_short_term_integrity(password: String) -> Self { - Self::new_short_term_integrity_with_provider( - password, - crypto::default_provider().expect("a default crypto provider is required"), - ) - } - /// Check checks MESSAGE-INTEGRITY attribute. /// /// CPU costly, see BenchmarkMessageIntegrity_Check. @@ -190,10 +151,11 @@ impl MessageIntegrity { let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); let b = &m.raw[..start_of_hmac]; // data before integrity attribute - let result = - self.provider - .crypto() - .verify_hmac(HmacAlgorithm::Sha1, self.key.as_ref(), &[b], &v); + let result = self + .provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, self.key.as_ref()) + .and_then(|mut mac| mac.verify(&[b], &v)); m.length = length as u32; m.write_length(); // writing length back match result { @@ -205,9 +167,3 @@ impl MessageIntegrity { } } } - -impl Default for MessageIntegrity { - fn default() -> Self { - Self::new_raw_integrity(Vec::new()) - } -} diff --git a/rtc-stun/src/integrity/integrity_test.rs b/rtc-stun/src/integrity/integrity_test.rs index 7aee6da2..19745bce 100644 --- a/rtc-stun/src/integrity/integrity_test.rs +++ b/rtc-stun/src/integrity/integrity_test.rs @@ -65,50 +65,63 @@ impl RTCCrypto for TestCrypto { Ok(output) } - fn hmac( + fn new_hmac( &self, algorithm: HmacAlgorithm, key: &[u8], - input: &[&[u8]], - output: &mut [u8], - ) -> std::result::Result<(), CryptoError> { + ) -> std::result::Result, CryptoError> { if algorithm != HmacAlgorithm::Sha1 { return Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( algorithm, ))); } - if output.len() != algorithm.output_len() { + Ok(Box::new(TestMac { + key: key.to_vec(), + output_len: algorithm.output_len(), + })) + } +} + +/// A deliberately fake MAC: an XOR fold, not HMAC-SHA1. It exists to prove the custom-provider +/// path is honoured, so tests asserting RFC 5389 vectors must use `builtin_provider()` instead. +struct TestMac { + key: Vec, + output_len: usize, +} + +impl crypto::Mac for TestMac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> std::result::Result<(), CryptoError> { + if output.len() != self.output_len { return Err(CryptoError::InvalidTagLength { - expected: algorithm.output_len(), + expected: self.output_len, actual: output.len(), }); } output.fill(0); - for (index, byte) in key + for (index, byte) in self + .key .iter() .chain(input.iter().flat_map(|part| part.iter())) .enumerate() { - output[index % algorithm.output_len()] ^= byte; + output[index % self.output_len] ^= byte; } Ok(()) } - fn verify_hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - expected: &[u8], - ) -> std::result::Result<(), CryptoError> { - if expected.len() != algorithm.output_len() { + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> std::result::Result<(), CryptoError> { + if expected.len() != self.output_len { return Err(CryptoError::InvalidTagLength { - expected: algorithm.output_len(), + expected: self.output_len, actual: expected.len(), }); } - let mut actual = vec![0_u8; algorithm.output_len()]; - self.hmac(algorithm, key, input, &mut actual)?; + let mut actual = vec![0_u8; self.output_len]; + self.sign(input, &mut actual)?; if constant_time_eq(&actual, expected) { Ok(()) } else { @@ -124,6 +137,12 @@ fn test_provider() -> Arc { }) } +/// A real built-in provider. Tests asserting RFC 5389 key/tag vectors need genuine MD5 and +/// HMAC-SHA1, not the `TestProvider` stand-in above. +fn builtin_provider() -> Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + #[test] fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<()> { let integrity = MessageIntegrity::new_long_term_integrity_with_provider( @@ -156,22 +175,24 @@ fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<( #[test] fn test_message_integrity_add_to_simple() -> Result<()> { { - let i = MessageIntegrity::new_long_term_integrity( + let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "passsss".to_owned(), - ); + builtin_provider(), + )?; let expected = vec![ 104, 228, 91, 113, 61, 154, 222, 34, 101, 61, 181, 146, 177, 90, 4, 29, ]; assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); } - let i = MessageIntegrity::new_long_term_integrity( + let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "pass".to_owned(), - ); + builtin_provider(), + )?; let expected = vec![ 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, 0xeb, @@ -215,7 +236,10 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { }; a.add_to(&mut m)?; - let i = MessageIntegrity::new_short_term_integrity("pwd".to_owned()); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "pwd".to_owned(), + builtin_provider(), + ); assert_eq!( i.to_string(), "MESSAGE-INTEGRITY key: [REDACTED; 3 bytes]", @@ -238,7 +262,10 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { #[test] fn test_message_integrity() -> Result<()> { let mut m = Message::new(); - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + builtin_provider(), + ); m.write_header(); i.add_to(&mut m)?; m.get(ATTR_MESSAGE_INTEGRITY)?; @@ -250,7 +277,10 @@ fn test_message_integrity_before_fingerprint() -> Result<()> { let mut m = Message::new(); m.write_header(); FINGERPRINT.add_to(&mut m)?; - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + builtin_provider(), + ); let result = i.add_to(&mut m); assert!(result.is_err(), "should error"); diff --git a/rtc-stun/src/lib.rs b/rtc-stun/src/lib.rs index d4dc63f7..57769974 100644 --- a/rtc-stun/src/lib.rs +++ b/rtc-stun/src/lib.rs @@ -61,6 +61,13 @@ #[macro_use] extern crate lazy_static; +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + /// Socket-address helpers shared by the address attributes. pub mod addr; /// Transaction tracking: which requests are outstanding and when they time out. diff --git a/rtc-stun/src/message/message_test.rs b/rtc-stun/src/message/message_test.rs index c119f4e1..ebb59872 100644 --- a/rtc-stun/src/message/message_test.rs +++ b/rtc-stun/src/message/message_test.rs @@ -623,11 +623,12 @@ fn test_message_full_size() -> Result<()> { ATTR_SOFTWARE, "webrtc-rs/stun".to_owned(), )), - Box::new(MessageIntegrity::new_long_term_integrity( + Box::new(MessageIntegrity::new_long_term_integrity_with_provider( "username".to_owned(), "realm".to_owned(), "password".to_owned(), - )), + crypto::default_provider().expect("a built-in provider is enabled for tests"), + )?), Box::new(FINGERPRINT), ])?; let l = m.raw.len(); @@ -652,11 +653,12 @@ fn test_message_clone_to() -> Result<()> { ATTR_SOFTWARE, "webrtc-rs/stun".to_owned(), )), - Box::new(MessageIntegrity::new_long_term_integrity( + Box::new(MessageIntegrity::new_long_term_integrity_with_provider( "username".to_owned(), "realm".to_owned(), "password".to_owned(), - )), + crypto::default_provider().expect("a built-in provider is enabled for tests"), + )?), Box::new(FINGERPRINT), ])?; m.encode(); diff --git a/rtc-turn/examples/turn_client_udp.rs b/rtc-turn/examples/turn_client_udp.rs index c661c486..0d66d01b 100644 --- a/rtc-turn/examples/turn_client_udp.rs +++ b/rtc-turn/examples/turn_client_udp.rs @@ -94,7 +94,9 @@ fn main() -> Result<()> { rto_in_ms: 0, }; - let mut client = Client::new(cfg)?; + // An application selects the crypto provider; library code never resolves a default. + let provider = crypto::default_provider().map_err(|e| Error::Other(e.to_string()))?; + let mut client = Client::new(cfg, provider)?; // Allocate a relay socket on the TURN server. let allocate_tid = client.allocate()?; diff --git a/rtc-turn/src/client/client_test.rs b/rtc-turn/src/client/client_test.rs index dff3e730..4a03149b 100644 --- a/rtc-turn/src/client/client_test.rs +++ b/rtc-turn/src/client/client_test.rs @@ -3,20 +3,28 @@ use sansio::Protocol; use std::collections::HashSet; use std::net::UdpSocket; +/// Tests may resolve the built-in provider; library code never does. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + fn create_listening_test_client(rto_in_ms: u64) -> Result<(UdpSocket, Client)> { let udp_socket = UdpSocket::bind("0.0.0.0:0")?; - let client = Client::new(ClientConfig { - stun_serv_addr: String::new(), - turn_serv_addr: String::new(), - local_addr: udp_socket.local_addr()?, - transport_protocol: TransportProtocol::UDP, - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms, - })?; + let client = Client::new( + ClientConfig { + stun_serv_addr: String::new(), + turn_serv_addr: String::new(), + local_addr: udp_socket.local_addr()?, + transport_protocol: TransportProtocol::UDP, + username: String::new(), + password: String::new(), + realm: String::new(), + software: "TEST SOFTWARE".to_owned(), + rto_in_ms, + }, + test_crypto_provider(), + )?; Ok((udp_socket, client)) } @@ -24,17 +32,20 @@ fn create_listening_test_client(rto_in_ms: u64) -> Result<(UdpSocket, Client)> { fn create_listening_test_client_with_stun_serv() -> Result<(UdpSocket, Client)> { let udp_socket = UdpSocket::bind("0.0.0.0:0")?; - let client = Client::new(ClientConfig { - stun_serv_addr: "stun1.l.google.com:19302".to_owned(), - turn_serv_addr: String::new(), - local_addr: udp_socket.local_addr()?, - transport_protocol: TransportProtocol::UDP, - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms: 0, - })?; + let client = Client::new( + ClientConfig { + stun_serv_addr: "stun1.l.google.com:19302".to_owned(), + turn_serv_addr: String::new(), + local_addr: udp_socket.local_addr()?, + transport_protocol: TransportProtocol::UDP, + username: String::new(), + password: String::new(), + realm: String::new(), + software: "TEST SOFTWARE".to_owned(), + rto_in_ms: 0, + }, + test_crypto_provider(), + )?; Ok((udp_socket, client)) } diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 6c4a12a5..76d2bdad 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -156,18 +156,10 @@ pub struct Client { } impl Client { - /// new returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0" - pub fn new(config: ClientConfig) -> Result { - let provider = - crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; - Self::new_with_provider(config, provider) - } - - /// Creates a client using an explicitly selected crypto provider. - pub fn new_with_provider( - config: ClientConfig, - crypto_provider: Arc, - ) -> Result { + /// Returns a new TURN client. + /// + /// The crypto provider is supplied by the caller; this crate never resolves a default. + pub fn new(config: ClientConfig, crypto_provider: Arc) -> Result { let stun_serv_addr = if config.stun_serv_addr.is_empty() { None } else { diff --git a/rtc-turn/src/lib.rs b/rtc-turn/src/lib.rs index e37f694b..fb87d7cc 100644 --- a/rtc-turn/src/lib.rs +++ b/rtc-turn/src/lib.rs @@ -49,6 +49,13 @@ //! [`rtc-ice`]: https://docs.rs/rtc-ice /// The Sans-I/O TURN client: allocate a relayed address and send through it. +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + pub mod client; /// The TURN-specific STUN attributes, methods and ChannelData framing. pub mod proto; diff --git a/scripts/check-crypto-boundary.py b/scripts/check-crypto-boundary.py new file mode 100755 index 00000000..0991fdd9 --- /dev/null +++ b/scripts/check-crypto-boundary.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Enforce the G3 crypto boundary: `rtc-crypto` is the only crate that names a crypto +implementation. + +Checks each crate's `[dependencies]` / `[dev-dependencies]` / `[build-dependencies]` and +`[target.*.dependencies]` sections. Deliberately ignored: + +* `[features]` entries such as `ring = ["crypto/ring"]`, which are provider-feature forwarding + and are how a standalone crate exposes backend selection. +* `[workspace.dependencies]` in the root manifest, which declares versions on behalf of + `rtc-crypto`. +* `rand`. It is an entropy source, not a crypto implementation, appears in no public signature, + and is a documented exception (see `docs/crypto-provider-decisions.md`). + +Run from the workspace root. Exits non-zero and prints offenders on failure. +""" + +from __future__ import annotations + +import glob +import re +import sys +from pathlib import Path + +CRYPTO_IMPLEMENTATIONS = { + "ring", + "aws-lc-rs", + "aes", + "aes-gcm", + "sha1", + "sha2", + "hmac", + "hkdf", + "p256", + "p384", + "ctr", + "cbc", + "ccm", + "md-5", + "subtle", + "x25519-dalek", + "chacha20poly1305", + "sec1", + "ed25519-dalek", + "rsa", + "openssl", +} + +# Certificate format and trust policy are deliberately outside the provider (design section 5.2), +# so these stay in the crates that own X.509 handling. +CERTIFICATE_FORMAT_ALLOWED = {"rcgen", "rustls", "x509-parser", "der-parser", "pem"} + +DEPENDENCY_SECTION = re.compile( + r"^\[(?:target\.[^\]]+\.)?(?:dev-|build-)?dependencies\]$" +) +SECTION = re.compile(r"^\[.*\]$") +DEPENDENCY_NAME = re.compile(r"^([A-Za-z0-9_-]+)\s*[.=]") + + +def dependencies_of(manifest: Path) -> set[str]: + names: set[str] = set() + in_dependencies = False + for line in manifest.read_text().splitlines(): + stripped = line.strip() + if SECTION.match(stripped): + in_dependencies = bool(DEPENDENCY_SECTION.match(stripped)) + continue + if not in_dependencies: + continue + match = DEPENDENCY_NAME.match(stripped) + if match: + names.add(match.group(1)) + return names + + +def main() -> int: + offenders: list[str] = [] + for manifest_path in sorted(glob.glob("rtc-*/Cargo.toml")) + ["Cargo.toml"]: + manifest = Path(manifest_path) + crate = manifest.parent.name if manifest.parent.name else "rtc" + if crate == "rtc-crypto": + continue + found = dependencies_of(manifest) & CRYPTO_IMPLEMENTATIONS + for dependency in sorted(found): + offenders.append(f"{manifest_path}: {dependency}") + + if offenders: + print("Crypto implementation dependencies found outside rtc-crypto:") + for offender in offenders: + print(f" {offender}") + print() + print("Route the operation through rtc-crypto's provider traits instead.") + print(f"Certificate-format crates remain allowed: {sorted(CERTIFICATE_FORMAT_ALLOWED)}") + return 1 + + print("Crypto boundary holds: no crypto implementation outside rtc-crypto.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index c31f84e3..8faffc0e 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -31,12 +31,16 @@ //! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { //! // Generate ECDSA certificate (recommended) -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Use in peer connection //! let peer_connection = RTCPeerConnectionBuilder::new() @@ -54,15 +58,19 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { //! // Ed25519 provides the best security with excellent performance -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::Ed25519, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Get fingerprint for SDP signaling -//! let fingerprints = certificate.get_fingerprints(); +//! let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; //! println!("Fingerprint: {}", fingerprints[0].value); //! # Ok(()) //! # } @@ -73,18 +81,22 @@ //! ```no_run //! # fn example() -> Result<(), Box> { //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! use std::fs; //! //! // First run: Generate and save certificate -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! let pem_data = certificate.serialize_pem()?; //! fs::write("my_cert.pem", pem_data)?; //! //! // Later runs: Load existing certificate //! let pem_data = fs::read_to_string("my_cert.pem")?; -//! let certificate = RTCCertificate::from_pem(&pem_data)?; +//! let certificate = RTCCertificate::from_pem(&pem_data, crypto::default_provider()?)?; //! // Same identity maintained across restarts! //! # Ok(()) //! # } @@ -94,14 +106,18 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Get fingerprints for SDP offer/answer -//! let fingerprints = certificate.get_fingerprints(); +//! let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; //! for fp in fingerprints { //! // Format for SDP: a=fingerprint:sha-256 XX:XX:XX:... //! println!("a=fingerprint:{} {}", fp.algorithm, fp.value); @@ -114,20 +130,27 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! use std::time::Instant; //! //! # fn example() -> Result<(), Box> { //! // ECDSA P-256: Good balance of speed and security //! let start = Instant::now(); -//! let ecdsa_kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let _ecdsa_cert = RTCCertificate::from_key_pair(ecdsa_kp)?; +//! let _ecdsa_cert = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! println!("ECDSA generation: {:?}", start.elapsed()); //! //! // Ed25519: Fastest and most secure //! let start = Instant::now(); -//! let ed_kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -//! let _ed_cert = RTCCertificate::from_key_pair(ed_kp)?; +//! let _ed_cert = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::Ed25519, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! println!("Ed25519 generation: {:?}", start.elapsed()); //! # Ok(()) //! # } @@ -183,17 +206,17 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; use crypto::{HashAlgorithm, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme, SigningKey}; -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -use dtls::crypto::CryptoPrivateKey; -use rcgen::CertificateParams; -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -use rcgen::KeyPair; +/// X.509 certificate parameters — subject alt names, validity window, distinguished name. +/// +/// Re-exported from `rcgen` because it appears in [`RTCCertificate::generate`]'s signature and +/// must therefore be nameable without adding a direct `rcgen` dependency. Certificate *format* +/// is deliberately not a crypto-provider concern (see `docs/crypto-provider-decisions.md`), so +/// this type stays an rcgen type rather than being wrapped. +pub use rcgen::CertificateParams; use rustls::pki_types::CertificateDer; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; use shared::error::{Error, Result}; -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] -use shared::util::math_rand_alpha; /// X.509 certificate used to authenticate WebRTC peer-to-peer communications. /// @@ -218,14 +241,18 @@ use shared::util::math_rand_alpha; /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { /// // Generate ECDSA P-256 key pair and certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// crypto::default_provider()?, +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Certificate is ready to use -/// let fingerprints = certificate.get_fingerprints(); +/// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -235,14 +262,18 @@ use shared::util::math_rand_alpha; /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { /// // Generate Ed25519 key pair and certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// crypto::default_provider()?, +/// SignatureScheme::Ed25519, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Get fingerprints for SDP signaling -/// let fingerprints = certificate.get_fingerprints(); +/// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; /// for fp in fingerprints { /// println!("Fingerprint ({}):\n{}", fp.algorithm, fp.value); /// } @@ -255,9 +286,14 @@ use shared::util::math_rand_alpha; /// ``` /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; -/// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// # let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; +/// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; +/// # let certificate = RTCCertificate::generate( +/// # crypto::default_provider()?, +/// # SignatureScheme::EcdsaP256Sha256, +/// # params, +/// # )?; /// // Serialize certificate to PEM format (includes private key) /// let pem_string = certificate.serialize_pem()?; /// @@ -265,7 +301,7 @@ use shared::util::math_rand_alpha; /// // std::fs::write("cert.pem", &pem_string)?; /// /// // Later, load the certificate back -/// let loaded_cert = RTCCertificate::from_pem(&pem_string)?; +/// let loaded_cert = RTCCertificate::from_pem(&pem_string, crypto::default_provider()?)?; /// assert_eq!(loaded_cert, certificate); /// # Ok(()) /// # } @@ -277,11 +313,15 @@ use shared::util::math_rand_alpha; /// # use rtc::peer_connection::RTCPeerConnectionBuilder; /// # use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { /// // Generate certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// crypto::default_provider()?, +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Configure peer connection with custom certificate /// let peer_connection = RTCPeerConnectionBuilder::new() @@ -366,7 +406,15 @@ impl RTCCertificate { } } - fn generate_from_signing_key( + /// Builds a self-signed certificate around an existing provider-owned signing key. + /// + /// Use this when the key already exists — imported from PKCS#8 with + /// [`RTCCrypto::import_signing_key`](crypto::RTCCrypto::import_signing_key), or held by an + /// HSM/KMS — and a fresh self-signed X.509 wrapper is needed. Use + /// [`generate`](Self::generate) instead when the provider should create the key too. + /// + /// This is the provider-neutral replacement for the removed `from_key_pair`. + pub fn generate_from_signing_key( params: CertificateParams, scheme: SignatureScheme, signing_key: Arc, @@ -384,108 +432,6 @@ impl RTCCertificate { )) } - /// Generates a new certificate from custom parameters. - /// - /// This is an internal method used to create certificates with specific configuration. - /// Most users should use [`from_key_pair`](Self::from_key_pair) instead. - /// - /// # Parameters - /// - /// * `params` - Certificate parameters including validity period and subject - /// * `key_pair` - The cryptographic key pair to use - /// - /// # Errors - /// - /// Returns an error if: - /// - The key pair type is not supported (must be Ed25519, ECDSA P-256, or RSA) - /// - Certificate generation fails - /// - /// # Platform Notes - /// - /// On ARM architectures, certificate expiration is capped at 48 hours due to - /// overflow issues with SystemTime arithmetic. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - fn from_params( - params: CertificateParams, - key_pair: KeyPair, - provider: Arc, - ) -> Result { - let not_after = params.not_after; - - let x509_cert = params - .self_signed(&key_pair) - .map_err(|error| Error::Other(error.to_string()))?; - let private_key = CryptoPrivateKey::from_key_pair_with_provider(&key_pair, provider)?; - let expires = certificate_expiration(not_after); - - Ok(Self { - dtls_certificate: dtls::crypto::Certificate { - certificate: vec![x509_cert.der().to_owned()], - private_key, - }, - expires, - }) - } - - /// Generates a new self-signed certificate with default parameters. - /// - /// Creates a certificate with a randomly generated common name and default - /// validity period. This is the recommended method for generating certificates - /// for WebRTC connections. - /// - /// # Parameters - /// - /// * `key_pair` - A cryptographic key pair. Must be one of: - /// - `rcgen::PKCS_ED25519` - Ed25519 (recommended for security) - /// - `rcgen::PKCS_ECDSA_P256_SHA256` - ECDSA P-256 (recommended for performance) - /// - `rcgen::PKCS_RSA_SHA256` - RSA (generation not available) - /// - /// # Errors - /// - /// Returns an error if the key pair type is not supported. - /// - /// # Examples - /// - /// ``` - /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # fn example() -> Result<(), Box> { - /// // Generate ECDSA certificate - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; - /// - /// // Certificate is ready to use in peer connection - /// let fingerprints = certificate.get_fingerprints(); - /// println!("Generated certificate with {} fingerprint(s)", fingerprints.len()); - /// # Ok(()) - /// # } - /// ``` - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn from_key_pair(key_pair: KeyPair) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::from_key_pair_with_provider(key_pair, provider) - } - - /// Imports an `rcgen` key pair through an explicit provider compatibility adapter. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] - pub fn from_key_pair_with_provider( - key_pair: KeyPair, - provider: Arc, - ) -> Result { - if !(key_pair.is_compatible(&rcgen::PKCS_ED25519) - || key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) - || key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256)) - { - return Err(Error::Other("Unsupported key_pair".to_owned())); - } - - RTCCertificate::from_params( - CertificateParams::new(vec![math_rand_alpha(16)]).unwrap(), - key_pair, - provider, - ) - } - /// Parses a certificate from PEM format string. /// /// Reconstructs an RTCCertificate from its PEM serialization, including the @@ -513,29 +459,25 @@ impl RTCCertificate { /// ``` /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// # let original = RTCCertificate::from_key_pair(key_pair)?; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; + /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let original = RTCCertificate::generate( + /// # crypto::default_provider()?, + /// # SignatureScheme::EcdsaP256Sha256, + /// # params, + /// # )?; /// // Load certificate from PEM string /// # let pem_str = original.serialize_pem()?; - /// let certificate = RTCCertificate::from_pem(&pem_str)?; + /// let certificate = RTCCertificate::from_pem(&pem_str, crypto::default_provider()?)?; /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; /// println!("Loaded certificate with {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } /// ``` - pub fn from_pem(pem_str: &str) -> Result { - let provider = crypto::default_provider().map_err(crypto_error)?; - Self::from_pem_with_provider(pem_str, provider) - } - - /// Parses PEM and imports its private key through an explicit provider. - pub fn from_pem_with_provider( - pem_str: &str, - provider: Arc, - ) -> Result { + pub fn from_pem(pem_str: &str, provider: Arc) -> Result { let mut pem_blocks = pem_str.split("\n\n"); let first_block = if let Some(b) = pem_blocks.next() { b @@ -559,7 +501,7 @@ impl RTCCertificate { } else { return Err(Error::InvalidPEM("failed to calculate SystemTime".into())); }; - let dtls_certificate = dtls::crypto::Certificate::from_pem_with_provider( + let dtls_certificate = dtls::crypto::Certificate::from_pem( &pem_blocks.collect::>().join("\n\n"), provider, )?; @@ -596,7 +538,7 @@ impl RTCCertificate { /// let certificate = RTCCertificate::from_existing(dtls_cert, expires); /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -631,9 +573,14 @@ impl RTCCertificate { /// ``` /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// # let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; + /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let certificate = RTCCertificate::generate( + /// # crypto::default_provider()?, + /// # SignatureScheme::EcdsaP256Sha256, + /// # params, + /// # )?; /// // Serialize for storage /// let pem_string = certificate.serialize_pem()?; /// @@ -641,7 +588,7 @@ impl RTCCertificate { /// // std::fs::write("private/cert.pem", &pem_string)?; /// /// // Later, reload it - /// let reloaded = RTCCertificate::from_pem(&pem_string)?; + /// let reloaded = RTCCertificate::from_pem(&pem_string, crypto::default_provider()?)?; /// assert_eq!(certificate, reloaded); /// # Ok(()) /// # } @@ -691,27 +638,24 @@ impl RTCCertificate { /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// let certificate = RTCCertificate::generate( + /// crypto::default_provider()?, + /// SignatureScheme::EcdsaP256Sha256, + /// CertificateParams::new(vec!["localhost".to_owned()])?, + /// )?; /// /// // Get fingerprints for SDP - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; /// for fp in fingerprints { /// println!("a=fingerprint:{} {}", fp.algorithm, fp.value); /// } /// # Ok(()) /// # } /// ``` - pub fn get_fingerprints(&self) -> Vec { - let provider = crypto::default_provider().expect("a default crypto provider is required"); - self.get_fingerprints_with_provider(provider) - .expect("the default crypto provider supports SHA-256") - } - - /// Returns SHA-256 fingerprints computed by an explicit crypto provider. - pub fn get_fingerprints_with_provider( + pub fn get_fingerprints( &self, provider: Arc, ) -> Result> { @@ -849,50 +793,86 @@ mod test { } } + fn default_test_provider() -> Result> { + crypto::default_provider().map_err(crypto_error) + } + fn provider_certificate(provider: Arc) -> Result { RTCCertificate::generate( provider, SignatureScheme::EcdsaP256Sha256, - CertificateParams::new(vec!["webrtc.rs".to_owned()])?, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, ) } #[test] fn test_generate_certificate_rsa() -> Result<()> { - match KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256) { - Ok(key_pair) => { - let _certificate = RTCCertificate::from_key_pair(key_pair)?; - } - Err(rcgen::Error::KeyGenerationUnavailable) => {} - Err(error) => return Err(Error::Other(error.to_string())), + let provider = default_test_provider()?; + + // Neither built-in provider generates RSA keys, mirroring rcgen's + // `KeyGenerationUnavailable` under `ring`. The certificate path must still work for any + // provider that does support it, so this asserts success or an explicit + // unsupported-algorithm error, never a silent failure. `tests/dtls_rsa_certificate.rs` + // covers RSA end to end using an imported fixture key. + if !provider + .crypto() + .supports(crypto::CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::RsaPkcs1Sha256, + )) + { + return Ok(()); } + let _certificate = RTCCertificate::generate( + provider, + SignatureScheme::RsaPkcs1Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; + Ok(()) } #[test] fn test_generate_certificate_ecdsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let _cert = RTCCertificate::from_key_pair(kp)?; + let _cert = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; Ok(()) } #[test] fn test_generate_certificate_eddsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; - let _cert = RTCCertificate::from_key_pair(kp)?; + let _cert = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::Ed25519, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; Ok(()) } #[test] fn test_certificate_equal() -> Result<()> { - let kp1 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert1 = RTCCertificate::from_key_pair(kp1)?; + let cert1 = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; - let kp2 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert2 = RTCCertificate::from_key_pair(kp2)?; + let cert2 = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; assert_ne!(cert1, cert2); @@ -901,8 +881,12 @@ mod test { #[test] fn test_generate_certificate_expires() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let cert = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; let now = SystemTime::now(); assert!(cert.expires.duration_since(now).is_ok()); @@ -912,11 +896,15 @@ mod test { #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let cert = RTCCertificate::generate( + default_test_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; let pem = cert.serialize_pem()?; - let loaded_cert = RTCCertificate::from_pem(&pem)?; + let loaded_cert = RTCCertificate::from_pem(&pem, default_test_provider()?)?; assert_eq!(loaded_cert, cert); @@ -937,12 +925,12 @@ mod test { fn provider_certificate_round_trip(provider: Arc) -> Result<()> { let certificate = provider_certificate(provider.clone())?; - let fingerprints = certificate.get_fingerprints_with_provider(provider.clone())?; + let fingerprints = certificate.get_fingerprints(provider.clone())?; assert_eq!(fingerprints.len(), 1); assert_eq!(fingerprints[0].algorithm, "sha-256"); let pem = certificate.serialize_pem()?; - let imported = RTCCertificate::from_pem_with_provider(&pem, provider.clone())?; + let imported = RTCCertificate::from_pem(&pem, provider.clone())?; assert_eq!(imported, certificate); let private_key = certificate diff --git a/src/peer_connection/configuration/mod.rs b/src/peer_connection/configuration/mod.rs index 9ba6dd02..fc0420c8 100644 --- a/src/peer_connection/configuration/mod.rs +++ b/src/peer_connection/configuration/mod.rs @@ -97,12 +97,16 @@ //! ``` //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { //! // Generate custom certificate for peer identity -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! let config = RTCConfigurationBuilder::new() //! .with_certificates(vec![certificate]) @@ -150,11 +154,15 @@ //! }; //! use rtc::peer_connection::configuration::RTCIceServer; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! crypto::default_provider()?, +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! let config = RTCConfigurationBuilder::new() //! .with_ice_servers(vec![ @@ -473,11 +481,15 @@ impl RTCConfiguration { /// ``` /// use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// use rtc::peer_connection::certificate::RTCCertificate; -/// use rcgen::KeyPair; +/// use rtc::crypto::{self, SignatureScheme}; +/// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// crypto::default_provider()?, +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// let config = RTCConfigurationBuilder::new() /// .with_certificates(vec![certificate]) @@ -672,11 +684,15 @@ impl RTCConfigurationBuilder { /// ``` /// use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// use rtc::peer_connection::certificate::RTCCertificate; - /// use rcgen::KeyPair; + /// use rtc::crypto::{self, SignatureScheme}; + /// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// let certificate = RTCCertificate::generate( + /// crypto::default_provider()?, + /// SignatureScheme::EcdsaP256Sha256, + /// CertificateParams::new(vec!["localhost".to_owned()])?, + /// )?; /// /// let config = RTCConfigurationBuilder::new() /// .with_certificates(vec![certificate]) diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index 63736317..a5f696af 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -20,7 +20,6 @@ use std::collections::VecDeque; use std::net::SocketAddr; use std::time::Instant; -#[derive(Default)] pub(crate) struct DtlsHandlerContext { pub(crate) dtls_transport: RTCDtlsTransport, @@ -90,8 +89,8 @@ impl<'a> DtlsHandler<'a> { // Register local certificate and set local_certificate_id if let Some(local_cert) = self.ctx.dtls_transport.certificates.first() { - let fingerprints = local_cert - .get_fingerprints_with_provider(self.ctx.dtls_transport.crypto_provider.clone())?; + let fingerprints = + local_cert.get_fingerprints(self.ctx.dtls_transport.crypto_provider.clone())?; if let Some(fp) = fingerprints.first() { // Register certificate in accumulator // Use hex encoding for certificate (base64 would need additional dependency) @@ -410,9 +409,9 @@ impl<'a> DtlsHandler<'a> { )?; srtp_config .set_session_keys_from_keying_material(keying_material.as_ref(), state.is_client())?; - let crypto_provider = state.crypto_provider()?; + let crypto_provider = state.crypto_provider(); - let local_context = srtp::context::Context::new_with_provider( + let local_context = srtp::context::Context::new( &srtp_config.keys.local_master_key, &srtp_config.keys.local_master_salt, srtp_config.profile, @@ -421,7 +420,7 @@ impl<'a> DtlsHandler<'a> { crypto_provider.clone(), )?; - let remote_context = srtp::context::Context::new_with_provider( + let remote_context = srtp::context::Context::new( &srtp_config.keys.remote_master_key, &srtp_config.keys.remote_master_salt, srtp_config.profile, @@ -452,7 +451,22 @@ mod tests { #[test] fn timeout_before_dtls_starts_is_a_noop() { - let mut context = DtlsHandlerContext::default(); + let provider = + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"); + let transport = RTCDtlsTransport::new( + crate::peer_connection::transport::dtls::RTCDtlsTransportConfig { + certificates: vec![], + answering_dtls_role: Default::default(), + srtp_protection_profiles: vec![], + dtls_cipher_suites: vec![], + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: Default::default(), + crypto_provider: provider, + }, + ) + .expect("transport"); + let mut context = DtlsHandlerContext::new(transport); let mut stats = RTCStatsAccumulator::default(); let mut handler = DtlsHandler::new(&mut context, &mut stats); diff --git a/src/peer_connection/handler/ice.rs b/src/peer_connection/handler/ice.rs index 9e7a57b9..a2c931e9 100644 --- a/src/peer_connection/handler/ice.rs +++ b/src/peer_connection/handler/ice.rs @@ -12,7 +12,6 @@ use shared::{TransportContext, TransportMessage}; use std::collections::VecDeque; use std::time::Instant; -#[derive(Default)] pub(crate) struct IceHandlerContext { pub(crate) ice_transport: RTCIceTransport, diff --git a/src/peer_connection/handler/mod.rs b/src/peer_connection/handler/mod.rs index e70be5de..1fc27154 100644 --- a/src/peer_connection/handler/mod.rs +++ b/src/peer_connection/handler/mod.rs @@ -101,7 +101,6 @@ macro_rules! for_each_handler { }; } -#[derive(Default)] pub(crate) struct PipelineContext { // Handler contexts pub(crate) demuxer_handler_context: DemuxerHandlerContext, diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index 9d266fde..eebe20e9 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -1,5 +1,10 @@ use super::*; use crate::peer_connection::event::{RTCPeerConnectionEvent, RTCPeerConnectionIceEvent}; +use crate::peer_connection::handler::datachannel::DataChannelHandlerContext; +use crate::peer_connection::handler::demuxer::DemuxerHandlerContext; +use crate::peer_connection::handler::endpoint::EndpointHandlerContext; +use crate::peer_connection::handler::interceptor::InterceptorHandlerContext; +use crate::peer_connection::handler::srtp::SrtpHandlerContext; use crate::peer_connection::sdp::{ MediaSection, PopulateSdpParams, add_candidates_to_media_descriptions, get_by_mid, get_peer_direction, get_rids, have_data_channel, is_ext_map_allow_mixed_set, @@ -21,6 +26,7 @@ use crate::statistics::accumulator::IceCandidateAccumulator; use ::sdp::description::session::*; use ::sdp::util::ConnectionRole; use std::collections::HashSet; +use std::collections::VecDeque; impl RTCPeerConnection where @@ -34,8 +40,13 @@ where ) -> Result { configuration.validate()?; - let crypto_provider = match setting_engine.crypto_provider.clone() { - Some(provider) => provider, + // The one place in the workspace that resolves a default crypto provider. The + // application either supplies one through `SettingEngine::set_crypto_provider` or gets + // the feature-selected built-in here, once, at construction. Everything downstream — + // ICE, DTLS, SRTP, STUN, certificates — receives it explicitly, so no library code + // reaches for a default behind the caller's back. + let crypto_provider = match setting_engine.crypto_provider.take() { + Some(crypto_provider) => crypto_provider, None => crypto::default_provider().map_err(|error| { Error::Crypto(format!( "peer connection requires a crypto provider: {error}; configure one with SettingEngine::set_crypto_provider" @@ -121,12 +132,22 @@ where let dtls_handler_context = DtlsHandlerContext::new(dtls_transport); let sctp_handler_context = SctpHandlerContext::new(sctp_transport); + // Listed in full rather than filled from `Default`: the ICE and DTLS handler contexts + // own a crypto provider, and deriving `Default` for them would mean resolving one + // implicitly. The provider is chosen once above and threaded from here. let pipeline_context = PipelineContext { + demuxer_handler_context: DemuxerHandlerContext::default(), ice_handler_context, dtls_handler_context, sctp_handler_context, - - ..Default::default() + datachannel_handler_context: DataChannelHandlerContext::default(), + srtp_handler_context: SrtpHandlerContext::default(), + interceptor_handler_context: InterceptorHandlerContext::default(), + endpoint_handler_context: EndpointHandlerContext::default(), + read_outs: VecDeque::new(), + write_outs: VecDeque::new(), + event_outs: VecDeque::new(), + stats: RTCStatsAccumulator::default(), }; Ok(Self { @@ -190,7 +211,7 @@ where } let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints_with_provider(self.dtls_transport().crypto_provider.clone())? + cert.get_fingerprints(self.dtls_transport().crypto_provider.clone())? } else { return Err(Error::ErrNonCertificate); }; @@ -335,7 +356,7 @@ where }; let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints_with_provider(self.dtls_transport().crypto_provider.clone())? + cert.get_fingerprints(self.dtls_transport().crypto_provider.clone())? } else { return Err(Error::ErrNonCertificate); }; diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 5383297b..8a4c1df4 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -64,26 +64,6 @@ pub(crate) struct RTCDtlsTransportConfig { pub(crate) crypto_provider: Arc, } -impl Default for RTCDtlsTransport { - fn default() -> Self { - Self { - crypto_provider: crypto::default_provider() - .expect("a default crypto provider is required for RTCDtlsTransport::default"), - dtls_role: RTCDtlsRole::default(), - dtls_handshake_config: None, - dtls_endpoint: None, - state: RTCDtlsTransportState::default(), - certificates: Vec::new(), - answering_dtls_role: RTCDtlsRole::default(), - srtp_protection_profiles: Vec::new(), - dtls_cipher_suites: Vec::new(), - allow_insecure_verification_algorithm: false, - disable_certificate_fingerprint_verification: false, - replay_protection: ReplayProtection::default(), - } - } -} - impl RTCDtlsTransport { pub(crate) fn new(config: RTCDtlsTransportConfig) -> Result { let RTCDtlsTransportConfig { @@ -423,7 +403,8 @@ mod tests { let certificate = RTCCertificate::generate( default_provider.clone(), crypto::SignatureScheme::EcdsaP256Sha256, - CertificateParams::new(vec!["webrtc.rs".to_owned()])?, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, )?; let provider: Arc = Arc::new(ProfileFilteringProvider { random_provider: default_provider, diff --git a/src/peer_connection/transport/ice/mod.rs b/src/peer_connection/transport/ice/mod.rs index 0d0b4830..2da4e713 100644 --- a/src/peer_connection/transport/ice/mod.rs +++ b/src/peer_connection/transport/ice/mod.rs @@ -22,7 +22,6 @@ pub(crate) mod state; /// ICETransport allows an application access to information about the ICE /// transport over which packets are sent and received. -#[derive(Default)] pub(crate) struct RTCIceTransport { pub(crate) agent: Agent, @@ -36,12 +35,12 @@ impl RTCIceTransport { agent_config: AgentConfig, crypto_provider: Arc, ) -> Result { - let agent = Agent::new_with_provider(Arc::new(agent_config), crypto_provider)?; + let agent = Agent::new(Arc::new(agent_config), crypto_provider)?; Ok(RTCIceTransport { agent, ice_gathering_state: RTCIceGatheringState::New, - ..Default::default() + ice_connection_state: RTCIceConnectionState::default(), }) } diff --git a/tests/crypto_provider_peer_connections.rs b/tests/crypto_provider_peer_connections.rs index d87e9a96..80b76d18 100644 --- a/tests/crypto_provider_peer_connections.rs +++ b/tests/crypto_provider_peer_connections.rs @@ -86,6 +86,30 @@ impl RTCRandom for RecordingProvider { } } +/// Counts every tag computation, so the test can assert the provider is actually used. The count +/// now happens per `sign`/`verify` rather than per one-shot `hmac` call, since keyed MACs are +/// created once per context. +struct CountingMac { + inner: Box, + calls: Arc, +} + +impl rtc::crypto::Mac for CountingMac { + fn output_len(&self) -> usize { + self.inner.output_len() + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner.sign(input, output) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner.verify(input, expected) + } +} + impl RTCCrypto for RecordingProvider { fn supports(&self, algorithm: CryptoAlgorithm) -> bool { self.inner.crypto().supports(algorithm) @@ -96,28 +120,15 @@ impl RTCCrypto for RecordingProvider { self.inner.crypto().hash(algorithm, data) } - fn hmac( + fn new_hmac( &self, algorithm: HmacAlgorithm, key: &[u8], - input: &[&[u8]], - output: &mut [u8], - ) -> Result<(), CryptoError> { - self.calls.hmac.fetch_add(1, Ordering::Relaxed); - self.inner.crypto().hmac(algorithm, key, input, output) - } - - fn verify_hmac( - &self, - algorithm: HmacAlgorithm, - key: &[u8], - input: &[&[u8]], - expected: &[u8], - ) -> Result<(), CryptoError> { - self.calls.hmac.fetch_add(1, Ordering::Relaxed); - self.inner - .crypto() - .verify_hmac(algorithm, key, input, expected) + ) -> Result, CryptoError> { + Ok(Box::new(CountingMac { + inner: self.inner.crypto().new_hmac(algorithm, key)?, + calls: Arc::clone(&self.calls), + })) } fn block_encrypt( diff --git a/tests/dtls_rsa_certificate.rs b/tests/dtls_rsa_certificate.rs index e3cfa21b..3103db34 100644 --- a/tests/dtls_rsa_certificate.rs +++ b/tests/dtls_rsa_certificate.rs @@ -8,8 +8,8 @@ //! under the `ring` backend. use anyhow::Result; -use rcgen::KeyPair; -use rtc::peer_connection::certificate::RTCCertificate; +use rtc::crypto::{self, SignatureScheme}; +use rtc::peer_connection::certificate::{CertificateParams, RTCCertificate}; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; @@ -34,14 +34,27 @@ enum KeyType { impl KeyType { fn certificate(self) -> Result { - let key_pair = match self { + let provider = crypto::default_provider()?; + let params = CertificateParams::new(vec!["webrtc.rs".to_owned()])?; + + Ok(match self { + // The RSA key is a fixture, so it is imported rather than generated and then + // wrapped in a fresh self-signed certificate. KeyType::Rsa2048(pem) => { - KeyPair::from_pkcs8_pem_and_sign_algo(pem, &rcgen::PKCS_RSA_SHA256)? + let der = pem::parse(pem)?.into_contents(); + let signing_key = provider + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &der)?; + RTCCertificate::generate_from_signing_key( + params, + SignatureScheme::RsaPkcs1Sha256, + signing_key, + )? } - KeyType::EcdsaP256 => KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?, - }; - - Ok(RTCCertificate::from_key_pair(key_pair)?) + KeyType::EcdsaP256 => { + RTCCertificate::generate(provider, SignatureScheme::EcdsaP256Sha256, params)? + } + }) } } From d8fb818d7ff8bbd2d16a52278217fcd2f60a4f92 Mon Sep 17 00:00:00 2001 From: Rain Liu Date: Mon, 3 Aug 2026 20:32:43 -0700 Subject: [PATCH 37/40] =?UTF-8?q?P8=20=E2=80=94=20Integrate=20with=20the?= =?UTF-8?q?=20async=20webrtc=20repository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cargo.yml | 16 +++++++------- .github/workflows/grcov.yml | 10 +++------ CHANGELOG.md | 2 +- Cargo.toml | 6 +++--- docs/benchmarking-crypto-migration.md | 4 ++-- docs/crypto-provider-migration.md | 8 +++---- rtc-crypto/Cargo.toml | 6 +++--- rtc-crypto/src/lib.rs | 2 +- rtc-crypto/src/provider.rs | 6 +++--- rtc-crypto/src/providers/mod.rs | 8 +++---- rtc-crypto/tests/conformance.rs | 4 ++-- rtc-crypto/tests/cross_provider.rs | 2 +- rtc-crypto/tests/custom_provider.rs | 4 ++-- rtc-crypto/tests/default_provider.rs | 4 ++-- rtc-crypto/tests/rsa_import.rs | 6 +++--- rtc-dtls/Cargo.toml | 6 +++--- rtc-dtls/benches/README.md | 2 +- rtc-dtls/benches/record_protection.rs | 4 ++-- rtc-dtls/src/config.rs | 14 ++++++------ rtc-dtls/src/config/config_test.rs | 4 ++-- rtc-dtls/src/crypto/crypto_test.rs | 2 +- rtc-dtls/src/crypto/mod.rs | 8 +++---- rtc-dtls/src/endpoint.rs | 6 +++--- rtc-dtls/src/prf/prf_test.rs | 4 ++-- rtc-ice/Cargo.toml | 6 +++--- rtc-srtp/Cargo.toml | 6 +++--- rtc-srtp/benches/README.md | 4 ++-- rtc-srtp/benches/bench.rs | 4 ++-- rtc-srtp/tests/provider_profiles.rs | 26 +++++++++++------------ rtc-stun/Cargo.toml | 6 +++--- rtc-turn/Cargo.toml | 6 +++--- src/peer_connection/certificate/mod.rs | 6 +++--- src/peer_connection/mod.rs | 13 ++++++++++++ tests/crypto_provider_peer_connections.rs | 2 +- tests/no_builtin_crypto_provider.rs | 2 +- 35 files changed, 114 insertions(+), 105 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 0941d593..2b114480 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -42,7 +42,7 @@ jobs: strategy: fail-fast: false matrix: - backend: [ ring, aws-lc-rs ] + backend: [ crypto-ring, crypto-aws-lc-rs ] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -68,11 +68,11 @@ jobs: - name: default args: --features test-support - name: ring - args: --no-default-features --features ring,test-support + args: --no-default-features --features crypto-ring,test-support - name: aws-lc-rs - args: --no-default-features --features aws-lc-rs,test-support + args: --no-default-features --features crypto-aws-lc-rs,test-support - name: ring + aws-lc-rs - args: --no-default-features --features ring,aws-lc-rs,test-support + args: --no-default-features --features crypto-ring,crypto-aws-lc-rs,test-support steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -102,14 +102,14 @@ jobs: - name: default args: "" - name: ring only - args: --no-default-features --features ring + args: --no-default-features --features crypto-ring - name: aws-lc-rs only - args: --no-default-features --features aws-lc-rs + args: --no-default-features --features crypto-aws-lc-rs # Both backends in one graph. This was a `compile_error!` before G3; it is now the # regression guard for Cargo feature additivity, since feature unification can enable # both from unrelated dependencies. - name: ring + aws-lc-rs - args: --features ring,aws-lc-rs + args: --features crypto-ring,crypto-aws-lc-rs steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -130,7 +130,7 @@ jobs: strategy: fail-fast: false matrix: - backend: [ ring, aws-lc-rs ] + backend: [ crypto-ring, crypto-aws-lc-rs ] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index c43bbb11..3f32a5aa 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -26,13 +26,9 @@ jobs: - nightly crypto: - name: ring - features: rtc/ring - packages: --workspace - test_args: "" + features: crypto-ring - name: aws-lc-rs - features: aws-lc-rs - packages: --workspace - test_args: "" + features: crypto-aws-lc-rs steps: - name: Checkout source code uses: actions/checkout@v4 @@ -49,7 +45,7 @@ jobs: tool: grcov - name: Test - run: cargo test ${{ matrix.crypto.packages }} --no-fail-fast --no-default-features --features ${{ matrix.crypto.features }} ${{ matrix.crypto.test_args }} + run: cargo test --workspace --no-fail-fast --no-default-features --features ${{ matrix.crypto.features }} env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Cinstrument-coverage" diff --git a/CHANGELOG.md b/CHANGELOG.md index 138eb57f..96d46660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Each protocol crate re-exports the crypto API (`rtc_srtp::crypto`, `rtc_stun::crypto`, `rtc_ice::crypto`, `rtc_turn::crypto`, `rtc_dtls::crypto_provider`) so standalone users can name `Arc` without a direct `rtc-crypto` dependency. -- **`ring` and `aws-lc-rs` Cargo features are now additive.** Enabling both builds successfully +- **`crypto-ring` and `crypto-aws-lc-rs` Cargo features are now additive.** Enabling both builds successfully and is covered in CI. Previously each of `rtc`, `rtc-dtls`, `rtc-srtp`, and `rtc-stun` carried a `compile_error!` rejecting the combination, which made an otherwise valid build fail whenever Cargo feature unification pulled in both. `rtc_crypto::default_provider()` still prefers `ring` diff --git a/Cargo.toml b/Cargo.toml index eae706ac..3dc39d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,9 +131,9 @@ categories.workspace = true readme = "README.md" [features] -default = ["ring"] -ring = ["crypto/ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] -aws-lc-rs = ["crypto/aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "dtls/crypto-ring", "rustls/ring", "rcgen/ring", "ice/crypto-ring", "stun/crypto-ring", "srtp/crypto-ring", "turn/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "dtls/crypto-aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs", "srtp/crypto-aws-lc-rs", "turn/crypto-aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } diff --git a/docs/benchmarking-crypto-migration.md b/docs/benchmarking-crypto-migration.md index 264494b7..c6537767 100644 --- a/docs/benchmarking-crypto-migration.md +++ b/docs/benchmarking-crypto-migration.md @@ -215,7 +215,7 @@ objects are built once. A `Setup/*` figure rising while `Encrypt/*` falls is the not a regression. Reporting only a combined number hides both directions. **Measure every enabled provider under identical inputs.** The benchmarks loop over the built-in -providers, so `--features ring,aws-lc-rs` reports both side by side. This is what showed that +providers, so `--features crypto-ring,crypto-aws-lc-rs` reports both side by side. This is what showed that `aws-lc-rs` was *worse* on DTLS encrypt while faster everywhere else, which localised the cause to the encrypt-only RNG call. @@ -240,7 +240,7 @@ file and derive every count from that file. **zsh eats `:r` in a revision string.** `git show "$c:rtc-srtp/Cargo.toml"` parses `$c:r` as a history modifier and looks up `425494ctc-srtp/Cargo.toml`. Use `"${c}:path"`. -**zsh does not word-split unquoted variables.** `F="--no-default-features --features ring"; cargo build $F` passes one bogus argument. This produced four false "failures" in a feature-matrix loop. +**zsh does not word-split unquoted variables.** `F="--no-default-features --features crypto-ring"; cargo build $F` passes one bogus argument. This produced four false "failures" in a feature-matrix loop. Write the invocations out, or use an array. **Bisecting can misattribute.** An early bisect blamed the AES-CTR rewrite for a set of DTLS test diff --git a/docs/crypto-provider-migration.md b/docs/crypto-provider-migration.md index 354a969b..3768ea35 100644 --- a/docs/crypto-provider-migration.md +++ b/docs/crypto-provider-migration.md @@ -90,13 +90,13 @@ fix available to the person hitting it. | Build | Result | |---|---| -| `--features ring` | ring only | -| `--features aws-lc-rs` | aws-lc-rs only | -| `--features ring,aws-lc-rs` | both compiled; `default_provider()` returns ring | +| `--features crypto-ring` | ring only | +| `--features crypto-aws-lc-rs` | aws-lc-rs only | +| `--features crypto-ring,crypto-aws-lc-rs` | both compiled; `default_provider()` returns ring | | `--no-default-features` | no built-in provider; supply your own | `default_provider()` prefers ring when both are enabled, matching the previous precedence. -Enabling `aws-lc-rs` alongside the default never silently switches the default. +Enabling `crypto-aws-lc-rs` alongside the default never silently switches the default. ## Removed and changed public items diff --git a/rtc-crypto/Cargo.toml b/rtc-crypto/Cargo.toml index 1b1cf9fe..6fa5fec1 100644 --- a/rtc-crypto/Cargo.toml +++ b/rtc-crypto/Cargo.toml @@ -13,9 +13,9 @@ categories.workspace = true readme = "README.md" [features] -default = ["ring"] -ring = ["dep:ring", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] -aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] +default = ["crypto-ring"] +crypto-ring = ["dep:ring", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] +crypto-aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] test-support = [] [dependencies] diff --git a/rtc-crypto/src/lib.rs b/rtc-crypto/src/lib.rs index 84bbb32b..99fc34a9 100644 --- a/rtc-crypto/src/lib.rs +++ b/rtc-crypto/src/lib.rs @@ -9,7 +9,7 @@ mod provider; mod secret; mod traits; -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] mod common; pub mod providers; diff --git a/rtc-crypto/src/provider.rs b/rtc-crypto/src/provider.rs index 70b72fbe..8bb430d7 100644 --- a/rtc-crypto/src/provider.rs +++ b/rtc-crypto/src/provider.rs @@ -7,17 +7,17 @@ use crate::{CryptoError, RTCCryptoProvider}; /// Ring remains the default whenever its feature is enabled. AWS-LC-RS is selected only when it is /// the sole built-in. With no built-in features this returns [`CryptoError::NoDefaultProvider`]. pub fn default_provider() -> Result, CryptoError> { - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] { Ok(Arc::new(crate::providers::RingProvider::new())) } - #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] { Ok(Arc::new(crate::providers::AwsLcRsProvider::new())) } - #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] { Err(CryptoError::NoDefaultProvider) } diff --git a/rtc-crypto/src/providers/mod.rs b/rtc-crypto/src/providers/mod.rs index b587050e..f6b8f91e 100644 --- a/rtc-crypto/src/providers/mod.rs +++ b/rtc-crypto/src/providers/mod.rs @@ -1,9 +1,9 @@ -#[cfg(feature = "aws-lc-rs")] +#[cfg(feature = "crypto-aws-lc-rs")] mod aws_lc_rs; -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] mod ring; -#[cfg(feature = "aws-lc-rs")] +#[cfg(feature = "crypto-aws-lc-rs")] pub use aws_lc_rs::{AwsLcRsCrypto, AwsLcRsProvider, AwsLcRsRandom}; -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] pub use ring::{RingCrypto, RingProvider, RingRandom}; diff --git a/rtc-crypto/tests/conformance.rs b/rtc-crypto/tests/conformance.rs index 9a09c1dc..51035093 100644 --- a/rtc-crypto/tests/conformance.rs +++ b/rtc-crypto/tests/conformance.rs @@ -1,12 +1,12 @@ #![cfg(feature = "test-support")] -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] #[test] fn ring_provider_conforms() { rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::RingProvider::new()); } -#[cfg(feature = "aws-lc-rs")] +#[cfg(feature = "crypto-aws-lc-rs")] #[test] fn aws_lc_rs_provider_conforms() { rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::AwsLcRsProvider::new()); diff --git a/rtc-crypto/tests/cross_provider.rs b/rtc-crypto/tests/cross_provider.rs index ada63a3f..03894257 100644 --- a/rtc-crypto/tests/cross_provider.rs +++ b/rtc-crypto/tests/cross_provider.rs @@ -1,4 +1,4 @@ -#![cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#![cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use rtc_crypto::providers::{AwsLcRsProvider, RingProvider}; use rtc_crypto::{ diff --git a/rtc-crypto/tests/custom_provider.rs b/rtc-crypto/tests/custom_provider.rs index 5ac889ac..14a0982d 100644 --- a/rtc-crypto/tests/custom_provider.rs +++ b/rtc-crypto/tests/custom_provider.rs @@ -1,6 +1,6 @@ use rtc_crypto::{CryptoAlgorithm, CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; -#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] +#[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] use rtc_crypto::default_provider; struct CustomProvider { @@ -66,7 +66,7 @@ fn downstream_provider_requires_no_registration_or_backend_types() { } } -#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] +#[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] #[test] fn no_builtin_provider_is_a_normal_error() { assert!(matches!( diff --git a/rtc-crypto/tests/default_provider.rs b/rtc-crypto/tests/default_provider.rs index f9792107..b375da58 100644 --- a/rtc-crypto/tests/default_provider.rs +++ b/rtc-crypto/tests/default_provider.rs @@ -1,10 +1,10 @@ -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] #[test] fn ring_is_the_default_when_enabled() { assert_eq!(rtc_crypto::default_provider().unwrap().name(), "ring"); } -#[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] +#[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] #[test] fn aws_lc_rs_is_the_default_when_it_is_the_only_builtin() { assert_eq!(rtc_crypto::default_provider().unwrap().name(), "aws-lc-rs"); diff --git a/rtc-crypto/tests/rsa_import.rs b/rtc-crypto/tests/rsa_import.rs index 6db87d33..d140ab08 100644 --- a/rtc-crypto/tests/rsa_import.rs +++ b/rtc-crypto/tests/rsa_import.rs @@ -1,14 +1,14 @@ -#![cfg(any(feature = "ring", feature = "aws-lc-rs"))] +#![cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use rtc_crypto::{RTCCryptoProvider, SignatureScheme}; -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] #[test] fn ring_imports_and_uses_rsa_pkcs8() { assert_rsa_import(&rtc_crypto::providers::RingProvider::new()); } -#[cfg(feature = "aws-lc-rs")] +#[cfg(feature = "crypto-aws-lc-rs")] #[test] fn aws_lc_rs_imports_and_uses_rsa_pkcs8() { assert_rsa_import(&rtc_crypto::providers::AwsLcRsProvider::new()); diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index ff2032c6..5ad4d75f 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -12,9 +12,9 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["crypto/ring", "rustls/ring", "rcgen/ring"] -aws-lc-rs = ["crypto/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "rustls/ring", "rcgen/ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] [dependencies] shared = { workspace = true, default-features = false, features = ["replay"] } diff --git a/rtc-dtls/benches/README.md b/rtc-dtls/benches/README.md index 8b956c38..1f9f9106 100644 --- a/rtc-dtls/benches/README.md +++ b/rtc-dtls/benches/README.md @@ -2,7 +2,7 @@ ```bash cargo bench --package rtc-dtls --bench record_protection -cargo bench --package rtc-dtls --bench record_protection --no-default-features --features aws-lc-rs +cargo bench --package rtc-dtls --bench record_protection --no-default-features --features crypto-aws-lc-rs ``` `Setup/*` constructs a cipher — provider dispatch, key import, key schedule, and (after G3) keying diff --git a/rtc-dtls/benches/record_protection.rs b/rtc-dtls/benches/record_protection.rs index dd8e540e..f40ab032 100644 --- a/rtc-dtls/benches/record_protection.rs +++ b/rtc-dtls/benches/record_protection.rs @@ -72,9 +72,9 @@ fn record() -> (RecordLayerHeader, Vec) { /// The built-in providers compiled into this benchmark. fn providers() -> Vec<(&'static str, Arc)> { let mut providers: Vec<(&'static str, Arc)> = Vec::new(); - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] providers.push(("ring", Arc::new(crypto::providers::RingProvider::default()))); - #[cfg(feature = "aws-lc-rs")] + #[cfg(feature = "crypto-aws-lc-rs")] providers.push(( "aws-lc-rs", Arc::new(crypto::providers::AwsLcRsProvider::default()), diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 9d902bcc..38780782 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -55,14 +55,14 @@ impl RustlsVerifierAdapter { } /// Uses rustls's ring verification backend. - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] #[must_use] pub fn ring() -> Self { Self::new(Arc::new(rustls::crypto::ring::default_provider())) } /// Uses rustls's AWS-LC-RS verification backend. - #[cfg(feature = "aws-lc-rs")] + #[cfg(feature = "crypto-aws-lc-rs")] #[must_use] pub fn aws_lc_rs() -> Self { Self::new(Arc::new(rustls::crypto::aws_lc_rs::default_provider())) @@ -70,15 +70,15 @@ impl RustlsVerifierAdapter { } fn default_verifier_adapter() -> Option { - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] { Some(RustlsVerifierAdapter::ring()) } - #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] { Some(RustlsVerifierAdapter::aws_lc_rs()) } - #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] { None } @@ -571,7 +571,7 @@ pub type VerifyPeerCertificateFn = /// Generates a self-signed certificate, as WebRTC endpoints use. pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] { let mut certs = rustls::RootCertStore::empty(); certs @@ -585,7 +585,7 @@ pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { .unwrap(); certs } - #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] { rustls::RootCertStore::empty() } diff --git a/rtc-dtls/src/config/config_test.rs b/rtc-dtls/src/config/config_test.rs index 0c94dafd..24c65c0d 100644 --- a/rtc-dtls/src/config/config_test.rs +++ b/rtc-dtls/src/config/config_test.rs @@ -84,7 +84,7 @@ fn test_config_rejects_incomplete_provider() { assert!(matches!(result, Err(Error::ErrNoAvailableCipherSuites))); } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] #[test] fn test_config_accepts_ring_provider() -> Result<()> { let handshake = ConfigBuilder::default() @@ -95,7 +95,7 @@ fn test_config_accepts_ring_provider() -> Result<()> { Ok(()) } -#[cfg(feature = "aws-lc-rs")] +#[cfg(feature = "crypto-aws-lc-rs")] #[test] fn test_config_accepts_aws_lc_rs_provider() -> Result<()> { let handshake = ConfigBuilder::default() diff --git a/rtc-dtls/src/crypto/crypto_test.rs b/rtc-dtls/src/crypto/crypto_test.rs index a3b943da..95fa0ac6 100644 --- a/rtc-dtls/src/crypto/crypto_test.rs +++ b/rtc-dtls/src/crypto/crypto_test.rs @@ -90,7 +90,7 @@ fn test_exported_signing_key_can_be_imported() -> Result<()> { .map_err(crypto_error) } -#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] #[test] fn test_cross_provider_signature_verification() -> Result<()> { let ring = crypto::providers::RingProvider::new(); diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index 518931d8..27f3bbc3 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -33,7 +33,7 @@ use crypto::{ PublicKey, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme as CryptoSignatureScheme, SigningKey, }; -#[cfg(any(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use rcgen::{CertifiedKey, KeyPair, generate_simple_self_signed}; use crate::curve::named_curve::*; @@ -67,7 +67,7 @@ pub struct Certificate { } impl Certificate { - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] /// Generates a self-signed certificate, importing its key into `provider`. pub fn generate_self_signed( subject_alt_names: impl Into>, @@ -81,7 +81,7 @@ impl Certificate { }) } - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] /// Generates a self-signed certificate with `alg`, importing its key into `provider`. pub fn generate_self_signed_with_alg( subject_alt_names: impl Into>, @@ -236,7 +236,7 @@ impl std::fmt::Debug for CryptoPrivateKey { impl CryptoPrivateKey { /// Imports an rcgen key pair into an explicit provider. - #[cfg(any(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] pub fn from_key_pair(key_pair: &KeyPair, provider: Arc) -> Result { let serialized_der = key_pair.serialize_der(); let scheme = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index 56775249..142999e0 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -432,7 +432,7 @@ mod tests { Ok(()) } - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] #[test] fn ring_provider_completes_handshake_and_record_exchange() -> Result<()> { let provider: Arc = Arc::new(crypto::providers::RingProvider::new()); @@ -443,7 +443,7 @@ mod tests { ) } - #[cfg(feature = "aws-lc-rs")] + #[cfg(feature = "crypto-aws-lc-rs")] #[test] fn aws_lc_rs_provider_completes_handshake_and_record_exchange() -> Result<()> { let provider: Arc = @@ -455,7 +455,7 @@ mod tests { ) } - #[cfg(all(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] #[test] fn ring_and_aws_lc_rs_complete_cross_provider_handshakes() -> Result<()> { let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); diff --git a/rtc-dtls/src/prf/prf_test.rs b/rtc-dtls/src/prf/prf_test.rs index 3f648d40..7b479a06 100644 --- a/rtc-dtls/src/prf/prf_test.rs +++ b/rtc-dtls/src/prf/prf_test.rs @@ -1,6 +1,6 @@ use super::*; use crate::cipher_suite::CipherSuiteHash; -#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use crypto::RTCCryptoProvider; #[test] @@ -24,7 +24,7 @@ fn test_pre_master_secret() -> Result<()> { Ok(()) } -#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] #[test] fn test_cross_provider_pre_master_secret() -> Result<()> { let ring = crypto::providers::RingProvider::new(); diff --git a/rtc-ice/Cargo.toml b/rtc-ice/Cargo.toml index 86cf2fce..3ac093e5 100644 --- a/rtc-ice/Cargo.toml +++ b/rtc-ice/Cargo.toml @@ -12,9 +12,9 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["crypto/ring", "stun/ring"] -aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "stun/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = [] } diff --git a/rtc-srtp/Cargo.toml b/rtc-srtp/Cargo.toml index fecc7b54..a718aefe 100644 --- a/rtc-srtp/Cargo.toml +++ b/rtc-srtp/Cargo.toml @@ -12,9 +12,9 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["crypto/ring"] -aws-lc-rs = ["crypto/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } diff --git a/rtc-srtp/benches/README.md b/rtc-srtp/benches/README.md index 5f698641..c30acabb 100644 --- a/rtc-srtp/benches/README.md +++ b/rtc-srtp/benches/README.md @@ -4,7 +4,7 @@ cargo bench --package rtc-srtp --bench bench ``` -Benchmarks run against every enabled built-in provider, so `--features ring,aws-lc-rs` reports +Benchmarks run against every enabled built-in provider, so `--features crypto-ring,crypto-aws-lc-rs` reports both backends side by side under identical inputs. The groups are: @@ -143,7 +143,7 @@ cd /tmp/rtc-baseline cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 # Both backends, identical inputs -cargo bench --package rtc-srtp --bench bench --features ring,aws-lc-rs +cargo bench --package rtc-srtp --bench bench --features crypto-ring,crypto-aws-lc-rs ``` Cross-machine comparison is not meaningful. Earlier revisions of this file recorded results from a diff --git a/rtc-srtp/benches/bench.rs b/rtc-srtp/benches/bench.rs index d85496d1..1840d1bf 100644 --- a/rtc-srtp/benches/bench.rs +++ b/rtc-srtp/benches/bench.rs @@ -166,12 +166,12 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) fn providers() -> Vec<(&'static str, std::sync::Arc)> { let mut providers: Vec<(&'static str, std::sync::Arc)> = Vec::new(); - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] providers.push(( "ring", std::sync::Arc::new(crypto::providers::RingProvider::default()), )); - #[cfg(feature = "aws-lc-rs")] + #[cfg(feature = "crypto-aws-lc-rs")] providers.push(( "aws-lc-rs", std::sync::Arc::new(crypto::providers::AwsLcRsProvider::default()), diff --git a/rtc-srtp/tests/provider_profiles.rs b/rtc-srtp/tests/provider_profiles.rs index 698e0f24..6a66cf46 100644 --- a/rtc-srtp/tests/provider_profiles.rs +++ b/rtc-srtp/tests/provider_profiles.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] use std::sync::atomic::{AtomicUsize, Ordering}; -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] use crypto::{ AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, HmacAlgorithm, StreamCipher, StreamCipherAlgorithm, @@ -24,16 +24,16 @@ const PROFILES: [ProtectionProfile; 6] = [ ]; fn providers() -> Vec> { - #[cfg(all(feature = "ring", feature = "aws-lc-rs"))] + #[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] return vec![ Arc::new(crypto::providers::RingProvider::new()), Arc::new(crypto::providers::AwsLcRsProvider::new()), ]; - #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + #[cfg(all(feature = "crypto-ring", not(feature = "crypto-aws-lc-rs")))] return vec![Arc::new(crypto::providers::RingProvider::new())]; - #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))] + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] return vec![Arc::new(crypto::providers::AwsLcRsProvider::new())]; - #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] Vec::new() } @@ -244,7 +244,7 @@ fn aead_profiles_reject_wrong_aad_tag_rollover_and_replay() -> Result<()> { Ok(()) } -#[cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] #[test] fn providers_produce_identical_packets_and_interoperate() -> Result<()> { let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); @@ -266,7 +266,7 @@ fn providers_produce_identical_packets_and_interoperate() -> Result<()> { Ok(()) } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] struct CountingCrypto { inner: crypto::providers::RingCrypto, stream_constructions: AtomicUsize, @@ -274,7 +274,7 @@ struct CountingCrypto { mac_constructions: AtomicUsize, } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] impl RTCCrypto for CountingCrypto { fn supports(&self, algorithm: CryptoAlgorithm) -> bool { self.inner.supports(algorithm) @@ -317,13 +317,13 @@ impl RTCCrypto for CountingCrypto { } } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] struct CountingProvider { crypto: CountingCrypto, random: crypto::providers::RingRandom, } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] impl CountingProvider { fn new() -> Self { Self { @@ -338,7 +338,7 @@ impl CountingProvider { } } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] impl RTCCryptoProvider for CountingProvider { fn name(&self) -> &'static str { "counting-ring" @@ -353,7 +353,7 @@ impl RTCCryptoProvider for CountingProvider { } } -#[cfg(feature = "ring")] +#[cfg(feature = "crypto-ring")] #[test] fn keyed_ciphers_are_constructed_once_per_context_not_per_packet() -> Result<()> { let stream_provider = Arc::new(CountingProvider::new()); diff --git a/rtc-stun/Cargo.toml b/rtc-stun/Cargo.toml index 51a48b6a..d6244bca 100644 --- a/rtc-stun/Cargo.toml +++ b/rtc-stun/Cargo.toml @@ -12,9 +12,9 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["crypto/ring"] -aws-lc-rs = ["crypto/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs"] bench = [] [dependencies] diff --git a/rtc-turn/Cargo.toml b/rtc-turn/Cargo.toml index b744853a..68ffbd65 100644 --- a/rtc-turn/Cargo.toml +++ b/rtc-turn/Cargo.toml @@ -12,9 +12,9 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["crypto/ring", "stun/ring"] -aws-lc-rs = ["crypto/aws-lc-rs", "stun/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "stun/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs"] metrics = [] [dependencies] diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index 8faffc0e..f1f8efea 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -769,7 +769,7 @@ impl rcgen::SigningKey for RcgenSigningKey { } } -#[cfg(all(test, any(feature = "ring", feature = "aws-lc-rs")))] +#[cfg(all(test, any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] mod test { use super::*; @@ -911,13 +911,13 @@ mod test { Ok(()) } - #[cfg(feature = "ring")] + #[cfg(feature = "crypto-ring")] #[test] fn ring_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { provider_certificate_round_trip(Arc::new(crypto::providers::RingProvider::new())) } - #[cfg(feature = "aws-lc-rs")] + #[cfg(feature = "crypto-aws-lc-rs")] #[test] fn aws_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { provider_certificate_round_trip(Arc::new(crypto::providers::AwsLcRsProvider::new())) diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index f0f1b6b8..0951c023 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -311,6 +311,7 @@ use sdp::MEDIA_SECTION_APPLICATION; use shared::error::{Error, Result}; use shared::util::math_rand_alpha; use std::collections::HashMap; +use std::sync::Arc; use std::time::Instant; /// Builder for creating RTCPeerConnection instances. @@ -1769,6 +1770,18 @@ where &self.configuration } + /// Returns the crypto provider this peer connection resolved at construction. + /// + /// Construction is the single place in the workspace that resolves a default provider: it + /// uses the one configured through + /// [`SettingEngine::set_crypto_provider`](crate::peer_connection::configuration::setting_engine::SettingEngine::set_crypto_provider), + /// or the feature-selected built-in. Callers that build additional components around a peer + /// connection — an async wrapper's TURN client, for instance — take the provider from here + /// so the whole connection shares one, rather than resolving a second. + pub fn crypto_provider(&self) -> &Arc { + &self.dtls_transport().crypto_provider + } + /// set_configuration updates the configuration of this PeerConnection object. pub fn set_configuration(&mut self, configuration: RTCConfiguration) -> Result<()> { // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2) diff --git a/tests/crypto_provider_peer_connections.rs b/tests/crypto_provider_peer_connections.rs index 80b76d18..a606415b 100644 --- a/tests/crypto_provider_peer_connections.rs +++ b/tests/crypto_provider_peer_connections.rs @@ -1,4 +1,4 @@ -#![cfg(all(feature = "ring", feature = "aws-lc-rs"))] +#![cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use std::net::SocketAddr; use std::sync::Arc; diff --git a/tests/no_builtin_crypto_provider.rs b/tests/no_builtin_crypto_provider.rs index 1ad9503a..8bd6593f 100644 --- a/tests/no_builtin_crypto_provider.rs +++ b/tests/no_builtin_crypto_provider.rs @@ -1,4 +1,4 @@ -#![cfg(not(any(feature = "ring", feature = "aws-lc-rs")))] +#![cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] use rtc::peer_connection::RTCPeerConnectionBuilder; From 4121c0c71de880f11133b01f7d287bdf829f020a Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:17:38 -0700 Subject: [PATCH 38/40] remove public API crypto_provider from RTCPeerConnection and refactor MessageIntegrity<'a> (#145) * remove-public-API-crypto_provider-from-RTCPeerConnection * refactor MessageIntegrity<'a> * fix compiler errors * refactor rtc-srtp * fix cargo build --workspace --all-targets --features crypto-ring,crypto-aws-lc-rs --- rtc-crypto/src/common.rs | 62 +-------- rtc-crypto/src/providers/ring.rs | 72 ++++++++-- rtc-dtls/src/crypto/crypto_test.rs | 5 +- rtc-dtls/src/crypto/mod.rs | 38 ++---- rtc-dtls/src/endpoint.rs | 9 +- rtc-dtls/src/state.rs | 5 - rtc-ice/src/agent/agent_selector.rs | 6 +- rtc-ice/src/agent/agent_test.rs | 16 +-- rtc-ice/src/agent/mod.rs | 16 +-- rtc-interceptor/src/lib.rs | 3 +- rtc-srtp/benches/bench.rs | 36 ++--- rtc-srtp/examples/srtp_micro.rs | 6 +- .../src/cipher/cipher_aes_cm_hmac_sha1.rs | 32 ++--- rtc-srtp/src/context/context_test.rs | 26 ++-- rtc-srtp/src/context/mod.rs | 15 +- rtc-srtp/src/context/srtcp_test.rs | 14 +- rtc-srtp/src/context/srtp_test.rs | 4 +- rtc-srtp/tests/provider_profiles.rs | 22 ++- rtc-stun/benches/bench.rs | 11 +- rtc-stun/src/integrity.rs | 51 +++---- rtc-stun/src/integrity/integrity_test.rs | 32 +++-- rtc-stun/src/message.rs | 2 +- rtc-stun/src/message/message_test.rs | 8 +- rtc-turn/src/client/mod.rs | 33 +++-- rtc-turn/src/client/relay.rs | 54 +++++--- src/peer_connection/certificate/mod.rs | 128 ++++++++++-------- src/peer_connection/configuration/mod.rs | 12 +- .../configuration/setting_engine.rs | 13 ++ src/peer_connection/handler/dtls.rs | 11 +- src/peer_connection/internal.rs | 13 +- src/peer_connection/mod.rs | 34 +---- src/peer_connection/transport/dtls/mod.rs | 6 +- tests/dtls_rsa_certificate.rs | 8 +- 33 files changed, 408 insertions(+), 395 deletions(-) diff --git a/rtc-crypto/src/common.rs b/rtc-crypto/src/common.rs index 6c848134..f7a1776b 100644 --- a/rtc-crypto/src/common.rs +++ b/rtc-crypto/src/common.rs @@ -1,3 +1,7 @@ +use crate::{ + AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoError, + SecretVec, StreamCipher, StreamCipherAlgorithm, +}; use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, KeyIvInit}; use aes::{Aes128, Aes256}; @@ -5,14 +9,7 @@ use ccm::Ccm; use ccm::aead::AeadInPlace; use ccm::consts::{U8, U12, U16}; use ctr::cipher::StreamCipher as CtrStreamCipher; -use hmac::{Hmac, Mac as RustCryptoMac}; use md5::{Digest, Md5}; -use sha1::Sha1; - -use crate::{ - AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoError, - Mac as StreamMac, SecretVec, StreamCipher, StreamCipherAlgorithm, -}; const AES_BLOCK_LEN: usize = 16; const CCM_NONCE_LEN: usize = 12; @@ -41,57 +38,6 @@ pub(crate) fn fill_random(output: &mut [u8]) -> Result<(), CryptoError> { Ok(()) } -type HmacSha1 = Hmac; - -/// HMAC-SHA1 backed by RustCrypto, keyed once. -/// -/// `ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 -/// instructions, measuring 4469 ns against RustCrypto's 1373 ns over a 1212-byte SRTP packet — -/// 3.3x, and the whole of the SRTP AES-CM/HMAC regression. The built-in providers are already -/// composite (AES-CTR, CCM, CBC and MD5 come from RustCrypto too), so HMAC-SHA1 is composed the -/// same way rather than making the slower backend the default. `aws-lc-rs` has fast SHA-1 and -/// keeps its own. -pub(crate) struct RustCryptoHmacSha1 { - keyed: HmacSha1, -} - -impl RustCryptoHmacSha1 { - pub(crate) fn new(key: &[u8]) -> Self { - Self { - // HMAC accepts any key length: it hashes longer keys and zero-pads shorter ones. - keyed: ::new_from_slice(key) - .expect("HMAC accepts keys of any length"), - } - } -} - -impl StreamMac for RustCryptoHmacSha1 { - fn output_len(&self) -> usize { - 20 - } - - fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { - check_tag_len(20, output.len())?; - let mut mac = self.keyed.clone(); - for part in input { - mac.update(part); - } - output.copy_from_slice(&mac.finalize().into_bytes()); - Ok(()) - } - - fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { - check_tag_len(20, expected.len())?; - let mut actual = [0u8; 20]; - self.sign(input, &mut actual)?; - if crate::constant_time_eq(&actual, expected) { - Ok(()) - } else { - Err(CryptoError::AuthenticationFailed) - } - } -} - pub(crate) fn md5(data: &[u8]) -> Vec { Md5::digest(data).to_vec() } diff --git a/rtc-crypto/src/providers/ring.rs b/rtc-crypto/src/providers/ring.rs index 75b9c2b1..6158bd69 100644 --- a/rtc-crypto/src/providers/ring.rs +++ b/rtc-crypto/src/providers/ring.rs @@ -1,19 +1,20 @@ -use std::sync::Arc; - -use ring::aead; -use ring::agreement; -use ring::digest; -use ring::hmac; -use ring::rand::SystemRandom; -use ring::signature::{self, KeyPair}; - use crate::common; +use crate::common::check_tag_len; use crate::{ ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, Mac, PublicKey, PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, SignatureScheme, SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, }; +use ::hmac::Mac as RustCryptoMac; +use ring::aead; +use ring::agreement; +use ring::digest; +use ring::hmac; +use ring::rand::SystemRandom; +use ring::signature::{self, KeyPair}; +use sha1::Sha1; +use std::sync::Arc; /// The built-in Ring provider bundle. #[derive(Default)] @@ -119,7 +120,7 @@ impl RTCCrypto for RingCrypto { // ring's SHA-1 is a software implementation and measures 3.3x slower than // RustCrypto's; see common::RustCryptoHmacSha1. SHA-256 stays on ring, which uses // the hardware instructions. - HmacAlgorithm::Sha1 => Ok(Box::new(common::RustCryptoHmacSha1::new(key))), + HmacAlgorithm::Sha1 => Ok(Box::new(RustCryptoHmacSha1::new(key))), HmacAlgorithm::Sha256 => Ok(Box::new(RingHmac { key: hmac::Key::new(hmac_algorithm(algorithm), key), output_len: algorithm.output_len(), @@ -549,3 +550,54 @@ fn verify_public_key_encoding( Err(CryptoError::InvalidPublicKey) } } + +type HmacSha1 = ::hmac::Hmac; + +/// HMAC-SHA1 backed by RustCrypto, keyed once. +/// +/// `ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 +/// instructions, measuring 4469 ns against RustCrypto's 1373 ns over a 1212-byte SRTP packet — +/// 3.3x, and the whole of the SRTP AES-CM/HMAC regression. The built-in providers are already +/// composite (AES-CTR, CCM, CBC and MD5 come from RustCrypto too), so HMAC-SHA1 is composed the +/// same way rather than making the slower backend the default. `aws-lc-rs` has fast SHA-1 and +/// keeps its own. +pub(crate) struct RustCryptoHmacSha1 { + keyed: HmacSha1, +} + +impl RustCryptoHmacSha1 { + pub(crate) fn new(key: &[u8]) -> Self { + Self { + // HMAC accepts any key length: it hashes longer keys and zero-pads shorter ones. + keyed: ::new_from_slice(key) + .expect("HMAC accepts keys of any length"), + } + } +} + +impl Mac for RustCryptoHmacSha1 { + fn output_len(&self) -> usize { + 20 + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + check_tag_len(20, output.len())?; + let mut mac = self.keyed.clone(); + for part in input { + mac.update(part); + } + output.copy_from_slice(&mac.finalize().into_bytes()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + check_tag_len(20, expected.len())?; + let mut actual = [0u8; 20]; + self.sign(input, &mut actual)?; + if crate::constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} diff --git a/rtc-dtls/src/crypto/crypto_test.rs b/rtc-dtls/src/crypto/crypto_test.rs index 95fa0ac6..24ad7a1e 100644 --- a/rtc-dtls/src/crypto/crypto_test.rs +++ b/rtc-dtls/src/crypto/crypto_test.rs @@ -3,6 +3,7 @@ use super::*; use crate::content::ContentType; use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE}; use crate::signature_hash_algorithm::HashAlgorithm; +use crypto::RTCCryptoProvider; #[test] fn test_generate_key_signature() -> Result<()> { @@ -207,7 +208,7 @@ fn test_certificate_verify() -> Result<()> { //test ECDSA256 let certificate_ecdsa256 = Certificate::generate_self_signed( vec!["localhost".to_owned()], - crypto::default_provider().map_err(crypto_error)?, + crypto::default_provider().map_err(crypto_error)?.crypto(), )?; let ecdsa_algorithm = SignatureHashAlgorithm { hash: HashAlgorithm::Sha256, @@ -235,7 +236,7 @@ fn test_certificate_verify() -> Result<()> { let certificate_ed25519 = Certificate::generate_self_signed_with_alg( vec!["localhost".to_owned()], &rcgen::PKCS_ED25519, - crypto::default_provider().map_err(crypto_error)?, + crypto::default_provider().map_err(crypto_error)?.crypto(), )?; let ed25519_algorithm = SignatureHashAlgorithm { hash: HashAlgorithm::Sha256, diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index 27f3bbc3..89ffff11 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -30,8 +30,7 @@ use rustls::pki_types::{CertificateDer, ServerName}; use rustls::server::danger::ClientCertVerifier; use crypto::{ - PublicKey, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme as CryptoSignatureScheme, - SigningKey, + PublicKey, PublicKeyEncoding, RTCCrypto, SignatureScheme as CryptoSignatureScheme, SigningKey, }; #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use rcgen::{CertifiedKey, KeyPair, generate_simple_self_signed}; @@ -71,13 +70,13 @@ impl Certificate { /// Generates a self-signed certificate, importing its key into `provider`. pub fn generate_self_signed( subject_alt_names: impl Into>, - provider: Arc, + crypto: &dyn RTCCrypto, ) -> Result { let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names) .map_err(|error| Error::Other(error.to_string()))?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::from_key_pair(&signing_key, provider)?, + private_key: CryptoPrivateKey::from_key_pair(&signing_key, crypto)?, }) } @@ -86,7 +85,7 @@ impl Certificate { pub fn generate_self_signed_with_alg( subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, - provider: Arc, + crypto: &dyn RTCCrypto, ) -> Result { let params = rcgen::CertificateParams::new(subject_alt_names) .map_err(|error| Error::Other(error.to_string()))?; @@ -98,13 +97,12 @@ impl Certificate { Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::from_key_pair(&key_pair, provider)?, + private_key: CryptoPrivateKey::from_key_pair(&key_pair, crypto)?, }) } - /// Parses a PEM certificate and imports its PKCS#8 key into `provider`. - /// Parses a PEM certificate and imports its PKCS#8 key into `provider`. - pub fn from_pem(pem_str: &str, provider: Arc) -> Result { + /// Parses a PEM certificate and imports its PKCS#8 key into `crypto`. + pub fn from_pem(pem_str: &str, crypto: &dyn RTCCrypto) -> Result { let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; if pems.len() < 2 { return Err(Error::InvalidPEM(format!( @@ -139,17 +137,8 @@ impl Certificate { ]; let signing_key = schemes .into_iter() - .filter(|scheme| { - provider - .crypto() - .supports(crypto::CryptoAlgorithm::SigningKeyImport(*scheme)) - }) - .find_map(|scheme| { - provider - .crypto() - .import_signing_key(scheme, &private_key_der) - .ok() - }) + .filter(|scheme| crypto.supports(crypto::CryptoAlgorithm::SigningKeyImport(*scheme))) + .find_map(|scheme| crypto.import_signing_key(scheme, &private_key_der).ok()) .ok_or_else(|| Error::InvalidPEM("can't decode PKCS#8 signing key".into()))?; Ok(Certificate::from_signing_key(rustls_certs, signing_key)) @@ -237,7 +226,7 @@ impl std::fmt::Debug for CryptoPrivateKey { impl CryptoPrivateKey { /// Imports an rcgen key pair into an explicit provider. #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] - pub fn from_key_pair(key_pair: &KeyPair, provider: Arc) -> Result { + pub fn from_key_pair(key_pair: &KeyPair, crypto: &dyn RTCCrypto) -> Result { let serialized_der = key_pair.serialize_der(); let scheme = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { CryptoSignatureScheme::Ed25519 @@ -248,8 +237,7 @@ impl CryptoPrivateKey { } else { return Err(Error::Other("Unsupported key_pair".to_owned())); }; - let signing_key = provider - .crypto() + let signing_key = crypto .import_signing_key(scheme, &serialized_der) .map_err(crypto_error)?; Ok(Self { signing_key }) @@ -472,10 +460,10 @@ mod test { fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { let provider = crypto::default_provider().map_err(crypto_error)?; let cert = - Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()], provider.clone())?; + Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()], provider.crypto())?; let pem = cert.serialize_pem()?; - let loaded_cert = Certificate::from_pem(&pem, provider)?; + let loaded_cert = Certificate::from_pem(&pem, provider.crypto())?; assert_eq!(loaded_cert, cert); diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index 142999e0..5e066c61 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -317,7 +317,7 @@ mod tests { } struct FailingRandomProvider { - crypto: Arc, + provider: Arc, } impl RTCCryptoProvider for FailingRandomProvider { @@ -326,7 +326,7 @@ mod tests { } fn crypto(&self) -> &dyn RTCCrypto { - self.crypto.crypto() + self.provider.crypto() } fn random(&self) -> &dyn RTCRandom { @@ -357,7 +357,7 @@ mod tests { } else if !is_client { builder = builder.with_certificates(vec![Certificate::generate_self_signed( vec!["localhost".to_owned()], - provider, + provider.crypto(), )?]); } Ok(Arc::new(builder.build(is_client, None)?)) @@ -479,7 +479,8 @@ mod tests { #[test] fn failing_random_provider_aborts_client_hello_cleanly() -> Result<()> { let base = crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; - let provider: Arc = Arc::new(FailingRandomProvider { crypto: base }); + let provider: Arc = + Arc::new(FailingRandomProvider { provider: base }); let config = config( provider, true, diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index f5b481b0..409a79ef 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -282,11 +282,6 @@ impl State { self.cipher_suite.as_deref() } - /// Returns the provider selected for this DTLS session. - pub fn crypto_provider(&self) -> Arc { - self.crypto_provider.clone() - } - /// Exports `length` bytes of keying material from an established session, as defined in /// RFC 5705. /// diff --git a/rtc-ice/src/agent/agent_selector.rs b/rtc-ice/src/agent/agent_selector.rs index 34bfe8e2..06270129 100644 --- a/rtc-ice/src/agent/agent_selector.rs +++ b/rtc-ice/src/agent/agent_selector.rs @@ -115,7 +115,7 @@ impl Agent { Box::new(PriorityAttr(pair.local_priority)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -283,7 +283,7 @@ impl ControllingSelector for Agent { Box::new(PriorityAttr(self.local_candidates[local_index].priority())), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -445,7 +445,7 @@ impl ControlledSelector for Agent { Box::new(PriorityAttr(self.local_candidates[local_index].priority())), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); diff --git a/rtc-ice/src/agent/agent_test.rs b/rtc-ice/src/agent/agent_test.rs index 49183252..5768bc50 100644 --- a/rtc-ice/src/agent/agent_test.rs +++ b/rtc-ice/src/agent/agent_test.rs @@ -270,7 +270,7 @@ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { Box::new(PriorityAttr(local_priority)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -354,7 +354,7 @@ fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { Box::new(tid), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -575,7 +575,7 @@ fn build_msg(c: MessageClass, username: String, key: String) -> Result Box::new(MessageType::new(METHOD_BINDING, c)), Box::new(TransactionId::new()), Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity_with_provider(key, test_crypto_provider())), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(key, test_crypto_provider().crypto())), Box::new(FINGERPRINT), ])?; Ok(msg) @@ -2083,7 +2083,7 @@ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<( Box::new(PriorityAttr(1000)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -2204,7 +2204,7 @@ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> { Box::new(PriorityAttr(1000)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -2300,7 +2300,7 @@ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> Box::new(PriorityAttr(1000)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -2402,7 +2402,7 @@ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> { Box::new(PriorityAttr(1000)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; @@ -2826,7 +2826,7 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> Box::new(PriorityAttr(local_priority)), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - test_crypto_provider(), + test_crypto_provider().crypto(), )), Box::new(FINGERPRINT), ])?; diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index 9ebabcbb..a1d17086 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -21,7 +21,7 @@ pub mod agent_stats; use agent_config::*; use bytes::BytesMut; -use crypto::RTCCryptoProvider; +use crypto::{RTCCrypto, RTCCryptoProvider}; use log::{debug, error, info, trace, warn}; use mdns::{Mdns, QueryId}; use sansio::Protocol; @@ -102,11 +102,9 @@ fn assert_inbound_username(m: &Message, expected_username: &str) -> Result<()> { fn assert_inbound_message_integrity( m: &mut Message, key: &[u8], - provider: Arc, + crypto: &dyn RTCCrypto, ) -> Result<()> { - let message_integrity_attr = - MessageIntegrity::new_raw_integrity_with_provider(key.to_vec(), provider); - message_integrity_attr.check(m) + MessageIntegrity::check(m, key, crypto) } /// What the agent reports to its caller. @@ -1003,7 +1001,7 @@ impl Agent { Box::new(XorMappedAddress { ip, port }), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -1047,7 +1045,7 @@ impl Agent { Box::new(CODE_ROLE_CONFLICT), Box::new(MessageIntegrity::new_short_term_integrity_with_provider( local_pwd, - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -1267,7 +1265,7 @@ impl Agent { if let Err(err) = assert_inbound_message_integrity( m, remote_credentials.pwd.as_bytes(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), ) { warn!( "[{}]: discard message from ({}), {}", @@ -1304,7 +1302,7 @@ impl Agent { } else if let Err(err) = assert_inbound_message_integrity( m, self.ufrag_pwd.local_credentials.pwd.as_bytes(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), ) { warn!( "[{}]: discard message from ({}), {}", diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index 9969559a..514aea2b 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -122,8 +122,9 @@ //! ``` //! use rtc_interceptor::{BoxedInterceptor, NackGeneratorBuilder, Registry, SenderReportBuilder}; //! +//! # let nack_enabled = true; // e.g. from configuration, negotiated SDP, … //! // Two different chain types, unified by `.boxed()`. -//! let chain: BoxedInterceptor = if cfg!(feature = "unstable") { +//! let chain: BoxedInterceptor = if nack_enabled { //! Registry::new() //! .with(SenderReportBuilder::new().build()) //! .with(NackGeneratorBuilder::new().build()) diff --git a/rtc-srtp/benches/bench.rs b/rtc-srtp/benches/bench.rs index 1840d1bf..9207a4f8 100644 --- a/rtc-srtp/benches/bench.rs +++ b/rtc-srtp/benches/bench.rs @@ -26,7 +26,7 @@ fn benchmark_encrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap(); @@ -70,7 +70,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap(); @@ -80,7 +80,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap(); @@ -122,7 +122,7 @@ fn benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap(); @@ -140,7 +140,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap() .encrypt_rtcp(RAW_RTCP) @@ -152,7 +152,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap(); @@ -203,7 +203,7 @@ fn benchmark_context_setup(g: &mut BenchmarkGroup) { profile, None, None, - std::sync::Arc::clone(&provider), + provider.crypto(), ) .unwrap() }); @@ -217,24 +217,10 @@ fn benchmark_aead_aes_128_gcm(g: &mut BenchmarkGroup) { for (name, provider) in providers() { let profile = ProtectionProfile::AeadAes128Gcm; let salt = master_salt_for(profile); - let mut encrypt_ctx = Context::new( - MASTER_KEY, - &salt, - profile, - None, - None, - std::sync::Arc::clone(&provider), - ) - .unwrap(); - let mut decrypt_ctx = Context::new( - MASTER_KEY, - &salt, - profile, - None, - None, - std::sync::Arc::clone(&provider), - ) - .unwrap(); + let mut encrypt_ctx = + Context::new(MASTER_KEY, &salt, profile, None, None, provider.crypto()).unwrap(); + let mut decrypt_ctx = + Context::new(MASTER_KEY, &salt, profile, None, None, provider.crypto()).unwrap(); let mut pld = BytesMut::new(); for i in 0..1200 { diff --git a/rtc-srtp/examples/srtp_micro.rs b/rtc-srtp/examples/srtp_micro.rs index 79d87e21..a56be269 100644 --- a/rtc-srtp/examples/srtp_micro.rs +++ b/rtc-srtp/examples/srtp_micro.rs @@ -33,7 +33,7 @@ fn new_ctx() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap() } @@ -48,7 +48,7 @@ fn new_gcm_ctx() -> Context { ProtectionProfile::AeadAes128Gcm, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap() } @@ -62,7 +62,7 @@ fn new_ctx_replay() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, Some(srtp_replay_protection(128)), None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .unwrap() } diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs index 050afb9d..96db595c 100644 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs +++ b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs @@ -1,12 +1,10 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, BytesMut}; use crypto::{ - HmacAlgorithm, Mac, RTCCryptoProvider, SecretVec, StreamCipher, StreamCipherAlgorithm, - constant_time_eq, + HmacAlgorithm, Mac, RTCCrypto, SecretVec, StreamCipher, StreamCipherAlgorithm, constant_time_eq, }; use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; use shared::marshal::*; -use std::sync::Arc; use super::{Cipher, Kdf, crypto_error}; use crate::key_derivation::*; @@ -23,7 +21,6 @@ pub(crate) struct CipherAesCmHmacSha1 { srtp_session_auth: Box, srtcp_session_salt: Vec, srtcp_session_auth: Box, - provider: Arc, srtp_cipher: Box, srtcp_cipher: Box, } @@ -33,7 +30,7 @@ impl CipherAesCmHmacSha1 { profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8], - provider: Arc, + crypto: &dyn RTCCrypto, ) -> Result { let (kdf, algorithm): (Kdf, StreamCipherAlgorithm) = match profile { ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 => { @@ -49,7 +46,7 @@ impl CipherAesCmHmacSha1 { } }; let srtp_session_key = SecretVec::new(kdf( - provider.crypto(), + crypto, LABEL_SRTP_ENCRYPTION, master_key, master_salt, @@ -57,7 +54,7 @@ impl CipherAesCmHmacSha1 { master_key.len(), )?); let srtcp_session_key = SecretVec::new(kdf( - provider.crypto(), + crypto, LABEL_SRTCP_ENCRYPTION, master_key, master_salt, @@ -65,16 +62,14 @@ impl CipherAesCmHmacSha1 { master_key.len(), )?); - let srtp_cipher = provider - .crypto() + let srtp_cipher = crypto .new_stream_cipher(algorithm, srtp_session_key.as_ref()) .map_err(crypto_error)?; - let srtcp_cipher = provider - .crypto() + let srtcp_cipher = crypto .new_stream_cipher(algorithm, srtcp_session_key.as_ref()) .map_err(crypto_error)?; let srtp_session_salt = kdf( - provider.crypto(), + crypto, LABEL_SRTP_SALT, master_key, master_salt, @@ -82,7 +77,7 @@ impl CipherAesCmHmacSha1 { master_salt.len(), )?; let srtcp_session_salt = kdf( - provider.crypto(), + crypto, LABEL_SRTCP_SALT, master_key, master_salt, @@ -91,7 +86,7 @@ impl CipherAesCmHmacSha1 { )?; let auth_key_len = profile.auth_key_len(); let srtp_session_auth = SecretVec::new(kdf( - provider.crypto(), + crypto, LABEL_SRTP_AUTHENTICATION_TAG, master_key, master_salt, @@ -99,7 +94,7 @@ impl CipherAesCmHmacSha1 { auth_key_len, )?); let srtcp_session_auth = SecretVec::new(kdf( - provider.crypto(), + crypto, LABEL_SRTCP_AUTHENTICATION_TAG, master_key, master_salt, @@ -109,12 +104,10 @@ impl CipherAesCmHmacSha1 { // Key the MACs once per context. Everything above is per-context setup; the auth tag on // each packet then costs only the message pass. - let srtp_session_auth = provider - .crypto() + let srtp_session_auth = crypto .new_hmac(HmacAlgorithm::Sha1, srtp_session_auth.as_ref()) .map_err(crypto_error)?; - let srtcp_session_auth = provider - .crypto() + let srtcp_session_auth = crypto .new_hmac(HmacAlgorithm::Sha1, srtcp_session_auth.as_ref()) .map_err(crypto_error)?; @@ -124,7 +117,6 @@ impl CipherAesCmHmacSha1 { srtp_session_auth, srtcp_session_salt, srtcp_session_auth, - provider, srtp_cipher, srtcp_cipher, }) diff --git a/rtc-srtp/src/context/context_test.rs b/rtc-srtp/src/context/context_test.rs index 53796152..481d1f69 100644 --- a/rtc-srtp/src/context/context_test.rs +++ b/rtc-srtp/src/context/context_test.rs @@ -24,7 +24,7 @@ fn test_context_roc() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let roc = c.get_roc(123); @@ -52,7 +52,7 @@ fn test_context_index() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let index = c.get_index(123); @@ -80,7 +80,7 @@ fn test_key_len() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ); assert!(result.is_err(), "CreateContext accepted a 0 length key"); @@ -90,7 +90,7 @@ fn test_key_len() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ); assert!(result.is_err(), "CreateContext accepted a 0 length salt"); @@ -100,7 +100,7 @@ fn test_key_len() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ); assert!( result.is_ok(), @@ -366,7 +366,7 @@ fn test_encrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -388,7 +388,7 @@ fn test_decrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -407,7 +407,7 @@ fn test_encrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -429,7 +429,7 @@ fn test_decrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -448,7 +448,7 @@ fn test_encrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -470,7 +470,7 @@ fn test_decrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -489,7 +489,7 @@ fn test_encrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -511,7 +511,7 @@ fn test_decrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); diff --git a/rtc-srtp/src/context/mod.rs b/rtc-srtp/src/context/mod.rs index 029ad33c..d1df6acf 100644 --- a/rtc-srtp/src/context/mod.rs +++ b/rtc-srtp/src/context/mod.rs @@ -6,9 +6,8 @@ mod srtcp_test; mod srtp_test; use std::collections::HashMap; -use std::sync::Arc; -use crypto::RTCCryptoProvider; +use crypto::RTCCrypto; use shared::replay_detector::*; use crate::cipher::cipher_aead_aes_gcm::*; @@ -107,14 +106,16 @@ pub struct Context { impl Context { /// Creates an SRTP context. /// - /// The crypto provider is supplied by the caller; this crate never resolves a default. + /// The crypto implementation is supplied by the caller; this crate never resolves a default. + /// Borrowed, not owned: it is only needed to build the keyed cipher/MAC objects below, which + /// are what the context retains. pub fn new( master_key: &[u8], master_salt: &[u8], profile: ProtectionProfile, srtp_ctx_opt: Option, srtcp_ctx_opt: Option, - provider: Arc, + crypto: &dyn RTCCrypto, ) -> Result { let key_len = profile.key_len(); let salt_len = profile.salt_len(); @@ -124,7 +125,7 @@ impl Context { } else if master_salt.len() != salt_len { return Err(Error::SrtpSaltLength(salt_len, master_salt.len())); } - profile.ensure_crypto_supported(provider.crypto())?; + profile.ensure_crypto_supported(crypto)?; let cipher: Box = match profile { ProtectionProfile::Aes128CmHmacSha1_32 @@ -134,7 +135,7 @@ impl Context { profile, master_key, master_salt, - provider, + crypto, )?), ProtectionProfile::AeadAes128Gcm | ProtectionProfile::AeadAes256Gcm => { @@ -144,7 +145,7 @@ impl Context { profile, master_key, master_salt, - provider.crypto(), + crypto, )?) } }; diff --git a/rtc-srtp/src/context/srtcp_test.rs b/rtc-srtp/src/context/srtcp_test.rs index 58d25441..cee31d3a 100644 --- a/rtc-srtp/src/context/srtcp_test.rs +++ b/rtc-srtp/src/context/srtcp_test.rs @@ -107,7 +107,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let mut decrypt_context = Context::new( &RTCP_TEST_MASTER_KEY, @@ -115,7 +115,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; for test_case in &*RTCP_TEST_CASES { @@ -146,7 +146,7 @@ fn test_rtcp_invalid_auth_tag() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let decrypt_result = decrypt_context.decrypt_rtcp(&RTCP_TEST_CASES[0].encrypted)?; @@ -178,7 +178,7 @@ fn test_rtcp_replay_detector_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let rtcp_packet1 = RTCP_TEST_CASES[0].encrypted.clone(); @@ -225,7 +225,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.rtcp_auth_tag_len(); @@ -236,7 +236,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), - test_crypto_provider(), + test_crypto_provider().crypto(), )?; let inputs = vec![ @@ -295,7 +295,7 @@ fn test_rtcp_short_packet_errors() -> Result<()> { profile, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; // Slices of a real packet (its first 4 bytes are a valid RTCP header, so diff --git a/rtc-srtp/src/context/srtp_test.rs b/rtc-srtp/src/context/srtp_test.rs index 24117f4d..18fb130f 100644 --- a/rtc-srtp/src/context/srtp_test.rs +++ b/rtc-srtp/src/context/srtp_test.rs @@ -78,7 +78,7 @@ fn build_test_context() -> Result { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), ) } @@ -99,7 +99,7 @@ fn test_rtp_invalid_auth() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, - test_crypto_provider(), + test_crypto_provider().crypto(), )?; for test_case in &*RTP_TEST_CASES { diff --git a/rtc-srtp/tests/provider_profiles.rs b/rtc-srtp/tests/provider_profiles.rs index 6a66cf46..1e7921d0 100644 --- a/rtc-srtp/tests/provider_profiles.rs +++ b/rtc-srtp/tests/provider_profiles.rs @@ -76,19 +76,13 @@ impl RTCCryptoProvider for IncompleteProvider { fn explicit_incomplete_provider_returns_actionable_capability_error() { let profile = ProtectionProfile::Aes128CmHmacSha1_80; let (key, salt) = key_material(profile); - let error = Context::new( - &key, - &salt, - profile, - None, - None, - Arc::new(IncompleteProvider { - crypto: IncompleteCrypto, - random: IncompleteRandom, - }), - ) - .err() - .expect("an incomplete provider must be rejected"); + let provider = IncompleteProvider { + crypto: IncompleteCrypto, + random: IncompleteRandom, + }; + let error = Context::new(&key, &salt, profile, None, None, provider.crypto()) + .err() + .expect("an incomplete provider must be rejected"); let message = error.to_string(); assert!(message.contains("Aes128CmHmacSha1_80")); assert!(message.contains("BlockCipher(Aes128)")); @@ -115,7 +109,7 @@ fn context( profile, replay.then(|| srtp_replay_protection(64)), replay.then(|| srtcp_replay_protection(64)), - provider, + provider.crypto(), ) } diff --git a/rtc-stun/benches/bench.rs b/rtc-stun/benches/bench.rs index 2252ac78..1804f3c4 100644 --- a/rtc-stun/benches/bench.rs +++ b/rtc-stun/benches/bench.rs @@ -238,11 +238,12 @@ fn benchmark_message_build_overhead(c: &mut Criterion) { } fn benchmark_message_integrity(c: &mut Criterion) { + let provider = bench_provider(); { let mut m = Message::new(); let integrity = MessageIntegrity::new_short_term_integrity_with_provider( "password".to_owned(), - bench_provider(), + provider.crypto(), ); m.write_header(); c.bench_function("BenchmarkMessageIntegrity_AddTo", |b| { @@ -261,20 +262,22 @@ fn benchmark_message_integrity(c: &mut Criterion) { let _ = software.add_to(&mut m); let integrity = MessageIntegrity::new_short_term_integrity_with_provider( "password".to_owned(), - bench_provider(), + provider.crypto(), ); m.write_header(); integrity.add_to(&mut m).unwrap(); m.write_header(); c.bench_function("BenchmarkMessageIntegrity_Check", |b| { b.iter(|| { - integrity.check(&mut m).unwrap(); + MessageIntegrity::<'_>::check(&mut m, "password".as_bytes(), provider.crypto()) + .unwrap(); }) }); } } fn benchmark_message(c: &mut Criterion) { + let provider = bench_provider(); { let mut m = Message::new(); c.bench_function("BenchmarkMessage_Write", |b| { @@ -466,7 +469,7 @@ fn benchmark_message(c: &mut Criterion) { "username".to_owned(), "realm".to_owned(), "password".to_owned(), - bench_provider(), + provider.crypto(), ) .unwrap(), ), diff --git a/rtc-stun/src/integrity.rs b/rtc-stun/src/integrity.rs index 734003cf..4006d6f9 100644 --- a/rtc-stun/src/integrity.rs +++ b/rtc-stun/src/integrity.rs @@ -3,11 +3,9 @@ mod integrity_test; use crate::attributes::*; use crate::message::*; -use crypto::{CryptoError, HashAlgorithm, HmacAlgorithm, RTCCryptoProvider, SecretVec}; +use crypto::{CryptoError, HashAlgorithm, HmacAlgorithm, RTCCrypto, SecretVec}; use shared::error::*; - use std::fmt; -use std::sync::Arc; // separator for credentials. pub(crate) const CREDENTIALS_SEP: &str = ":"; @@ -22,16 +20,16 @@ pub(crate) const CREDENTIALS_SEP: &str = ":"; /// The `MESSAGE-INTEGRITY` key: an HMAC-SHA1 is computed over the message with it. /// /// Built from a short-term password, or from a long-term username/realm/password triple. -pub struct MessageIntegrity { +pub struct MessageIntegrity<'a> { key: SecretVec, - provider: Arc, + crypto: &'a dyn RTCCrypto, } fn crypto_error(error: CryptoError) -> Error { Error::Crypto(error.to_string()) } -impl fmt::Display for MessageIntegrity { +impl<'a> fmt::Display for MessageIntegrity<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -41,7 +39,7 @@ impl fmt::Display for MessageIntegrity { } } -impl Setter for MessageIntegrity { +impl<'a> Setter for MessageIntegrity<'a> { // add_to adds MESSAGE-INTEGRITY attribute to message. // // CPU costly, see BenchmarkMessageIntegrity_AddTo. @@ -64,8 +62,7 @@ impl Setter for MessageIntegrity { // A STUN message is authenticated once, so the MAC is keyed here rather than held. On a // per-packet path the keyed object belongs in the surrounding state instead. let result = self - .provider - .crypto() + .crypto .new_hmac(HmacAlgorithm::Sha1, self.key.as_ref()) .and_then(|mut mac| mac.sign(&[&m.raw], &mut value)); m.length = length; // changing m.Length back @@ -80,16 +77,16 @@ impl Setter for MessageIntegrity { pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; -impl MessageIntegrity { +impl<'a> MessageIntegrity<'a> { /// Creates a raw-key integrity attribute with an explicit crypto provider. #[must_use] pub fn new_raw_integrity_with_provider( key: impl Into>, - provider: Arc, + crypto: &'a dyn RTCCrypto, ) -> Self { Self { key: SecretVec::new(key.into()), - provider, + crypto, } } @@ -97,9 +94,9 @@ impl MessageIntegrity { #[must_use] pub fn new_short_term_integrity_with_provider( password: String, - provider: Arc, + crypto: &'a dyn RTCCrypto, ) -> Self { - Self::new_raw_integrity_with_provider(password.into_bytes(), provider) + Self::new_raw_integrity_with_provider(password.into_bytes(), crypto) } /// Creates a long-term integrity attribute with an explicit crypto provider. @@ -107,11 +104,21 @@ impl MessageIntegrity { username: String, realm: String, password: String, - provider: Arc, + crypto: &'a dyn RTCCrypto, ) -> Result { + let key = MessageIntegrity::long_term_integrity_key(username, realm, password, crypto)?; + Ok(Self::new_raw_integrity_with_provider(key, crypto)) + } + + /// Creates a long-term integrity key with an explicit crypto provider. + pub fn long_term_integrity_key( + username: String, + realm: String, + password: String, + crypto: &'a dyn RTCCrypto, + ) -> Result> { let credentials = [username, realm, password].join(CREDENTIALS_SEP); - let key = provider - .crypto() + let key = crypto .hash(HashAlgorithm::Md5, credentials.as_bytes()) .map_err(crypto_error)?; if key.len() != 16 { @@ -120,13 +127,13 @@ impl MessageIntegrity { key.len() ))); } - Ok(Self::new_raw_integrity_with_provider(key, provider)) + Ok(key) } /// Check checks MESSAGE-INTEGRITY attribute. /// /// CPU costly, see BenchmarkMessageIntegrity_Check. - pub fn check(&self, m: &mut Message) -> Result<()> { + pub fn check(m: &mut Message, key: &[u8], crypto: &dyn RTCCrypto) -> Result<()> { let v = m.get(ATTR_MESSAGE_INTEGRITY)?; // Adjusting length in header to match m.Raw that was @@ -151,10 +158,8 @@ impl MessageIntegrity { let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); let b = &m.raw[..start_of_hmac]; // data before integrity attribute - let result = self - .provider - .crypto() - .new_hmac(HmacAlgorithm::Sha1, self.key.as_ref()) + let result = crypto + .new_hmac(HmacAlgorithm::Sha1, key) .and_then(|mut mac| mac.verify(&[b], &v)); m.length = length as u32; m.write_length(); // writing length back diff --git a/rtc-stun/src/integrity/integrity_test.rs b/rtc-stun/src/integrity/integrity_test.rs index 19745bce..1e8833a7 100644 --- a/rtc-stun/src/integrity/integrity_test.rs +++ b/rtc-stun/src/integrity/integrity_test.rs @@ -145,16 +145,17 @@ fn builtin_provider() -> Arc { #[test] fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<()> { + let provider = test_provider(); let integrity = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "password".to_owned(), - test_provider(), + provider.crypto(), )?; let mut message = Message::new(); message.write_header(); integrity.add_to(&mut message)?; - integrity.check(&mut message)?; + MessageIntegrity::check(&mut message, integrity.key.as_ref(), provider.crypto())?; let attribute = message .attributes @@ -165,7 +166,7 @@ fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<( attribute.value.pop(); attribute.length -= 1; assert_eq!( - integrity.check(&mut message), + MessageIntegrity::check(&mut message, integrity.key.as_ref(), provider.crypto()), Err(Error::ErrIntegrityMismatch) ); @@ -175,11 +176,12 @@ fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<( #[test] fn test_message_integrity_add_to_simple() -> Result<()> { { + let provider = builtin_provider(); let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "passsss".to_owned(), - builtin_provider(), + provider.crypto(), )?; let expected = vec![ 104, 228, 91, 113, 61, 154, 222, 34, 101, 61, 181, 146, 177, 90, 4, 29, @@ -187,11 +189,12 @@ fn test_message_integrity_add_to_simple() -> Result<()> { assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); } + let provider = builtin_provider(); let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "pass".to_owned(), - builtin_provider(), + provider.crypto(), )?; let expected = vec![ 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, @@ -214,11 +217,11 @@ fn test_message_integrity_add_to_simple() -> Result<()> { let mut d_m = Message::new(); d_m.raw = m.raw.clone(); d_m.decode()?; - i.check(&mut d_m)?; + MessageIntegrity::check(&mut d_m, i.key.as_ref(), provider.crypto())?; d_m.raw[24] += 12; // HMAC now invalid d_m.decode()?; - let result = i.check(&mut d_m); + let result = MessageIntegrity::check(&mut d_m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "should be invalid"); } @@ -236,24 +239,25 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { }; a.add_to(&mut m)?; + let provider = builtin_provider(); let i = MessageIntegrity::new_short_term_integrity_with_provider( "pwd".to_owned(), - builtin_provider(), + provider.crypto(), ); assert_eq!( i.to_string(), "MESSAGE-INTEGRITY key: [REDACTED; 3 bytes]", "bad string {i}" ); - let result = i.check(&mut m); + let result = MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "should error"); i.add_to(&mut m)?; FINGERPRINT.add_to(&mut m)?; - i.check(&mut m)?; + MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto())?; m.raw[24] = 33; m.decode()?; - let result = i.check(&mut m); + let result = MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "mismatch expected"); Ok(()) @@ -262,9 +266,10 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { #[test] fn test_message_integrity() -> Result<()> { let mut m = Message::new(); + let provider = builtin_provider(); let i = MessageIntegrity::new_short_term_integrity_with_provider( "password".to_owned(), - builtin_provider(), + provider.crypto(), ); m.write_header(); i.add_to(&mut m)?; @@ -277,9 +282,10 @@ fn test_message_integrity_before_fingerprint() -> Result<()> { let mut m = Message::new(); m.write_header(); FINGERPRINT.add_to(&mut m)?; + let provider = builtin_provider(); let i = MessageIntegrity::new_short_term_integrity_with_provider( "password".to_owned(), - builtin_provider(), + provider.crypto(), ); let result = i.add_to(&mut m); assert!(result.is_err(), "should error"); diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index 16a3b80b..02d1efc1 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -493,7 +493,7 @@ impl Message { /// # Ok(()) /// # } /// ``` - pub fn build(&mut self, setters: &[Box]) -> Result<()> { + pub fn build(&mut self, setters: &[Box]) -> Result<()> { self.reset(); self.write_header(); for s in setters { diff --git a/rtc-stun/src/message/message_test.rs b/rtc-stun/src/message/message_test.rs index ebb59872..000260df 100644 --- a/rtc-stun/src/message/message_test.rs +++ b/rtc-stun/src/message/message_test.rs @@ -627,7 +627,9 @@ fn test_message_full_size() -> Result<()> { "username".to_owned(), "realm".to_owned(), "password".to_owned(), - crypto::default_provider().expect("a built-in provider is enabled for tests"), + crypto::default_provider() + .expect("a built-in provider is enabled for tests") + .crypto(), )?), Box::new(FINGERPRINT), ])?; @@ -657,7 +659,9 @@ fn test_message_clone_to() -> Result<()> { "username".to_owned(), "realm".to_owned(), "password".to_owned(), - crypto::default_provider().expect("a built-in provider is enabled for tests"), + crypto::default_provider() + .expect("a built-in provider is enabled for tests") + .crypto(), )?), Box::new(FINGERPRINT), ])?; diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 76d2bdad..dd3fa883 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -144,7 +144,6 @@ pub struct Client { username: Username, password: String, realm: Realm, - integrity: MessageIntegrity, software: Software, tr_map: TransactionMap, binding_mgr: BindingManager, @@ -195,11 +194,6 @@ impl Client { } else { DEFAULT_RTO_IN_MS }, - integrity: MessageIntegrity::new_short_term_integrity_with_provider( - String::new(), - crypto_provider, - ), - relays: HashMap::new(), transmits: VecDeque::new(), events: VecDeque::new(), @@ -406,7 +400,7 @@ impl Client { /// return key to find out corresponding Event either BindingResponse or BindingRequestTimeout pub fn send_binding_request_to(&mut self, to: SocketAddr) -> Result { let msg = { - let attrs: Vec> = if !self.software.text.is_empty() { + let attrs: Vec> = if !self.software.text.is_empty() { vec![ Box::new(TransactionId::new()), Box::new(BINDING_REQUEST), @@ -506,21 +500,20 @@ impl Client { /// [RFC 5766 §6.2]: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 pub fn update_credentials(&mut self, username: String, password: String) -> Result<()> { let username = Username::new(ATTR_USERNAME, username); - let integrity = MessageIntegrity::new_long_term_integrity_with_provider( + let long_term_integrity_key = MessageIntegrity::long_term_integrity_key( username.text.clone(), self.realm.text.clone(), password.clone(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )?; self.username = username; self.password = password; - self.integrity = integrity; // Each allocation carries the integrity it will sign its own Refresh / // CreatePermission / ChannelBind with, so they have to be re-signed too — otherwise // the next refresh would still present the retired credential. for relay in self.relays.values_mut() { - relay.integrity = self.integrity.clone(); + relay.long_term_integrity_key = long_term_integrity_key.clone(); } Ok(()) @@ -595,11 +588,11 @@ impl Client { } }; - self.integrity = MessageIntegrity::new_long_term_integrity_with_provider( + let integrity = MessageIntegrity::new_long_term_integrity_with_provider( self.username.text.clone(), self.realm.text.clone(), self.password.clone(), - self.crypto_provider.clone(), + self.crypto_provider.crypto(), )?; let mut msg = Message::new(); @@ -623,7 +616,7 @@ impl Client { Box::new(self.username.clone()), Box::new(self.realm.clone()), Box::new(nonce.clone()), - Box::new(self.integrity.clone()), + Box::new(integrity), Box::new(FINGERPRINT), ])?; @@ -658,7 +651,17 @@ impl Client { self.relays.insert( relayed_addr, - RelayState::new(relayed_addr, self.integrity.clone(), nonce, lifetime.0), + RelayState::new( + relayed_addr, + MessageIntegrity::long_term_integrity_key( + self.username.text.clone(), + self.realm.text.clone(), + self.password.clone(), + self.crypto_provider.crypto(), + )?, + nonce, + lifetime.0, + ), ); self.events.push_back(Event::AllocateResponse( response.transaction_id, diff --git a/rtc-turn/src/client/relay.rs b/rtc-turn/src/client/relay.rs index 638d8581..22064fa9 100644 --- a/rtc-turn/src/client/relay.rs +++ b/rtc-turn/src/client/relay.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::ops::Add; use std::time::{Duration, Instant}; - use stun::attributes::*; use stun::error_code::*; use stun::fingerprint::*; @@ -28,7 +27,7 @@ const MAX_RETRY_ATTEMPTS: u16 = 3; // RelayState is a set of params use by Relay pub(crate) struct RelayState { pub(crate) relayed_addr: RelayedAddr, - pub(crate) integrity: MessageIntegrity, + pub(crate) long_term_integrity_key: Vec, pub(crate) nonce: Nonce, pub(crate) lifetime: Duration, perm_map: HashMap, @@ -39,7 +38,7 @@ pub(crate) struct RelayState { impl RelayState { pub(super) fn new( relayed_addr: RelayedAddr, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, nonce: Nonce, lifetime: Duration, ) -> Self { @@ -47,7 +46,7 @@ impl RelayState { Self { relayed_addr, - integrity, + long_term_integrity_key, nonce, lifetime, perm_map: HashMap::new(), @@ -163,7 +162,7 @@ impl Relay<'_> { if perm.state() != PermState::Permitted { Err(Error::ErrNoPermission) } else { - Ok((relay.integrity.clone(), relay.nonce.clone())) + Ok((relay.long_term_integrity_key.clone(), relay.nonce.clone())) } } else { Err(Error::ErrNoPermission) @@ -172,16 +171,16 @@ impl Relay<'_> { Err(Error::ErrConnClosed) }; - let (integrity, nonce) = result?; + let (long_term_integrity_key, nonce) = result?; - self.send(p, peer_addr, integrity, nonce) + self.send(p, peer_addr, long_term_integrity_key, nonce) } fn send( &mut self, p: &[u8], peer_addr: SocketAddr, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, nonce: Nonce, ) -> Result<()> { let channel_number = { @@ -208,7 +207,13 @@ impl Relay<'_> { if let Some(b) = self.client.binding_mgr.get_by_addr(&bind_addr) { b.set_state(BindingState::Request); } - self.channel_bind(self.relayed_addr, bind_addr, bind_number, nonce, integrity)?; + self.channel_bind( + self.relayed_addr, + bind_addr, + bind_number, + nonce, + long_term_integrity_key, + )?; } // send data using SendIndication @@ -241,7 +246,13 @@ impl Relay<'_> { if let Some(b) = self.client.binding_mgr.get_by_addr(&bind_addr) { b.set_state(BindingState::Refresh); } - self.channel_bind(self.relayed_addr, bind_addr, bind_number, nonce, integrity)?; + self.channel_bind( + self.relayed_addr, + bind_addr, + bind_number, + nonce, + long_term_integrity_key, + )?; } bind_number @@ -270,7 +281,7 @@ impl Relay<'_> { let (username, realm) = (self.client.username(), self.client.realm()); if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { let msg = { - let mut setters: Vec> = vec![ + let mut setters: Vec> = vec![ Box::new(TransactionId::new()), Box::new(MessageType::new(METHOD_CREATE_PERMISSION, CLASS_REQUEST)), ]; @@ -285,7 +296,10 @@ impl Relay<'_> { setters.push(Box::new(username)); setters.push(Box::new(realm)); setters.push(Box::new(relay.nonce.clone())); - setters.push(Box::new(relay.integrity.clone())); + setters.push(Box::new(MessageIntegrity::new_raw_integrity_with_provider( + relay.long_term_integrity_key.clone(), + self.client.crypto_provider.crypto(), + ))); setters.push(Box::new(FINGERPRINT)); let mut msg = Message::new(); @@ -355,7 +369,10 @@ impl Relay<'_> { Box::new(username), Box::new(realm), Box::new(relay.nonce.clone()), - Box::new(relay.integrity.clone()), + Box::new(MessageIntegrity::new_raw_integrity_with_provider( + relay.long_term_integrity_key.clone(), + self.client.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -421,10 +438,10 @@ impl Relay<'_> { bind_addr: SocketAddr, bind_number: u16, nonce: Nonce, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, ) -> Result<()> { let (msg, turn_server_addr) = { - let setters: Vec> = vec![ + let setters: Vec> = vec![ Box::new(TransactionId::new()), Box::new(MessageType::new(METHOD_CHANNEL_BIND, CLASS_REQUEST)), Box::new(proto::peeraddr::PeerAddress { @@ -435,7 +452,12 @@ impl Relay<'_> { Box::new(self.client.username()), Box::new(self.client.realm()), Box::new(nonce), - Box::new(integrity), + // Built here rather than by the caller: it borrows `crypto_provider`, and that + // borrow must end with `setters` so the `&mut self` transaction below is free. + Box::new(MessageIntegrity::new_raw_integrity_with_provider( + long_term_integrity_key, + self.client.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ]; diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index f1f8efea..b9f582e1 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -35,9 +35,10 @@ //! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Generate ECDSA certificate (recommended) //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -62,15 +63,16 @@ //! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Ed25519 provides the best security with excellent performance //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::Ed25519, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; //! //! // Get fingerprint for SDP signaling -//! let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; +//! let fingerprints = certificate.get_fingerprints(provider.crypto())?; //! println!("Fingerprint: {}", fingerprints[0].value); //! # Ok(()) //! # } @@ -80,6 +82,7 @@ //! //! ```no_run //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! use rtc::peer_connection::certificate::RTCCertificate; //! use rtc::crypto::{self, SignatureScheme}; //! use rtc::peer_connection::certificate::CertificateParams; @@ -87,7 +90,7 @@ //! //! // First run: Generate and save certificate //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -96,7 +99,7 @@ //! //! // Later runs: Load existing certificate //! let pem_data = fs::read_to_string("my_cert.pem")?; -//! let certificate = RTCCertificate::from_pem(&pem_data, crypto::default_provider()?)?; +//! let certificate = RTCCertificate::from_pem(&pem_data, provider.crypto())?; //! // Same identity maintained across restarts! //! # Ok(()) //! # } @@ -110,14 +113,15 @@ //! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; //! //! // Get fingerprints for SDP offer/answer -//! let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; +//! let fingerprints = certificate.get_fingerprints(provider.crypto())?; //! for fp in fingerprints { //! // Format for SDP: a=fingerprint:sha-256 XX:XX:XX:... //! println!("a=fingerprint:{} {}", fp.algorithm, fp.value); @@ -135,10 +139,11 @@ //! use std::time::Instant; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // ECDSA P-256: Good balance of speed and security //! let start = Instant::now(); //! let _ecdsa_cert = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -147,7 +152,7 @@ //! // Ed25519: Fastest and most secure //! let start = Instant::now(); //! let _ed_cert = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::Ed25519, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -205,7 +210,7 @@ use std::ops::Add; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use crypto::{HashAlgorithm, PublicKeyEncoding, RTCCryptoProvider, SignatureScheme, SigningKey}; +use crypto::{HashAlgorithm, PublicKeyEncoding, RTCCrypto, SignatureScheme, SigningKey}; /// X.509 certificate parameters — subject alt names, validity window, distinguished name. /// /// Re-exported from `rcgen` because it appears in [`RTCCertificate::generate`]'s signature and @@ -244,15 +249,16 @@ use shared::error::{Error, Result}; /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate ECDSA P-256 key pair and certificate /// let certificate = RTCCertificate::generate( -/// crypto::default_provider()?, +/// provider.crypto(), /// SignatureScheme::EcdsaP256Sha256, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; /// /// // Certificate is ready to use -/// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; +/// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -265,15 +271,16 @@ use shared::error::{Error, Result}; /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate Ed25519 key pair and certificate /// let certificate = RTCCertificate::generate( -/// crypto::default_provider()?, +/// provider.crypto(), /// SignatureScheme::Ed25519, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; /// /// // Get fingerprints for SDP signaling -/// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; +/// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// for fp in fingerprints { /// println!("Fingerprint ({}):\n{}", fp.algorithm, fp.value); /// } @@ -289,8 +296,9 @@ use shared::error::{Error, Result}; /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; +/// # let provider = crypto::default_provider()?; /// # let certificate = RTCCertificate::generate( -/// # crypto::default_provider()?, +/// # provider.crypto(), /// # SignatureScheme::EcdsaP256Sha256, /// # params, /// # )?; @@ -301,7 +309,7 @@ use shared::error::{Error, Result}; /// // std::fs::write("cert.pem", &pem_string)?; /// /// // Later, load the certificate back -/// let loaded_cert = RTCCertificate::from_pem(&pem_string, crypto::default_provider()?)?; +/// let loaded_cert = RTCCertificate::from_pem(&pem_string, provider.crypto())?; /// assert_eq!(loaded_cert, certificate); /// # Ok(()) /// # } @@ -316,9 +324,10 @@ use shared::error::{Error, Result}; /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate certificate /// let certificate = RTCCertificate::generate( -/// crypto::default_provider()?, +/// provider.crypto(), /// SignatureScheme::EcdsaP256Sha256, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; @@ -360,27 +369,23 @@ impl RTCCertificate { /// `params` controls X.509 formatting and validity while `provider` owns key generation and /// signing. This keeps certificate formatting independent from the primitive backend. pub fn generate( - provider: Arc, + crypto: &dyn RTCCrypto, scheme: SignatureScheme, params: CertificateParams, ) -> Result { - let signing_key = provider - .crypto() - .generate_signing_key(scheme) - .map_err(crypto_error)?; + let signing_key = crypto.generate_signing_key(scheme).map_err(crypto_error)?; Self::generate_from_signing_key(params, scheme, signing_key) } /// Imports a PKCS#8 private key through `provider` and associates it with an existing chain. pub fn from_pkcs8( - provider: Arc, + crypto: &dyn RTCCrypto, scheme: SignatureScheme, certificate_chain: Vec>, private_key_der: &[u8], expires: SystemTime, ) -> Result { - let signing_key = provider - .crypto() + let signing_key = crypto .import_signing_key(scheme, private_key_der) .map_err(crypto_error)?; Ok(Self::from_signing_key( @@ -462,22 +467,23 @@ impl RTCCertificate { /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let provider = crypto::default_provider()?; /// # let original = RTCCertificate::generate( - /// # crypto::default_provider()?, + /// # provider.crypto(), /// # SignatureScheme::EcdsaP256Sha256, /// # params, /// # )?; /// // Load certificate from PEM string /// # let pem_str = original.serialize_pem()?; - /// let certificate = RTCCertificate::from_pem(&pem_str, crypto::default_provider()?)?; + /// let certificate = RTCCertificate::from_pem(&pem_str, provider.crypto())?; /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Loaded certificate with {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } /// ``` - pub fn from_pem(pem_str: &str, provider: Arc) -> Result { + pub fn from_pem(pem_str: &str, crypto: &dyn RTCCrypto) -> Result { let mut pem_blocks = pem_str.split("\n\n"); let first_block = if let Some(b) = pem_blocks.next() { b @@ -503,7 +509,7 @@ impl RTCCertificate { }; let dtls_certificate = dtls::crypto::Certificate::from_pem( &pem_blocks.collect::>().join("\n\n"), - provider, + crypto, )?; Ok(RTCCertificate::from_existing(dtls_certificate, expires)) } @@ -529,16 +535,19 @@ impl RTCCertificate { /// /// ```no_run /// # use rtc::peer_connection::certificate::RTCCertificate; + /// # use rtc::crypto; + /// # use rtc::dtls; /// # use std::time::{SystemTime, Duration}; /// # fn example( /// # dtls_cert: dtls::crypto::Certificate /// # ) -> Result<(), Box> { + /// # let provider = crypto::default_provider()?; /// // Use an externally managed certificate /// let expires = SystemTime::now() + Duration::from_secs(86400 * 30); // 30 days /// let certificate = RTCCertificate::from_existing(dtls_cert, expires); /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -576,8 +585,9 @@ impl RTCCertificate { /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let provider = crypto::default_provider()?; /// # let certificate = RTCCertificate::generate( - /// # crypto::default_provider()?, + /// # provider.crypto(), /// # SignatureScheme::EcdsaP256Sha256, /// # params, /// # )?; @@ -588,7 +598,7 @@ impl RTCCertificate { /// // std::fs::write("private/cert.pem", &pem_string)?; /// /// // Later, reload it - /// let reloaded = RTCCertificate::from_pem(&pem_string, crypto::default_provider()?)?; + /// let reloaded = RTCCertificate::from_pem(&pem_string, provider.crypto())?; /// assert_eq!(certificate, reloaded); /// # Ok(()) /// # } @@ -641,29 +651,26 @@ impl RTCCertificate { /// # use rtc::crypto::{self, SignatureScheme}; /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { + /// let provider = crypto::default_provider()?; /// let certificate = RTCCertificate::generate( - /// crypto::default_provider()?, + /// provider.crypto(), /// SignatureScheme::EcdsaP256Sha256, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; /// /// // Get fingerprints for SDP - /// let fingerprints = certificate.get_fingerprints(crypto::default_provider()?)?; + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// for fp in fingerprints { /// println!("a=fingerprint:{} {}", fp.algorithm, fp.value); /// } /// # Ok(()) /// # } /// ``` - pub fn get_fingerprints( - &self, - provider: Arc, - ) -> Result> { + pub fn get_fingerprints(&self, crypto: &dyn RTCCrypto) -> Result> { let mut fingerprints = Vec::new(); for c in &self.dtls_certificate.certificate { - let hashed = provider - .crypto() + let hashed = crypto .hash(HashAlgorithm::Sha256, c.as_ref()) .map_err(crypto_error)?; let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); @@ -772,6 +779,7 @@ impl rcgen::SigningKey for RcgenSigningKey { #[cfg(all(test, any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] mod test { use super::*; + use crypto::RTCCryptoProvider; struct NonExportableSigningKey(Arc); @@ -797,9 +805,9 @@ mod test { crypto::default_provider().map_err(crypto_error) } - fn provider_certificate(provider: Arc) -> Result { + fn provider_certificate(crypto: &dyn RTCCrypto) -> Result { RTCCertificate::generate( - provider, + crypto, SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -825,7 +833,7 @@ mod test { } let _certificate = RTCCertificate::generate( - provider, + provider.crypto(), SignatureScheme::RsaPkcs1Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -837,7 +845,7 @@ mod test { #[test] fn test_generate_certificate_ecdsa() -> Result<()> { let _cert = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -849,7 +857,7 @@ mod test { #[test] fn test_generate_certificate_eddsa() -> Result<()> { let _cert = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::Ed25519, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -861,14 +869,14 @@ mod test { #[test] fn test_certificate_equal() -> Result<()> { let cert1 = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, )?; let cert2 = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -882,7 +890,7 @@ mod test { #[test] fn test_generate_certificate_expires() -> Result<()> { let cert = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, @@ -897,14 +905,14 @@ mod test { #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { let cert = RTCCertificate::generate( - default_test_provider()?, + default_test_provider()?.crypto(), SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, )?; let pem = cert.serialize_pem()?; - let loaded_cert = RTCCertificate::from_pem(&pem, default_test_provider()?)?; + let loaded_cert = RTCCertificate::from_pem(&pem, default_test_provider()?.crypto())?; assert_eq!(loaded_cert, cert); @@ -914,23 +922,25 @@ mod test { #[cfg(feature = "crypto-ring")] #[test] fn ring_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { - provider_certificate_round_trip(Arc::new(crypto::providers::RingProvider::new())) + provider_certificate_round_trip(Arc::new(crypto::providers::RingProvider::new()).crypto()) } #[cfg(feature = "crypto-aws-lc-rs")] #[test] fn aws_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { - provider_certificate_round_trip(Arc::new(crypto::providers::AwsLcRsProvider::new())) + provider_certificate_round_trip( + Arc::new(crypto::providers::AwsLcRsProvider::new()).crypto(), + ) } - fn provider_certificate_round_trip(provider: Arc) -> Result<()> { - let certificate = provider_certificate(provider.clone())?; - let fingerprints = certificate.get_fingerprints(provider.clone())?; + fn provider_certificate_round_trip(crypto: &dyn RTCCrypto) -> Result<()> { + let certificate = provider_certificate(crypto)?; + let fingerprints = certificate.get_fingerprints(crypto)?; assert_eq!(fingerprints.len(), 1); assert_eq!(fingerprints[0].algorithm, "sha-256"); let pem = certificate.serialize_pem()?; - let imported = RTCCertificate::from_pem(&pem, provider.clone())?; + let imported = RTCCertificate::from_pem(&pem, crypto)?; assert_eq!(imported, certificate); let private_key = certificate @@ -941,7 +951,7 @@ mod test { .map_err(crypto_error)? .expect("built-in generated keys are exportable"); let imported = RTCCertificate::from_pkcs8( - provider, + crypto, SignatureScheme::EcdsaP256Sha256, certificate.dtls_certificate.certificate.clone(), private_key.as_ref(), @@ -954,7 +964,7 @@ mod test { #[test] fn non_exportable_signing_key_returns_an_explicit_pem_error() -> Result<()> { let provider = crypto::default_provider().map_err(crypto_error)?; - let certificate = provider_certificate(provider)?; + let certificate = provider_certificate(provider.crypto())?; let signing_key = certificate.dtls_certificate.private_key.signing_key.clone(); let certificate = RTCCertificate::from_signing_key( certificate.dtls_certificate.certificate, diff --git a/src/peer_connection/configuration/mod.rs b/src/peer_connection/configuration/mod.rs index fc0420c8..705a6e12 100644 --- a/src/peer_connection/configuration/mod.rs +++ b/src/peer_connection/configuration/mod.rs @@ -101,9 +101,10 @@ //! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Generate custom certificate for peer identity //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -158,8 +159,9 @@ //! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! let certificate = RTCCertificate::generate( -//! crypto::default_provider()?, +//! provider.crypto(), //! SignatureScheme::EcdsaP256Sha256, //! CertificateParams::new(vec!["localhost".to_owned()])?, //! )?; @@ -485,8 +487,9 @@ impl RTCConfiguration { /// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// let certificate = RTCCertificate::generate( -/// crypto::default_provider()?, +/// provider.crypto(), /// SignatureScheme::EcdsaP256Sha256, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; @@ -688,8 +691,9 @@ impl RTCConfigurationBuilder { /// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { + /// let provider = crypto::default_provider()?; /// let certificate = RTCCertificate::generate( - /// crypto::default_provider()?, + /// provider.crypto(), /// SignatureScheme::EcdsaP256Sha256, /// CertificateParams::new(vec!["localhost".to_owned()])?, /// )?; diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 484e03c8..680520a5 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -372,6 +372,19 @@ impl SettingEngine { self.crypto_provider = Some(provider); } + /// The crypto provider configured on this engine, if any. + /// + /// `None` means no provider has been set, so building a peer connection will resolve the + /// feature-selected built-in. + /// + /// Callers assembling additional components around a peer connection — an async wrapper's + /// TURN client, for instance — read the provider from here and pass it to + /// [`set_crypto_provider`](Self::set_crypto_provider) before building, so the whole + /// connection provably shares one provider instead of resolving a second. + pub fn crypto_provider(&self) -> Option<&Arc> { + self.crypto_provider.as_ref() + } + /// Returns the configured receive MTU, or the default if not set. pub(crate) fn get_receive_mtu(&self) -> usize { if self.receive_mtu != 0 { diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index a5f696af..ee018ef8 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -8,6 +8,7 @@ use crate::peer_connection::transport::dtls::RTCDtlsTransport; use crate::peer_connection::transport::dtls::role::RTCDtlsRole; use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState; use crate::statistics::accumulator::{CertificateStatsAccumulator, RTCStatsAccumulator}; +use crypto::RTCCryptoProvider; use dtls::endpoint::EndpointEvent; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; use dtls::state::State; @@ -18,6 +19,7 @@ use srtp::option::{srtcp_replay_protection, srtp_replay_protection}; use srtp::protection_profile::ProtectionProfile; use std::collections::VecDeque; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Instant; pub(crate) struct DtlsHandlerContext { @@ -90,7 +92,7 @@ impl<'a> DtlsHandler<'a> { // Register local certificate and set local_certificate_id if let Some(local_cert) = self.ctx.dtls_transport.certificates.first() { let fingerprints = - local_cert.get_fingerprints(self.ctx.dtls_transport.crypto_provider.clone())?; + local_cert.get_fingerprints(self.ctx.dtls_transport.crypto_provider.crypto())?; if let Some(fp) = fingerprints.first() { // Register certificate in accumulator // Use hex encoding for certificate (base64 would need additional dependency) @@ -199,6 +201,7 @@ impl<'a> sansio::Protocol DtlsHandler<'a> { pub(crate) fn update_srtp_contexts( state: &State, replay_protection: &ReplayProtection, + crypto_provider: &Arc, ) -> Result<(srtp::context::Context, srtp::context::Context)> { let profile = match state.srtp_protection_profile() { SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm => ProtectionProfile::AeadAes128Gcm, @@ -409,7 +413,6 @@ impl<'a> DtlsHandler<'a> { )?; srtp_config .set_session_keys_from_keying_material(keying_material.as_ref(), state.is_client())?; - let crypto_provider = state.crypto_provider(); let local_context = srtp::context::Context::new( &srtp_config.keys.local_master_key, @@ -417,7 +420,7 @@ impl<'a> DtlsHandler<'a> { srtp_config.profile, srtp_config.local_rtp_options, srtp_config.local_rtcp_options, - crypto_provider.clone(), + crypto_provider.crypto(), )?; let remote_context = srtp::context::Context::new( @@ -438,7 +441,7 @@ impl<'a> DtlsHandler<'a> { } else { srtp_config.remote_rtcp_options }, - crypto_provider, + crypto_provider.crypto(), )?; Ok((local_context, remote_context)) diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index eebe20e9..bb75fa67 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -40,9 +40,10 @@ where ) -> Result { configuration.validate()?; - // The one place in the workspace that resolves a default crypto provider. The - // application either supplies one through `SettingEngine::set_crypto_provider` or gets - // the feature-selected built-in here, once, at construction. Everything downstream — + // The one place in `rtc` that resolves a default crypto provider. The application + // either supplies one through `SettingEngine::set_crypto_provider` — which is also how + // a wrapper crate injects a provider it resolved itself — or gets the feature-selected + // built-in here, once, at construction. Everything downstream — // ICE, DTLS, SRTP, STUN, certificates — receives it explicitly, so no library code // reaches for a default behind the caller's back. let crypto_provider = match setting_engine.crypto_provider.take() { @@ -53,6 +54,8 @@ where )) })?, }; + // Record the resolution so `SettingEngine::crypto_provider` reports the provider this + // connection actually uses, not merely what was requested. setting_engine.crypto_provider = Some(crypto_provider.clone()); let mut candidate_types = vec![]; @@ -211,7 +214,7 @@ where } let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints(self.dtls_transport().crypto_provider.clone())? + cert.get_fingerprints(self.dtls_transport().crypto_provider.crypto())? } else { return Err(Error::ErrNonCertificate); }; @@ -356,7 +359,7 @@ where }; let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints(self.dtls_transport().crypto_provider.clone())? + cert.get_fingerprints(self.dtls_transport().crypto_provider.crypto())? } else { return Err(Error::ErrNonCertificate); }; diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index 0951c023..4c3c0ee1 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -311,7 +311,6 @@ use sdp::MEDIA_SECTION_APPLICATION; use shared::error::{Error, Result}; use shared::util::math_rand_alpha; use std::collections::HashMap; -use std::sync::Arc; use std::time::Instant; /// Builder for creating RTCPeerConnection instances. @@ -531,18 +530,6 @@ where self } - /// Overrides the SCTP receive-buffer size (the a_rwnd flow-control window), in bytes, - /// on this builder's [`SettingEngine`]. - /// - /// Convenience for [`SettingEngine::set_sctp_max_receive_buffer_size`]; see it for the - /// throughput/memory tradeoff. Applies to whichever `SettingEngine` is currently set, - /// so call it after [`with_setting_engine`](Self::with_setting_engine) if you also - /// supply a custom engine. Leaving it unset keeps the 1 MiB default. - pub fn with_sctp_receive_buffer_size(mut self, size: u32) -> Self { - self.setting_engine.set_sctp_max_receive_buffer_size(size); - self - } - /// Configures the peer connection with an interceptor registry. /// /// Interceptors process RTP/RTCP packets as they flow through the pipeline, @@ -1770,18 +1757,6 @@ where &self.configuration } - /// Returns the crypto provider this peer connection resolved at construction. - /// - /// Construction is the single place in the workspace that resolves a default provider: it - /// uses the one configured through - /// [`SettingEngine::set_crypto_provider`](crate::peer_connection::configuration::setting_engine::SettingEngine::set_crypto_provider), - /// or the feature-selected built-in. Callers that build additional components around a peer - /// connection — an async wrapper's TURN client, for instance — take the provider from here - /// so the whole connection shares one, rather than resolving a second. - pub fn crypto_provider(&self) -> &Arc { - &self.dtls_transport().crypto_provider - } - /// set_configuration updates the configuration of this PeerConnection object. pub fn set_configuration(&mut self, configuration: RTCConfiguration) -> Result<()> { // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2) @@ -2347,7 +2322,10 @@ mod tests { #[test] fn with_sctp_receive_buffer_size_sets_and_clamps() { - let builder = RTCPeerConnectionBuilder::new().with_sctp_receive_buffer_size(200_000); + let mut setting_engine = SettingEngine::default(); + setting_engine.set_sctp_max_receive_buffer_size(200_000); + + let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine); assert_eq!( builder.setting_engine.sctp_max_receive_buffer_size, Some(200_000) @@ -2356,7 +2334,9 @@ mod tests { // Values below the RFC 4960 §6 floor (1500 bytes), including 0, are clamped up so // they cannot break the SCTP handshake. for input in [0u32, 500, 1499] { - let builder = RTCPeerConnectionBuilder::new().with_sctp_receive_buffer_size(input); + let mut setting_engine = SettingEngine::default(); + setting_engine.set_sctp_max_receive_buffer_size(input); + let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine); assert_eq!( builder.setting_engine.sctp_max_receive_buffer_size, Some(1500), diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 8a4c1df4..0315c13b 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -87,7 +87,7 @@ impl RTCDtlsTransport { let params = CertificateParams::new(vec![shared::util::math_rand_alpha(16)]) .map_err(|error| Error::Other(error.to_string()))?; let cert = RTCCertificate::generate( - crypto_provider.clone(), + crypto_provider.crypto(), crypto::SignatureScheme::EcdsaP256Sha256, params, )?; @@ -160,9 +160,9 @@ impl RTCDtlsTransport { // need this. libp2p's WebRTC-Direct is the canonical case: the server synthesizes the // client's offer locally with a placeholder fingerprint and authenticates the peer // afterwards with a Noise handshake over the data channel. - let fingerprint_crypto = self.crypto_provider.clone(); let verify_peer_certificate: Option = if !self.disable_certificate_fingerprint_verification { + let fingerprint_crypto = self.crypto_provider.clone(); Some(Arc::new( move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { if certs.is_empty() { @@ -401,7 +401,7 @@ mod tests { fn filters_srtp_profiles_by_provider_capabilities() -> Result<()> { let default_provider = crypto::default_provider().expect("test crypto provider"); let certificate = RTCCertificate::generate( - default_provider.clone(), + default_provider.crypto(), crypto::SignatureScheme::EcdsaP256Sha256, CertificateParams::new(vec!["webrtc.rs".to_owned()]) .map_err(|e| Error::Other(e.to_string()))?, diff --git a/tests/dtls_rsa_certificate.rs b/tests/dtls_rsa_certificate.rs index 3103db34..2bd5616e 100644 --- a/tests/dtls_rsa_certificate.rs +++ b/tests/dtls_rsa_certificate.rs @@ -51,9 +51,11 @@ impl KeyType { signing_key, )? } - KeyType::EcdsaP256 => { - RTCCertificate::generate(provider, SignatureScheme::EcdsaP256Sha256, params)? - } + KeyType::EcdsaP256 => RTCCertificate::generate( + provider.crypto(), + SignatureScheme::EcdsaP256Sha256, + params, + )?, }) } } From b3ac944f6f15d5370d355b4af2a22de4e6ad4079 Mon Sep 17 00:00:00 2001 From: Rusty Rain <2069201+rainliu@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:57:15 -0700 Subject: [PATCH 39/40] optimize dtls/sctp poll_timeout --- rtc-dtls/src/endpoint.rs | 13 ++++--------- src/peer_connection/handler/dtls.rs | 10 +++++----- src/peer_connection/handler/mod.rs | 4 +--- src/peer_connection/handler/sctp.rs | 12 ++++-------- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index 5e066c61..fb609861 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -278,16 +278,11 @@ impl Endpoint { } /// When `remote`'s association next needs [`Self::handle_timeout`]. - pub fn poll_timeout(&self, remote: SocketAddr, eto: &mut Instant) -> Result<()> { - if let Some(conn) = self.connections.get(&remote) { - if let Some(current_retransmit_timer) = &conn.current_retransmit_timer - && *current_retransmit_timer < *eto - { - *eto = *current_retransmit_timer; - } - Ok(()) + pub fn poll_timeout(&self, remote: &SocketAddr) -> Option { + if let Some(conn) = self.connections.get(remote) { + conn.current_retransmit_timer } else { - Err(Error::InvalidRemoteAddress(remote)) + None } } } diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index ee018ef8..555f6381 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -1,6 +1,5 @@ use crate::peer_connection::configuration::setting_engine::ReplayProtection; use crate::peer_connection::event::RTCEventInternal; -use crate::peer_connection::handler::DEFAULT_TIMEOUT_DURATION; use crate::peer_connection::message::internal::{ DTLSMessage, RTCMessageInternal, TaggedRTCMessageInternal, }; @@ -352,15 +351,16 @@ impl<'a> sansio::Protocol Option { if let Some(dtls_endpoint) = self.ctx.dtls_transport.dtls_endpoint.as_ref() { - let max_eto = Instant::now() + DEFAULT_TIMEOUT_DURATION; - let mut eto = max_eto; + let mut eto = None; let remotes = dtls_endpoint.get_connections_keys(); for remote in remotes { - let _ = dtls_endpoint.poll_timeout(*remote, &mut eto); + if let Some(timeout) = dtls_endpoint.poll_timeout(remote) { + eto = Some(eto.map_or(timeout, |e: Instant| e.min(timeout))); + } } - if eto != max_eto { Some(eto) } else { None } + eto } else { None } diff --git a/src/peer_connection/handler/mod.rs b/src/peer_connection/handler/mod.rs index 1fc27154..6e517f47 100644 --- a/src/peer_connection/handler/mod.rs +++ b/src/peer_connection/handler/mod.rs @@ -34,9 +34,7 @@ use log::warn; use shared::TaggedBytesMut; use shared::error::{Error, flatten_errs}; use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -pub(crate) const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(86400); // 1 day duration +use std::time::Instant; /// Forward handler list - invokes callback with handler list macro_rules! forward_handlers { diff --git a/src/peer_connection/handler/sctp.rs b/src/peer_connection/handler/sctp.rs index 3ed53edb..ba02ebd7 100644 --- a/src/peer_connection/handler/sctp.rs +++ b/src/peer_connection/handler/sctp.rs @@ -1,7 +1,6 @@ use crate::peer_connection::event::RTCEventInternal; use crate::peer_connection::event::RTCPeerConnectionEvent; use crate::peer_connection::event::data_channel_event::RTCDataChannelEvent; -use crate::peer_connection::handler::DEFAULT_TIMEOUT_DURATION; use crate::peer_connection::message::internal::{ DTLSMessage, RTCMessageInternal, TaggedRTCMessageInternal, }; @@ -544,18 +543,15 @@ impl<'a> sansio::Protocol Option { - let max_eto = Instant::now() + DEFAULT_TIMEOUT_DURATION; - let mut eto = max_eto; + let mut eto = None; for conn in self.ctx.sctp_transport.sctp_associations.values() { - if let Some(timeout) = conn.poll_timeout() - && timeout < eto - { - eto = timeout; + if let Some(timeout) = conn.poll_timeout() { + eto = Some(eto.map_or(timeout, |e: Instant| e.min(timeout))); } } - if eto != max_eto { Some(eto) } else { None } + eto } fn close(&mut self) -> Result<()> { From 89f518ea4bba0fa92ede7bdbee32b65d4ff3578c Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Sun, 26 Jul 2026 19:51:52 -0400 Subject: [PATCH 40/40] sctp: don't discard received-but-undelivered data on incoming stream reset When a peer sends messages and then immediately resets the stream (close-after-send), the DATA chunks and the outgoing-stream-reset RECONFIG can be processed in the same input batch. reset_streams_if_any would unregister the stream outright, dropping the reassembly queue with received-but-unread messages still inside, so the application saw the channel close with only a prefix of the messages delivered. Per RFC 6525, data received before the reset must still be delivered to the upper layer. Defer the stream teardown when the reassembly queue is still readable: keep the stream registered in read-only state (marked reset_pending), refuse new inbound DATA for it, and complete unregister_stream (emitting AssociationLost/Reset) once the consumer drains the queue via read_sctp. The reset is still answered "Success - Performed" as before. --- rtc-sctp/src/association/mod.rs | 29 ++++++++++-- rtc-sctp/src/association/stream.rs | 17 ++++++- rtc-sctp/src/endpoint/endpoint_test.rs | 65 ++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/rtc-sctp/src/association/mod.rs b/rtc-sctp/src/association/mod.rs index 38a45367..46052f85 100644 --- a/rtc-sctp/src/association/mod.rs +++ b/rtc-sctp/src/association/mod.rs @@ -1249,7 +1249,10 @@ impl Association { let immediate_sack = d.immediate_sack; - if stream_handle_data && let Some(s) = self.streams.get_mut(&d.stream_identifier) { + if stream_handle_data + && let Some(s) = self.streams.get_mut(&d.stream_identifier) + && !s.reset_pending + { self.events.push_back(Event::DatagramReceived); if s.handle_data(d) && s.reassembly_queue.is_readable() { self.events.push_back(Event::Stream(StreamEvent::Readable { @@ -1952,11 +1955,31 @@ impl Association { self.side, p.sender_last_tsn, self.peer_last_tsn ); for id in &p.stream_identifiers { - if self.streams.contains_key(id) { + if let Some(s) = self.streams.get_mut(id) { if respond { sis_to_reset.push(*id); } - self.unregister_stream(*id, AssociationError::Reset); + if s.reassembly_queue.is_readable() { + // The reassembly queue still holds messages that were + // received before the reset but not yet delivered to + // the consumer (RFC 6525: data received prior to the + // reset must still be delivered). The reset is + // performed at the SCTP level (we respond "Success - + // Performed" as usual), but the local teardown is + // deferred until the consumer drains the queue via + // read_sctp. No new inbound DATA is accepted for the + // stream in the meantime. + debug!( + "[{}] resetStream(): deferring teardown of stream {} until reassembly queue is drained", + self.side, id + ); + s.reset_pending = true; + s.state = RecvSendState::Readable; + self.events + .push_back(Event::Stream(StreamEvent::Readable { id: *id })); + } else { + self.unregister_stream(*id, AssociationError::Reset); + } } } self.reconfig_requests diff --git a/rtc-sctp/src/association/stream.rs b/rtc-sctp/src/association/stream.rs index f2697bdc..96320457 100644 --- a/rtc-sctp/src/association/stream.rs +++ b/rtc-sctp/src/association/stream.rs @@ -2,7 +2,7 @@ use crate::association::Association; use crate::association::state::AssociationState; use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; use crate::queue::reassembly_queue::{Chunks, ReassemblyQueue}; -use crate::{ErrorCauseCode, Event, Side}; +use crate::{AssociationError, ErrorCauseCode, Event, Side}; use shared::error::{Error, Result}; use crate::util::{ByteSlice, BytesArray, BytesChunk, BytesSource}; @@ -127,7 +127,15 @@ impl Stream<'_> { if let Some(s) = self.association.streams.get_mut(&self.stream_identifier) && (s.state == RecvSendState::ReadWritable || s.state == RecvSendState::Readable) { - Ok(s.reassembly_queue.read()) + let chunks = s.reassembly_queue.read(); + if s.reset_pending && !s.reassembly_queue.is_readable() { + // The peer reset this stream while data was still queued for + // delivery; now that the consumer has drained the queue, + // complete the deferred teardown. + self.association + .unregister_stream(self.stream_identifier, AssociationError::Reset); + } + Ok(chunks) } else { Err(Error::ErrStreamClosed) } @@ -429,6 +437,10 @@ pub struct StreamState { pub(crate) buffered_amount: usize, pub(crate) buffered_amount_low: usize, pub(crate) buffered_amount_high: usize, + /// The peer reset this incoming stream while the reassembly queue still + /// held deliverable data. Teardown is deferred until the consumer drains + /// the queue via `read_sctp`; no new inbound DATA is accepted meanwhile. + pub(crate) reset_pending: bool, } impl StreamState { pub(crate) fn new( @@ -451,6 +463,7 @@ impl StreamState { buffered_amount: 0, buffered_amount_low: 0, buffered_amount_high: u32::MAX as usize, + reset_pending: false, } } diff --git a/rtc-sctp/src/endpoint/endpoint_test.rs b/rtc-sctp/src/endpoint/endpoint_test.rs index 81dc6d07..e13300f5 100644 --- a/rtc-sctp/src/endpoint/endpoint_test.rs +++ b/rtc-sctp/src/endpoint/endpoint_test.rs @@ -2078,6 +2078,71 @@ fn test_assoc_reset_close_both_ways() -> Result<()> { Ok(()) } +/// Regression test: an incoming stream reset must not discard messages that +/// were received (and reassembled) but not yet delivered to the application. +/// +/// A peer that sends N messages and then immediately closes the channel +/// produces DATA chunks and the outgoing-stream-reset RECONFIG so close +/// together that the receiver processes them in the same input batch. The +/// stream teardown must be deferred until the consumer drains the reassembly +/// queue via `read_sctp`. +#[test] +fn test_assoc_reset_defers_teardown_until_undelivered_data_read() -> Result<()> { + let si: u16 = 1; + let msgs: Vec = (0..4u8).map(|i| Bytes::from(vec![b'A' + i; 256])).collect(); + + let (mut pair, client_ch, server_ch) = create_association_pair(AckMode::NoDelay, 0)?; + establish_session_pair(&mut pair, client_ch, server_ch, si)?; + + // Send all messages, then immediately reset the stream (close-after-send) + // without letting the server read in between. The server then handles the + // tail DATA chunk(s) and the RECONFIG in the same batch. + for msg in &msgs { + let n = pair + .client_stream(client_ch, si)? + .write_sctp(msg, PayloadProtocolIdentifier::Binary)?; + assert_eq!(msg.len(), n); + } + pair.client_stream(client_ch, si)?.stop()?; // send reset + + pair.drive(); + + // Every message must still be deliverable even though the reset has + // already been processed (and answered "Success - Performed"). + let mut buf = vec![0u8; 1024]; + for (i, msg) in msgs.iter().enumerate() { + let chunks = pair + .server_stream(server_ch, si) + .unwrap_or_else(|e| panic!("stream gone before reading message {i}: {e:?}")) + .read_sctp()? + .unwrap_or_else(|| panic!("no data for message {i}")); + let n = chunks.read(&mut buf)?; + assert_eq!(&buf[..n], msg.as_ref(), "message {i} mismatch"); + } + + // Only after the queue is drained should the stream be torn down. + assert!( + pair.server_stream(server_ch, si).is_err(), + "stream should be unregistered once drained" + ); + + // ... and the close (AssociationLost/Reset) surfaced to the consumer. + let mut saw_reset = false; + while let Some(ev) = pair.server_conn_mut(server_ch).poll() { + if let Event::AssociationLost { reason, id } = ev { + assert_eq!(id, si); + assert_matches!(reason, AssociationError::Reset); + saw_reset = true; + } + } + assert!(saw_reset, "AssociationLost(Reset) should be emitted"); + + pair.drive(); + close_association_pair(&mut pair, client_ch, server_ch, si); + + Ok(()) +} + #[test] fn test_assoc_abort() -> Result<()> { //let _guard = subscribe();