From 563f5156e6dded64e912e895f3b8380955a1dd51 Mon Sep 17 00:00:00 2001 From: ghost Date: Mon, 29 Dec 2025 02:35:42 +0100 Subject: [PATCH] Modified message validation to allow nonzero MI bytes. --- src/auth.rs | 55 +++++++++++++++++++++++++++++-- src/stun.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 1c4f745..d9ad55e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,10 +5,15 @@ use crate::constants::{ATTR_NONCE, ATTR_REALM, ATTR_USERNAME}; use crate::models::stun::StunMessage; use crate::stun::{ compute_message_integrity_adjusted, + compute_message_integrity_adjusted_nozero, compute_message_integrity_full, + compute_message_integrity_full_nozero, compute_message_integrity_len_preserved as compute_mi_len_preserved, + compute_message_integrity_len_preserved_nozero, find_message_integrity, validate_message_integrity, + validate_message_integrity_len_preserved_nozero, + validate_message_integrity_nozero, validate_message_integrity_len_preserved, }; use crate::traits::CredentialStore; @@ -233,6 +238,17 @@ impl AuthManager { return AuthStatus::Granted { username, key }; } + // Interop: some clients appear to compute MESSAGE-INTEGRITY without zeroing the MI bytes. + if validate_message_integrity_nozero(msg, &key) + || validate_message_integrity_len_preserved_nozero(msg, &key) + { + warn!( + "auth accept via MI nozero username={} realm={} peer={} (interop)", + username, realm, peer + ); + return AuthStatus::Granted { username, key }; + } + // Workaround: also accept short-term style (raw password as key) for test clients like turnutils_uclient. let short_key = password.as_bytes(); if validate_message_integrity(msg, short_key) @@ -245,6 +261,19 @@ impl AuthManager { }; } + if validate_message_integrity_nozero(msg, short_key) + || validate_message_integrity_len_preserved_nozero(msg, short_key) + { + warn!( + "auth accept via short-term nozero username={} realm={} peer={} (interop)", + username, realm, peer + ); + return AuthStatus::Granted { + username, + key: short_key.to_vec(), + }; + } + // Additional interop fallback: some clients miscompute length when adding FINGERPRINT; // try validation without adjusting the header length. if validate_message_integrity_len_preserved(msg, &key) { @@ -312,21 +341,33 @@ impl AuthManager { let mi_attr = find_message_integrity(msg).map(|a| hex::encode(&a.value)); let mi_long_adj = compute_message_integrity_adjusted(msg, &key).map(hex::encode); let mi_long_len = compute_mi_len_preserved(msg, &key).map(hex::encode); + let mi_long_adj_nozero = compute_message_integrity_adjusted_nozero(msg, &key).map(hex::encode); + let mi_long_len_nozero = compute_message_integrity_len_preserved_nozero(msg, &key).map(hex::encode); let mi_short_adj = compute_message_integrity_adjusted(msg, short_key).map(hex::encode); let mi_short_len = compute_mi_len_preserved(msg, short_key).map(hex::encode); + let mi_short_adj_nozero = compute_message_integrity_adjusted_nozero(msg, short_key).map(hex::encode); + let mi_short_len_nozero = compute_message_integrity_len_preserved_nozero(msg, short_key).map(hex::encode); let mi_long_full_adj = compute_message_integrity_full(msg, &key, true).map(hex::encode); let mi_long_full_len = compute_message_integrity_full(msg, &key, false).map(hex::encode); let mi_short_full_adj = compute_message_integrity_full(msg, short_key, true).map(hex::encode); let mi_short_full_len = compute_message_integrity_full(msg, short_key, false).map(hex::encode); + let mi_long_full_adj_nozero = compute_message_integrity_full_nozero(msg, &key, true, false).map(hex::encode); + let mi_long_full_adj_nozero_zfp = compute_message_integrity_full_nozero(msg, &key, true, true).map(hex::encode); + let mi_short_full_adj_nozero = compute_message_integrity_full_nozero(msg, short_key, true, false).map(hex::encode); + let mi_short_full_adj_nozero_zfp = compute_message_integrity_full_nozero(msg, short_key, true, true).map(hex::encode); // Accept if any variant matches received MI (still requires correct key). if let Some(mi_attr_val) = find_message_integrity(msg) { let mi_bytes = &mi_attr_val.value; - let variants: [(&str, Option>); 8] = [ + let variants: [(&str, Option>); 12] = [ ("long_adj", compute_message_integrity_adjusted(msg, &key)), ("long_len", compute_mi_len_preserved(msg, &key)), + ("long_adj_nozero", compute_message_integrity_adjusted_nozero(msg, &key)), + ("long_len_nozero", compute_message_integrity_len_preserved_nozero(msg, &key)), ("short_adj", compute_message_integrity_adjusted(msg, short_key)), ("short_len", compute_mi_len_preserved(msg, short_key)), + ("short_adj_nozero", compute_message_integrity_adjusted_nozero(msg, short_key)), + ("short_len_nozero", compute_message_integrity_len_preserved_nozero(msg, short_key)), ("long_full_adj", compute_message_integrity_full(msg, &key, true)), ("long_full_len", compute_message_integrity_full(msg, &key, false)), ("short_full_adj", compute_message_integrity_full(msg, short_key, true)), @@ -344,7 +385,7 @@ impl AuthManager { } warn!( - "auth reject: bad credentials username={} realm={} peer={} a1_md5={} mi_attr={:?} mi_long_adj={:?} mi_long_len={:?} mi_short_adj={:?} mi_short_len={:?} mi_long_full_adj={:?} mi_long_full_len={:?} mi_short_full_adj={:?} mi_short_full_len={:?}", + "auth reject: bad credentials username={} realm={} peer={} a1_md5={} mi_attr={:?} mi_long_adj={:?} mi_long_len={:?} mi_long_adj_nozero={:?} mi_long_len_nozero={:?} mi_short_adj={:?} mi_short_len={:?} mi_short_adj_nozero={:?} mi_short_len_nozero={:?} mi_long_full_adj={:?} mi_long_full_len={:?} mi_short_full_adj={:?} mi_short_full_len={:?} mi_long_full_adj_nozero={:?} mi_long_full_adj_nozero_zfp={:?} mi_short_full_adj_nozero={:?} mi_short_full_adj_nozero_zfp={:?}", username, realm, peer, @@ -352,12 +393,20 @@ impl AuthManager { mi_attr, mi_long_adj, mi_long_len, + mi_long_adj_nozero, + mi_long_len_nozero, mi_short_adj, mi_short_len, + mi_short_adj_nozero, + mi_short_len_nozero, mi_long_full_adj, mi_long_full_len, mi_short_full_adj, - mi_short_full_len + mi_short_full_len, + mi_long_full_adj_nozero, + mi_long_full_adj_nozero_zfp, + mi_short_full_adj_nozero, + mi_short_full_adj_nozero_zfp ); AuthStatus::Reject { code: 401, diff --git a/src/stun.rs b/src/stun.rs index b559813..aed7fd8 100644 --- a/src/stun.rs +++ b/src/stun.rs @@ -458,6 +458,100 @@ pub fn compute_message_integrity_full(msg: &StunMessage, key: &[u8], adjust_len: Some(crate::stun::compute_message_integrity(key, &signed)) } +/// Compute MESSAGE-INTEGRITY over the message up to end-of-MI, but **without** zeroing the MI bytes. +/// +/// This is non-standard, but some clients have been observed to sign the message as-is. +pub fn compute_message_integrity_adjusted_nozero(msg: &StunMessage, key: &[u8]) -> Option> { + let mi = find_message_integrity(msg)?; + if mi.value.len() != HMAC_SHA1_LEN { + return None; + } + let mi_end = mi.offset + 4 + HMAC_SHA1_LEN; + if mi_end > msg.raw.len() { + return None; + } + + let mut signed = msg.raw[..mi_end].to_vec(); + let len = (mi_end - 20) as u16; + signed[2..4].copy_from_slice(&len.to_be_bytes()); + + Some(crate::stun::compute_message_integrity(key, &signed)) +} + +/// Compute MESSAGE-INTEGRITY up to end-of-MI without adjusting length and without zeroing MI bytes. +pub fn compute_message_integrity_len_preserved_nozero(msg: &StunMessage, key: &[u8]) -> Option> { + let mi = find_message_integrity(msg)?; + if mi.value.len() != HMAC_SHA1_LEN { + return None; + } + let mi_end = mi.offset + 4 + HMAC_SHA1_LEN; + if mi_end > msg.raw.len() { + return None; + } + let signed = msg.raw[..mi_end].to_vec(); + Some(crate::stun::compute_message_integrity(key, &signed)) +} + +/// Compute MESSAGE-INTEGRITY over full raw message without zeroing MI bytes. +/// If `adjust_len` is true, the header length is set to `raw.len() - 20`, otherwise preserved. +/// If `zero_fingerprint` is true and a FINGERPRINT is present as last attr, the 4-byte FP value is zeroed +/// in the signed bytes (another observed interop quirk). +pub fn compute_message_integrity_full_nozero( + msg: &StunMessage, + key: &[u8], + adjust_len: bool, + zero_fingerprint: bool, +) -> Option> { + let mi = find_message_integrity(msg)?; + if mi.value.len() != HMAC_SHA1_LEN { + return None; + } + + let mut signed = msg.raw.clone(); + if adjust_len { + let len = (signed.len() - 20) as u16; + signed[2..4].copy_from_slice(&len.to_be_bytes()); + } + + if zero_fingerprint { + if let Some(fp) = find_fingerprint(msg) { + if fp.value.len() == 4 && fp.offset + 8 == msg.raw.len() { + // Zero just the 4-byte fingerprint value in the signed bytes. + let v = fp.offset + 4; + if v + 4 <= signed.len() { + signed[v..v + 4].fill(0); + } + } + } + } + + Some(crate::stun::compute_message_integrity(key, &signed)) +} + +pub fn validate_message_integrity_nozero(msg: &StunMessage, key: &[u8]) -> bool { + if let Some(mi) = find_message_integrity(msg) { + if mi.value.len() != HMAC_SHA1_LEN { + return false; + } + if let Some(c) = compute_message_integrity_adjusted_nozero(msg, key) { + return &c[..HMAC_SHA1_LEN] == mi.value.as_slice(); + } + } + false +} + +pub fn validate_message_integrity_len_preserved_nozero(msg: &StunMessage, key: &[u8]) -> bool { + if let Some(mi) = find_message_integrity(msg) { + if mi.value.len() != HMAC_SHA1_LEN { + return false; + } + if let Some(c) = compute_message_integrity_len_preserved_nozero(msg, key) { + return &c[..HMAC_SHA1_LEN] == mi.value.as_slice(); + } + } + false +} + /// Fallback validator: compute MESSAGE-INTEGRITY without adjusting the STUN header length. /// Some clients incorrectly leave the header length unchanged when appending FINGERPRINT; /// this matches that behaviour for interop.