[Fix] Bound Iota storage, relay and transport resources

This commit is contained in:
Alex Emmet 2026-09-24 18:37:28 +02:00
commit e19c3c3d12
19 changed files with 609 additions and 48 deletions

View file

@ -1,4 +1,5 @@
use crate::message_common::*;
use iota_logger::log;
use iota_storage::util::chat_files::{self, MessageState};
use iota_storage::util::chats_util::{self, get_user, has_user, mod_user};
use iota_storage::util::communities_util::CommunitiesUtil;
@ -2084,8 +2085,50 @@ fn upload_response(
}
fn asset_error(cv: &CommunicationValue, error: StorageError) -> CommunicationValue {
error_response(cv, CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string()))
log!(
"Asset request rejected sender={:?} request_id={:?}: {error}",
cv.sender(),
cv.id()
);
let (kind, category) = match error {
StorageError::AssetResourceLimit(_) => {
(CommunicationType::ErrorInvalidData, "asset_resource_limit")
}
_ => (CommunicationType::ErrorInternal, "asset_request_failed"),
};
error_response(cv, kind).add_typed_default(DataType::ErrorType, DataValue::Str(category.into()))
}
#[cfg(test)]
mod asset_error_tests {
use super::*;
#[test]
fn storage_errors_do_not_expose_internal_paths() {
let request = CommunicationValue::new(CommunicationType::UserAssetUploadStart).with_id(7);
let internal = asset_error(
&request,
StorageError::Other("private path /srv/assets/key".into()),
);
assert!(internal.is_type(CommunicationType::ErrorInternal));
assert_eq!(
internal
.get_data(DataType::ErrorType)
.and_then(DataValue::as_str),
Some("asset_request_failed")
);
let limit = asset_error(
&request,
StorageError::AssetResourceLimit("max_asset_bytes"),
);
assert!(limit.is_type(CommunicationType::ErrorInvalidData));
assert_eq!(
limit
.get_data(DataType::ErrorType)
.and_then(DataValue::as_str),
Some("asset_resource_limit")
);
}
}
fn upload_request(cv: &CommunicationValue) -> Result<(i64, String), CommunicationValue> {
@ -2404,10 +2447,21 @@ pub fn handle_user_blob_list(cv: &CommunicationValue) -> CommunicationValue {
if user_id <= 0 {
return error_response(cv, CommunicationType::ErrorInvalidData);
}
match user_blobs::list_metadata(user_id) {
Ok(blobs) => CommunicationValue::new(CommunicationType::UserBlobList)
let after_id = match cv.get_data(DataType::Offset).and_then(DataValue::as_number) {
Some(raw) => match i64::try_from(raw) {
Ok(id) if id > 0 => Some(id),
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
},
None => None,
};
match user_blobs::list_metadata_page(user_id, after_id) {
Ok((blobs, next)) => CommunicationValue::new(CommunicationType::UserBlobList)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::Offset,
DataValue::SignedNumber(next.unwrap_or(0).into()),
)
.add_typed_default(
DataType::Blobs,
DataValue::Array(blobs.iter().map(blob_metadata_value).collect()),
@ -2542,10 +2596,21 @@ pub fn handle_user_asset_list(cv: &CommunicationValue) -> CommunicationValue {
Ok(id) if id > 0 => id,
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
};
match user_assets::list(user_id) {
Ok(assets) => CommunicationValue::new(CommunicationType::UserAssetList)
let after_id = match cv.get_data(DataType::Offset).and_then(DataValue::as_number) {
Some(raw) => match i64::try_from(raw) {
Ok(id) if id > 0 => Some(id),
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
},
None => None,
};
match user_assets::list_page(user_id, after_id) {
Ok((assets, next)) => CommunicationValue::new(CommunicationType::UserAssetList)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::Offset,
DataValue::SignedNumber(next.unwrap_or(0).into()),
)
.add_typed_default(
DataType::Assets,
DataValue::Array(assets.iter().map(asset_metadata_value).collect()),

View file

@ -1,3 +1,7 @@
use iota_storage::util::{
config_util::CONFIG,
relay_replay::{MAX_RELAY_FUTURE_SKEW_MILLIS, RELAY_RETENTION_MILLIS},
};
use iota_util::route_target::RouteTarget;
use mtp::codec::{
CommunicationValue, ProtectionPolicy, RelayError, RelayOpenOptions, SignaturePolicy, TypeMap,
@ -107,6 +111,9 @@ pub enum RelayValidationError {
MissingTypeMap,
InvalidRouteTarget(u64),
KeyLookup(String),
Clock(String),
StaleRelay { created_at: u64, now: u64 },
RelayFromFuture { created_at: u64, now: u64 },
Relay(RelayError),
}
@ -129,6 +136,14 @@ impl fmt::Display for RelayValidationError {
write!(formatter, "relay has invalid route target {target}")
}
Self::KeyLookup(error) => write!(formatter, "trusted signer lookup failed: {error}"),
Self::Clock(error) => write!(formatter, "relay clock failed: {error}"),
Self::StaleRelay { created_at, now } => {
write!(formatter, "relay timestamp {created_at} is stale at {now}")
}
Self::RelayFromFuture { created_at, now } => write!(
formatter,
"relay timestamp {created_at} is in the future at {now}"
),
Self::Relay(error) => error.fmt(formatter),
}
}
@ -142,6 +157,23 @@ impl From<RelayError> for RelayValidationError {
}
}
fn validate_created_at(
created_at: u64,
now: u64,
max_future_skew_millis: u64,
) -> Result<(), RelayValidationError> {
if created_at > now.saturating_add(max_future_skew_millis) {
return Err(RelayValidationError::RelayFromFuture { created_at, now });
}
let max_age = u64::try_from(RELAY_RETENTION_MILLIS)
.unwrap_or(0)
.saturating_sub(MAX_RELAY_FUTURE_SKEW_MILLIS);
if now.saturating_sub(created_at) > max_age {
return Err(RelayValidationError::StaleRelay { created_at, now });
}
Ok(())
}
/*
* Relay metadata is opened only after the claimed signer selects trusted key
* history. Replay reservation happens after verification and durable
@ -195,6 +227,14 @@ where
RelayOpenOptions::new(RELAY_PROTECTION_POLICY),
)?;
let now = mtp::common::unix_time_millis()
.map_err(|error| RelayValidationError::Clock(error.to_string()))?;
validate_created_at(
metadata.created_at(),
now,
CONFIG.load().max_relay_future_skew_millis,
)?;
let context = VerifiedRelayContext {
signer_id: metadata.signer_id(),
final_recipient_id: metadata.final_recipient_id(),
@ -244,6 +284,23 @@ mod tests {
use mtp::codec::SealedRelayBuilder;
use mtp::crypto::{DualSigner, Ed25519Signer, Keyring};
#[test]
fn relay_freshness_has_bounded_future_and_retention() {
let now = 1_000_000_000_000_u64;
let max_age = u64::try_from(RELAY_RETENTION_MILLIS).unwrap_or(0) - 300_000;
assert!(validate_created_at(now, now, 300_000).is_ok());
assert!(validate_created_at(now - max_age, now, 300_000).is_ok());
assert!(matches!(
validate_created_at(now - max_age - 1, now, 300_000),
Err(RelayValidationError::StaleRelay { .. })
));
assert!(validate_created_at(now + 300_000, now, 300_000).is_ok());
assert!(matches!(
validate_created_at(now + 300_001, now, 300_000),
Err(RelayValidationError::RelayFromFuture { .. })
));
}
fn relay(message_id: u128) -> Result<(Keyring, Keyring, CommunicationValue), String> {
let signer_keyring = Keyring::generate();
let recipient_keyring = Keyring::generate();
@ -264,7 +321,7 @@ mod tests {
&signer,
)
.message_id(message_id)
.created_at(123)
.created_at(mtp::common::unix_time_millis().map_err(|error| error.to_string())?)
.metadata_recipients(vec![recipient_keyring.public_key_bundle()])
.content_recipients(vec![recipient_keyring.public_key_bundle()])
.build()
@ -339,7 +396,7 @@ mod tests {
&signer,
)
.message_id(7_u128)
.created_at(123)
.created_at(mtp::common::unix_time_millis().map_err(|error| error.to_string())?)
.metadata_recipients(vec![recipient_keyring.public_key_bundle()])
.content_recipients(vec![recipient_keyring.public_key_bundle()])
.build()