use core::net::SocketAddr;
use core::ops::Deref;
use core::time::Duration;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use stun_types::attribute::{
pad_attribute_len, ErrorCode, MessageIntegrity, MessageIntegritySha256, Nonce,
PasswordAlgorithm, PasswordAlgorithmValue, PasswordAlgorithms, Realm, Userhash, Username,
};
use stun_types::message::{
IntegrityAlgorithm, IntegrityKey, LongTermCredentials, Message, MessageClass,
ShortTermCredentials, StunParseError, StunWriteError, ValidateError,
};
use stun_types::prelude::{
Attribute, AttributeExt, AttributeFromRaw, AttributeStaticType, MessageWrite, MessageWriteExt,
};
use sans_io_time::Instant;
use tracing::{debug, trace, warn};
use base64::prelude::*;
use hashbrown::HashMap;
use siphasher::sip::SipHasher;
#[derive(Debug, Default)]
pub struct ShortTermAuth {
credentials: Option<(ShortTermCredentials, IntegrityAlgorithm, IntegrityKey)>,
signature_bytes: usize,
}
impl ShortTermAuth {
pub fn new() -> Self {
Self::default()
}
pub fn set_credentials(
&mut self,
credentials: ShortTermCredentials,
algorithm: IntegrityAlgorithm,
) {
let key = credentials.make_key();
self.credentials = Some((credentials, algorithm, key));
self.signature_bytes = bytes_for_integrity(algorithm);
}
pub fn credentials(&self) -> Option<(&ShortTermCredentials, IntegrityAlgorithm)> {
self.credentials
.as_ref()
.map(|(creds, algo, _key)| (creds, *algo))
}
pub fn integrity_key(&self) -> Option<&IntegrityKey> {
self.credentials.as_ref().map(|(_creds, _algo, key)| key)
}
pub fn message_signature_bytes(&self) -> usize {
self.signature_bytes
}
#[tracing::instrument(skip(self, msg), err(Debug))]
pub fn sign_outgoing_message<W: MessageWrite>(
&mut self,
mut msg: W,
) -> Result<W, StunWriteError> {
if let Some((_creds, algo, key)) = self.credentials.as_ref() {
msg.add_message_integrity_with_key(key, *algo)?;
Ok(msg)
} else {
Ok(msg)
}
}
#[tracing::instrument(skip(self, msg), err(Debug))]
pub fn validate_incoming_message(
&mut self,
msg: &Message<'_>,
) -> Result<Option<IntegrityAlgorithm>, ValidateError> {
let Some((_creds, _algo, key)) = self.credentials.as_ref() else {
return Ok(None);
};
msg.validate_integrity_with_key(key).map(Some)
}
}
fn bytes_for_integrity(integrity: IntegrityAlgorithm) -> usize {
match integrity {
IntegrityAlgorithm::Sha1 => 24,
IntegrityAlgorithm::Sha256 => 36,
}
}
#[derive(Debug)]
pub enum LongTermClientValidation {
ResendRequest(Option<IntegrityAlgorithm>),
Validated(IntegrityAlgorithm),
}
#[derive(Debug, Clone, Copy, thiserror::Error, PartialEq, Eq)]
pub enum LongTermClientAuthErrorReason {
#[error("The message is not sufficiently authenticated")]
Unauthorized,
#[error("The message failed integrity checks")]
IntegrityFailed,
#[error("A required feature is not supported")]
UnsupportedFeature,
}
impl From<ValidateError> for LongTermClientAuthErrorReason {
fn from(_value: ValidateError) -> Self {
Self::IntegrityFailed
}
}
#[derive(Debug)]
pub struct LongTermClientAuthError {
reason: LongTermClientAuthErrorReason,
integrity: Option<IntegrityAlgorithm>,
}
impl LongTermClientAuthError {
pub fn reason(&self) -> LongTermClientAuthErrorReason {
self.reason
}
pub fn integrity(&self) -> Option<IntegrityAlgorithm> {
self.integrity
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
enum User {
Name(String),
Hash([u8; 32]),
}
#[derive(Debug)]
struct RequestAuth {
user: User,
realm: String,
nonce: String,
password_algos: smallvec::SmallVec<[PasswordAlgorithmValue; 2]>,
key: IntegrityKey,
algo: IntegrityAlgorithm,
}
#[derive(Debug, Default)]
enum AuthState {
#[default]
Initial,
Authenticating(RequestAuth),
Authenticated(RequestAuth),
}
impl AuthState {
fn auth(&self) -> Option<&RequestAuth> {
match self {
Self::Initial => None,
Self::Authenticating(auth) => Some(auth),
Self::Authenticated(auth) => Some(auth),
}
}
fn as_authenticated(&mut self) {
let mut old = Self::Initial;
core::mem::swap(&mut old, self);
*self = match old {
Self::Initial => Self::Initial,
Self::Authenticating(auth) | Self::Authenticated(auth) => Self::Authenticated(auth),
};
}
fn replace_nonce(&mut self, new_nonce: String, new_realm: String) {
match self {
Self::Initial => (),
Self::Authenticating(auth) => {
auth.nonce = new_nonce;
auth.realm = new_realm;
}
Self::Authenticated(auth) => {
auth.nonce = new_nonce;
auth.realm = new_realm;
}
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum Feature {
#[default]
Auto,
Required,
Disabled,
}
impl Feature {
fn possible(&self) -> bool {
matches!(self, Self::Auto | Self::Required)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
struct NonceSecurityBits {
bytes: [u8; 3],
}
impl NonceSecurityBits {
fn required(&self) -> bool {
self.bytes[0] != 0 || self.bytes[1] != 0 || self.bytes[2] != 0
}
fn from_nonce(bytes: &str) -> Option<Self> {
if !bytes.starts_with(LONG_TERM_RFC8489_NONCE_COOKIE) {
return None;
}
let security = &bytes[LONG_TERM_RFC8489_NONCE_COOKIE.len()..];
if security.len() < 4 {
return None;
}
let mut bytes = [0; 3];
let len = BASE64_STANDARD
.decode_slice(&security[..4], &mut bytes)
.ok()?;
if len != 3 {
return None;
}
Some(Self { bytes })
}
fn as_string(&self) -> String {
let mut ret = String::with_capacity(LONG_TERM_RFC8489_NONCE_COOKIE.len() + 4);
ret.push_str(LONG_TERM_RFC8489_NONCE_COOKIE);
BASE64_STANDARD.encode_string(self.bytes, &mut ret);
ret
}
fn password_algorithms(&self) -> bool {
(self.bytes[0] & 0x80) != 0
}
fn set_password_algorithms(&mut self, password_algorithms: bool) {
if password_algorithms {
self.bytes[0] |= 0x80;
} else {
self.bytes[0] &= !0x80;
}
}
fn username_anonymity(&self) -> bool {
(self.bytes[0] & 0x40) != 0
}
fn set_username_anonymity(&mut self, username_anonymity: bool) {
if username_anonymity {
self.bytes[0] |= 0x40;
} else {
self.bytes[0] &= !0x40;
}
}
}
#[derive(Debug)]
pub struct LongTermClientAuth {
credentials: Option<LongTermCredentials>,
auth: AuthState,
signature_bytes: usize,
supported_integrity: smallvec::SmallVec<[IntegrityAlgorithm; 2]>,
anonymous_username: Feature,
}
impl Default for LongTermClientAuth {
fn default() -> Self {
Self {
credentials: None,
auth: AuthState::Initial,
signature_bytes: 0,
supported_integrity: smallvec::SmallVec::from_iter([IntegrityAlgorithm::Sha1]),
anonymous_username: Feature::default(),
}
}
}
impl LongTermClientAuth {
pub fn new() -> Self {
Self::default()
}
pub fn set_credentials(&mut self, credentials: LongTermCredentials) {
self.credentials = Some(credentials);
}
pub fn credentials(&self) -> Option<&LongTermCredentials> {
self.credentials.as_ref()
}
pub fn supported_integrity(&self) -> &[IntegrityAlgorithm] {
&self.supported_integrity
}
pub fn add_supported_integrity(&mut self, integrity: IntegrityAlgorithm) {
if !self.supported_integrity.contains(&integrity) {
self.supported_integrity.push(integrity);
}
}
pub fn set_supported_integrity(&mut self, integrity: IntegrityAlgorithm) {
self.supported_integrity = smallvec::SmallVec::from_iter([integrity]);
}
pub fn set_anonymous_username(&mut self, anonymous: Feature) {
self.anonymous_username = anonymous;
}
pub fn anonymous_username(&mut self) -> Feature {
self.anonymous_username
}
pub fn message_signature_bytes(&self) -> usize {
self.signature_bytes
}
#[tracing::instrument(name = "client_sign_outgoing_message", skip(self, msg), err(Debug))]
pub fn sign_outgoing_message<W: MessageWrite>(
&mut self,
mut msg: W,
) -> Result<W, StunWriteError> {
if let Some(auth) = self.auth.auth() {
if msg.has_class(MessageClass::Request) {
msg.add_attribute(&Nonce::new(&auth.nonce)?)?;
msg.add_attribute(&Realm::new(&auth.realm)?)?;
match &auth.user {
User::Name(name) => msg.add_attribute(&Username::new(name)?)?,
User::Hash(hash) => msg.add_attribute(&Userhash::new(*hash))?,
}
}
if !auth.password_algos.is_empty() {
msg.add_attribute(&PasswordAlgorithms::new(&auth.password_algos))?;
}
if auth.algo != IntegrityAlgorithm::Sha1 || !auth.password_algos.is_empty() {
msg.add_attribute(&PasswordAlgorithm::new(match auth.algo {
IntegrityAlgorithm::Sha1 => PasswordAlgorithmValue::MD5,
IntegrityAlgorithm::Sha256 => PasswordAlgorithmValue::SHA256,
}))?;
}
msg.add_message_integrity_with_key(&auth.key, auth.algo)?;
Ok(msg)
} else {
Ok(msg)
}
}
#[tracing::instrument(name = "client_validate_incoming_message", skip(self, msg), err(Debug))]
pub fn validate_incoming_message(
&mut self,
msg: &Message<'_>,
) -> Result<LongTermClientValidation, LongTermClientAuthError> {
let ret = if let Some(auth) = self.auth.auth() {
msg.validate_integrity_with_key(&auth.key)
} else {
Err(ValidateError::IntegrityFailed)
};
if msg.is_response() {
if msg.has_class(MessageClass::Error) {
let mut realm = None;
let mut nonce = None;
let mut error_code = Err(StunParseError::MissingAttribute(ErrorCode::TYPE));
let mut password_algos = None;
for (_offset, attr) in msg.iter_attributes() {
match attr.get_type() {
Realm::TYPE => realm = Realm::from_raw(attr).ok(),
Nonce::TYPE => nonce = Nonce::from_raw(attr).ok(),
ErrorCode::TYPE => error_code = ErrorCode::from_raw(attr),
PasswordAlgorithms::TYPE => {
password_algos = PasswordAlgorithms::from_raw(attr).ok()
}
_ => (),
}
}
if let Ok(error_code) = error_code {
match error_code.code() {
ErrorCode::UNAUTHORIZED
if !matches!(self.auth, AuthState::Authenticated(_)) =>
{
if let Some(((realm, nonce), credentials)) =
realm.zip(nonce).zip(self.credentials.as_ref())
{
if let AuthState::Authenticating(auth) = &self.auth {
if auth.realm == realm.realm() && auth.nonce == nonce.nonce() {
return Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::Unauthorized,
integrity: None,
});
}
}
let (algo, anon) = if let Some(security) =
NonceSecurityBits::from_nonce(nonce.nonce())
{
let algo = if let Some(password_algos) = password_algos.as_ref()
{
let Some(algo) = password_algos
.algorithms()
.iter()
.map(|algo| algo.as_integrity())
.find(|algo| self.supported_integrity.contains(algo))
else {
trace!("No compatible password algorithms supported");
return Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::UnsupportedFeature,
integrity: None,
});
};
algo
} else if security.password_algorithms() {
trace!("nonce indicates password algorithms attribute that does not exist on message");
return Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::Unauthorized,
integrity: None,
});
} else {
IntegrityAlgorithm::Sha1
};
(
algo,
security.username_anonymity()
&& self.anonymous_username.possible(),
)
} else {
if matches!(self.anonymous_username, Feature::Required) {
trace!("nonce does not support anonymous username");
return Err(LongTermClientAuthError {
reason:
LongTermClientAuthErrorReason::UnsupportedFeature,
integrity: None,
});
}
if !self.supported_integrity.contains(&IntegrityAlgorithm::Sha1)
{
trace!("nonce does not support integrity other than Sha1");
return Err(LongTermClientAuthError {
reason:
LongTermClientAuthErrorReason::UnsupportedFeature,
integrity: None,
});
}
(IntegrityAlgorithm::Sha1, false)
};
self.signature_bytes = nonce.padded_len()
+ realm.padded_len()
+ bytes_for_integrity(algo);
let realm = realm.realm().to_string();
let key = credentials.to_key(realm.clone()).make_key(algo);
let username = credentials.username();
let user = if anon {
User::Hash(Userhash::compute(username, &realm))
} else {
User::Name(username.to_string())
};
let password_algos = if let Some(algos) = password_algos {
smallvec::SmallVec::from_slice(algos.algorithms())
} else {
smallvec::SmallVec::new()
};
self.auth = AuthState::Authenticating(RequestAuth {
user,
realm,
nonce: nonce.nonce().to_string(),
password_algos,
key,
algo,
});
trace!("retry request as credentials have changed");
return Ok(LongTermClientValidation::ResendRequest(ret.ok()));
} else {
return Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::Unauthorized,
integrity: None,
});
}
}
ErrorCode::STALE_NONCE => {
if let Some((new_nonce, new_realm)) = is_valid_stale_nonce(msg) {
self.auth.replace_nonce(
new_nonce.nonce().to_string(),
new_realm.realm().to_string(),
);
self.signature_bytes = 4
+ pad_attribute_len(new_nonce.padded_len())
+ 4
+ pad_attribute_len(new_realm.padded_len())
+ self
.auth
.auth()
.map(|auth| bytes_for_integrity(auth.algo))
.unwrap_or_default();
return Ok(LongTermClientValidation::ResendRequest(ret.ok()));
}
}
_ => (),
};
return ret.map(LongTermClientValidation::Validated).map_err(|e| {
LongTermClientAuthError {
reason: e.into(),
integrity: None,
}
});
}
} else if msg.has_class(MessageClass::Success) && ret.is_ok() {
self.auth.as_authenticated();
}
}
ret.map(LongTermClientValidation::Validated)
.map_err(|e| LongTermClientAuthError {
reason: e.into(),
integrity: None,
})
}
}
#[derive(Debug)]
struct ClientNonce {
expires_at: Instant,
value: String,
password_algorithms: smallvec::SmallVec<[PasswordAlgorithmValue; 2]>,
}
#[derive(Debug)]
struct ClientAuth {
credentials: LongTermCredentials,
keys: HashMap<IntegrityAlgorithm, IntegrityKey, RandomState>,
}
pub static LONG_TERM_RFC8489_NONCE_COOKIE: &str = "obMatJos2";
static MINIMUM_NONCE_EXPIRY_DURATION: Duration = Duration::from_secs(30);
static DEFAULT_NONCE_EXPIRY_DURATION: Duration = Duration::from_secs(3600);
#[derive(Debug)]
pub struct LongTermServerAuth {
realm: String,
user_hash: HashMap<[u8; 32], String, RandomState>,
users: HashMap<String, ClientAuth, RandomState>,
nonces: LongTermNonce,
clients: BTreeMap<SocketAddr, (String, IntegrityAlgorithm)>,
backup_integrity: HashMap<IntegrityAlgorithm, IntegrityKey, RandomState>,
}
#[derive(Debug)]
struct LongTermNonce {
generate_config: NonceConfiguration,
nonce_expiry_duration: Duration,
nonces: HashMap<SocketAddr, ClientNonce, RandomState>,
}
#[derive(Debug)]
struct NonceConfiguration {
supported_integrity: smallvec::SmallVec<[IntegrityAlgorithm; 2]>,
anonymous_username: Feature,
}
impl NonceConfiguration {
fn generate_random_nonce_string() -> String {
#[cfg(not(feature = "std"))]
{
use rand::Rng;
use rand::TryRngCore;
let mut rng = rand::rngs::OsRng.unwrap_err();
String::from_iter((0..16).map(|_| rng.sample(rand::distr::Alphanumeric) as char))
}
#[cfg(feature = "std")]
{
use rand::Rng;
let mut rng = rand::rng();
String::from_iter((0..16).map(|_| rng.sample(rand::distr::Alphanumeric) as char))
}
}
fn generate_nonce(&self) -> String {
let random_string = Self::generate_random_nonce_string();
let mut security = NonceSecurityBits::default();
if self
.supported_integrity
.iter()
.any(|&algo| algo != IntegrityAlgorithm::Sha1)
{
security.set_password_algorithms(true);
}
if self.anonymous_username.possible() {
security.set_username_anonymity(true);
}
if security.required() {
let mut ret = security.as_string();
ret.push_str(&random_string);
ret
} else {
random_string
}
}
}
impl LongTermNonce {
fn validate_nonce(&mut self, from: SocketAddr, now: Instant) -> &mut ClientNonce {
let nonce_expiry_duration = self.nonce_expiry_duration;
let nonce_data = self.nonces.entry(from).or_insert_with(|| ClientNonce {
expires_at: now + self.nonce_expiry_duration,
value: self.generate_config.generate_nonce(),
password_algorithms: smallvec::SmallVec::new(),
});
if nonce_data.expires_at < now {
nonce_data.value = self.generate_config.generate_nonce();
nonce_data.expires_at = now + nonce_expiry_duration;
}
nonce_data
}
}
struct RandomState {
k0: u64,
k1: u64,
}
fn new_hashmap_keys() -> (u64, u64) {
#[cfg(not(feature = "std"))]
{
use rand::Rng;
use rand::TryRngCore;
let mut rng = rand::rngs::OsRng.unwrap_err();
rng.random()
}
#[cfg(feature = "std")]
{
use rand::Rng;
let mut rng = rand::rng();
rng.random()
}
}
impl RandomState {
fn new() -> Self {
#[cfg(not(feature = "std"))]
{
let (k0, k1) = new_hashmap_keys();
RandomState { k0, k1 }
}
#[cfg(feature = "std")]
{
std::thread_local!(static KEYS: core::cell::Cell<(u64, u64)> = {
core::cell::Cell::new(new_hashmap_keys())
});
KEYS.with(|keys| {
let (k0, k1) = keys.get();
keys.set((k0.wrapping_add(1), k1));
RandomState { k0, k1 }
})
}
}
}
impl core::hash::BuildHasher for RandomState {
type Hasher = SipHasher;
fn build_hasher(&self) -> Self::Hasher {
SipHasher::new_with_keys(self.k0, self.k1)
}
}
fn new_hash<K, V>() -> HashMap<K, V, RandomState> {
HashMap::with_hasher(RandomState::new())
}
#[derive(Debug)]
pub enum LongTermServerValidation {
Validated(IntegrityAlgorithm),
}
#[derive(Debug, Clone, Copy, thiserror::Error, PartialEq, Eq)]
pub enum LongTermServerAuthErrorReason {
#[error("The message is not sufficiently authenticated")]
Unauthorized,
#[error("The message failed integrity checks")]
IntegrityFailed,
#[error("A required feature is not supported")]
UnsupportedFeature,
#[error("The request message is not well-formed")]
BadRequest,
#[error("The request presented with an out of date nonce.")]
StaleNonce,
}
impl From<ValidateError> for LongTermServerAuthErrorReason {
fn from(_value: ValidateError) -> Self {
Self::IntegrityFailed
}
}
#[derive(Debug)]
pub struct LongTermServerAuthError {
reason: LongTermServerAuthErrorReason,
integrity: Option<IntegrityAlgorithm>,
}
impl LongTermServerAuthError {
pub fn reason(&self) -> LongTermServerAuthErrorReason {
self.reason
}
pub fn integrity(&self) -> Option<IntegrityAlgorithm> {
self.integrity
}
}
impl LongTermServerAuth {
pub fn new(realm: String) -> Self {
let backup_credentials =
LongTermCredentials::new("default-user".to_string(), "default-password".to_string());
let mut backup_integrity = new_hash();
backup_integrity.insert(
IntegrityAlgorithm::Sha1,
backup_credentials
.to_key(realm.clone())
.make_key(IntegrityAlgorithm::Sha1),
);
backup_integrity.insert(
IntegrityAlgorithm::Sha256,
backup_credentials
.to_key(realm.clone())
.make_key(IntegrityAlgorithm::Sha256),
);
Self {
realm: realm.clone(),
nonces: LongTermNonce {
generate_config: NonceConfiguration {
supported_integrity: smallvec::SmallVec::from_iter([IntegrityAlgorithm::Sha1]),
anonymous_username: Feature::default(),
},
nonce_expiry_duration: DEFAULT_NONCE_EXPIRY_DURATION,
nonces: new_hash(),
},
user_hash: new_hash(),
users: new_hash(),
clients: BTreeMap::default(),
backup_integrity,
}
}
pub fn realm(&self) -> &str {
&self.realm
}
pub fn add_user(&mut self, credentials: LongTermCredentials) {
if self.anonymous_username().possible() {
let hash = Userhash::compute(credentials.username(), &self.realm);
self.user_hash
.entry(hash)
.or_insert_with(|| credentials.username().to_string());
}
self.users
.entry(credentials.username().to_string())
.and_modify(|client| {
for algo in self.nonces.generate_config.supported_integrity.iter() {
client
.keys
.entry(*algo)
.and_modify(|key| {
*key = credentials.to_key(self.realm.clone()).make_key(*algo)
})
.or_insert_with_key(|algo| {
credentials.to_key(self.realm.clone()).make_key(*algo)
});
}
client.credentials = credentials.clone();
})
.or_insert({
let mut keys = new_hash();
for algo in self.nonces.generate_config.supported_integrity.iter() {
keys.insert(
*algo,
credentials.to_key(self.realm.clone()).make_key(*algo),
);
}
ClientAuth { keys, credentials }
});
}
pub fn supported_integrity(&self) -> &[IntegrityAlgorithm] {
&self.nonces.generate_config.supported_integrity
}
pub fn add_supported_integrity(&mut self, integrity: IntegrityAlgorithm) {
let supported_integrity = &mut self.nonces.generate_config.supported_integrity;
let idx = supported_integrity
.partition_point(|item| rank_integrity(*item) >= rank_integrity(integrity));
if idx == supported_integrity.len() || supported_integrity[idx] != integrity {
debug!(
"inserting {integrity:?} at {idx} of {:?}",
supported_integrity
);
supported_integrity.insert(idx, integrity);
for user in self.users.values_mut() {
user.keys.insert(
integrity,
user.credentials
.to_key(self.realm.clone())
.make_key(integrity),
);
}
}
}
pub fn set_supported_integrity(&mut self, integrity: IntegrityAlgorithm) {
if self.nonces.generate_config.supported_integrity.as_ref() != [integrity] {
self.nonces.generate_config.supported_integrity =
smallvec::SmallVec::from_iter([integrity]);
for user in self.users.values_mut() {
user.keys.clear();
user.keys.insert(
integrity,
user.credentials
.to_key(self.realm.clone())
.make_key(integrity),
);
}
}
}
pub fn set_anonymous_username(&mut self, anonymous: Feature) {
if self.nonces.generate_config.anonymous_username.possible() != anonymous.possible() {
if anonymous.possible() {
for user in self.users.keys() {
let hash = Userhash::compute(user, &self.realm);
self.user_hash.insert(hash, user.clone());
}
} else {
self.user_hash.clear();
}
}
self.nonces.generate_config.anonymous_username = anonymous;
}
pub fn anonymous_username(&mut self) -> Feature {
self.nonces.generate_config.anonymous_username
}
pub fn remove_user(&mut self, user: &str) {
self.users.remove(user);
}
pub fn set_nonce_expiry_duration(&mut self, expiry_duration: Duration) {
if expiry_duration < MINIMUM_NONCE_EXPIRY_DURATION {
panic!("Attempted to set a nonce expiry duration ({expiry_duration:?}) of less than the allowed minimum ({MINIMUM_NONCE_EXPIRY_DURATION:?})");
}
self.nonces.nonce_expiry_duration = expiry_duration;
}
pub fn nonce_for_client(&self, client: SocketAddr) -> Option<&str> {
self.nonces
.nonces
.get(&client)
.map(|nonce| nonce.value.deref())
}
pub fn message_signature_bytes(&self, to: SocketAddr, is_request: bool) -> usize {
if let Some(nonce_len) = self
.nonces
.nonces
.get(&to)
.map(|nonce| 4 + pad_attribute_len(nonce.value.len()))
{
let user_algo_len = self
.clients
.get(&to)
.map(|(_user, algo)| bytes_for_integrity(*algo))
.unwrap_or_default();
if is_request {
user_algo_len
} else {
nonce_len + user_algo_len + 4 + pad_attribute_len(self.realm.len())
}
} else {
0
}
}
#[tracing::instrument(name = "server_sign_outgoing_message", skip(self, msg), err(Debug))]
pub fn sign_outgoing_message<W: MessageWrite>(
&mut self,
mut msg: W,
to: SocketAddr,
) -> Result<W, StunWriteError> {
let Some(nonce) = self.nonces.nonces.get_mut(&to) else {
return Ok(msg);
};
if msg.is_response() {
msg.add_attribute(&Nonce::new(&nonce.value)?)?;
msg.add_attribute(&Realm::new(&self.realm)?)?;
}
if let Some((user, algo)) = self.clients.get(&to) {
let Some(auth) = self.users.get(user) else {
return Err(StunWriteError::IntegrityFailed);
};
let Some(key) = auth.keys.get(algo) else {
return Err(StunWriteError::IntegrityFailed);
};
if *algo != IntegrityAlgorithm::Sha1 {
msg.add_attribute(&PasswordAlgorithm::new(match algo {
IntegrityAlgorithm::Sha1 => unreachable!(),
IntegrityAlgorithm::Sha256 => PasswordAlgorithmValue::SHA256,
}))?;
}
msg.add_message_integrity_with_key(key, *algo)?;
} else if msg.is_response() {
let algos = self
.nonces
.generate_config
.supported_integrity
.iter()
.map(|algo| match algo {
IntegrityAlgorithm::Sha1 => PasswordAlgorithmValue::MD5,
IntegrityAlgorithm::Sha256 => PasswordAlgorithmValue::SHA256,
})
.collect::<smallvec::SmallVec<_>>();
msg.add_attribute(&PasswordAlgorithms::new(&algos))?;
nonce.password_algorithms = algos;
}
Ok(msg)
}
#[tracing::instrument(
name = "server_validate_incoming_message",
skip(self, msg, now),
err(Debug)
)]
pub fn validate_incoming_message(
&mut self,
msg: &Message<'_>,
from: SocketAddr,
now: Instant,
) -> Result<LongTermServerValidation, LongTermServerAuthError> {
if msg.is_response() {
if let Some(key) = self.clients.get(&from).and_then(|user| {
self.users
.get(&user.0)
.and_then(|auth| auth.keys.get(&user.1))
}) {
msg.validate_integrity_with_key(key)
.map(LongTermServerValidation::Validated)
.map_err(|e| LongTermServerAuthError {
reason: e.into(),
integrity: None,
})
} else {
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::IntegrityFailed,
integrity: None,
})
}
} else {
let mut integrity = None;
let mut integrity_sha256 = None;
let mut username = None;
let mut userhash = None;
let mut realm = None;
let mut nonce = None;
let mut password_algo = None;
let mut password_algos = None;
for (_offset, attr) in msg.iter_attributes() {
match attr.get_type() {
MessageIntegrity::TYPE => integrity = MessageIntegrity::from_raw(attr).ok(),
MessageIntegritySha256::TYPE => {
integrity_sha256 = MessageIntegritySha256::from_raw(attr).ok()
}
Username::TYPE => username = Username::from_raw(attr).ok(),
Userhash::TYPE => userhash = Userhash::from_raw(attr).ok(),
Realm::TYPE => realm = Realm::from_raw(attr).ok(),
Nonce::TYPE => nonce = Nonce::from_raw(attr).ok(),
PasswordAlgorithm::TYPE => {
password_algo = PasswordAlgorithm::from_raw(attr).ok()
}
PasswordAlgorithms::TYPE => {
password_algos = PasswordAlgorithms::from_raw(attr).ok()
}
_ => (),
}
}
if integrity.is_none() && integrity_sha256.is_none() {
let nonce_value = self.nonces.validate_nonce(from, now);
trace!(
"no message-integrity, returning unauthorized with nonce: {}",
nonce_value.value
);
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
}
let Some((_realm, nonce)) = realm.zip(nonce) else {
trace!("bad request due to missing realm, or nonce");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
});
};
let user = if let Some(hash) = userhash.as_ref() {
self.user_hash.get(hash.hash()).map(|user| user.deref())
} else if let Some(user) = username.as_ref() {
Some(user.username())
} else {
trace!("bad request due to missing username, or userhash");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
});
};
let nonce_value = self.nonces.validate_nonce(from, now);
let password_algo = if let Some(security) = NonceSecurityBits::from_nonce(nonce.nonce())
{
if security.password_algorithms() {
if let Some((algo, algos)) = password_algo.as_ref().zip(password_algos.as_ref())
{
if !algos.algorithms().contains(&algo.algorithm()) {
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
});
}
if algos.algorithms() != nonce_value.password_algorithms.as_ref() {
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
});
}
algo.algorithm().as_integrity()
} else if password_algo.is_none() && password_algos.is_none() {
IntegrityAlgorithm::Sha1
} else {
trace!(
"bad request due to missing password algorithm, or password algorithms"
);
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
});
}
} else {
IntegrityAlgorithm::Sha1
}
} else {
IntegrityAlgorithm::Sha1
};
if nonce_value.value != nonce.nonce() {
if let Some(security) = NonceSecurityBits::from_nonce(&nonce_value.value) {
if let Some(request_security) = NonceSecurityBits::from_nonce(nonce.nonce()) {
if security != request_security {
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
}
}
}
trace!("stale nonce {} vs {}", nonce_value.value, nonce.nonce());
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::StaleNonce,
integrity: None,
});
}
let Some(client) = user.and_then(|user| self.users.get(user)) else {
let Some(key) = self.backup_integrity.get(&password_algo) else {
warn!("No backup integrity! Timing attack on usernames possible!");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
};
let _ = msg.validate_integrity_with_key(key);
trace!("integrity failed");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
};
let Some(key) = client.keys.get(&password_algo) else {
trace!("no key for password algo {password_algo:?}");
let Some(key) = self.backup_integrity.get(&password_algo) else {
warn!("No backup integrity! Timing attack on usernames possible!");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
};
let _ = msg.validate_integrity_with_key(key);
trace!("integrity failed");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
};
if msg.validate_integrity_with_key(key).is_err() {
trace!("integrity failed");
return Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
});
}
self.clients.insert(
from,
(client.credentials.username().to_string(), password_algo),
);
Ok(LongTermServerValidation::Validated(password_algo))
}
}
pub fn remove_client(&mut self, client: SocketAddr) {
self.clients.remove(&client);
}
}
fn is_valid_stale_nonce(msg: &Message<'_>) -> Option<(Nonce, Realm)> {
if !msg.has_class(MessageClass::Error) {
return None;
}
let Ok(error) = msg.attribute::<ErrorCode>() else {
return None;
};
if error.code() != ErrorCode::STALE_NONCE {
return None;
}
let Ok(nonce) = msg.attribute::<Nonce>() else {
return None;
};
let Ok(realm) = msg.attribute::<Realm>() else {
return None;
};
Some((nonce, realm))
}
fn rank_integrity(integrity: IntegrityAlgorithm) -> usize {
match integrity {
IntegrityAlgorithm::Sha1 => 1,
IntegrityAlgorithm::Sha256 => 2,
}
}
#[cfg(test)]
mod tests {
use stun_types::{
attribute::{MessageIntegritySha256, Userhash},
message::{MessageType, MessageWriteVec, TransactionId, BINDING},
};
use super::*;
use alloc::vec::Vec;
#[test]
fn nonce_security() {
let _log = crate::tests::test_init_log();
for i in 0..24 {
let n: u32 = 1 << i;
let security = NonceSecurityBits {
bytes: n.to_be_bytes()[1..].try_into().unwrap(),
};
assert_eq!(security.password_algorithms(), i == 23);
assert_eq!(security.username_anonymity(), i == 22);
trace!("initial security bits {security:x?}");
let nonce = security.as_string();
trace!("security nonce: {nonce}");
let security2 = NonceSecurityBits::from_nonce(&nonce).unwrap();
trace!("parsed security bits {security2:x?}");
assert_eq!(security, security2);
}
}
#[test]
fn short_term_getters() {
let _log = crate::tests::test_init_log();
let mut auth = ShortTermAuth::new();
assert!(auth.integrity_key().is_none());
assert_eq!(auth.message_signature_bytes(), 0);
let credentials = ShortTermCredentials::new(String::from("password"));
auth.set_credentials(credentials.clone(), IntegrityAlgorithm::Sha1);
assert_eq!(
auth.credentials(),
Some((&credentials, IntegrityAlgorithm::Sha1))
);
assert!(auth.integrity_key().is_some());
assert_eq!(auth.message_signature_bytes(), 24);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = auth.sign_outgoing_message(msg).unwrap();
let request = msg.finish();
let request = Message::from_bytes(&request).unwrap();
assert_eq!(
request.validate_integrity(&credentials.into()).unwrap(),
IntegrityAlgorithm::Sha1
);
assert_eq!(
request
.validate_integrity_with_key(auth.integrity_key().unwrap())
.unwrap(),
IntegrityAlgorithm::Sha1
);
}
#[test]
fn short_term_no_credentials() {
let _log = crate::tests::test_init_log();
let mut auth = ShortTermAuth::new();
assert!(auth.integrity_key().is_none());
assert_eq!(auth.message_signature_bytes(), 0);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = auth.sign_outgoing_message(msg).unwrap();
let request = msg.finish();
let request = Message::from_bytes(&request).unwrap();
assert!(msg_has_no_auth(&request));
assert!(matches!(auth.validate_incoming_message(&request), Ok(None)));
}
#[test]
fn long_term_client_getters() {
let _log = crate::tests::test_init_log();
let mut auth = LongTermClientAuth::new();
assert!(auth.credentials().is_none());
assert_eq!(auth.message_signature_bytes(), 0);
assert_eq!(auth.supported_integrity(), &[IntegrityAlgorithm::Sha1]);
auth.add_supported_integrity(IntegrityAlgorithm::Sha256);
assert_eq!(
auth.supported_integrity(),
&[IntegrityAlgorithm::Sha1, IntegrityAlgorithm::Sha256]
);
auth.set_supported_integrity(IntegrityAlgorithm::Sha1);
assert_eq!(auth.supported_integrity(), &[IntegrityAlgorithm::Sha1]);
auth.set_anonymous_username(Feature::Required);
assert_eq!(auth.anonymous_username(), Feature::Required);
auth.set_anonymous_username(Feature::Disabled);
assert_eq!(auth.anonymous_username(), Feature::Disabled);
auth.set_anonymous_username(Feature::Auto);
assert_eq!(auth.anonymous_username(), Feature::Auto);
let credentials = LongTermCredentials::new("user".to_string(), "password".to_string());
auth.set_credentials(credentials.clone());
assert_eq!(auth.credentials(), Some(&credentials));
assert_eq!(auth.message_signature_bytes(), 0);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = auth.sign_outgoing_message(msg).unwrap();
let request = msg.finish();
let request = Message::from_bytes(&request).unwrap();
assert!(msg_has_no_auth(&request));
let response = Message::builder_success(&request, MessageWriteVec::new());
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
auth.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
#[test]
fn auth_error_getters() {
for reason in [
LongTermClientAuthErrorReason::Unauthorized,
LongTermClientAuthErrorReason::IntegrityFailed,
LongTermClientAuthErrorReason::UnsupportedFeature,
] {
for integrity in [
None,
Some(IntegrityAlgorithm::Sha1),
Some(IntegrityAlgorithm::Sha256),
] {
let err = LongTermClientAuthError { reason, integrity };
assert_eq!(err.reason(), reason);
assert_eq!(err.integrity(), integrity);
}
}
for reason in [
LongTermServerAuthErrorReason::Unauthorized,
LongTermServerAuthErrorReason::IntegrityFailed,
LongTermServerAuthErrorReason::UnsupportedFeature,
LongTermServerAuthErrorReason::BadRequest,
LongTermServerAuthErrorReason::StaleNonce,
] {
for integrity in [
None,
Some(IntegrityAlgorithm::Sha1),
Some(IntegrityAlgorithm::Sha256),
] {
let err = LongTermServerAuthError { reason, integrity };
assert_eq!(err.reason(), reason);
assert_eq!(err.integrity(), integrity);
}
}
}
#[test]
fn long_term_server_getters() {
let _log = crate::tests::test_init_log();
let client_addr = "10.0.0.1:12345".parse().unwrap();
let now = Instant::ZERO;
let mut auth = LongTermServerAuth::new("realm".to_string());
let credentials = LongTermCredentials::new("user".to_string(), "password".to_string());
auth.add_user(credentials.clone());
assert_eq!(auth.message_signature_bytes(client_addr, false), 0);
assert_eq!(auth.message_signature_bytes(client_addr, true), 0);
auth.add_supported_integrity(IntegrityAlgorithm::Sha256);
assert_eq!(
auth.supported_integrity(),
&[IntegrityAlgorithm::Sha256, IntegrityAlgorithm::Sha1]
);
auth.set_supported_integrity(IntegrityAlgorithm::Sha1);
assert_eq!(auth.supported_integrity(), &[IntegrityAlgorithm::Sha1]);
auth.set_anonymous_username(Feature::Required);
assert_eq!(auth.anonymous_username(), Feature::Required);
auth.set_anonymous_username(Feature::Disabled);
assert_eq!(auth.anonymous_username(), Feature::Disabled);
auth.set_anonymous_username(Feature::Auto);
assert_eq!(auth.anonymous_username(), Feature::Auto);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = auth.sign_outgoing_message(msg, client_addr).unwrap();
let request = msg.finish();
let request = Message::from_bytes(&request).unwrap();
assert!(msg_has_no_auth(&request));
let response = Message::builder_success(&request, MessageWriteVec::new());
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
auth.validate_incoming_message(&response, client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
fn msg_has_no_auth(msg: &Message<'_>) -> bool {
msg.attribute::<Username>().is_err()
&& msg.attribute::<Userhash>().is_err()
&& msg.attribute::<Nonce>().is_err()
&& msg.attribute::<Realm>().is_err()
&& msg.attribute::<MessageIntegrity>().is_err()
&& msg.attribute::<MessageIntegritySha256>().is_err()
}
fn server_unauthorized_response(msg: &Message<'_>) -> MessageWriteVec {
let mut response = Message::builder_error(msg, MessageWriteVec::new());
response
.add_attribute(&ErrorCode::builder(ErrorCode::UNAUTHORIZED).build().unwrap())
.unwrap();
response
}
#[derive(Debug)]
struct LongTermTest {
client: LongTermClientAuth,
server: LongTermServerAuth,
client_addr: SocketAddr,
}
impl LongTermTest {
fn new() -> Self {
let client_addr = "10.0.0.1:12345".parse().unwrap();
let realm = "realm".to_string();
let credentials = LongTermCredentials::new("user".to_string(), "pass".to_string());
let mut client = LongTermClientAuth::new();
client.set_credentials(credentials.clone());
let mut server = LongTermServerAuth::new(realm);
server.add_user(credentials);
Self {
client,
server,
client_addr,
}
}
fn initial_auth(
&mut self,
now: Instant,
) -> Result<LongTermClientValidation, LongTermClientAuthError> {
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = self.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
self.server
.validate_incoming_message(&msg, self.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
})
));
let response = server_unauthorized_response(&msg);
let response = self
.server
.sign_outgoing_message(response, self.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
self.client.validate_incoming_message(&response)
}
fn full_auth(
&mut self,
now: Instant,
integrity: IntegrityAlgorithm,
) -> Result<LongTermClientValidation, LongTermClientAuthError> {
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = self.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
assert!(matches!(
self.server
.validate_incoming_message(&msg, self.client_addr, now),
Ok(LongTermServerValidation::Validated(algo)) if algo == integrity
));
let response = Message::builder_success(&msg, MessageWriteVec::new());
let response = self
.server
.sign_outgoing_message(response, self.client_addr)
.unwrap();
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
self.client.validate_incoming_message(&response)
}
fn server_generate_stale_nonce(&self, msg: &Message<'_>) -> MessageWriteVec {
let mut response = Message::builder_error(msg, MessageWriteVec::new());
response
.add_attribute(&ErrorCode::builder(ErrorCode::STALE_NONCE).build().unwrap())
.unwrap();
response
.add_attribute(&Realm::new(self.server.realm()).unwrap())
.unwrap();
response
.add_attribute(
&Nonce::new(self.server.nonce_for_client(self.client_addr).unwrap()).unwrap(),
)
.unwrap();
response
}
}
#[test]
fn long_term_initial_client_sign_noop() {
let _log = crate::tests::test_init_log();
let mut test = LongTermTest::new();
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(msg_has_no_auth(&msg));
}
#[test]
fn long_term_full_auth_username_sha1() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_anonymous_username(Feature::Disabled);
test.server.set_anonymous_username(Feature::Disabled);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
assert_eq!(
test.server.message_signature_bytes(test.client_addr, true),
24
);
assert_eq!(
test.server.message_signature_bytes(test.client_addr, false),
56
);
}
#[test]
fn long_term_full_auth_userhash_sha1() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_anonymous_username(Feature::Required);
test.server.set_anonymous_username(Feature::Required);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
assert_eq!(
test.server.message_signature_bytes(test.client_addr, true),
24
);
assert_eq!(
test.server.message_signature_bytes(test.client_addr, false),
72
);
}
#[test]
fn long_term_full_auth_userhash_sha256() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_anonymous_username(Feature::Required);
test.client
.set_supported_integrity(IntegrityAlgorithm::Sha256);
test.server.set_anonymous_username(Feature::Required);
test.server
.set_supported_integrity(IntegrityAlgorithm::Sha256);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha256),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha256
))
));
assert_eq!(
test.server.message_signature_bytes(test.client_addr, true),
36
);
assert_eq!(
test.server.message_signature_bytes(test.client_addr, false),
84
);
}
#[test]
fn long_term_full_auth_uses_sha256() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let auths = [
[IntegrityAlgorithm::Sha256].as_ref(),
[IntegrityAlgorithm::Sha1, IntegrityAlgorithm::Sha256].as_ref(),
[IntegrityAlgorithm::Sha256, IntegrityAlgorithm::Sha1].as_ref(),
];
for client in auths.iter() {
for server in auths.iter() {
std::println!(
"creating test with client auth {client:?} and server auth {server:?}"
);
let mut test = LongTermTest::new();
test.client.set_supported_integrity(client[0]);
for client in client[1..].iter() {
test.client.add_supported_integrity(*client);
}
test.server.set_supported_integrity(server[0]);
for server in server[1..].iter() {
test.server.add_supported_integrity(*server);
}
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha256),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha256
))
));
}
}
}
#[test]
fn long_term_full_auth_uses_sha1() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let auths = [
(
[IntegrityAlgorithm::Sha1].as_ref(),
[IntegrityAlgorithm::Sha1].as_ref(),
),
(
[IntegrityAlgorithm::Sha1, IntegrityAlgorithm::Sha256].as_ref(),
[IntegrityAlgorithm::Sha1].as_ref(),
),
(
[IntegrityAlgorithm::Sha256, IntegrityAlgorithm::Sha1].as_ref(),
[IntegrityAlgorithm::Sha1].as_ref(),
),
(
[IntegrityAlgorithm::Sha1].as_ref(),
[IntegrityAlgorithm::Sha1, IntegrityAlgorithm::Sha256].as_ref(),
),
(
[IntegrityAlgorithm::Sha1].as_ref(),
[IntegrityAlgorithm::Sha256, IntegrityAlgorithm::Sha1].as_ref(),
),
];
for (client, server) in auths.iter() {
std::println!("creating test with client auth {client:?} and server auth {server:?}");
let mut test = LongTermTest::new();
test.client.set_supported_integrity(client[0]);
for client in client[1..].iter() {
test.client.add_supported_integrity(*client);
}
test.server.set_supported_integrity(server[0]);
for server in server[1..].iter() {
test.server.add_supported_integrity(*server);
}
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
}
#[test]
fn long_term_full_auth_flow_userhash_mismatch() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_anonymous_username(Feature::Required);
test.server.set_anonymous_username(Feature::Disabled);
assert!(matches!(
test.initial_auth(now),
Err(LongTermClientAuthError {
reason,
integrity,
}) if reason == LongTermClientAuthErrorReason::UnsupportedFeature
));
}
#[test]
fn long_term_full_auth_flow_server_sha256_mismatch() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.server
.set_supported_integrity(IntegrityAlgorithm::Sha256);
assert!(matches!(
test.initial_auth(now),
Err(LongTermClientAuthError {
reason,
integrity,
}) if reason == LongTermClientAuthErrorReason::UnsupportedFeature
));
}
#[test]
fn long_term_full_auth_flow_client_sha256_mismatch() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.server.set_anonymous_username(Feature::Disabled);
test.client
.set_supported_integrity(IntegrityAlgorithm::Sha256);
assert!(matches!(
test.initial_auth(now),
Err(LongTermClientAuthError {
reason,
integrity,
}) if reason == LongTermClientAuthErrorReason::UnsupportedFeature
));
}
#[test]
fn long_term_full_auth_flow_client_sha1_mismatch() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.server.set_anonymous_username(Feature::Disabled);
test.client
.set_supported_integrity(IntegrityAlgorithm::Sha256);
assert!(matches!(
test.initial_auth(now),
Err(LongTermClientAuthError {
reason,
integrity,
}) if reason == LongTermClientAuthErrorReason::UnsupportedFeature
));
}
#[test]
fn long_term_full_auth_stale_nonce() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.server
.set_nonce_expiry_duration(MINIMUM_NONCE_EXPIRY_DURATION);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let now = now + MINIMUM_NONCE_EXPIRY_DURATION + Duration::from_secs(1);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::StaleNonce,
integrity: None,
})
));
let response = test.server_generate_stale_nonce(&msg).finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
))
}
#[test]
fn long_term_initial_auth_bad_request() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_anonymous_username(Feature::Disabled);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
for atype in [Realm::TYPE, Nonce::TYPE, Username::TYPE] {
let mut msg = Message::builder_request(BINDING, MessageWriteVec::new());
if atype != Username::TYPE {
msg.add_attribute(
&Username::new(test.client.credentials().unwrap().username()).unwrap(),
)
.unwrap();
}
if atype != Realm::TYPE {
msg.add_attribute(&Realm::new(test.server.realm()).unwrap())
.unwrap();
}
if atype != Nonce::TYPE {
msg.add_attribute(
&Nonce::new(test.server.nonce_for_client(test.client_addr).unwrap()).unwrap(),
)
.unwrap();
}
msg.add_message_integrity(
&test
.client
.credentials()
.unwrap()
.to_key(test.server.realm().to_string())
.into(),
IntegrityAlgorithm::Sha1,
)
.unwrap();
let msg = msg.finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::BadRequest,
integrity: None,
})
));
}
}
#[test]
fn long_term_full_auth_client_stale_nonce_missing_attributes() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
for atype in [Realm::TYPE, Nonce::TYPE] {
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let mut response = Message::builder(
MessageType::from_class_method(MessageClass::Error, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
response
.add_attribute(&ErrorCode::builder(ErrorCode::STALE_NONCE).build().unwrap())
.unwrap();
if atype != Realm::TYPE {
response
.add_attribute(&Realm::new(test.server.realm()).unwrap())
.unwrap();
}
if atype != Nonce::TYPE {
response
.add_attribute(
&Nonce::new(test.server.nonce_for_client(test.client_addr).unwrap())
.unwrap(),
)
.unwrap();
}
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
}
#[test]
fn long_term_full_auth_client_bad_request_authed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let mut response = Message::builder(
MessageType::from_class_method(MessageClass::Error, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
response
.add_attribute(&ErrorCode::builder(ErrorCode::BAD_REQUEST).build().unwrap())
.unwrap();
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_full_auth_client_bad_request_unauthed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let mut response = Message::builder(
MessageType::from_class_method(MessageClass::Error, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
response
.add_attribute(&ErrorCode::builder(ErrorCode::BAD_REQUEST).build().unwrap())
.unwrap();
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
#[test]
fn long_term_full_auth_client_other_error_response_unauthed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let mut response = Message::builder(
MessageType::from_class_method(MessageClass::Error, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
response
.add_attribute(
&ErrorCode::builder(ErrorCode::WRONG_CREDENTIALS)
.build()
.unwrap(),
)
.unwrap();
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
#[test]
fn long_term_full_auth_client_other_error_response_authed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let mut response = Message::builder(
MessageType::from_class_method(MessageClass::Error, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
response
.add_attribute(
&ErrorCode::builder(ErrorCode::WRONG_CREDENTIALS)
.build()
.unwrap(),
)
.unwrap();
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_full_auth_client_success_response_unauthed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let response = Message::builder(
MessageType::from_class_method(MessageClass::Success, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::IntegrityFailed,
integrity: None,
})
));
}
#[test]
fn long_term_full_auth_client_success_response_authed() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let response = Message::builder(
MessageType::from_class_method(MessageClass::Success, BINDING),
TransactionId::generate(),
MessageWriteVec::new(),
);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_initial_auth_wrong_password() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
let user = test.client.credentials().unwrap().username().to_string();
test.client.set_credentials(LongTermCredentials::new(
user.clone(),
"wrong-password".to_string(),
));
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None
})
));
let response = server_unauthorized_response(&msg);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::Unauthorized,
integrity: None
})
));
}
#[test]
fn long_term_server_change_password() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_credentials(LongTermCredentials::new(
test.client.credentials().unwrap().username().to_string(),
"wrong-password".to_string(),
));
test.server.add_user(LongTermCredentials::new(
test.client.credentials().unwrap().username().to_string(),
"wrong-password".to_string(),
));
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_initial_auth_wrong_user() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_credentials(LongTermCredentials::new(
"wrong-user".to_string(),
test.client.credentials().unwrap().password().to_string(),
));
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None
})
));
let response = server_unauthorized_response(&msg);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Err(LongTermClientAuthError {
reason: LongTermClientAuthErrorReason::Unauthorized,
integrity: None
})
));
}
#[test]
fn long_term_server_add_user() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client.set_credentials(LongTermCredentials::new(
"wrong-user".to_string(),
test.client.credentials().unwrap().password().to_string(),
));
test.server.add_user(LongTermCredentials::new(
"wrong-user".to_string(),
test.client.credentials().unwrap().password().to_string(),
));
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_server_remove_user() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
test.server
.remove_user(test.client.credentials.as_ref().unwrap().username());
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None
})
));
}
#[test]
fn long_term_client_request() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
let request = Message::builder_request(BINDING, MessageWriteVec::new());
let request = test
.server
.sign_outgoing_message(request, test.client_addr)
.unwrap();
let request = request.finish();
let request = Message::from_bytes(&request).unwrap();
trace!("sending request to client {request}");
assert!(matches!(
test.client.validate_incoming_message(&request).unwrap(),
LongTermClientValidation::Validated(IntegrityAlgorithm::Sha1)
));
let response = Message::builder_success(&request, MessageWriteVec::new());
let response = test.client.sign_outgoing_message(response).unwrap();
let response = response.finish();
let response = Message::from_bytes(&response).unwrap();
trace!("sending response to server {response}");
assert!(matches!(
test.server
.validate_incoming_message(&response, test.client_addr, now)
.unwrap(),
LongTermServerValidation::Validated(IntegrityAlgorithm::Sha1)
));
}
fn test_server_bid_down_attack<F: FnOnce(&Message<'_>) -> Vec<u8>>(
modify_response: F,
) -> Result<LongTermServerValidation, LongTermServerAuthError> {
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client
.add_supported_integrity(IntegrityAlgorithm::Sha256);
test.server
.add_supported_integrity(IntegrityAlgorithm::Sha256);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
})
));
let response = server_unauthorized_response(&msg);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
let response = modify_response(&response);
let response = Message::from_bytes(&response).unwrap();
assert!(matches!(
test.client.validate_incoming_message(&response),
Ok(LongTermClientValidation::ResendRequest(None)),
));
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(!msg_has_no_auth(&msg));
test.server
.validate_incoming_message(&msg, test.client_addr, now)
}
#[test]
fn long_term_full_server_bid_down_attack_remove_password_algorithms_from_server_response() {
let _log = crate::tests::test_init_log();
let ret = test_server_bid_down_attack(|response| {
let mut new_response = Message::builder(
response.get_type(),
response.transaction_id(),
MessageWriteVec::new(),
);
for (_offset, attr) in response.iter_attributes() {
if attr.get_type() == Nonce::TYPE {
let nonce = Nonce::from_raw_ref(&attr).unwrap();
let mut security = NonceSecurityBits::from_nonce(nonce.nonce()).unwrap();
assert!(security.password_algorithms());
security.set_password_algorithms(false);
let mut new_nonce = security.as_string();
new_nonce.push_str(&nonce.nonce()[LONG_TERM_RFC8489_NONCE_COOKIE.len() + 4..]);
new_response
.add_attribute(&Nonce::new(&new_nonce).unwrap())
.unwrap();
} else if ![
PasswordAlgorithms::TYPE,
MessageIntegrity::TYPE,
MessageIntegritySha256::TYPE,
]
.contains(&attr.get_type())
{
new_response.add_attribute(&attr).unwrap();
}
}
new_response.finish()
});
assert!(matches!(
ret,
Err(LongTermServerAuthError {
reason,
integrity,
}) if reason == LongTermServerAuthErrorReason::Unauthorized
));
}
#[test]
fn long_term_full_server_bid_down_attack_remove_password_algorithms_value_from_server_response()
{
let _log = crate::tests::test_init_log();
let ret = test_server_bid_down_attack(|response| {
let mut new_response = Message::builder(
response.get_type(),
response.transaction_id(),
MessageWriteVec::new(),
);
for (_offset, attr) in response.iter_attributes() {
if attr.get_type() == PasswordAlgorithms::TYPE {
new_response
.add_attribute(&PasswordAlgorithms::new(&[PasswordAlgorithmValue::MD5]))
.unwrap();
} else if ![MessageIntegrity::TYPE, MessageIntegritySha256::TYPE]
.contains(&attr.get_type())
{
new_response.add_attribute(&attr).unwrap();
}
}
new_response.finish()
});
assert!(matches!(
ret,
Err(LongTermServerAuthError {
reason,
integrity,
}) if reason == LongTermServerAuthErrorReason::BadRequest
));
}
fn test_client_bid_down_attack<F: FnOnce(&Message<'_>) -> Vec<u8>>(
modify_response: F,
) -> Result<LongTermClientValidation, LongTermClientAuthError> {
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client
.add_supported_integrity(IntegrityAlgorithm::Sha256);
test.server
.add_supported_integrity(IntegrityAlgorithm::Sha256);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
})
));
let response = server_unauthorized_response(&msg);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
let response = modify_response(&response);
let response = Message::from_bytes(&response).unwrap();
test.client.validate_incoming_message(&response)
}
#[test]
fn long_term_full_client_bid_down_attack_remove_password_algorithms_from_server_response() {
let _log = crate::tests::test_init_log();
let ret = test_client_bid_down_attack(|response| {
let mut new_response = Message::builder(
response.get_type(),
response.transaction_id(),
MessageWriteVec::new(),
);
for (_offset, attr) in response.iter_attributes() {
if ![
PasswordAlgorithms::TYPE,
MessageIntegrity::TYPE,
MessageIntegritySha256::TYPE,
]
.contains(&attr.get_type())
{
new_response.add_attribute(&attr).unwrap();
}
}
new_response.finish()
});
assert!(matches!(
ret,
Err(LongTermClientAuthError {
reason,
integrity,
}) if reason == LongTermClientAuthErrorReason::Unauthorized
));
}
#[test]
fn long_term_rfc5389_backward_compat() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
test.client
.add_supported_integrity(IntegrityAlgorithm::Sha256);
test.server
.add_supported_integrity(IntegrityAlgorithm::Sha256);
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Err(LongTermServerAuthError {
reason: LongTermServerAuthErrorReason::Unauthorized,
integrity: None,
})
));
let response = server_unauthorized_response(&msg);
let response = test
.server
.sign_outgoing_message(response, test.client_addr)
.unwrap()
.finish();
let response = Message::from_bytes(&response).unwrap();
let mut new_response = Message::builder(
response.get_type(),
response.transaction_id(),
MessageWriteVec::new(),
);
let mut old_nonce = None;
for (_offset, attr) in response.iter_attributes() {
if attr.get_type() == Nonce::TYPE {
let nonce = Nonce::from_raw_ref(&attr).unwrap();
let mut security = NonceSecurityBits::from_nonce(nonce.nonce()).unwrap();
assert!(security.password_algorithms());
security.set_password_algorithms(false);
let mut new_nonce = security.as_string();
new_nonce.push_str(&nonce.nonce()[LONG_TERM_RFC8489_NONCE_COOKIE.len() + 4..]);
new_response
.add_attribute(&Nonce::new(&new_nonce).unwrap())
.unwrap();
old_nonce = Some(nonce);
} else if ![
PasswordAlgorithm::TYPE,
PasswordAlgorithms::TYPE,
MessageIntegrity::TYPE,
MessageIntegritySha256::TYPE,
]
.contains(&attr.get_type())
{
new_response.add_attribute(&attr).unwrap();
}
}
let old_nonce = old_nonce.unwrap();
let response = new_response.finish();
let response = Message::from_bytes(&response).unwrap();
let ret = test.client.validate_incoming_message(&response);
assert!(matches!(
ret,
Ok(LongTermClientValidation::ResendRequest(None))
));
let msg = Message::builder_request(BINDING, MessageWriteVec::new());
let msg = test.client.sign_outgoing_message(msg).unwrap().finish();
let msg = Message::from_bytes(&msg).unwrap();
let mut new_request =
Message::builder(msg.get_type(), msg.transaction_id(), MessageWriteVec::new());
for (_offset, attr) in msg.iter_attributes() {
if attr.get_type() == Nonce::TYPE {
new_request.add_attribute(&old_nonce).unwrap();
} else if ![
PasswordAlgorithm::TYPE,
PasswordAlgorithms::TYPE,
MessageIntegrity::TYPE,
MessageIntegritySha256::TYPE,
]
.contains(&attr.get_type())
{
new_request.add_attribute(&attr).unwrap();
}
}
new_request
.add_message_integrity(
&test
.client
.credentials()
.unwrap()
.to_key(test.server.realm().to_string())
.into(),
IntegrityAlgorithm::Sha1,
)
.unwrap();
let msg = new_request.finish();
let msg = Message::from_bytes(&msg).unwrap();
assert!(matches!(
test.server
.validate_incoming_message(&msg, test.client_addr, now),
Ok(LongTermServerValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
#[test]
fn long_term_server_remove_client() {
let _log = crate::tests::test_init_log();
let now = Instant::ZERO;
let mut test = LongTermTest::new();
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
test.server.remove_client(test.client_addr);
let credentials = test.client.credentials().cloned().unwrap();
test.client = LongTermClientAuth::new();
test.client.set_credentials(credentials);
assert!(matches!(
test.initial_auth(now),
Ok(LongTermClientValidation::ResendRequest(None))
));
assert!(matches!(
test.full_auth(now, IntegrityAlgorithm::Sha1),
Ok(LongTermClientValidation::Validated(
IntegrityAlgorithm::Sha1
))
));
}
}