use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::error::{CoreError, Result}; const MAX_MANIFEST_JSON_BYTES: usize = 128 * 1024; const MAX_CREDENTIAL_JSON_BYTES: usize = 196 * 1024; const MAX_ADMISSION_JSON_BYTES: usize = 16 * 1024; fn decode_strict(bytes: &[u8], maximum: usize) -> Result { if bytes.is_empty() || bytes.len() > maximum { return Err(CoreError::InvalidArgument); } serde_json::from_slice(bytes).map_err(|_| CoreError::InvalidArgument) } fn bounded(value: &str, minimum: usize, maximum: usize) -> bool { (minimum..=maximum).contains(&value.len()) } const fn base64url_value(value: u8) -> Option { match value { b'A'..=b'Z' => Some(value - b'A'), b'a'..=b'z' => Some(value - b'a' + 26), b'0'..=b'9' => Some(value - b'0' + 52), b'-' => Some(62), b'_' => Some(63), _ => None, } } const fn base64_value(value: u8) -> Option { match value { b'A'..=b'Z' => Some(value - b'A'), b'a'..=b'z' => Some(value - b'a' + 26), b'0'..=b'9' => Some(value - b'0' + 52), b'+' => Some(62), b'/' => Some(63), _ => None, } } fn valid_base64(value: &str) -> bool { let bytes = value.as_bytes(); if bytes.is_empty() || !bytes.len().is_multiple_of(4) { return false; } let data_length = bytes .iter() .position(|byte| *byte == b'=') .unwrap_or(bytes.len()); let padding = bytes.len() - data_length; if data_length == 0 || padding > 2 || !bytes[..data_length] .iter() .all(|byte| base64_value(*byte).is_some()) || !bytes[data_length..].iter().all(|byte| *byte == b'=') { return false; } match padding { 0 => true, 1 => base64_value(bytes[data_length - 1]).is_some_and(|value| value.trailing_zeros() >= 2), 2 => base64_value(bytes[data_length - 1]).is_some_and(|value| value.trailing_zeros() >= 4), _ => false, } } fn certificate_only_pem(value: &str) -> bool { let mut lines = value.lines().peekable(); let mut blocks = 0_u32; loop { while lines.next_if(|line| line.trim().is_empty()).is_some() {} let Some(begin) = lines.next() else { return blocks > 0; }; if begin != "-----BEGIN CERTIFICATE-----" { return false; } blocks += 1; let mut body = String::new(); let mut complete = false; for line in lines.by_ref() { if line == "-----END CERTIFICATE-----" { complete = true; break; } if line.is_empty() || line.trim() != line { return false; } body.push_str(line); } if !complete || !valid_base64(&body) { return false; } } } fn raw_base64url_decoded_len(value: &str) -> Option { let bytes = value.as_bytes(); if bytes.is_empty() || bytes.iter().any(|byte| base64url_value(*byte).is_none()) { return None; } let remainder_bytes = match bytes.len() % 4 { 0 => 0, 2 if base64url_value(*bytes.last()?)?.trailing_zeros() >= 4 => 1, 3 if base64url_value(*bytes.last()?)?.trailing_zeros() >= 2 => 2, _ => return None, }; bytes .len() .checked_div(4)? .checked_mul(3)? .checked_add(remainder_bytes) } #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] struct Timestamp { year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32, } pub(crate) fn now_utc() -> Result { system_time_utc(SystemTime::now()) } fn system_time_utc(now: SystemTime) -> Result { let seconds = now .duration_since(UNIX_EPOCH) .map_err(|_| CoreError::InvalidArgument)? .as_secs(); let days = seconds / 86_400; let day_seconds = seconds % 86_400; let shifted = days .checked_add(719_468) .ok_or(CoreError::InvalidArgument)?; let era = shifted / 146_097; let day_of_era = shifted % 146_097; let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; let mut year = year_of_era + era * 400; let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let month_prime = (5 * day_of_year + 2) / 153; let day = day_of_year - (153 * month_prime + 2) / 5 + 1; let month = if month_prime < 10 { month_prime + 3 } else { month_prime - 9 }; if month <= 2 { year += 1; } if year > 9_999 { return Err(CoreError::InvalidArgument); } let hour = day_seconds / 3_600; let minute = (day_seconds % 3_600) / 60; let second = day_seconds % 60; Ok(format!( "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z" )) } fn timestamp(value: &str, exact_seconds: bool) -> Option { let bytes = value.as_bytes(); if bytes.len() < 20 || bytes.len() > 30 || bytes[4] != b'-' || bytes[7] != b'-' || bytes[10] != b'T' || bytes[13] != b':' || bytes[16] != b':' || *bytes.last()? != b'Z' { return None; } let digits = |start: usize, end: usize| { bytes .get(start..end)? .iter() .try_fold(0_u32, |number, byte| { byte.is_ascii_digit() .then_some(number * 10 + u32::from(*byte - b'0')) }) }; let year = u16::try_from(digits(0, 4)?).ok()?; let month = u8::try_from(digits(5, 7)?).ok()?; let day = u8::try_from(digits(8, 10)?).ok()?; let hour = u8::try_from(digits(11, 13)?).ok()?; let minute = u8::try_from(digits(14, 16)?).ok()?; let second = u8::try_from(digits(17, 19)?).ok()?; if hour > 23 || minute > 59 || second > 59 { return None; } let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); let maximum_day = match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if leap => 29, 2 => 28, _ => return None, }; if day == 0 || day > maximum_day { return None; } let nanosecond = if bytes.len() == 20 { 0 } else { if exact_seconds || bytes[19] != b'.' { return None; } let fraction = bytes.get(20..bytes.len() - 1)?; if fraction.is_empty() || fraction.len() > 9 || !fraction.iter().all(u8::is_ascii_digit) || *fraction.last()? == b'0' { return None; } let mut value = fraction .iter() .fold(0_u32, |number, byte| number * 10 + u32::from(*byte - b'0')); for _ in fraction.len()..9 { value *= 10; } value }; Some(Timestamp { year, month, day, hour, minute, second, nanosecond, }) } fn valid_dns_name(value: &str) -> bool { bounded(value, 1, 253) && value.parse::().is_err() && !uuid_shaped(value) && value.split('.').all(|label| { bounded(label, 1, 63) && !label.starts_with('-') && !label.ends_with('-') && label .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') }) } fn uuid_shaped(value: &str) -> bool { value.len() == 36 && value.bytes().enumerate().all(|(index, byte)| match index { 8 | 13 | 18 | 23 => byte == b'-', _ => byte.is_ascii_hexdigit(), }) } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct CapabilityProfile { transport: String, framing: String, media: String, audio: String, source_rate_control: String, client_decode: Vec, } impl CapabilityProfile { /// Creates and validates an RC5 capability profile. /// /// # Errors /// /// Returns `invalid_argument` when a field violates RC5 bounds or registry values. pub fn new( transport: &str, framing: &str, media: &str, audio: &str, source_rate_control: &str, client_decode: Vec, ) -> Result { let profile = Self { transport: transport.to_owned(), framing: framing.to_owned(), media: media.to_owned(), audio: audio.to_owned(), source_rate_control: source_rate_control.to_owned(), client_decode, }; profile.validate()?; Ok(profile) } fn validate(&self) -> Result<()> { if !bounded(&self.transport, 1, 64) || !matches!(self.framing.as_str(), "datagram-v1" | "datagram-v2") || !bounded(&self.media, 1, 64) || !bounded(&self.audio, 1, 64) || !bounded(&self.source_rate_control, 1, 64) || !(1..=2).contains(&self.client_decode.len()) || self .client_decode .iter() .any(|value| !matches!(value.as_str(), "h264-opus" | "hevc-opus")) || self.client_decode.len() != self .client_decode .iter() .collect::>() .len() { return Err(CoreError::InvalidArgument); } Ok(()) } fn is_subset_of(&self, offered: &Self) -> bool { self.transport == offered.transport && self.framing == offered.framing && self.media == offered.media && self.audio == offered.audio && self.source_rate_control == offered.source_rate_control && self .client_decode .iter() .all(|codec| offered.client_decode.contains(codec)) } } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ManifestGateway { id: String, addresses: Vec, public_identity: String, } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ManifestTunnel { versions: Vec, features: Vec, } #[allow(clippy::struct_field_names)] #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ManifestBounds { minimum_kbps: u64, target_kbps: u64, maximum_kbps: u64, } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct DisplayMode { resolution_width: u16, resolution_height: u16, fps: u16, } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ManifestProfile { id: String, bounds: ManifestBounds, display_mode: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] struct GrantReference { opaque_value: String, expires_at: String, audience: String, } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct ConnectionManifest { version: String, purpose: String, session_id: String, reconnect_sequence: u64, gateway: ManifestGateway, tunnel: ManifestTunnel, profile: ManifestProfile, grant: GrantReference, correlation_id: String, } impl ConnectionManifest { /// Strictly decodes and validates an RC5 connection manifest. /// /// # Errors /// /// Returns `invalid_argument` for malformed, duplicate, trailing, unknown, or invalid data. pub fn decode(bytes: &[u8]) -> Result { let manifest: Self = decode_strict(bytes, MAX_MANIFEST_JSON_BYTES)?; manifest.validate()?; Ok(manifest) } fn validate(&self) -> Result<()> { let bounds = &self.profile.bounds; let valid_display = self.profile.display_mode.as_ref().is_none_or(|mode| { (320..=16_384).contains(&mode.resolution_width) && (200..=8_640).contains(&mode.resolution_height) && (1..=240).contains(&mode.fps) }); if self.version != "1" || !matches!(self.purpose.as_str(), "launch" | "reconnect") || !bounded(&self.session_id, 1, 128) || !bounded(&self.gateway.id, 1, 128) || !(1..=4).contains(&self.gateway.addresses.len()) || self .gateway .addresses .iter() .any(|address| !bounded(address, 1, 256)) || !valid_dns_name(&self.gateway.public_identity) || self.gateway.public_identity == self.gateway.id || !(1..=4).contains(&self.tunnel.versions.len()) || self .tunnel .versions .iter() .any(|version| !bounded(version, 1, 64)) || self.tunnel.features.len() > 32 || self .tunnel .features .iter() .any(|feature| !bounded(feature, 1, 64)) || !bounded(&self.profile.id, 1, 128) || !(1..=100_000_000).contains(&bounds.minimum_kbps) || !(1..=100_000_000).contains(&bounds.target_kbps) || !(1..=100_000_000).contains(&bounds.maximum_kbps) || bounds.minimum_kbps > bounds.target_kbps || bounds.target_kbps > bounds.maximum_kbps || !valid_display || !bounded(&self.grant.opaque_value, 43, 256) || timestamp(&self.grant.expires_at, false).is_none() || !bounded(&self.grant.audience, 1, 128) || !bounded(&self.correlation_id, 1, 128) { return Err(CoreError::InvalidArgument); } Ok(()) } /// Checks expiry and the supported tunnel binding at a supplied UTC instant. /// /// # Errors /// /// Returns `invalid_argument` for unsupported input or `expired` for an expired grant. pub fn validate_at(&self, now_utc: &str) -> Result<()> { let now = timestamp(now_utc, false).ok_or(CoreError::InvalidArgument)?; if timestamp(&self.grant.expires_at, false).ok_or(CoreError::InvalidArgument)? <= now { return Err(CoreError::Expired); } if !self .tunnel .versions .iter() .any(|value| value == "verse-gateway-v1/1") || !self .tunnel .features .iter() .any(|value| value == "control.v1") { return Err(CoreError::InvalidArgument); } Ok(()) } pub(crate) fn addresses(&self) -> &[String] { &self.gateway.addresses } pub(crate) fn public_identity(&self) -> &str { &self.gateway.public_identity } pub(crate) fn features(&self) -> &[String] { &self.tunnel.features } pub(crate) fn admission( &self, client_nonce: String, device_signature: String, capabilities: CapabilityProfile, ) -> Result { let request = TunnelAdmissionRequest { version: "1".to_owned(), session_id: self.session_id.clone(), gateway_id: self.gateway.id.clone(), audience: self.grant.audience.clone(), grant: self.grant.opaque_value.clone(), reconnect_sequence: self.reconnect_sequence, client_nonce, device_signature, capabilities, }; request.validate()?; Ok(request) } } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct NativeTunnelCredential { client_device_id: String, device_key_id: String, certificate_chain_pem: String, trust_bundle_pem: String, expires_at: String, } impl NativeTunnelCredential { /// Strictly decodes and validates an RC5 native tunnel credential. /// /// # Errors /// /// Returns `invalid_argument` for malformed, unknown, or out-of-bound data. pub fn decode(bytes: &[u8]) -> Result { let credential: Self = decode_strict(bytes, MAX_CREDENTIAL_JSON_BYTES)?; if !bounded(&credential.client_device_id, 1, 128) || !bounded(&credential.device_key_id, 1, 128) || !bounded(&credential.certificate_chain_pem, 1, 65_536) || !bounded(&credential.trust_bundle_pem, 1, 65_536) || !certificate_only_pem(&credential.certificate_chain_pem) || !certificate_only_pem(&credential.trust_bundle_pem) || timestamp(&credential.expires_at, false).is_none() { return Err(CoreError::InvalidArgument); } Ok(credential) } /// Checks credential expiry at a supplied UTC instant. /// /// # Errors /// /// Returns `invalid_argument` for an invalid instant or `expired` after expiry. pub fn validate_at(&self, now_utc: &str) -> Result<()> { let now = timestamp(now_utc, false).ok_or(CoreError::InvalidArgument)?; if timestamp(&self.expires_at, false).ok_or(CoreError::InvalidArgument)? <= now { return Err(CoreError::Expired); } Ok(()) } pub(crate) fn certificate_chain_pem(&self) -> &str { &self.certificate_chain_pem } pub(crate) fn trust_bundle_pem(&self) -> &str { &self.trust_bundle_pem } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct TunnelAdmissionRequest { version: String, session_id: String, gateway_id: String, audience: String, grant: String, reconnect_sequence: u64, client_nonce: String, device_signature: String, capabilities: CapabilityProfile, } impl TunnelAdmissionRequest { /// Strictly decodes and validates an RC5 tunnel admission request. /// /// # Errors /// /// Returns `invalid_argument` for malformed, unknown, or out-of-bound data. pub fn decode(bytes: &[u8]) -> Result { let request: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?; request.validate()?; Ok(request) } fn validate(&self) -> Result<()> { if self.version != "1" || !bounded(&self.session_id, 1, 128) || !bounded(&self.gateway_id, 1, 128) || !bounded(&self.audience, 1, 256) || !bounded(&self.grant, 43, 256) || !bounded(&self.client_nonce, 16, 128) || self.device_signature.len() != 86 || !matches!(raw_base64url_decoded_len(&self.client_nonce), Some(12..=96)) || raw_base64url_decoded_len(&self.device_signature) != Some(64) { return Err(CoreError::InvalidArgument); } self.capabilities.validate()?; Ok(()) } #[must_use] pub fn admission_transcript(&self) -> Vec { let reconnect_sequence = self.reconnect_sequence.to_string(); let decode_count = self.capabilities.client_decode.len().to_string(); let mut fields = vec![ self.session_id.as_str(), self.gateway_id.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.client_nonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.source_rate_control.as_str(), decode_count.as_str(), ]; fields.extend(self.capabilities.client_decode.iter().map(String::as_str)); let mut transcript = String::from("versevdi/tunnel-admission/v1"); for field in fields { transcript.push_str(&field.len().to_string()); transcript.push(':'); transcript.push_str(field); } transcript.into_bytes() } } #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct ClientSessionAuthority { version: String, session_id: String, gateway_id: String, audience: String, reconnect_sequence: u64, expires_at: String, capabilities: CapabilityProfile, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct StableError { version: String, code: String, message: String, retryable: bool, } pub(crate) struct DecodedStableError { pub(crate) error: CoreError, pub(crate) code: String, pub(crate) retryable: bool, } impl ClientSessionAuthority { /// Strictly decodes and validates a provider-free RC5 client authority. /// /// # Errors /// /// Returns `invalid_argument` for malformed, unknown, provider-shaped, or invalid data. pub fn decode(bytes: &[u8]) -> Result { let authority: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?; if authority.version != "1" || !bounded(&authority.session_id, 1, 128) || !bounded(&authority.gateway_id, 1, 128) || !bounded(&authority.audience, 1, 256) || timestamp(&authority.expires_at, true).is_none() { return Err(CoreError::InvalidArgument); } authority.capabilities.validate()?; Ok(authority) } /// Checks session, gateway, audience, reconnect, expiry, and capability bindings. /// /// # Errors /// /// Returns `invalid_argument` for an invalid clock value or `authority_rejected` on mismatch. pub fn validate_binding( &self, manifest: &ConnectionManifest, offered: &CapabilityProfile, now_utc: &str, ) -> Result<()> { let now = timestamp(now_utc, false).ok_or(CoreError::InvalidArgument)?; let expires = timestamp(&self.expires_at, true).ok_or(CoreError::AuthorityRejected)?; let grant_expires = timestamp(&manifest.grant.expires_at, false).ok_or(CoreError::AuthorityRejected)?; if self.session_id != manifest.session_id || self.gateway_id != manifest.gateway.id || self.audience != manifest.grant.audience || self.reconnect_sequence != manifest.reconnect_sequence || expires <= now || expires > grant_expires || !self.capabilities.is_subset_of(offered) { return Err(CoreError::AuthorityRejected); } Ok(()) } #[must_use] pub fn session_id(&self) -> &str { &self.session_id } } pub(crate) fn decode_stable_error(bytes: &[u8]) -> Result { let stable: StableError = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?; if stable.version != "1" || !bounded(&stable.code, 1, 128) || !bounded(&stable.message, 1, 512) { return Err(CoreError::Protocol); } let error = match stable.code.as_str() { "expired_grant" => CoreError::Expired, "admission_rejected" | "gateway_draining" | "invalid_authority" | "no_capability_overlap" | "wrong_gateway" | "provider_work_unavailable" | "clipboard_audit_unavailable" => CoreError::AuthorityRejected, "provider_identity_rejected" | "provider_malformed" | "provider_timeout" | "provider_unavailable" | "provider_state_unavailable" => CoreError::Transport, "invalid_hello" => CoreError::Protocol, _ => return Err(CoreError::Protocol), }; Ok(DecodedStableError { error, code: stable.code, retryable: stable.retryable, }) } #[cfg(test)] mod stable_error_tests { use super::{decode_stable_error, system_time_utc}; use crate::error::CoreError; use std::time::{Duration, UNIX_EPOCH}; #[test] fn system_clock_conversion_is_exact_at_epoch_and_leap_day() { assert_eq!( system_time_utc(UNIX_EPOCH).as_deref(), Ok("1970-01-01T00:00:00Z") ); assert_eq!( system_time_utc(UNIX_EPOCH + Duration::from_secs(1_709_251_199)).as_deref(), Ok("2024-02-29T23:59:59Z") ); } #[test] fn stable_error_uses_exact_rc5_bounds_and_preserves_retryability() { let message = "m".repeat(512); let bytes = serde_json::to_vec(&serde_json::json!({ "version": "1", "code": "gateway_draining", "message": message, "retryable": true, })) .expect("encode stable error"); let decoded = decode_stable_error(&bytes).expect("RC5 stable error"); assert_eq!(decoded.error, CoreError::AuthorityRejected); assert!(decoded.retryable); for invalid in [ serde_json::json!({"version":"1","code":"gateway_draining","message":"","retryable":true}), serde_json::json!({"version":"1","code":"c".repeat(129),"message":"m","retryable":true}), serde_json::json!({"version":"1","code":"gateway_draining","message":"m".repeat(513),"retryable":true}), serde_json::json!({"version":"1","code":"unknown","message":"m","retryable":true}), ] { assert_eq!( decode_stable_error(&serde_json::to_vec(&invalid).expect("encode invalid")).err(), Some(CoreError::Protocol) ); } } }