[Fix] Harden MTP codec, transport, and SDK security
This commit is contained in:
parent
188caf56cc
commit
a7e804c603
73 changed files with 11892 additions and 5756 deletions
|
|
@ -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
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue