[Fix] Harden MTP codec, transport, and SDK security

This commit is contained in:
Alex Emmet 2026-08-18 20:57:45 +02:00
commit a7e804c603
No known key found for this signature in database
73 changed files with 11892 additions and 5756 deletions

View file

@ -2,7 +2,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::fmt;
use std::io::Cursor;
use crate::data_value::{DataKind, DataValue, DecodeLimits};
use crate::data_value::{DataKind, DataValue, DecodeError, DecodeLimits, EncodeLimits};
use crate::rand_u32;
use mtp_common::CodecError;
use mtp_type_map::{
@ -260,23 +260,50 @@ impl CommunicationValue {
#[must_use]
pub fn reply_to(&self, comm_type: CommunicationType) -> Self {
let mut response = Self::new(comm_type);
let type_map = self
.type_map
.as_ref()
.cloned()
.unwrap_or_else(TypeMap::latest);
let mut response = Self::new_with_type_map(comm_type, &type_map);
response.sender = self.receiver;
response.receiver = self.sender;
response
}
pub fn merge(&mut self, other: &Self) {
if self.mapping_error.is_none() {
self.mapping_error.clone_from(&other.mapping_error);
/// Merge clear container fields after confirming both values use the same
/// negotiated type map.
pub fn try_merge(&mut self, other: &Self) -> Result<(), CodecError> {
if let Some(error) = &self.mapping_error {
return Err(error.clone());
}
let Some(other_entries) = other.payload.container_entries() else {
self.mapping_error
.get_or_insert(CodecError::InvalidEncoding);
return;
};
let left = self.type_map().ok_or(CodecError::MissingTypeMap)?;
let right = other.type_map().ok_or(CodecError::MissingTypeMap)?;
if left.version != right.version {
return Err(CodecError::TypeMapMismatch {
expected: left.version.to_string(),
actual: right.version.to_string(),
});
}
if let Some(error) = &other.mapping_error {
return Err(error.clone());
}
let other_entries = other
.payload
.container_entries()
.ok_or(CodecError::InvalidEncoding)?;
for (id, value) in other_entries {
let _ = self.insert_data(*id, value.clone());
self.insert_data(*id, value.clone())?;
}
Ok(())
}
// Migrate to `try_merge` so a map mismatch cannot be silently recorded in
// a frame that is later sent over the wire.
#[deprecated(note = "migrate to try_merge to handle negotiated type-map mismatches")]
pub fn merge(&mut self, other: &Self) {
if let Err(error) = self.try_merge(other) {
self.mapping_error.get_or_insert(error);
}
}
@ -315,9 +342,22 @@ impl CommunicationValue {
}
pub fn to_bytes(&self) -> Result<Vec<u8>, CodecError> {
self.to_bytes_with_limits(EncodeLimits::default())
}
pub fn to_bytes_with_limits(&self, limits: EncodeLimits) -> Result<Vec<u8>, CodecError> {
if let Some(error) = &self.mapping_error {
return Err(error.clone());
}
let header_len = self.frame_header_len();
let payload_limit = limits
.max_output_size
.checked_sub(header_len)
.ok_or(CodecError::TooManyEntries)?;
let payload = self.payload.to_bytes_with_limits(EncodeLimits {
max_output_size: payload_limit,
..limits
})?;
let mut body = Vec::new();
body.write_u16::<BigEndian>(self.comm_type.0)
.map_err(|_| CodecError::InvalidEncoding)?;
@ -344,44 +384,71 @@ impl CommunicationValue {
body.write_u64::<BigEndian>(receiver)
.map_err(|_| CodecError::InvalidEncoding)?;
}
body.extend_from_slice(&self.payload.to_bytes()?);
body.extend_from_slice(&payload);
let length = u32::try_from(body.len()).map_err(|_| CodecError::TooManyEntries)?;
let mut out = Vec::with_capacity(4 + body.len());
let total_len = 4usize
.checked_add(body.len())
.ok_or(CodecError::TooManyEntries)?;
if total_len > limits.max_output_size {
return Err(CodecError::TooManyEntries);
}
let mut out = Vec::with_capacity(total_len);
out.write_u32::<BigEndian>(length)
.map_err(|_| CodecError::InvalidEncoding)?;
out.extend_from_slice(&body);
Ok(out)
}
fn frame_header_len(&self) -> usize {
4 + 2
+ 1
+ self.id.is_some() as usize * 4
+ self.sender.is_some() as usize * 8
+ self.receiver.is_some() as usize * 8
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CodecError> {
Self::from_bytes_with_limits(bytes, DecodeLimits::default())
}
pub fn from_bytes_with_limits(bytes: &[u8], limits: DecodeLimits) -> Result<Self, CodecError> {
Self::try_from_bytes_with_limits(bytes, limits).map_err(|_| CodecError::InvalidEncoding)
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
Self::try_from_bytes_with_limits(bytes, DecodeLimits::default())
}
pub fn try_from_bytes_with_limits(
bytes: &[u8],
limits: DecodeLimits,
) -> Result<Self, DecodeError> {
let mut cursor = Cursor::new(bytes);
let length = cursor
.read_u32::<BigEndian>()
.map_err(|_| CodecError::InvalidEncoding)? as usize;
.map_err(|_| DecodeError::MalformedEncoding)? as usize;
let end = 4usize
.checked_add(length)
.ok_or(CodecError::InvalidEncoding)?;
.ok_or(DecodeError::MalformedEncoding)?;
if end != bytes.len() {
return Err(CodecError::InvalidEncoding);
return Err(DecodeError::MalformedEncoding);
}
let comm_type = CommunicationTypeId(
cursor
.read_u16::<BigEndian>()
.map_err(|_| CodecError::InvalidEncoding)?,
.map_err(|_| DecodeError::MalformedEncoding)?,
);
let flags = cursor.read_u8().map_err(|_| CodecError::InvalidEncoding)?;
let flags = cursor
.read_u8()
.map_err(|_| DecodeError::MalformedEncoding)?;
if flags & !FLAG_KNOWN != 0 {
return Err(CodecError::InvalidEncoding);
return Err(DecodeError::MalformedEncoding);
}
let id = if flags & FLAG_HAS_ID != 0 {
Some(
cursor
.read_u32::<BigEndian>()
.map_err(|_| CodecError::InvalidEncoding)?,
.map_err(|_| DecodeError::MalformedEncoding)?,
)
} else {
None
@ -390,7 +457,7 @@ impl CommunicationValue {
Some(
cursor
.read_u64::<BigEndian>()
.map_err(|_| CodecError::InvalidEncoding)?,
.map_err(|_| DecodeError::MalformedEncoding)?,
)
} else {
None
@ -399,14 +466,14 @@ impl CommunicationValue {
Some(
cursor
.read_u64::<BigEndian>()
.map_err(|_| CodecError::InvalidEncoding)?,
.map_err(|_| DecodeError::MalformedEncoding)?,
)
} else {
None
};
let payload = DataValue::read_from_with_limits(&mut cursor, limits)?;
let payload = DataValue::read_from_with_diagnostics(&mut cursor, limits)?;
if cursor.position() as usize != end {
return Err(CodecError::InvalidEncoding);
return Err(DecodeError::MalformedEncoding);
}
Ok(Self {
id,
@ -420,13 +487,36 @@ impl CommunicationValue {
}
pub fn from_bytes_with(bytes: &[u8], type_map: &TypeMap) -> Result<Self, CodecError> {
let mut value = Self::from_bytes(bytes)?;
Self::try_from_bytes_with(bytes, type_map).map_err(|_| CodecError::InvalidEncoding)
}
pub fn try_from_bytes_with(bytes: &[u8], type_map: &TypeMap) -> Result<Self, DecodeError> {
Self::try_from_bytes_with_type_map_and_limits(bytes, type_map, DecodeLimits::default())
}
pub fn try_from_bytes_with_type_map_and_limits(
bytes: &[u8],
type_map: &TypeMap,
limits: DecodeLimits,
) -> Result<Self, DecodeError> {
let mut value = Self::try_from_bytes_with_limits(bytes, limits)?;
value.set_type_map(type_map);
Ok(value)
}
#[cfg(feature = "registry")]
pub fn migrate(&self, target: &TypeMap) -> Result<Self, CodecError> {
self.migrate_with_limits(target, EncodeLimits::default())
}
/// Migrate a clear frame while bounding the recursive traversal used to
/// translate its type IDs.
#[cfg(feature = "registry")]
pub fn migrate_with_limits(
&self,
target: &TypeMap,
limits: EncodeLimits,
) -> Result<Self, CodecError> {
if let Some(error) = &self.mapping_error {
return Err(error.clone());
}
@ -441,7 +531,8 @@ impl CommunicationValue {
.comm_id_enum(comm)
.ok_or_else(|| CodecError::UnknownCommunicationType(comm_name.to_string()))?,
);
let payload = migrate_data_value(&self.payload, source, target)?;
let mut context = MigrationContext::new(limits);
let payload = migrate_data_value(&self.payload, source, target, &mut context)?;
Ok(Self {
id: self.id,
comm_type,
@ -454,15 +545,63 @@ impl CommunicationValue {
}
}
#[cfg(feature = "registry")]
struct MigrationContext {
limits: EncodeLimits,
depth: usize,
values: usize,
}
#[cfg(feature = "registry")]
impl MigrationContext {
fn new(limits: EncodeLimits) -> Self {
Self {
limits,
depth: 0,
values: 0,
}
}
fn value(&mut self) -> Result<(), CodecError> {
self.values = self
.values
.checked_add(1)
.ok_or(CodecError::TooManyEntries)?;
if self.values > self.limits.max_values {
return Err(CodecError::TooManyEntries);
}
Ok(())
}
fn enter(&mut self) -> Result<(), CodecError> {
self.depth = self
.depth
.checked_add(1)
.ok_or(CodecError::TooManyEntries)?;
if self.depth > self.limits.max_depth {
return Err(CodecError::TooManyEntries);
}
Ok(())
}
fn leave(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
}
#[cfg(feature = "registry")]
fn migrate_data_value(
value: &DataValue,
source: &TypeMap,
target: &TypeMap,
context: &mut MigrationContext,
) -> Result<DataValue, CodecError> {
context.value()?;
match value {
DataValue::Container(entries) => {
let mut migrated = Vec::with_capacity(entries.len());
context.enter()?;
let count = u16::try_from(entries.len()).map_err(|_| CodecError::TooManyEntries)?;
let mut migrated = Vec::with_capacity(usize::from(count));
for (old_id, value) in entries {
let name = source
.data_type_name(old_id.0)
@ -474,16 +613,20 @@ fn migrate_data_value(
.data_id_enum(data)
.ok_or_else(|| CodecError::UnknownDataType(name.to_string()))?,
);
migrated.push((new_id, migrate_data_value(value, source, target)?));
migrated.push((new_id, migrate_data_value(value, source, target, context)?));
}
context.leave();
Ok(DataValue::Container(migrated))
}
DataValue::Array(values) => Ok(DataValue::Array(
values
.iter()
.map(|value| migrate_data_value(value, source, target))
.collect::<Result<Vec<_>, _>>()?,
)),
DataValue::Array(values) => {
context.enter()?;
let mut migrated = Vec::with_capacity(values.len());
for value in values {
migrated.push(migrate_data_value(value, source, target, context)?);
}
context.leave();
Ok(DataValue::Array(migrated))
}
#[cfg(feature = "crypto")]
DataValue::Signed(_) | DataValue::Encrypted(_) => Err(CodecError::InvalidEncoding),
scalar => Ok(scalar.clone()),
@ -641,6 +784,39 @@ mod tests {
assert_eq!(frame.get_data(DataType::Version), None);
}
#[test]
fn replies_retain_the_request_type_map() {
let type_map = TypeMap::new(mtp_type_map::Version::new(3, 0));
let request = CommunicationValue::new_with_type_map(CommunicationType::Ping, &type_map)
.with_sender(7)
.with_receiver(9);
let reply = request.reply_to(CommunicationType::Pong);
assert_eq!(
reply.type_map().map(|map| &map.version),
Some(&type_map.version)
);
assert_eq!(reply.sender(), Some(9));
assert_eq!(reply.receiver(), Some(7));
}
#[test]
fn try_merge_rejects_frames_from_different_type_maps() {
let left_map = TypeMap::new(mtp_type_map::Version::new(3, 0));
let right_map = TypeMap::new(mtp_type_map::Version::new(4, 0));
let mut left = CommunicationValue::new_with_type_map(CommunicationType::Ping, &left_map);
let right = CommunicationValue::new_with_type_map(CommunicationType::Ping, &right_map);
assert_eq!(
left.try_merge(&right),
Err(CodecError::TypeMapMismatch {
expected: "3.0".into(),
actual: "4.0".into(),
})
);
assert_eq!(left.data_len(), 0);
}
#[test]
fn generic_payload_roundtrips_without_becoming_a_container() {
let payload = DataValue::Array(vec![

File diff suppressed because it is too large Load diff

View file

@ -11,20 +11,33 @@ pub use data_value::{
ApplicationProtectionPurpose, EncryptedValue, MtpProtectionPurpose, ProtectionError,
ProtectionPolicy, ProtectionPurpose, ProtectionPurposeError, SignaturePolicy, SignedValue,
};
pub use data_value::{DataKind, DataValue, DecodeLimits};
pub use data_value::{
DEFAULT_TRANSPORT_ALLOCATION_FACTOR, DataKind, DataValue, DecodeError, DecodeLimits,
EncodeLimits,
};
pub use mtp_common::{CodecError, TimeError, unix_time_millis};
#[cfg(feature = "crypto")]
#[allow(deprecated)]
pub use protected::{
CURRENT_PROTECTED_VERSION, InMemoryReplayGuard, ProtectedError, ProtectedMessageBuilder,
ProtectedOpenOptions, ReplayError, ReplayGuard, VerifiedProtectedMessage, open_protected,
open_protected_with, open_protected_with_keys, protected_claimed_signer_id,
CURRENT_PROTECTED_VERSION, InMemoryReplayGuard, ProtectedError, ProtectedLimits,
ProtectedMessageBuilder, ProtectedOpenOptions, ReplayError, ReplayGuard,
VerifiedProtectedMessage, open_protected_checked, open_protected_with_checked,
open_protected_with_keys_checked, open_protected_with_keys_without_replay,
open_protected_with_without_replay, open_protected_without_replay, protected_claimed_signer_id,
protected_claimed_signer_id_with_limits, protected_claimed_signer_id_with_options,
};
#[cfg(feature = "crypto")]
#[allow(deprecated)]
pub use relay::{
CURRENT_RELAY_VERSION, RelayError, SealedRelayBuilder, VerifiedRelayContent,
CURRENT_RELAY_VERSION, RelayError, RelayOpenOptions, SealedRelayBuilder, VerifiedRelayContent,
VerifiedRelayMetadata, forward_relay_frame, open_relay_content,
open_relay_content_with_keyrings, open_relay_content_with_keys, open_relay_metadata,
open_relay_metadata_with, open_relay_metadata_with_keys, relay_metadata_claimed_signer_id,
open_relay_content_with_keyrings, open_relay_content_with_keyrings_and_limits,
open_relay_content_with_keys, open_relay_content_with_limits,
open_relay_content_with_limits_without_replay, open_relay_metadata_checked,
open_relay_metadata_with_checked, open_relay_metadata_with_limits_checked,
open_relay_metadata_with_limits_without_replay, open_relay_metadata_with_without_replay,
open_relay_metadata_without_replay, relay_metadata_claimed_signer_id,
relay_metadata_claimed_signer_id_with_limits, relay_metadata_claimed_signer_id_with_options,
};
pub use mtp_type_map::{

View file

@ -7,15 +7,39 @@
#![cfg(feature = "crypto")]
use mtp_crypto::{Keyring, PublicKeyBundle, SignatureScheme};
use mtp_type_map::{CommunicationType, DataType, TypeMap};
use std::collections::HashSet;
use mtp_type_map::{CommunicationType, DataType, DataTypeId, TypeMap};
use std::collections::{HashSet, VecDeque};
use crate::{CommunicationValue, DataValue, ProtectionError, ProtectionPolicy, ProtectionPurpose};
use crate::{
CommunicationValue, DataValue, DecodeLimits, EncodeLimits, ProtectionError, ProtectionPolicy,
ProtectionPurpose,
};
/// The direct protected-message envelope schema version emitted by this
/// codec.
pub const CURRENT_PROTECTED_VERSION: u64 = 1;
/// Semantic limits for fields that are retained after a protected message is
/// opened. These are intentionally separate from generic transport blobs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProtectedLimits {
pub max_message_id_bytes: usize,
pub max_metadata_encoded_bytes: usize,
pub max_signer_key_history: usize,
pub max_decryption_key_history: usize,
}
impl Default for ProtectedLimits {
fn default() -> Self {
Self {
max_message_id_bytes: 256,
max_metadata_encoded_bytes: 1024 * 1024,
max_signer_key_history: 8,
max_decryption_key_history: 8,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProtectedError {
#[error("value is not an application communication frame")]
@ -46,6 +70,8 @@ pub enum ProtectedError {
ReservedApplicationType(String),
#[error("protected message was already accepted")]
Replay,
#[error("protected resource limit exceeded: {0}")]
ResourceLimit(&'static str),
#[error("protection error: {0}")]
Protection(#[from] ProtectionError),
#[error("replay guard error: {0}")]
@ -78,9 +104,35 @@ pub enum ReplayError {
/// Small in-memory guard useful for tests and short-lived clients. Production
/// consumers should implement [`ReplayGuard`] over persistent storage.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct InMemoryReplayGuard {
accepted: HashSet<(u64, String)>,
order: VecDeque<(u64, String)>,
capacity: usize,
}
impl Default for InMemoryReplayGuard {
fn default() -> Self {
Self::with_capacity(10_000)
}
}
impl InMemoryReplayGuard {
pub fn new(capacity: usize) -> Self {
Self::with_capacity(capacity)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
accepted: HashSet::new(),
order: VecDeque::new(),
capacity,
}
}
pub fn len(&self) -> usize {
self.accepted.len()
}
}
impl ReplayGuard for InMemoryReplayGuard {
@ -90,7 +142,21 @@ impl ReplayGuard for InMemoryReplayGuard {
message_id: &str,
_created_at: u64,
) -> Result<bool, ReplayError> {
Ok(self.accepted.insert((signer_id, message_id.to_owned())))
let key = (signer_id, message_id.to_owned());
if self.accepted.contains(&key) {
return Ok(false);
}
if self.capacity == 0 {
return Ok(false);
}
self.accepted.insert(key.clone());
self.order.push_back(key);
while self.accepted.len() > self.capacity {
if let Some(oldest) = self.order.pop_front() {
self.accepted.remove(&oldest);
}
}
Ok(true)
}
}
@ -110,6 +176,8 @@ pub struct ProtectedMessageBuilder<'a> {
type_map: Option<TypeMap>,
frame_id: Option<u32>,
expose_sender: bool,
limits: ProtectedLimits,
encode_limits: EncodeLimits,
}
impl<'a> ProtectedMessageBuilder<'a> {
@ -136,6 +204,8 @@ impl<'a> ProtectedMessageBuilder<'a> {
type_map: None,
frame_id: None,
expose_sender: false,
limits: ProtectedLimits::default(),
encode_limits: EncodeLimits::default(),
}
}
@ -176,6 +246,16 @@ impl<'a> ProtectedMessageBuilder<'a> {
self
}
pub fn protected_limits(mut self, limits: ProtectedLimits) -> Self {
self.limits = limits;
self
}
pub fn encode_limits(mut self, limits: EncodeLimits) -> Self {
self.encode_limits = limits;
self
}
pub fn build(self) -> Result<CommunicationValue, ProtectedError> {
let message_id = self.message_id.ok_or(ProtectedError::InvalidLayout(
"protected builder requires a message ID",
@ -188,6 +268,9 @@ impl<'a> ProtectedMessageBuilder<'a> {
"protected identifiers must be non-empty",
));
}
if message_id.len() > self.limits.max_message_id_bytes {
return Err(ProtectedError::ResourceLimit("message ID"));
}
if self.recipients.is_empty() {
return Err(ProtectedError::InvalidLayout(
"protected builder requires at least one recipient",
@ -217,8 +300,17 @@ impl<'a> ProtectedMessageBuilder<'a> {
(created_at_id, DataValue::UnsignedNumber(created_at as u128)),
(content_id, self.content),
]);
let signed = envelope.sign(self.signer_id, self.signature_purpose, self.signer)?;
let encrypted = signed.encrypt_for(&self.recipients, self.encryption_purpose)?;
let signed = envelope.sign_with_limits(
self.signer_id,
self.signature_purpose,
self.signer,
self.encode_limits,
)?;
let encrypted = signed.encrypt_for_with_limits(
&self.recipients,
self.encryption_purpose,
self.encode_limits,
)?;
let mut frame = CommunicationValue::new_with_type_map(application_type, &type_map)
.with_receiver(self.final_recipient_id)
@ -258,6 +350,12 @@ pub struct ProtectedOpenOptions {
pub encryption_purpose: ProtectionPurpose,
/// Signature algorithms accepted by the receiver.
pub policy: ProtectionPolicy,
/// Recursive and cumulative allocation policy used while opening.
pub decode_limits: DecodeLimits,
/// Bound used when reconstructing signed bytes for verification.
pub encode_limits: EncodeLimits,
/// Semantic limits for retained protected fields and key histories.
pub protected_limits: ProtectedLimits,
}
impl ProtectedOpenOptions {
@ -272,8 +370,41 @@ impl ProtectedOpenOptions {
signature_purpose,
encryption_purpose,
policy,
decode_limits: DecodeLimits {
max_depth: 64,
max_values: 65_536,
max_blob_size: 16 * 1024 * 1024,
max_recipients: 64,
max_allocated_bytes: 64 * 1024 * 1024,
},
encode_limits: EncodeLimits {
max_depth: 64,
max_values: 65_536,
max_output_size: 16 * 1024 * 1024,
},
protected_limits: ProtectedLimits {
max_message_id_bytes: 256,
max_metadata_encoded_bytes: 1024 * 1024,
max_signer_key_history: 8,
max_decryption_key_history: 8,
},
}
}
pub const fn with_limits(
mut self,
decode_limits: DecodeLimits,
protected_limits: ProtectedLimits,
) -> Self {
self.decode_limits = decode_limits;
self.protected_limits = protected_limits;
self
}
pub const fn with_encode_limits(mut self, encode_limits: EncodeLimits) -> Self {
self.encode_limits = encode_limits;
self
}
}
fn protected_field_id(
@ -328,23 +459,26 @@ fn validate_protected_frame(frame: &CommunicationValue) -> Result<TypeMap, Prote
}
fn field<'a>(
value: &'a DataValue,
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<&'a DataValue, ProtectedError> {
value
.get_field(protected_field_id(data_type, type_map)?)
let field_id = protected_field_id(data_type, type_map)?;
entries
.iter()
.find(|(id, _)| *id == field_id)
.map(|(_, value)| value)
.ok_or(ProtectedError::InvalidLayout(
"required protected field is missing",
))
}
fn unsigned_field(
value: &DataValue,
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<u128, ProtectedError> {
field(value, data_type, type_map)?
field(entries, data_type, type_map)?
.as_unsigned_number()
.ok_or(ProtectedError::InvalidLayout(
"protected field is not unsigned",
@ -352,11 +486,11 @@ fn unsigned_field(
}
fn string_field(
value: &DataValue,
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<String, ProtectedError> {
field(value, data_type, type_map)?
field(entries, data_type, type_map)?
.as_string()
.filter(|value| !value.is_empty())
.ok_or(ProtectedError::InvalidLayout(
@ -364,10 +498,28 @@ fn string_field(
))
}
fn protected_version(value: &DataValue, type_map: &TypeMap) -> Result<u64, ProtectedError> {
fn string_field_ref<'a>(
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<&'a str, ProtectedError> {
field(entries, data_type, type_map)?
.as_str()
.filter(|value| !value.is_empty())
.ok_or(ProtectedError::InvalidLayout(
"protected field is not a non-empty string",
))
}
fn protected_version(
entries: &[(DataTypeId, DataValue)],
type_map: &TypeMap,
) -> Result<u64, ProtectedError> {
let version_id = protected_field_id(DataType::ProtectedVersion, type_map)?;
let version = value
.get_field(version_id)
let version = entries
.iter()
.find(|(id, _)| *id == version_id)
.map(|(_, value)| value)
.ok_or(ProtectedError::MissingProtectedVersion)?
.as_unsigned_number()
.ok_or(ProtectedError::InvalidLayout(
@ -381,10 +533,15 @@ fn decrypt_protected_payload(
frame: &CommunicationValue,
keyrings: &[&Keyring],
encryption_purpose: ProtectionPurpose,
decode_limits: DecodeLimits,
max_decryption_key_history: usize,
) -> Result<DataValue, ProtectedError> {
if keyrings.len() > max_decryption_key_history {
return Err(ProtectedError::ResourceLimit("decryption key history"));
}
frame
.payload()
.decrypt_with_keyrings(keyrings, encryption_purpose)
.decrypt_with_keyrings_and_limits(keyrings, encryption_purpose, decode_limits)
.map_err(|error| match error {
ProtectionError::NotEncrypted => ProtectedError::PayloadNotEncrypted,
other => ProtectedError::Protection(other),
@ -394,22 +551,63 @@ fn decrypt_protected_payload(
/// Return the claimed signer ID after decryption, without verifying its
/// signature. The value is untrusted and may only select the key history that
/// is then bound to the same signer ID during the subsequent open.
#[deprecated(note = "use protected_claimed_signer_id_with_limits; pass the receive DecodeLimits")]
pub fn protected_claimed_signer_id(
frame: &CommunicationValue,
keyrings: &[&Keyring],
encryption_purpose: ProtectionPurpose,
) -> Result<u64, ProtectedError> {
// Migrate to `protected_claimed_signer_id_with_limits` at receive boundaries.
protected_claimed_signer_id_with_limits(
frame,
keyrings,
encryption_purpose,
DecodeLimits::default(),
)
}
pub fn protected_claimed_signer_id_with_limits(
frame: &CommunicationValue,
keyrings: &[&Keyring],
encryption_purpose: ProtectionPurpose,
decode_limits: DecodeLimits,
) -> Result<u64, ProtectedError> {
protected_claimed_signer_id_with_options(
frame,
keyrings,
encryption_purpose,
decode_limits,
ProtectedLimits::default(),
)
}
/// Return the claimed signer ID while applying the complete receive policy.
///
/// This is deliberately separate from the compatibility decoder above: the
/// claimed ID is used to select a signer-key history, so the decryption-key
/// history bound must be the same bound used by the eventual open operation.
pub fn protected_claimed_signer_id_with_options(
frame: &CommunicationValue,
keyrings: &[&Keyring],
encryption_purpose: ProtectionPurpose,
decode_limits: DecodeLimits,
protected_limits: ProtectedLimits,
) -> Result<u64, ProtectedError> {
validate_protected_frame(frame)?;
let decrypted = decrypt_protected_payload(frame, keyrings, encryption_purpose)?;
let decrypted = decrypt_protected_payload(
frame,
keyrings,
encryption_purpose,
decode_limits,
protected_limits.max_decryption_key_history,
)?;
let signed = decrypted
.as_signed()
.ok_or(ProtectedError::PayloadNotSigned)?;
Ok(signed.signer_id)
}
/// Open a direct protected message using a resolver for trusted signer keys.
/// The resolver receives a claimed, unverified signer ID only as a lookup key.
pub fn open_protected_with<F>(
fn open_protected_with_impl<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
@ -422,7 +620,13 @@ where
{
validate_protected_frame(frame)?;
let type_map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest);
let decrypted = decrypt_protected_payload(frame, keyrings, options.encryption_purpose)?;
let decrypted = decrypt_protected_payload(
frame,
keyrings,
options.encryption_purpose,
options.decode_limits,
options.protected_limits.max_decryption_key_history,
)?;
let signed = decrypted
.as_signed()
.ok_or(ProtectedError::PayloadNotSigned)?;
@ -437,13 +641,54 @@ where
}
let signer_keys = resolve_signer_keys(signed.signer_id)
.ok_or(ProtectionError::SignerKeyNotFound(signed.signer_id))?;
if signer_keys.len() > options.protected_limits.max_signer_key_history {
return Err(ProtectedError::ResourceLimit("signer key history"));
}
open_decrypted_protected(frame, type_map, signed, &signer_keys, options, replay_guard)
}
/// Open a direct protected message against already resolved trusted signer
/// keys. The signer ID is mandatory so a key history cannot be applied to a
/// different claimed identity.
pub fn open_protected_with_keys(
pub fn open_protected_with_checked<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: ProtectedOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedProtectedMessage, ProtectedError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_protected_with_impl(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
Some(replay_guard),
)
}
pub fn open_protected_with_without_replay<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: ProtectedOpenOptions,
) -> Result<VerifiedProtectedMessage, ProtectedError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_protected_with_impl(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
None,
)
}
fn open_protected_with_keys_impl(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
@ -452,7 +697,13 @@ pub fn open_protected_with_keys(
replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
let type_map = validate_protected_frame(frame)?;
let decrypted = decrypt_protected_payload(frame, keyrings, options.encryption_purpose)?;
let decrypted = decrypt_protected_payload(
frame,
keyrings,
options.encryption_purpose,
options.decode_limits,
options.protected_limits.max_decryption_key_history,
)?;
let signed = decrypted
.as_signed()
.ok_or(ProtectedError::PayloadNotSigned)?;
@ -473,23 +724,73 @@ pub fn open_protected_with_keys(
)
}
/// Open a direct protected message when the expected signer and one trusted
/// public key are already known.
pub fn open_protected(
pub fn open_protected_with_keys_checked(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
signer_public_keys: &[PublicKeyBundle],
options: ProtectedOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
open_protected_with_keys_impl(
frame,
keyrings,
expected_signer_id,
signer_public_keys,
options,
Some(replay_guard),
)
}
pub fn open_protected_with_keys_without_replay(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
signer_public_keys: &[PublicKeyBundle],
options: ProtectedOpenOptions,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
open_protected_with_keys_impl(
frame,
keyrings,
expected_signer_id,
signer_public_keys,
options,
None,
)
}
pub fn open_protected_checked(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
options: ProtectedOpenOptions,
replay_guard: Option<&mut dyn ReplayGuard>,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
open_protected_with_keys(
open_protected_with_keys_impl(
frame,
std::slice::from_ref(&keyring),
expected_signer_id,
std::slice::from_ref(signer_public_key),
options,
replay_guard,
Some(replay_guard),
)
}
pub fn open_protected_without_replay(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
options: ProtectedOpenOptions,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
open_protected_with_keys_impl(
frame,
std::slice::from_ref(&keyring),
expected_signer_id,
std::slice::from_ref(signer_public_key),
options,
None,
)
}
@ -501,11 +802,15 @@ fn open_decrypted_protected(
options: ProtectedOpenOptions,
mut replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
let matched_signer_key_index = signed.verify_with_key_history_index(
if signer_public_keys.len() > options.protected_limits.max_signer_key_history {
return Err(ProtectedError::ResourceLimit("signer key history"));
}
let matched_signer_key_index = signed.verify_with_key_history_index_and_limits(
signed.signer_id,
signer_public_keys,
options.signature_purpose,
options.policy,
options.encode_limits,
)?;
let receiver_id = frame.receiver().ok_or(ProtectedError::MissingReceiver)?;
if options
@ -520,22 +825,23 @@ fn open_decrypted_protected(
{
return Err(ProtectedError::SenderMismatch);
}
/* The authenticated value is already owned by the decoder. Keep this
inspection borrowed so opening a large envelope does not clone it. */
let envelope = signed
.value
.as_container()
.container_entries()
.ok_or(ProtectedError::MissingEnvelope)?;
let envelope = DataValue::Container(envelope);
let version = protected_version(&envelope, &type_map)?;
let version = protected_version(envelope, &type_map)?;
if version != CURRENT_PROTECTED_VERSION {
return Err(ProtectedError::UnsupportedProtectedVersion(version));
}
let message_type = string_field(&envelope, DataType::MessageType, &type_map)?;
let message_type = string_field(envelope, DataType::MessageType, &type_map)?;
let application_type = validate_application_message_type(&message_type, &type_map)?;
if frame.get_comm_type_enum() != Some(application_type) {
return Err(ProtectedError::MessageTypeMismatch);
}
let final_recipient_id = u64::try_from(unsigned_field(
&envelope,
envelope,
DataType::FinalRecipientId,
&type_map,
)?)
@ -543,10 +849,14 @@ fn open_decrypted_protected(
if final_recipient_id != receiver_id {
return Err(ProtectedError::FinalRecipientMismatch);
}
let message_id = string_field(&envelope, DataType::MessageId, &type_map)?;
let created_at = u64::try_from(unsigned_field(&envelope, DataType::CreatedAt, &type_map)?)
let message_id = string_field_ref(envelope, DataType::MessageId, &type_map)?;
if message_id.len() > options.protected_limits.max_message_id_bytes {
return Err(ProtectedError::ResourceLimit("message ID"));
}
let message_id = message_id.to_owned();
let created_at = u64::try_from(unsigned_field(envelope, DataType::CreatedAt, &type_map)?)
.map_err(|_| ProtectedError::InvalidLayout("created-at value is out of range"))?;
let content = field(&envelope, DataType::Content, &type_map)?.clone();
let content = field(envelope, DataType::Content, &type_map)?.clone();
if let Some(guard) = replay_guard.as_mut()
&& !guard.accept(signed.signer_id, &message_id, created_at)?
@ -569,7 +879,7 @@ fn open_decrypted_protected(
#[cfg(test)]
mod tests {
use super::*;
use mtp_crypto::{Ed25519Signer, Keyring};
use mtp_crypto::{Ed25519Signer, Keyring, PublicKeyBundle};
use mtp_type_map::{DataType, DataTypeId};
const SIGNATURE_PURPOSE: ProtectionPurpose = ProtectionPurpose(0x40);
@ -584,10 +894,98 @@ mod tests {
)
}
// Keep the existing test cases concise while making the production API
// choice explicit: every call below is routed to either the checked or
// the named without-replay entry point.
fn open_protected(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
options: ProtectedOpenOptions,
replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
match replay_guard {
Some(replay_guard) => super::open_protected_checked(
frame,
keyring,
expected_signer_id,
signer_public_key,
options,
replay_guard,
),
None => super::open_protected_without_replay(
frame,
keyring,
expected_signer_id,
signer_public_key,
options,
),
}
}
fn open_protected_with<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: ProtectedOpenOptions,
replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedProtectedMessage, ProtectedError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
match replay_guard {
Some(replay_guard) => super::open_protected_with_checked(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
replay_guard,
),
None => super::open_protected_with_without_replay(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
),
}
}
fn open_protected_with_keys(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
signer_public_keys: &[PublicKeyBundle],
options: ProtectedOpenOptions,
replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedProtectedMessage, ProtectedError> {
match replay_guard {
Some(replay_guard) => super::open_protected_with_keys_checked(
frame,
keyrings,
expected_signer_id,
signer_public_keys,
options,
replay_guard,
),
None => super::open_protected_with_keys_without_replay(
frame,
keyrings,
expected_signer_id,
signer_public_keys,
options,
),
}
}
#[derive(Default)]
struct RecordingReplayGuard {
created_at: Option<u64>,
accepted: bool,
calls: usize,
}
impl ReplayGuard for RecordingReplayGuard {
@ -597,6 +995,7 @@ mod tests {
_message_id: &str,
created_at: u64,
) -> Result<bool, ReplayError> {
self.calls += 1;
self.created_at = Some(created_at);
if self.accepted {
Ok(false)
@ -607,6 +1006,38 @@ mod tests {
}
}
#[test]
fn in_memory_replay_guard_is_bounded_and_deduplicates() {
let mut guard = InMemoryReplayGuard::with_capacity(2);
assert!(guard.accept(7, "first", 1).expect("first replay decision"));
assert!(
guard
.accept(7, "second", 2)
.expect("second replay decision")
);
assert!(
!guard
.accept(7, "first", 3)
.expect("duplicate replay decision")
);
assert_eq!(guard.len(), 2);
assert!(guard.accept(7, "third", 4).expect("third replay decision"));
assert_eq!(guard.len(), 2);
assert!(
guard
.accept(7, "first", 5)
.expect("evicted replay decision")
);
let mut disabled = InMemoryReplayGuard::with_capacity(0);
assert!(
!disabled
.accept(7, "disabled", 1)
.expect("disabled replay decision")
);
}
fn protected_field(data_type: DataType, type_map: &TypeMap) -> DataTypeId {
data_type
.try_to_id(type_map)
@ -743,6 +1174,7 @@ mod tests {
assert_eq!(opened.message_type, "ProtectedMessage");
assert_eq!(opened.message_id, "protected-test");
assert_eq!(guard.created_at, Some(1_700_000_000_000));
assert_eq!(guard.calls, 1);
assert_eq!(opened.content, DataValue::Str("hello".into()));
assert!(matches!(
open_protected(
@ -757,6 +1189,29 @@ mod tests {
));
}
#[test]
fn oversized_message_id_is_rejected_before_replay_guard() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let frame = valid_frame(&sender, &recipient, DataValue::Str("hello".into()));
let mut options = open_options(Some(42));
options.protected_limits.max_message_id_bytes = 3;
let mut guard = RecordingReplayGuard::default();
assert!(matches!(
open_protected_checked(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
options,
&mut guard,
),
Err(ProtectedError::ResourceLimit("message ID"))
));
assert_eq!(guard.calls, 0);
}
#[test]
fn builder_owns_outer_sender_and_frame_id() {
let sender = Keyring::generate();

View file

@ -2,6 +2,7 @@ use mtp_common::CodecError;
use mtp_type_map::{PROTOCOL_VERSION, TypeMap, Version};
use crate::CommunicationValue;
use crate::EncodeLimits;
pub use mtp_type_map::Registry;
@ -42,7 +43,41 @@ impl VersionedCodec {
/// Encode a value using the codec's negotiated framing rules.
pub fn encode(&self, value: &CommunicationValue) -> Result<Vec<u8>, CodecError> {
value.to_bytes()
self.encode_with_limits(value, EncodeLimits::default())
}
/// Encode using an explicit output/resource limit after verifying the
/// value belongs to this codec's negotiated type map.
pub fn encode_with_limits(
&self,
value: &CommunicationValue,
limits: EncodeLimits,
) -> Result<Vec<u8>, CodecError> {
let value_map = value.type_map().ok_or(CodecError::MissingTypeMap)?;
if value_map.version != self.type_map.version {
return Err(CodecError::TypeMapMismatch {
expected: self.type_map.version.to_string(),
actual: value_map.version.to_string(),
});
}
value.to_bytes_with_limits(limits)
}
/// Explicitly migrate a clear frame to this codec's negotiated type map
/// before encoding it.
pub fn encode_migrating(&self, value: &CommunicationValue) -> Result<Vec<u8>, CodecError> {
self.encode_migrating_with_limits(value, EncodeLimits::default())
}
/// Explicitly migrate and encode with bounded traversal/output.
pub fn encode_migrating_with_limits(
&self,
value: &CommunicationValue,
limits: EncodeLimits,
) -> Result<Vec<u8>, CodecError> {
value
.migrate_with_limits(&self.type_map, limits)?
.to_bytes_with_limits(limits)
}
/// Decode a frame and retain the negotiated type map for typed access.
@ -58,3 +93,34 @@ impl VersionedCodec {
&self.registry
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DataValue;
use mtp_type_map::{CommunicationType, Version};
#[test]
fn encode_rejects_a_value_from_another_negotiated_map() {
let mut registry = Registry::new();
let version_a = Version::new(3, 0);
let version_b = Version::new(4, 0);
registry.register(TypeMap::new(version_a.clone()));
registry.register(TypeMap::new(version_b.clone()));
let codec = VersionedCodec::for_version(registry, version_b).expect("codec version");
let value = CommunicationValue::new_with_type_map(
CommunicationType::Ping,
&TypeMap::new(version_a.clone()),
)
.with_payload(DataValue::Null);
assert_eq!(
codec.encode(&value),
Err(CodecError::TypeMapMismatch {
expected: "4.0".into(),
actual: "3.0".into(),
})
);
}
}

View file

@ -9,11 +9,11 @@
#![cfg(feature = "crypto")]
use mtp_crypto::{Keyring, PublicKeyBundle, SignatureScheme};
use mtp_type_map::{CommunicationType, DataType, TypeMap};
use mtp_type_map::{CommunicationType, DataType, DataTypeId, TypeMap};
use crate::{
CommunicationValue, DataValue, MtpProtectionPurpose, ProtectionError, ProtectionPolicy,
ReplayError, ReplayGuard,
CommunicationValue, DataValue, DecodeLimits, EncodeLimits, MtpProtectionPurpose,
ProtectedLimits, ProtectionError, ProtectionPolicy, ReplayError, ReplayGuard,
};
/// The relay metadata schema emitted by [`SealedRelayBuilder`].
@ -37,6 +37,8 @@ pub enum RelayError {
NotFinalRecipient,
#[error("relay message was already accepted")]
Replay,
#[error("relay resource limit exceeded: {0}")]
ResourceLimit(&'static str),
#[error("relay application message type is reserved: {0}")]
ReservedApplicationType(String),
#[error("protection error: {0}")]
@ -62,6 +64,9 @@ pub struct VerifiedRelayMetadata {
encrypted_content: DataValue,
type_map: TypeMap,
matched_signer_key_index: usize,
decode_limits: DecodeLimits,
encode_limits: EncodeLimits,
protected_limits: ProtectedLimits,
// There is intentionally no public constructor. This marker documents
// that the fields originate from a successful authenticated open.
_verified: VerifiedMarker,
@ -84,6 +89,18 @@ impl VerifiedRelayMetadata {
self.final_recipient_id
}
pub fn decode_limits(&self) -> DecodeLimits {
self.decode_limits
}
pub fn protected_limits(&self) -> ProtectedLimits {
self.protected_limits
}
pub fn encode_limits(&self) -> EncodeLimits {
self.encode_limits
}
pub fn message_id(&self) -> &str {
&self.message_id
}
@ -120,6 +137,40 @@ pub struct VerifiedRelayContent {
pub content: DataValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RelayOpenOptions {
pub policy: ProtectionPolicy,
pub decode_limits: DecodeLimits,
pub encode_limits: EncodeLimits,
pub protected_limits: ProtectedLimits,
}
impl RelayOpenOptions {
pub fn new(policy: ProtectionPolicy) -> Self {
Self {
policy,
decode_limits: DecodeLimits::default(),
encode_limits: EncodeLimits::default(),
protected_limits: ProtectedLimits::default(),
}
}
pub const fn with_limits(
mut self,
decode_limits: DecodeLimits,
protected_limits: ProtectedLimits,
) -> Self {
self.decode_limits = decode_limits;
self.protected_limits = protected_limits;
self
}
pub const fn with_encode_limits(mut self, encode_limits: EncodeLimits) -> Self {
self.encode_limits = encode_limits;
self
}
}
/// Native sealed-relay builder shared by non-WASM applications.
///
/// The browser SDK and this builder intentionally produce the same reserved
@ -139,6 +190,8 @@ pub struct SealedRelayBuilder<'a> {
metadata_recipients: Vec<PublicKeyBundle>,
content_recipients: Vec<PublicKeyBundle>,
type_map: Option<TypeMap>,
limits: ProtectedLimits,
encode_limits: EncodeLimits,
}
impl<'a> SealedRelayBuilder<'a> {
@ -163,6 +216,8 @@ impl<'a> SealedRelayBuilder<'a> {
metadata_recipients: Vec::new(),
content_recipients: Vec::new(),
type_map: None,
limits: ProtectedLimits::default(),
encode_limits: EncodeLimits::default(),
}
}
@ -192,6 +247,16 @@ impl<'a> SealedRelayBuilder<'a> {
self
}
pub fn protected_limits(mut self, limits: ProtectedLimits) -> Self {
self.limits = limits;
self
}
pub fn encode_limits(mut self, limits: EncodeLimits) -> Self {
self.encode_limits = limits;
self
}
/// Build the reserved relay fields against a negotiated type map. The
/// default is the current map, but native callers handling an older
/// negotiated frame should pass that map explicitly.
@ -212,11 +277,17 @@ impl<'a> SealedRelayBuilder<'a> {
"relay builder identifiers must be non-empty",
));
}
if message_id.len() > self.limits.max_message_id_bytes {
return Err(RelayError::ResourceLimit("message ID"));
}
if self.metadata_recipients.is_empty() || self.content_recipients.is_empty() {
return Err(RelayError::InvalidLayout(
"relay builder requires metadata and content recipients",
));
}
if let Some(metadata) = self.metadata.as_ref() {
validate_metadata_size(metadata, &self.limits)?;
}
let type_map = self.type_map.unwrap_or_else(TypeMap::latest);
validate_application_message_type(&self.message_type, &type_map)?;
@ -232,14 +303,16 @@ impl<'a> SealedRelayBuilder<'a> {
(message_type_id, DataValue::Str(self.message_type)),
(content_id, self.content),
]);
let signed_content = content.sign(
let signed_content = content.sign_with_limits(
self.signer_id,
MtpProtectionPurpose::RelayContentSignature.into(),
self.signer,
self.encode_limits,
)?;
let encrypted_content = signed_content.encrypt_for(
let encrypted_content = signed_content.encrypt_for_with_limits(
&self.content_recipients,
MtpProtectionPurpose::RelayContentEncryption.into(),
self.encode_limits,
)?;
let mut metadata_fields = vec![
(
@ -258,14 +331,16 @@ impl<'a> SealedRelayBuilder<'a> {
metadata_fields.push((metadata_id, application_metadata));
}
let metadata = DataValue::Container(metadata_fields);
let signed_metadata = metadata.sign(
let signed_metadata = metadata.sign_with_limits(
self.signer_id,
MtpProtectionPurpose::RelayMetadataSignature.into(),
self.signer,
self.encode_limits,
)?;
let encrypted_metadata = signed_metadata.encrypt_for(
let encrypted_metadata = signed_metadata.encrypt_for_with_limits(
&self.metadata_recipients,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
self.encode_limits,
)?;
Ok(
CommunicationValue::new_with_type_map(CommunicationType::Relay, &type_map)
@ -305,22 +380,44 @@ fn validate_application_message_type(
Ok(())
}
fn validate_metadata_size(
metadata: &DataValue,
limits: &ProtectedLimits,
) -> Result<(), RelayError> {
let encoded = metadata
.to_bytes_with_limits(EncodeLimits {
max_output_size: limits.max_metadata_encoded_bytes,
..EncodeLimits::default()
})
.map_err(|error| match error {
mtp_common::CodecError::TooManyEntries => RelayError::ResourceLimit("metadata"),
_ => RelayError::InvalidLayout("metadata cannot be encoded"),
})?;
if encoded.len() > limits.max_metadata_encoded_bytes {
return Err(RelayError::ResourceLimit("metadata"));
}
Ok(())
}
fn field<'a>(
value: &'a DataValue,
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<&'a DataValue, RelayError> {
value
.get_field(relay_field(data_type, type_map)?)
let field_id = relay_field(data_type, type_map)?;
entries
.iter()
.find(|(id, _)| *id == field_id)
.map(|(_, value)| value)
.ok_or(RelayError::InvalidLayout("required relay field is missing"))
}
fn string_field(
value: &DataValue,
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<String, RelayError> {
field(value, data_type, type_map)?
field(entries, data_type, type_map)?
.as_string()
.filter(|value| !value.is_empty())
.ok_or(RelayError::InvalidLayout(
@ -328,21 +425,36 @@ fn string_field(
))
}
fn optional_metadata_field(
value: &DataValue,
fn string_field_ref<'a>(
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<Option<DataValue>, RelayError> {
Ok(value
.get_field(relay_field(DataType::Metadata, type_map)?)
.cloned())
) -> Result<&'a str, RelayError> {
field(entries, data_type, type_map)?
.as_str()
.filter(|value| !value.is_empty())
.ok_or(RelayError::InvalidLayout(
"relay field is not a non-empty string",
))
}
fn optional_metadata_field<'a>(
entries: &'a [(DataTypeId, DataValue)],
type_map: &TypeMap,
) -> Result<Option<&'a DataValue>, RelayError> {
let field_id = relay_field(DataType::Metadata, type_map)?;
Ok(entries
.iter()
.find(|(id, _)| *id == field_id)
.map(|(_, value)| value))
}
fn unsigned_field(
value: &DataValue,
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<u128, RelayError> {
field(value, data_type, type_map)?
field(entries, data_type, type_map)?
.as_unsigned_number()
.ok_or(RelayError::InvalidLayout("relay field is not unsigned"))
}
@ -356,55 +468,86 @@ struct RelayMetadataV1 {
encrypted_content: DataValue,
}
fn relay_version(value: &DataValue, type_map: &TypeMap) -> Result<u64, RelayError> {
fn relay_version(
entries: &[(DataTypeId, DataValue)],
type_map: &TypeMap,
) -> Result<u64, RelayError> {
let version_id = relay_field(DataType::RelayVersion, type_map)?;
let version = value
.get_field(version_id)
let version = entries
.iter()
.find(|(id, _)| *id == version_id)
.map(|(_, value)| value)
.ok_or(RelayError::MissingRelayVersion)?
.as_unsigned_number()
.ok_or(RelayError::InvalidLayout("relay version is not unsigned"))?;
u64::try_from(version).map_err(|_| RelayError::InvalidLayout("relay version is out of range"))
}
fn parse_relay_v1(value: &DataValue, type_map: &TypeMap) -> Result<RelayMetadataV1, RelayError> {
let final_recipient_id =
u64::try_from(unsigned_field(value, DataType::FinalRecipientId, type_map)?)
.map_err(|_| RelayError::InvalidLayout("final recipient ID is out of range"))?;
let created_at = u64::try_from(unsigned_field(value, DataType::CreatedAt, type_map)?)
fn parse_relay_v1(
entries: &[(DataTypeId, DataValue)],
type_map: &TypeMap,
limits: &ProtectedLimits,
) -> Result<RelayMetadataV1, RelayError> {
let final_recipient_id = u64::try_from(unsigned_field(
entries,
DataType::FinalRecipientId,
type_map,
)?)
.map_err(|_| RelayError::InvalidLayout("final recipient ID is out of range"))?;
let created_at = u64::try_from(unsigned_field(entries, DataType::CreatedAt, type_map)?)
.map_err(|_| RelayError::InvalidLayout("created-at value is out of range"))?;
let encrypted_content = field(value, DataType::Content, type_map)?.clone();
let encrypted_content = field(entries, DataType::Content, type_map)?.clone();
if encrypted_content.as_encrypted().is_none() {
return Err(RelayError::InvalidLayout("content is not encrypted"));
}
let message_id = string_field_ref(entries, DataType::MessageId, type_map)?;
if message_id.len() > limits.max_message_id_bytes {
return Err(RelayError::ResourceLimit("message ID"));
}
let metadata = optional_metadata_field(entries, type_map)?;
if let Some(metadata) = metadata {
validate_metadata_size(metadata, limits)?;
}
Ok(RelayMetadataV1 {
final_recipient_id,
message_id: string_field(value, DataType::MessageId, type_map)?,
message_id: message_id.to_owned(),
created_at,
metadata: optional_metadata_field(value, type_map)?,
metadata: metadata.cloned(),
encrypted_content,
})
}
/// Decrypt and verify relay metadata, without opening its content.
///
/// `expected_signer_id` is required when the caller already knows the sender.
/// For sealed-sender operation use [`open_relay_metadata_with`] and resolve a
/// trusted key by the claimed, unverified signer ID. The ID is authenticated
/// only after the returned key history verifies the signature.
pub fn open_relay_metadata(
pub fn open_relay_metadata_checked(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
policy: ProtectionPolicy,
options: RelayOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedRelayMetadata, RelayError> {
open_relay_metadata_with(
open_relay_metadata_with_limits_checked(
frame,
std::slice::from_ref(&keyring),
Some(expected_signer_id),
|_| Some(vec![signer_public_key.clone()]),
policy,
None,
options,
replay_guard,
)
}
pub fn open_relay_metadata_without_replay(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
options: RelayOpenOptions,
) -> Result<VerifiedRelayMetadata, RelayError> {
open_relay_metadata_with_limits_without_replay(
frame,
std::slice::from_ref(&keyring),
Some(expected_signer_id),
|_| Some(vec![signer_public_key.clone()]),
options,
)
}
@ -425,15 +568,45 @@ fn validate_relay_frame(frame: &CommunicationValue) -> Result<(), RelayError> {
/// signature or interpreting the versioned relay schema. The result is
/// untrusted and may only select the key history that is then bound to the
/// same signer ID during [`open_relay_metadata_with_keys`].
#[deprecated(note = "use relay_metadata_claimed_signer_id_with_limits")]
pub fn relay_metadata_claimed_signer_id(
frame: &CommunicationValue,
keyrings: &[&Keyring],
) -> Result<u64, RelayError> {
validate_relay_frame(frame)?;
// Migrate to `relay_metadata_claimed_signer_id_with_limits` at receive boundaries.
relay_metadata_claimed_signer_id_with_limits(frame, keyrings, DecodeLimits::default())
}
let decrypted = frame.payload().decrypt_with_keyrings(
pub fn relay_metadata_claimed_signer_id_with_limits(
frame: &CommunicationValue,
keyrings: &[&Keyring],
decode_limits: DecodeLimits,
) -> Result<u64, RelayError> {
relay_metadata_claimed_signer_id_with_options(
frame,
keyrings,
decode_limits,
ProtectedLimits::default(),
)
}
/// Return the claimed relay signer ID while applying the complete receive
/// policy, including the caller's decryption-key history bound.
pub fn relay_metadata_claimed_signer_id_with_options(
frame: &CommunicationValue,
keyrings: &[&Keyring],
decode_limits: DecodeLimits,
protected_limits: ProtectedLimits,
) -> Result<u64, RelayError> {
validate_relay_frame(frame)?;
if keyrings.len() > protected_limits.max_decryption_key_history {
return Err(RelayError::ResourceLimit("decryption key history"));
}
let decrypted = frame.payload().decrypt_with_keyrings_and_limits(
keyrings,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
decode_limits,
)?;
let signed = decrypted
.as_signed()
@ -441,49 +614,118 @@ pub fn relay_metadata_claimed_signer_id(
Ok(signed.signer_id)
}
/// Decrypt and verify relay metadata against an already resolved signing-key
/// history. All relay version and field interpretation remains in the native
/// codec rather than being duplicated by language bindings.
pub fn open_relay_metadata_with_keys(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
signer_public_keys: &[PublicKeyBundle],
policy: ProtectionPolicy,
) -> Result<VerifiedRelayMetadata, RelayError> {
let signer_public_keys = signer_public_keys.to_vec();
open_relay_metadata_with(
frame,
keyrings,
Some(expected_signer_id),
move |_| Some(signer_public_keys.clone()),
policy,
None,
)
}
/// Decrypt and verify relay metadata using recipient-key history and a
/// signer-key resolver. The resolver receives a claimed, unverified signer
/// ID used only as a trusted-key lookup key. The ID becomes authenticated
/// only after signature verification. This is the native counterpart of the
/// browser relay API and supports sealed sender plus signing-key rotation.
pub fn open_relay_metadata_with<F>(
pub fn open_relay_metadata_with_checked<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
policy: ProtectionPolicy,
options: RelayOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_relay_metadata_with_limits_checked(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
replay_guard,
)
}
/// Open relay metadata for message processing with replay protection required
/// by the type system.
pub fn open_relay_metadata_with_limits_checked<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: RelayOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_relay_metadata_impl(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
Some(replay_guard),
)
}
pub fn open_relay_metadata_with_without_replay<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: RelayOpenOptions,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_relay_metadata_with_limits_without_replay(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
)
}
/// Open relay metadata for stored/forensic use without replay protection.
/// The name makes the security trade-off explicit at the call site.
pub fn open_relay_metadata_with_limits_without_replay<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: RelayOpenOptions,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
open_relay_metadata_impl(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
options,
None,
)
}
fn open_relay_metadata_impl<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
options: RelayOpenOptions,
mut replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: Fn(u64) -> Option<Vec<PublicKeyBundle>>,
F: FnOnce(u64) -> Option<Vec<PublicKeyBundle>>,
{
validate_relay_frame(frame)?;
if keyrings.len() > options.protected_limits.max_decryption_key_history {
return Err(RelayError::ResourceLimit("decryption key history"));
}
let type_map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest);
let decrypted = frame.payload().decrypt_with_keyrings(
let decrypted = frame.payload().decrypt_with_keyrings_and_limits(
keyrings,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
options.decode_limits,
)?;
let signed = decrypted
.as_signed()
@ -499,23 +741,30 @@ where
}
let signer_keys = resolve_signer_keys(signed.signer_id)
.ok_or(ProtectionError::SignerKeyNotFound(signed.signer_id))?;
let matched_signer_key_index = signed.verify_with_key_history_index(
if signer_keys.len() > options.protected_limits.max_signer_key_history {
return Err(RelayError::ResourceLimit("signer key history"));
}
let matched_signer_key_index = signed.verify_with_key_history_index_and_limits(
signed.signer_id,
&signer_keys,
MtpProtectionPurpose::RelayMetadataSignature.into(),
policy,
options.policy,
options.encode_limits,
)?;
/* Verification leaves the signed envelope owned by `decrypted`; inspect
its entries in place to avoid a second attacker-controlled clone. */
let metadata = signed
.value
.as_container()
.container_entries()
.ok_or(RelayError::InvalidLayout("metadata is not a container"))?;
let metadata = DataValue::Container(metadata);
let relay_version = relay_version(&metadata, &type_map)?;
let relay_version = relay_version(metadata, &type_map)?;
let parsed = match relay_version {
1 => parse_relay_v1(&metadata, &type_map)?,
1 => parse_relay_v1(metadata, &type_map, &options.protected_limits)?,
other => return Err(RelayError::UnsupportedRelayVersion(other)),
};
if parsed.message_id.len() > options.protected_limits.max_message_id_bytes {
return Err(RelayError::ResourceLimit("message ID"));
}
let result = VerifiedRelayMetadata {
relay_version,
signer_id: signed.signer_id,
@ -526,6 +775,9 @@ where
encrypted_content: parsed.encrypted_content,
type_map,
matched_signer_key_index,
decode_limits: options.decode_limits,
encode_limits: options.encode_limits,
protected_limits: options.protected_limits,
_verified: VerifiedMarker,
};
if let Some(guard) = replay_guard.as_mut()
@ -537,6 +789,7 @@ where
}
/// Open and verify content after metadata has been authenticated.
#[deprecated(note = "use open_relay_content_with_limits_without_replay")]
pub fn open_relay_content(
metadata: &VerifiedRelayMetadata,
keyring: &Keyring,
@ -544,17 +797,23 @@ pub fn open_relay_content(
expected_recipient_id: u64,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_keys(
open_relay_content_with_limits_without_replay(
metadata,
keyring,
std::slice::from_ref(&keyring),
std::slice::from_ref(signer_public_key),
expected_recipient_id,
policy,
Some(expected_recipient_id),
RelayOpenOptions {
policy,
decode_limits: metadata.decode_limits,
encode_limits: metadata.encode_limits,
protected_limits: metadata.protected_limits,
},
)
}
/// Open relay content against trusted signing-key history for the metadata's
/// authenticated signer ID.
#[deprecated(note = "use open_relay_content_with_limits_without_replay")]
pub fn open_relay_content_with_keys(
metadata: &VerifiedRelayMetadata,
keyring: &Keyring,
@ -562,18 +821,24 @@ pub fn open_relay_content_with_keys(
expected_recipient_id: u64,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_keyrings(
open_relay_content_with_limits_without_replay(
metadata,
std::slice::from_ref(&keyring),
signer_public_keys,
Some(expected_recipient_id),
policy,
RelayOpenOptions {
policy,
decode_limits: metadata.decode_limits,
encode_limits: metadata.encode_limits,
protected_limits: metadata.protected_limits,
},
)
}
/// Open relay content against recipient-key history and trusted signing-key
/// history. The expected final recipient is optional for callers that only
/// have decryption material and do not have a local identity ID.
#[deprecated(note = "use open_relay_content_with_limits_without_replay")]
pub fn open_relay_content_with_keyrings(
metadata: &VerifiedRelayMetadata,
keyrings: &[&Keyring],
@ -581,15 +846,96 @@ pub fn open_relay_content_with_keyrings(
expected_recipient_id: Option<u64>,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayContent, RelayError> {
// Migrate to `open_relay_content_with_limits_without_replay` to keep the decode policy
// explicit across metadata and content opening.
open_relay_content_with_limits_without_replay(
metadata,
keyrings,
signer_public_keys,
expected_recipient_id,
RelayOpenOptions {
policy,
decode_limits: metadata.decode_limits,
encode_limits: metadata.encode_limits,
protected_limits: metadata.protected_limits,
},
)
}
#[deprecated(note = "use open_relay_content_with_limits_without_replay")]
pub fn open_relay_content_with_keyrings_and_limits(
metadata: &VerifiedRelayMetadata,
keyrings: &[&Keyring],
signer_public_keys: &[PublicKeyBundle],
expected_recipient_id: Option<u64>,
options: RelayOpenOptions,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_limits_without_replay(
metadata,
keyrings,
signer_public_keys,
expected_recipient_id,
options,
)
}
/// Compatibility alias for callers that already hold authenticated relay
/// metadata. New code should use the explicit `_without_replay` name.
#[deprecated(note = "use open_relay_content_with_limits_without_replay")]
pub fn open_relay_content_with_limits(
metadata: &VerifiedRelayMetadata,
keyrings: &[&Keyring],
signer_public_keys: &[PublicKeyBundle],
expected_recipient_id: Option<u64>,
options: RelayOpenOptions,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_limits_without_replay(
metadata,
keyrings,
signer_public_keys,
expected_recipient_id,
options,
)
}
/// Open relay content after the authenticated metadata operation without
/// making a second replay decision. Replay is consumed by the metadata
/// processing boundary; this explicit name prevents callers from mistaking
/// content opening for an independent replay check.
pub fn open_relay_content_with_limits_without_replay(
metadata: &VerifiedRelayMetadata,
keyrings: &[&Keyring],
signer_public_keys: &[PublicKeyBundle],
expected_recipient_id: Option<u64>,
options: RelayOpenOptions,
) -> Result<VerifiedRelayContent, RelayError> {
let options = RelayOpenOptions {
policy: options.policy,
decode_limits: restrict_decode_limits(options.decode_limits, metadata.decode_limits),
encode_limits: restrict_encode_limits(options.encode_limits, metadata.encode_limits),
protected_limits: restrict_protected_limits(
options.protected_limits,
metadata.protected_limits,
),
};
if expected_recipient_id.is_some_and(|id| metadata.final_recipient_id != id) {
return Err(RelayError::NotFinalRecipient);
}
if keyrings.len() > options.protected_limits.max_decryption_key_history {
return Err(RelayError::ResourceLimit("decryption key history"));
}
if signer_public_keys.len() > options.protected_limits.max_signer_key_history {
return Err(RelayError::ResourceLimit("signer key history"));
}
let type_map = &metadata.type_map;
let decrypted = metadata.encrypted_content.decrypt_with_keyrings(
keyrings,
MtpProtectionPurpose::RelayContentEncryption.into(),
)?;
let decrypted = metadata
.encrypted_content
.decrypt_with_keyrings_and_limits(
keyrings,
MtpProtectionPurpose::RelayContentEncryption.into(),
options.decode_limits,
)?;
let signed = decrypted
.as_signed()
.ok_or(RelayError::InvalidLayout("content is not signed"))?;
@ -600,27 +946,60 @@ pub fn open_relay_content_with_keyrings(
}
// Content is a separately signed value and must not inherit a weaker
// metadata policy.
signed.verify_with_key_history(
signed.verify_with_key_history_and_limits(
metadata.signer_id,
signer_public_keys,
MtpProtectionPurpose::RelayContentSignature.into(),
policy,
options.policy,
options.encode_limits,
)?;
let content = signed
.value
.as_container()
.container_entries()
.ok_or(RelayError::InvalidLayout("content is not a container"))?;
let content = DataValue::Container(content);
let message_type = string_field(&content, DataType::MessageType, type_map)?;
let message_type = string_field(content, DataType::MessageType, type_map)?;
validate_application_message_type(&message_type, type_map)?;
Ok(VerifiedRelayContent {
signer_id: signed.signer_id,
final_recipient_id: metadata.final_recipient_id,
message_type,
content: field(&content, DataType::Content, type_map)?.clone(),
content: field(content, DataType::Content, type_map)?.clone(),
})
}
fn restrict_decode_limits(left: DecodeLimits, right: DecodeLimits) -> DecodeLimits {
DecodeLimits {
max_depth: left.max_depth.min(right.max_depth),
max_values: left.max_values.min(right.max_values),
max_blob_size: left.max_blob_size.min(right.max_blob_size),
max_recipients: left.max_recipients.min(right.max_recipients),
max_allocated_bytes: left.max_allocated_bytes.min(right.max_allocated_bytes),
}
}
fn restrict_encode_limits(left: EncodeLimits, right: EncodeLimits) -> EncodeLimits {
EncodeLimits {
max_depth: left.max_depth.min(right.max_depth),
max_values: left.max_values.min(right.max_values),
max_output_size: left.max_output_size.min(right.max_output_size),
}
}
fn restrict_protected_limits(left: ProtectedLimits, right: ProtectedLimits) -> ProtectedLimits {
ProtectedLimits {
max_message_id_bytes: left.max_message_id_bytes.min(right.max_message_id_bytes),
max_metadata_encoded_bytes: left
.max_metadata_encoded_bytes
.min(right.max_metadata_encoded_bytes),
max_signer_key_history: left
.max_signer_key_history
.min(right.max_signer_key_history),
max_decryption_key_history: left
.max_decryption_key_history
.min(right.max_decryption_key_history),
}
}
/// Change only the clear next-hop routing field of a sealed relay frame.
/// The authenticated encrypted payload is cloned byte-for-byte, so a relay
/// cannot alter the final recipient or message metadata while forwarding.
@ -636,13 +1015,78 @@ pub fn forward_relay_frame(
mod tests {
use super::*;
use crate::InMemoryReplayGuard;
use mtp_crypto::{Ed25519Signer, Keyring};
use mtp_crypto::{Ed25519Signer, Keyring, PublicKeyBundle};
use mtp_type_map::DataTypeId;
fn ed_signer(keyring: &Keyring) -> Ed25519Signer {
Ed25519Signer::new(&keyring.sig_cl_secret_key).expect("Ed25519 signer")
}
// Test-only compatibility shims keep older fixture setup readable while
// routing every invocation to an explicit replay choice in production.
fn open_relay_metadata(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayMetadata, RelayError> {
super::open_relay_metadata_without_replay(
frame,
keyring,
expected_signer_id,
signer_public_key,
RelayOpenOptions::new(policy),
)
}
fn open_relay_metadata_with<F>(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: Option<u64>,
resolve_signer_keys: F,
policy: ProtectionPolicy,
replay_guard: Option<&mut dyn ReplayGuard>,
) -> Result<VerifiedRelayMetadata, RelayError>
where
F: Fn(u64) -> Option<Vec<PublicKeyBundle>>,
{
match replay_guard {
Some(replay_guard) => super::open_relay_metadata_with_checked(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
RelayOpenOptions::new(policy),
replay_guard,
),
None => super::open_relay_metadata_with_without_replay(
frame,
keyrings,
expected_signer_id,
resolve_signer_keys,
RelayOpenOptions::new(policy),
),
}
}
fn open_relay_metadata_with_keys(
frame: &CommunicationValue,
keyrings: &[&Keyring],
expected_signer_id: u64,
signer_public_keys: &[PublicKeyBundle],
policy: ProtectionPolicy,
) -> Result<VerifiedRelayMetadata, RelayError> {
let signer_public_keys = signer_public_keys.to_vec();
super::open_relay_metadata_with_limits_without_replay(
frame,
keyrings,
Some(expected_signer_id),
move |_| Some(signer_public_keys),
RelayOpenOptions::new(policy),
)
}
fn relay_frame_with_version(
version: Option<u64>,
include_v1_fields: bool,
@ -874,6 +1318,22 @@ mod tests {
assert_eq!(metadata.created_at(), 123);
assert_eq!(metadata.metadata(), Some(&application_metadata));
let mut limited_options = RelayOpenOptions::new(policy);
limited_options.protected_limits.max_message_id_bytes = 1;
let mut limited_guard = InMemoryReplayGuard::default();
assert!(matches!(
open_relay_metadata_with_limits_checked(
&frame,
&[&metadata_recipient],
None,
|signer_id| (signer_id == 7).then(|| vec![sender.public_key_bundle()]),
limited_options,
&mut limited_guard,
),
Err(RelayError::ResourceLimit("message ID"))
));
assert_eq!(limited_guard.len(), 0);
// A final recipient may be included in the metadata recipient set and
// therefore open both authenticated layers directly.
let final_metadata = open_relay_metadata(