use std::{ fs::OpenOptions, io::{ErrorKind, Write}, net::{IpAddr, SocketAddr}, path::Path as FsPath, sync::{ Arc, Mutex as StdMutex, atomic::{AtomicBool, AtomicU8, Ordering}, }, time::{Duration, SystemTime, UNIX_EPOCH}, }; use async_trait::async_trait; use axum::{ Router, body::Bytes, extract::{ConnectInfo, DefaultBodyLimit, Path, State}, http::{ HeaderMap, HeaderValue, StatusCode, header::{CONTENT_TYPE, HOST}, }, response::{IntoResponse, Response}, routing::{get, post}, }; use base64::{Engine, engine::general_purpose}; use bytes::{BufMut, BytesMut}; use pumpkin_util::jwt::Jwks; use pumpkin_util::p384::{ PublicKey, ecdsa::{ Signature, SigningKey, signature::{Signer, Verifier}, }, pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey}, }; use serde_json::{Value, json}; use tokio::{ net::TcpListener, sync::{Mutex, OnceCell, RwLock, mpsc}, }; use tokio_util::sync::CancellationToken; use tracing::{debug, info, trace, warn}; use webrtc::{ data_channel::{DataChannel, DataChannelEvent}, peer_connection::{ PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCConfiguration, RTCConfigurationBuilder, RTCIceCandidateInit, RTCIceCandidateType, RTCIceConnectionState, RTCIceGatheringState, RTCIceServer, RTCPeerConnectionState, RTCSessionDescription, SettingEngine, }, }; use crate::STOP_INTERRUPT; use crate::net::bedrock::status::IceSocket; pub mod discovery; mod ice_router; use ice_router::{IceRouter, Registration, proxy_answer, proxy_offer}; const RELIABLE_CHANNEL: &str = "ReliableDataChannel"; const UNRELIABLE_CHANNEL: &str = "UnreliableDataChannel"; // NetherNet splits encoded packets that exceed 10,000 bytes into application-level // segments. Larger SCTP messages are rejected by some Bedrock clients. const MAX_FRAGMENT_SIZE: usize = 10_000; // Bedrock may send its login batch as one maximum-sized NetherNet segment. This // exceeds webrtc-rs's 65,535-byte callback buffer when the skin data is large. #[allow(dead_code)] const MAX_INBOUND_MESSAGE_SIZE: usize = 262_144; const MAX_SDP_SIZE: usize = 1 << 20; type IncomingSession = (Arc, SocketAddr); /// Accepts Bedrock `NetherNet` connections negotiated through Mojang's HTTP endpoint. pub struct NetherNetListener { incoming: Mutex>, local_addr: SocketAddr, state: EndpointState, } #[derive(Clone)] struct EndpointState { incoming: mpsc::Sender, identity_key: Arc, require_client_identity: bool, oidc_verifier: Option>>, stun_servers: Arc<[String]>, ice_local_addr: SocketAddr, external_ip: Option, ice_router: Arc, } impl NetherNetListener { pub async fn bind( address: SocketAddr, ice_socket: IceSocket, external_ip: Option, identity_key: Arc, require_client_identity: bool, oidc_verifier: Option>>, stun_servers: Vec, ) -> std::io::Result { let listener = TcpListener::bind(address).await?; let local_addr = listener.local_addr()?; let ice_router = Arc::new(IceRouter::bind(ice_socket).await?); let ice_local_addr = ice_router.public_addr(); let (incoming, receiver) = mpsc::channel(128); let state = EndpointState { incoming, identity_key, require_client_identity, oidc_verifier, stun_servers: stun_servers.into(), ice_local_addr, external_ip, ice_router, }; let router = Router::new() .route("/v1/join", get(ping)) .route("/v1/join/{network_id}", post(join)) .layer(DefaultBodyLimit::max(MAX_SDP_SIZE)) .with_state(state.clone()); tokio::spawn(async move { let result = axum::serve( listener, router.into_make_service_with_connect_info::(), ) .with_graceful_shutdown(STOP_INTERRUPT.clone().cancelled_owned()) .await; if let Err(error) = result { warn!("NetherNet signaling server stopped: {error}"); } }); info!("Bedrock NetherNet signaling is listening on {local_addr}"); info!("Bedrock NetherNet ICE is listening on {ice_local_addr}"); Ok(Self { incoming: Mutex::new(receiver), local_addr, state, }) } pub async fn accept(&self) -> Option { self.incoming.lock().await.recv().await } pub const fn local_addr(&self) -> SocketAddr { self.local_addr } } pub fn load_or_create_identity_key(path: &FsPath) -> std::io::Result> { loop { match std::fs::read(path) { Ok(bytes) => { let key = SigningKey::from_pkcs8_der(&bytes).map_err(|error| { std::io::Error::new( ErrorKind::InvalidData, format!("invalid NetherNet identity key: {error}"), ) })?; return Ok(Arc::new(key)); } Err(error) if error.kind() == ErrorKind::NotFound => { let key = loop { let bytes = rand::random::<[u8; 48]>(); if let Ok(key) = SigningKey::from_slice(&bytes) { break key; } }; let document = key .to_pkcs8_der() .map_err(|error| std::io::Error::other(error.to_string()))?; let mut options = OpenOptions::new(); options.write(true).create_new(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600) }; match options.open(path) { Ok(mut file) => { file.write_all(document.as_bytes())?; file.sync_all()?; return Ok(Arc::new(key)); } Err(error) if error.kind() == ErrorKind::AlreadyExists => {} Err(error) => return Err(error), } } Err(error) => return Err(error), } } } async fn ping(ConnectInfo(address): ConnectInfo) -> StatusCode { trace!(%address, "Accepted NetherNet capability probe"); StatusCode::OK } async fn join( State(state): State, ConnectInfo(address): ConnectInfo, Path(network_id): Path, headers: HeaderMap, offer: Bytes, ) -> Response { trace!(%address, %network_id, length = offer.len(), "Received NetherNet SDP offer"); if offer.is_empty() { debug!(%address, %network_id, "Rejected empty NetherNet SDP offer"); return (StatusCode::BAD_REQUEST, "Missing SDP offer").into_response(); } let Ok(offer) = String::from_utf8(offer.to_vec()) else { debug!(%address, %network_id, "Rejected non-UTF-8 NetherNet SDP offer"); return (StatusCode::BAD_REQUEST, "SDP offer must be UTF-8").into_response(); }; let advertised_ip = headers .get(HOST) .and_then(|host| host.to_str().ok()) .and_then(|host| host.parse::().ok()) .and_then(|authority| authority.host().parse().ok()); match Box::pin(negotiate_direct(&state, address, &offer, advertised_ip)).await { Ok((answer, _session)) => { trace!(%address, %network_id, length = answer.len(), "Returning NetherNet SDP answer"); let mut response = (StatusCode::OK, answer).into_response(); response .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("application/sdp")); response } Err(error) => { warn!("NetherNet negotiation with {address} failed: {error}"); (StatusCode::BAD_REQUEST, error).into_response() } } } async fn negotiate( state: &EndpointState, address: SocketAddr, offer: &str, candidates: Option>, ) -> Result<(String, Arc), String> { Box::pin(negotiate_inner(state, address, offer, candidates, None)).await } async fn negotiate_direct( state: &EndpointState, address: SocketAddr, offer: &str, advertised_ip: Option, ) -> Result<(String, Arc), String> { Box::pin(negotiate_inner(state, address, offer, None, advertised_ip)).await } #[expect(clippy::too_many_lines)] async fn negotiate_inner( state: &EndpointState, address: SocketAddr, offer: &str, candidates: Option>, advertised_ip: Option, ) -> Result<(String, Arc), String> { let signaling = if candidates.is_some() { "LAN" } else { "HTTP" }; let direct_ip = candidates.is_none(); trace!(%address, signaling, "Starting NetherNet negotiation"); let (offer, client_public_key) = { let offer = offer.to_string(); let require_identity = state.require_client_identity; let oidc_verifier = state.oidc_verifier.as_ref().and_then(|c| c.get()).cloned(); let (tx, rx) = tokio::sync::oneshot::channel(); rayon::spawn(move || { let res = authenticate_client_offer(&offer, require_identity, oidc_verifier.as_ref()); let _ = tx.send(res); }); rx.await.map_err(|_| "Task cancelled".to_string())?? }; trace!( %address, signaling, authenticated = client_public_key.is_some(), candidates = ?candidate_summary(&offer), "Received NetherNet ICE candidates" ); let gathering_notify = Arc::new(tokio::sync::Notify::new()); let handler = Arc::new(NetherNetEventHandler { session: Mutex::new(None), address, gathering_notify: gathering_notify.clone(), }); let configuration = rtc_configuration(&state.stun_servers); let (offer, remote_candidates) = if direct_ip { proxy_offer(&offer, state.ice_router.internal_addr()) } else { (offer, Vec::new()) }; let peer = Box::pin(build_peer( state, configuration, handler.clone(), direct_ip, advertised_ip, )) .await?; let session = Arc::new(NetherNetSession::new( peer.clone(), client_public_key, address, state.incoming.clone(), )); *handler.session.lock().await = Some(session.clone()); let offer = RTCSessionDescription::offer(offer).map_err(|error| error.to_string())?; peer.set_remote_description(offer) .await .map_err(|error| error.to_string())?; trace!(%address, signaling, "Applied NetherNet remote description"); if let Some(mut candidates) = candidates { let peer = peer.clone(); tokio::spawn(async move { while let Some(candidate) = candidates.recv().await { if let Err(error) = peer.add_ice_candidate(candidate).await { debug!("Failed to add NetherNet LAN ICE candidate: {error}"); } } }); } let answer = peer .create_answer(None) .await .map_err(|error| error.to_string())?; peer.set_local_description(answer) .await .map_err(|error| error.to_string())?; trace!(%address, signaling, "Gathering NetherNet ICE candidates"); let _ = tokio::time::timeout(Duration::from_secs(2), gathering_notify.notified()).await; let answer = peer .local_description() .await .ok_or_else(|| "WebRTC did not produce a local description".to_string())?; let answer = remove_component_two_candidates(&answer.sdp); let answer = if direct_ip { let (answer, ufrag, internal) = proxy_answer(&answer, state.ice_router.public_addr().port())?; session.set_ice_route( state .ice_router .register(ufrag, internal, remote_candidates), ); answer } else { answer }; trace!( %address, signaling, candidates = ?candidate_summary(&answer), "Gathered NetherNet ICE candidates" ); trace!(%address, signaling, "Completed NetherNet negotiation"); let answer = { let identity_key = state.identity_key.clone(); let (tx, rx) = tokio::sync::oneshot::channel(); rayon::spawn(move || { let res = add_server_identity(&answer, &identity_key); let _ = tx.send(res); }); rx.await.map_err(|_| "Task cancelled".to_string())?? }; Ok((answer, session)) } async fn build_peer( state: &EndpointState, configuration: RTCConfiguration, handler: Arc, direct_ip: bool, advertised_ip: Option, ) -> Result, String> { let ice_bind_addr = if direct_ip { // Direct connections are relayed through the router's loopback socket. // Binding every interface would make the inner ICE source differ from // the loopback route registered after SDP generation. SocketAddr::new(state.ice_router.internal_addr().ip(), 0) } else { SocketAddr::new(state.ice_local_addr.ip(), 0) }; let mut setting_engine = SettingEngine::default(); if direct_ip && let Some(external_ip) = state.external_ip.or(advertised_ip) { setting_engine.set_nat_1to1_ips(vec![external_ip.to_string()], RTCIceCandidateType::Host); } Ok(Arc::new( Box::pin( PeerConnectionBuilder::new() .with_configuration(configuration) .with_setting_engine(setting_engine) .with_handler(handler) .with_udp_addrs(vec![ice_bind_addr]) .build(), ) .await .map_err(|error| error.to_string())?, )) } fn rtc_configuration(stun_servers: &[String]) -> RTCConfiguration { if stun_servers.is_empty() { RTCConfigurationBuilder::default().build() } else { RTCConfigurationBuilder::default() .with_ice_servers(vec![RTCIceServer { urls: stun_servers.to_vec(), ..Default::default() }]) .build() } } struct NetherNetEventHandler { session: Mutex>>, address: SocketAddr, gathering_notify: Arc, } #[async_trait] impl PeerConnectionEventHandler for NetherNetEventHandler { async fn on_data_channel(&self, channel: Arc) { if let Some(session) = self.session.lock().await.as_ref() { let label = channel.label().await; let ordered = channel.ordered().await; let negotiated = channel.negotiated().await; let max_retransmits = channel.max_retransmits().await; trace!( address = %self.address, ?label, ?ordered, ?negotiated, ?max_retransmits, "Received NetherNet data channel" ); if let Err(error) = session.attach_channel(channel).await { warn!("Rejected NetherNet data channel: {error}"); session.close().await; } } } async fn on_connection_state_change(&self, connection_state: RTCPeerConnectionState) { trace!(?connection_state, address = %self.address, "NetherNet peer connection state changed"); if matches!( connection_state, RTCPeerConnectionState::Failed | RTCPeerConnectionState::Disconnected | RTCPeerConnectionState::Closed ) && let Some(session) = self.session.lock().await.as_ref() { session.mark_closed(); } } async fn on_ice_connection_state_change(&self, connection_state: RTCIceConnectionState) { trace!(?connection_state, address = %self.address, "NetherNet ICE connection state changed"); } async fn on_ice_gathering_state_change(&self, state: RTCIceGatheringState) { trace!(?state, address = %self.address, "NetherNet ICE gathering state changed"); if state == RTCIceGatheringState::Complete { self.gathering_notify.notify_waiters(); } } } fn candidate_summary(sdp: &str) -> Vec { sdp.lines() .filter_map(|line| line.strip_prefix("a=candidate:")) .map(|candidate| { let fields = candidate.split_whitespace().collect::>(); match fields.as_slice() { [ foundation, component, protocol, _, address, port, "typ", kind, .., ] => { format!("{foundation}/{component} {protocol} {address}:{port} {kind}") } _ => "malformed candidate".to_owned(), } }) .collect() } fn remove_component_two_candidates(sdp: &str) -> String { let mut filtered = String::with_capacity(sdp.len()); for line in sdp.lines().filter(|line| { line.strip_prefix("a=candidate:") .and_then(|candidate| candidate.split_whitespace().nth(1)) != Some("2") }) { filtered.push_str(line); filtered.push_str("\r\n"); } filtered } /// A WebRTC connection carrying complete Bedrock batch packets. pub struct NetherNetSession { #[allow(dead_code)] peer: Arc, reliable: RwLock>>, unreliable: RwLock>>, fragments: Mutex, packets: Mutex>, packet_sender: mpsc::Sender, open_channels: AtomicU8, accepted: AtomicBool, closed: CancellationToken, client_public_key: Option, address: SocketAddr, incoming: mpsc::Sender, ice_route: StdMutex>, } impl NetherNetSession { fn new( peer: Arc, client_public_key: Option, address: SocketAddr, incoming: mpsc::Sender, ) -> Self { let (packet_sender, packets) = mpsc::channel(4096); Self { peer, reliable: RwLock::new(None), unreliable: RwLock::new(None), fragments: Mutex::new(FragmentBuffer::default()), packets: Mutex::new(packets), packet_sender, open_channels: AtomicU8::new(0), accepted: AtomicBool::new(false), closed: CancellationToken::new(), client_public_key, address, incoming, ice_route: StdMutex::new(None), } } fn set_ice_route(&self, route: Registration) { if !self.closed.is_cancelled() { *self .ice_route .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(route); } } async fn attach_channel(self: &Arc, channel: Arc) -> Result<(), String> { let label = channel.label().await.map_err(|e| e.to_string())?; let ordered = channel.ordered().await.map_err(|e| e.to_string())?; let protocol = channel.protocol().await.map_err(|e| e.to_string())?; let negotiated = channel.negotiated().await.map_err(|e| e.to_string())?; let max_packet_lifetime = channel .max_packet_life_time() .await .map_err(|e| e.to_string())?; let max_retransmits = channel.max_retransmits().await.map_err(|e| e.to_string())?; let has_default_parameters = protocol.is_empty() && !negotiated && max_packet_lifetime.is_none(); let bit = match label.as_str() { RELIABLE_CHANNEL if ordered && has_default_parameters && max_retransmits.is_none() => { *self.reliable.write().await = Some(channel.clone()); 1 } UNRELIABLE_CHANNEL if !ordered && has_default_parameters && (max_retransmits.is_none() || max_retransmits == Some(0)) => { *self.unreliable.write().await = Some(channel.clone()); 2 } label => return Err(format!("invalid channel {label:?}")), }; let session = self.clone(); tokio::spawn(async move { let mut opened = false; while let Some(event) = channel.poll().await { match event { DataChannelEvent::OnOpen => { opened = true; session.channel_opened(bit).await; } DataChannelEvent::OnMessage(msg) => { if !opened { opened = true; session.channel_opened(bit).await; } if let Err(error) = session.receive_segment(bit, msg.data.into()).await { warn!( "Invalid NetherNet message from {}: {error}", session.address ); break; } } DataChannelEvent::OnClose => break, DataChannelEvent::OnError => { warn!(address = %session.address, "Failed to read NetherNet data channel"); break; } _ => {} } } session.close().await; }); Ok(()) } async fn channel_opened(self: &Arc, bit: u8) { let open = self.open_channels.fetch_or(bit, Ordering::AcqRel) | bit; trace!( address = %self.address, channel = if bit == 1 { "reliable" } else { "unreliable" }, both_open = open == 3, "NetherNet data channel opened" ); if open == 3 && !self.accepted.swap(true, Ordering::AcqRel) { debug!( "Accepted Bedrock NetherNet connection from {}", self.address ); if self .incoming .send((self.clone(), self.address)) .await .is_err() { self.close().await; } } } async fn receive_segment(&self, channel: u8, data: Bytes) -> Result<(), String> { let (&remaining, payload) = data .split_first() .ok_or_else(|| "empty data-channel message".to_string())?; if payload.is_empty() { return Err("empty NetherNet packet segment".to_string()); } if channel == 2 { if remaining != 0 { return Err("fragmented unreliable message".to_string()); } self.packet_sender .send(Bytes::copy_from_slice(payload)) .await .map_err(|_| "connection is closed".to_string())?; return Ok(()); } let packet = { let mut fragments = self.fragments.lock().await; fragments.push(remaining, payload)? }; if let Some(packet) = packet { self.packet_sender .send(packet) .await .map_err(|_| "connection is closed".to_string())?; } Ok(()) } pub async fn recv(&self) -> Option { let mut packets = self.packets.lock().await; tokio::select! { () = self.closed.cancelled() => None, packet = packets.recv() => packet, } } pub async fn send(&self, data: Bytes) -> Result<(), String> { if self.is_closed() { return Err("connection is closed".to_string()); } let channel = self .reliable .read() .await .clone() .ok_or_else(|| "reliable channel is not open".to_string())?; let segment_count = data.len().div_ceil(MAX_FRAGMENT_SIZE).max(1); if segment_count > 256 { return Err("Bedrock batch is too large for NetherNet".to_string()); } for (index, chunk) in data.chunks(MAX_FRAGMENT_SIZE).enumerate() { let mut segment = BytesMut::with_capacity(chunk.len() + 1); segment.put_u8((segment_count - index - 1) as u8); segment.extend_from_slice(chunk); channel .send(segment) .await .map_err(|error| error.to_string())?; } Ok(()) } pub async fn send_unreliable(&self, data: Bytes) -> Result<(), String> { if self.is_closed() { return Err("connection is closed".to_string()); } if data.len() > MAX_FRAGMENT_SIZE { return Err("unreliable NetherNet packet is too large".to_string()); } let channel = self .unreliable .read() .await .clone() .ok_or_else(|| "unreliable channel is not open".to_string())?; let mut segment = BytesMut::with_capacity(data.len() + 1); segment.put_u8(0); segment.extend_from_slice(&data); channel .send(segment) .await .map_err(|error| error.to_string())?; Ok(()) } pub const fn client_public_key(&self) -> Option<&PublicKey> { self.client_public_key.as_ref() } pub fn is_closed(&self) -> bool { self.closed.is_cancelled() } fn mark_closed(&self) { if !self.closed.is_cancelled() { trace!(address = %self.address, "NetherNet session closed"); self.closed.cancel(); self.ice_route .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); } } pub async fn close(&self) { if self.closed.is_cancelled() { return; } self.closed.cancel(); self.ice_route .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); let _ = self.peer.close().await; } } #[derive(Default)] struct FragmentBuffer { next_remaining: Option, data: Vec, } impl FragmentBuffer { fn push(&mut self, remaining: u8, payload: &[u8]) -> Result, String> { match self.next_remaining { None if remaining > 0 => self.next_remaining = Some(remaining - 1), None => return Ok(Some(Bytes::copy_from_slice(payload))), Some(expected) if expected == remaining => { self.next_remaining = remaining.checked_sub(1); } Some(expected) => { self.next_remaining = None; self.data.clear(); return Err(format!( "out-of-order fragment: expected {expected}, got {remaining}" )); } } self.data.extend_from_slice(payload); if remaining == 0 { self.next_remaining = None; return Ok(Some(Bytes::from(std::mem::take(&mut self.data)))); } Ok(None) } } fn verify_and_strip_identity( offer: &str, oidc_verifier: Option<&(String, Jwks)>, ) -> Result<(String, PublicKey), String> { let identity = offer .lines() .find_map(|line| line.strip_prefix("a=identity:")) .ok_or_else(|| "SDP offer is missing its identity assertion".to_string())?; let identity = general_purpose::STANDARD .decode(identity) .map_err(|error| format!("invalid identity encoding: {error}"))?; let identity: Value = serde_json::from_slice(&identity) .map_err(|error| format!("invalid identity JSON: {error}"))?; let assertion = identity["assertion"] .as_str() .ok_or_else(|| "identity assertion is missing".to_string())?; let assertion: Value = serde_json::from_str(assertion) .map_err(|error| format!("invalid nested identity assertion: {error}"))?; let token = assertion["token"] .as_str() .ok_or_else(|| "identity token is missing".to_string())?; if let Some((issuer, keys)) = oidc_verifier { if identity["idp"]["protocol"] != "default" || identity["idp"]["domain"].as_str().is_none_or(str::is_empty) { return Err("invalid identity provider".to_string()); } pumpkin_util::jwt::verify_oidc_token(token, issuer, keys) .map_err(|error| format!("invalid GameServerToken: {error}"))?; } else { validate_token_expiration(token)?; } let public_key = pumpkin_util::jwt::extract_cpk_from_token(token) .map_err(|error| format!("invalid identity public key: {error}"))?; let fingerprints = assertion["fingerprints"] .as_str() .ok_or_else(|| "fingerprint assertion is missing".to_string())?; verify_fingerprint_assertion(fingerprints, offer, &public_key)?; let mut stripped = offer .lines() .filter(|line| !line.starts_with("a=identity:")) .collect::>() .join("\r\n"); stripped.push_str("\r\n"); Ok((stripped, public_key)) } fn authenticate_client_offer( offer: &str, require_identity: bool, oidc_verifier: Option<&(String, Jwks)>, ) -> Result<(String, Option), String> { if offer.lines().any(|line| line.starts_with("a=identity:")) { let (offer, public_key) = verify_and_strip_identity(offer, oidc_verifier)?; return Ok((offer, Some(public_key))); } if require_identity { return Err("SDP offer is missing its identity assertion".to_string()); } Ok((offer.to_owned(), None)) } fn validate_token_expiration(token: &str) -> Result<(), String> { let payload = token .split('.') .nth(1) .ok_or_else(|| "malformed identity token".to_string())?; let payload = general_purpose::URL_SAFE_NO_PAD .decode(payload) .map_err(|error| format!("invalid identity token payload: {error}"))?; let payload: Value = serde_json::from_slice(&payload) .map_err(|error| format!("invalid identity token claims: {error}"))?; let expiration = payload["exp"] .as_i64() .ok_or_else(|| "identity token has no expiration".to_string())?; if expiration < unix_time() { return Err("identity token has expired".to_string()); } Ok(()) } fn verify_fingerprint_assertion( assertion: &str, sdp: &str, public_key: &PublicKey, ) -> Result<(), String> { let mut parts = assertion.split('.'); let header = parts.next().ok_or_else(|| "malformed JWS".to_string())?; let detached = parts.next().ok_or_else(|| "malformed JWS".to_string())?; let signature = parts.next().ok_or_else(|| "malformed JWS".to_string())?; if !detached.is_empty() || parts.next().is_some() { return Err("fingerprint assertion is not a detached JWS".to_string()); } let header_json = general_purpose::URL_SAFE_NO_PAD .decode(header) .map_err(|error| format!("invalid fingerprint header: {error}"))?; let header_json: Value = serde_json::from_slice(&header_json) .map_err(|error| format!("invalid fingerprint header: {error}"))?; if header_json["alg"] != "ES384" { return Err("fingerprint assertion must use ES384".to_string()); } let payload = fingerprint_payload(sdp)?; let payload_b64 = general_purpose::URL_SAFE_NO_PAD.encode(payload); let signature = general_purpose::URL_SAFE_NO_PAD .decode(signature) .map_err(|error| format!("invalid fingerprint signature: {error}"))?; let signature = Signature::from_slice(&signature) .map_err(|error| format!("invalid ES384 signature: {error}"))?; let verifying_key = pumpkin_util::p384::ecdsa::VerifyingKey::from(public_key); verifying_key .verify(format!("{header}.{payload_b64}").as_bytes(), &signature) .map_err(|_| "fingerprint signature verification failed".to_string()) } fn add_server_identity(sdp: &str, key: &SigningKey) -> Result { let public_key = PublicKey::from(key.verifying_key()); let public_der = public_key .to_public_key_der() .map_err(|error| error.to_string())?; let public_key = general_purpose::STANDARD.encode(public_der.as_bytes()); let now = unix_time(); let token = sign_jws( key, &json!({"alg": "ES384", "x5u": public_key}), &json!({"exp": now + 60, "iat": now, "cpk": public_key}), )?; let fingerprints = sign_detached(key, fingerprint_payload(sdp)?); let assertion = serde_json::to_string(&json!({ "fingerprints": fingerprints, "token": token, })) .map_err(|error| error.to_string())?; let identity = json!({ "assertion": assertion, "idp": {"domain": "self", "protocol": "default"}, }); let identity = general_purpose::STANDARD .encode(serde_json::to_vec(&identity).map_err(|error| error.to_string())?); let marker = "m=application"; let position = sdp .find(marker) .ok_or_else(|| "answer SDP has no application section".to_string())?; let mut answer = String::with_capacity(sdp.len() + identity.len() + 14); answer.push_str(&sdp[..position]); answer.push_str("a=identity:"); answer.push_str(&identity); answer.push_str("\r\n"); answer.push_str(&sdp[position..]); Ok(answer) } fn fingerprint_payload(sdp: &str) -> Result, String> { let fingerprints = sdp .lines() .filter_map(|line| line.strip_prefix("a=fingerprint:")) .map(|fingerprint| { let (algorithm, digest) = fingerprint .split_once(' ') .ok_or_else(|| "malformed DTLS fingerprint".to_string())?; Ok(json!({"algorithm": algorithm, "digest": digest})) }) .collect::, String>>()?; if fingerprints.is_empty() { return Err("SDP has no DTLS fingerprint".to_string()); } serde_json::to_vec(&json!({ "fingerprint": fingerprints, })) .map_err(|error| error.to_string()) } fn sign_jws(key: &SigningKey, header: &Value, payload: &Value) -> Result { let header = general_purpose::URL_SAFE_NO_PAD .encode(serde_json::to_vec(header).map_err(|error| error.to_string())?); let payload = general_purpose::URL_SAFE_NO_PAD .encode(serde_json::to_vec(payload).map_err(|error| error.to_string())?); let input = format!("{header}.{payload}"); let signature: Signature = key.sign(input.as_bytes()); Ok(format!( "{input}.{}", general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes()) )) } fn sign_detached(key: &SigningKey, payload: Vec) -> String { let header = general_purpose::URL_SAFE_NO_PAD.encode(b"{\"alg\":\"ES384\"}"); let payload = general_purpose::URL_SAFE_NO_PAD.encode(payload); let signature: Signature = key.sign(format!("{header}.{payload}").as_bytes()); format!( "{header}..{}", general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes()) ) } fn unix_time() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64 } #[cfg(test)] mod tests { use super::*; #[test] fn fragments_round_trip() { let mut fragments = FragmentBuffer::default(); assert!(fragments.push(2, b"one").unwrap().is_none()); assert!(fragments.push(1, b"two").unwrap().is_none()); assert_eq!(fragments.push(0, b"three").unwrap().unwrap(), "onetwothree"); } #[test] fn outbound_payloads_are_split_at_the_nethernet_limit() { let payload = vec![0; MAX_FRAGMENT_SIZE + 1]; let chunks = payload.chunks(MAX_FRAGMENT_SIZE).collect::>(); assert_eq!(chunks.len(), 2); assert_eq!(chunks[0].len(), 10_000); assert_eq!(chunks[1].len(), 1); } #[test] fn rejects_out_of_order_fragments_and_recovers() { let mut fragments = FragmentBuffer::default(); assert!(fragments.push(2, b"one").unwrap().is_none()); assert!(fragments.push(0, b"three").is_err()); assert_eq!(fragments.push(0, b"complete").unwrap().unwrap(), "complete"); } #[test] fn server_identity_is_valid_and_verifiable() { let key = SigningKey::from_slice(&[7; 48]).unwrap(); let sdp = "v=0\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=fingerprint:sha-256 AA:BB\r\n"; let answer = add_server_identity(sdp, &key).unwrap(); let (_, public_key) = verify_and_strip_identity(&answer, None).unwrap(); assert_eq!(public_key, PublicKey::from(key.verifying_key())); } #[test] fn fingerprint_payload_contains_every_sdp_fingerprint() { let sdp = "v=0\r\na=fingerprint:sha-256 AA:BB\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=fingerprint:sha-384 CC:DD\r\n"; assert_eq!( fingerprint_payload(sdp).unwrap(), br#"{"fingerprint":[{"algorithm":"sha-256","digest":"AA:BB"},{"algorithm":"sha-384","digest":"CC:DD"}]}"#, ); } #[test] fn data_channel_sdp_only_advertises_component_one() { let sdp = "v=0\r\na=candidate:1 1 udp 1 192.0.2.1 19134 typ host\r\na=candidate:1 2 udp 1 192.0.2.1 19134 typ host\r\na=end-of-candidates\r\n"; assert_eq!( remove_component_two_candidates(sdp), "v=0\r\na=candidate:1 1 udp 1 192.0.2.1 19134 typ host\r\na=end-of-candidates\r\n" ); } #[test] fn summarizes_candidates_without_credentials() { let sdp = "a=ice-ufrag:secret\r\na=ice-pwd:also-secret\r\n\ a=candidate:123 1 udp 2130706431 192.0.2.1 19132 typ host\r\n"; assert_eq!(candidate_summary(sdp), ["123/1 udp 192.0.2.1:19132 host"]); } #[test] fn configured_oidc_validation_rejects_untrusted_identity_tokens() { let key = SigningKey::from_slice(&[7; 48]).unwrap(); let sdp = "v=0\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=fingerprint:sha-256 AA:BB\r\n"; let answer = add_server_identity(sdp, &key).unwrap(); let verifier = ( "https://issuer.example".to_string(), Jwks { keys: Vec::new() }, ); assert!(verify_and_strip_identity(&answer, Some(&verifier)).is_err()); } #[test] fn offline_mode_accepts_an_unverified_identity_provider() { let key = SigningKey::from_slice(&[7; 48]).unwrap(); let sdp = "v=0\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=fingerprint:sha-256 AA:BB\r\n"; let answer = add_server_identity(sdp, &key).unwrap(); let encoded = answer .lines() .find_map(|line| line.strip_prefix("a=identity:")) .unwrap(); let mut identity: Value = serde_json::from_slice(&general_purpose::STANDARD.decode(encoded).unwrap()).unwrap(); identity["idp"] = json!({"domain": "", "protocol": "offline"}); let offline_identity = general_purpose::STANDARD.encode(serde_json::to_vec(&identity).unwrap()); let offer = answer.replace(encoded, &offline_identity); verify_and_strip_identity(&offer, None).unwrap(); let verifier = ( "https://issuer.example".to_string(), Jwks { keys: Vec::new() }, ); assert_eq!( verify_and_strip_identity(&offer, Some(&verifier)).unwrap_err(), "invalid identity provider" ); } #[test] fn offline_mode_accepts_an_offer_without_identity() { let offer = "v=0\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n"; let (offer, public_key) = authenticate_client_offer(offer, false, None).unwrap(); assert_eq!( offer, "v=0\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" ); assert!(public_key.is_none()); } #[test] fn online_mode_rejects_an_offer_without_identity() { let error = authenticate_client_offer("v=0\r\n", true, None).unwrap_err(); assert_eq!(error, "SDP offer is missing its identity assertion"); } }