mtp/codec/src/relay.rs
Alex Emmet b331b9f6a3
Some checks failed
CI / checks (push) Failing after 16m34s
[Fix] Clean
2026-08-18 21:53:02 +02:00

1753 lines
60 KiB
Rust

// Relay-specific opening helpers.
//
// The generic [`DataValue`] protection operations remain the primitive API.
// These helpers add the protocol boundary needed by relay participants:
// metadata can be authenticated and returned with an opaque content value,
// while content opening is a separate operation that requires the final
// recipient identity.
#![cfg(feature = "crypto")]
use mtp_crypto::{Keyring, PublicKeyBundle, SignatureScheme};
use mtp_type_map::{CommunicationType, DataType, DataTypeId, TypeMap};
use crate::{
CommunicationValue, DataValue, DecodeLimits, EncodeLimits, MtpProtectionPurpose,
ProtectedLimits, ProtectionError, ProtectionPolicy, ReplayError, ReplayGuard,
};
/// The relay metadata schema emitted by [`SealedRelayBuilder`].
pub const CURRENT_RELAY_VERSION: u64 = 1;
#[derive(Debug, thiserror::Error)]
pub enum RelayError {
#[error("value is not a Relay communication frame")]
NotRelay,
#[error("sealed relay frame must not expose an outer sender")]
OuterSenderPresent,
#[error("sealed relay frame must contain an explicit next-hop receiver")]
MissingNextHop,
#[error("relay frame has an invalid protected layout: {0}")]
InvalidLayout(&'static str),
#[error("relay frame does not declare a relay version")]
MissingRelayVersion,
#[error("unsupported relay version {0}")]
UnsupportedRelayVersion(u64),
#[error("relay content is addressed to a different final recipient")]
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}")]
Protection(#[from] ProtectionError),
#[error("replay guard error: {0}")]
ReplayGuard(#[from] ReplayError),
}
/// Metadata authenticated by the signer and decryptable by metadata
/// recipients.
///
/// `encrypted_content` is intentionally kept as an opaque `DataValue` so a
/// metadata-only relay participant can store or forward it without possessing
/// a content key.
#[derive(Debug, Clone)]
pub struct VerifiedRelayMetadata {
relay_version: u64,
signer_id: u64,
final_recipient_id: u64,
message_id: String,
created_at: u64,
metadata: Option<DataValue>,
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,
}
#[derive(Debug, Clone, Copy)]
struct VerifiedMarker;
impl VerifiedRelayMetadata {
/// Return the authenticated MTP relay metadata schema version.
pub fn relay_version(&self) -> u64 {
self.relay_version
}
pub fn signer_id(&self) -> u64 {
self.signer_id
}
pub fn final_recipient_id(&self) -> u64 {
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
}
/// Return the authenticated creation time as Unix epoch milliseconds.
pub fn created_at(&self) -> u64 {
self.created_at
}
/// Return the authenticated application metadata without interpreting it.
pub fn metadata(&self) -> Option<&DataValue> {
self.metadata.as_ref()
}
/// Return the authenticated content envelope for forwarding. The value
/// remains opaque to metadata-only relay participants.
pub fn encrypted_content(&self) -> &DataValue {
&self.encrypted_content
}
/// Return the index of the trusted signing key that verified the
/// authenticated metadata.
pub fn matched_signer_key_index(&self) -> usize {
self.matched_signer_key_index
}
}
/// Content opened and authenticated for the final recipient.
#[derive(Debug, Clone, PartialEq)]
pub struct VerifiedRelayContent {
pub signer_id: u64,
pub final_recipient_id: u64,
pub message_type: String,
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
/// metadata layout. Routing recipients are supplied separately from content
/// recipients so a metadata-only relay participant can open metadata without
/// receiving content keys.
pub struct SealedRelayBuilder<'a> {
message_type: String,
content: DataValue,
signer_id: u64,
final_recipient_id: u64,
next_hop_id: u64,
message_id: Option<String>,
created_at: Option<u64>,
metadata: Option<DataValue>,
signer: &'a dyn SignatureScheme,
metadata_recipients: Vec<PublicKeyBundle>,
content_recipients: Vec<PublicKeyBundle>,
type_map: Option<TypeMap>,
limits: ProtectedLimits,
encode_limits: EncodeLimits,
}
impl<'a> SealedRelayBuilder<'a> {
pub fn new(
message_type: impl Into<String>,
content: DataValue,
signer_id: u64,
final_recipient_id: u64,
next_hop_id: u64,
signer: &'a dyn SignatureScheme,
) -> Self {
Self {
message_type: message_type.into(),
content,
signer_id,
final_recipient_id,
next_hop_id,
message_id: None,
created_at: None,
metadata: None,
signer,
metadata_recipients: Vec::new(),
content_recipients: Vec::new(),
type_map: None,
limits: ProtectedLimits::default(),
encode_limits: EncodeLimits::default(),
}
}
pub fn message_id(mut self, value: impl Into<String>) -> Self {
self.message_id = Some(value.into());
self
}
pub fn metadata(mut self, value: DataValue) -> Self {
self.metadata = Some(value);
self
}
/// Set `CreatedAt` as Unix epoch milliseconds.
pub fn created_at(mut self, value: u64) -> Self {
self.created_at = Some(value);
self
}
pub fn metadata_recipients(mut self, recipients: Vec<PublicKeyBundle>) -> Self {
self.metadata_recipients = recipients;
self
}
pub fn content_recipients(mut self, recipients: Vec<PublicKeyBundle>) -> Self {
self.content_recipients = recipients;
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.
pub fn type_map(mut self, type_map: &TypeMap) -> Self {
self.type_map = Some(type_map.clone());
self
}
pub fn build(self) -> Result<CommunicationValue, RelayError> {
let message_id = self.message_id.ok_or(RelayError::InvalidLayout(
"relay builder requires a message ID",
))?;
let created_at = self.created_at.ok_or(RelayError::InvalidLayout(
"relay builder requires a creation timestamp",
))?;
if self.message_type.is_empty() || message_id.is_empty() {
return Err(RelayError::InvalidLayout(
"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)?;
let message_type_id = relay_field(DataType::MessageType, &type_map)?;
let content_id = relay_field(DataType::Content, &type_map)?;
let message_id_id = relay_field(DataType::MessageId, &type_map)?;
let final_recipient_id = relay_field(DataType::FinalRecipientId, &type_map)?;
let created_at_id = relay_field(DataType::CreatedAt, &type_map)?;
let metadata_id = relay_field(DataType::Metadata, &type_map)?;
let relay_version_id = relay_field(DataType::RelayVersion, &type_map)?;
let content = DataValue::Container(vec![
(message_type_id, DataValue::Str(self.message_type)),
(content_id, self.content),
]);
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_with_limits(
&self.content_recipients,
MtpProtectionPurpose::RelayContentEncryption.into(),
self.encode_limits,
)?;
let mut metadata_fields = vec![
(
relay_version_id,
DataValue::UnsignedNumber(CURRENT_RELAY_VERSION as u128),
),
(message_id_id, DataValue::Str(message_id)),
(
final_recipient_id,
DataValue::UnsignedNumber(self.final_recipient_id as u128),
),
(created_at_id, DataValue::UnsignedNumber(created_at as u128)),
(content_id, encrypted_content),
];
if let Some(application_metadata) = self.metadata {
metadata_fields.push((metadata_id, application_metadata));
}
let metadata = DataValue::Container(metadata_fields);
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_with_limits(
&self.metadata_recipients,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
self.encode_limits,
)?;
Ok(
CommunicationValue::new_with_type_map(CommunicationType::Relay, &type_map)
.without_sender()
.with_receiver(self.next_hop_id)
.with_payload(encrypted_metadata),
)
}
}
fn relay_field(
data_type: DataType,
type_map: &TypeMap,
) -> Result<mtp_type_map::DataTypeId, RelayError> {
data_type
.try_to_id(type_map)
.ok_or(RelayError::InvalidLayout(
"reserved relay type is unavailable",
))
}
fn validate_application_message_type(
message_type: &str,
type_map: &TypeMap,
) -> Result<(), RelayError> {
let communication_type = CommunicationType::from_name(message_type)
.ok_or(RelayError::InvalidLayout("relay message type is unknown"))?;
let communication_id =
communication_type
.try_to_id(type_map)
.ok_or(RelayError::InvalidLayout(
"relay message type is unavailable",
))?;
if communication_id.is_reserved() {
return Err(RelayError::ReservedApplicationType(message_type.to_owned()));
}
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>(
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<&'a DataValue, RelayError> {
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(
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<String, RelayError> {
field(entries, data_type, type_map)?
.as_string()
.filter(|value| !value.is_empty())
.ok_or(RelayError::InvalidLayout(
"relay field is not a non-empty string",
))
}
fn string_field_ref<'a>(
entries: &'a [(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> 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(
entries: &[(DataTypeId, DataValue)],
data_type: DataType,
type_map: &TypeMap,
) -> Result<u128, RelayError> {
field(entries, data_type, type_map)?
.as_unsigned_number()
.ok_or(RelayError::InvalidLayout("relay field is not unsigned"))
}
#[derive(Debug)]
struct RelayMetadataV1 {
final_recipient_id: u64,
message_id: String,
created_at: u64,
metadata: Option<DataValue>,
encrypted_content: DataValue,
}
fn relay_version(
entries: &[(DataTypeId, DataValue)],
type_map: &TypeMap,
) -> Result<u64, RelayError> {
let version_id = relay_field(DataType::RelayVersion, type_map)?;
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(
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(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: message_id.to_owned(),
created_at,
metadata: metadata.cloned(),
encrypted_content,
})
}
pub fn open_relay_metadata_checked(
frame: &CommunicationValue,
keyring: &Keyring,
expected_signer_id: u64,
signer_public_key: &PublicKeyBundle,
options: RelayOpenOptions,
replay_guard: &mut dyn ReplayGuard,
) -> Result<VerifiedRelayMetadata, RelayError> {
open_relay_metadata_with_limits_checked(
frame,
std::slice::from_ref(&keyring),
Some(expected_signer_id),
|_| Some(vec![signer_public_key.clone()]),
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,
)
}
fn validate_relay_frame(frame: &CommunicationValue) -> Result<(), RelayError> {
if !frame.is_type(CommunicationType::Relay) {
return Err(RelayError::NotRelay);
}
if frame.sender().is_some() {
return Err(RelayError::OuterSenderPresent);
}
if frame.receiver().is_none() {
return Err(RelayError::MissingNextHop);
}
Ok(())
}
/// Return the claimed signer ID from relay metadata without verifying its
/// 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> {
// Migrate to `relay_metadata_claimed_signer_id_with_limits` at receive boundaries.
relay_metadata_claimed_signer_id_with_limits(frame, keyrings, DecodeLimits::default())
}
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()
.ok_or(RelayError::InvalidLayout("metadata is not signed"))?;
Ok(signed.signer_id)
}
/// 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_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_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: 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_and_limits(
keyrings,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
options.decode_limits,
)?;
let signed = decrypted
.as_signed()
.ok_or(RelayError::InvalidLayout("metadata is not signed"))?;
if let Some(expected_signer_id) = expected_signer_id
&& signed.signer_id != expected_signer_id
{
return Err(ProtectionError::SignerIdMismatch {
expected: expected_signer_id,
actual: signed.signer_id,
}
.into());
}
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(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(),
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
.container_entries()
.ok_or(RelayError::InvalidLayout("metadata is not a container"))?;
let relay_version = relay_version(metadata, &type_map)?;
let parsed = match relay_version {
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,
final_recipient_id: parsed.final_recipient_id,
message_id: parsed.message_id,
created_at: parsed.created_at,
metadata: parsed.metadata,
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()
&& !guard.accept(result.signer_id, &result.message_id, result.created_at)?
{
return Err(RelayError::Replay);
}
Ok(result)
}
/// 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,
signer_public_key: &PublicKeyBundle,
expected_recipient_id: u64,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_limits_without_replay(
metadata,
std::slice::from_ref(&keyring),
std::slice::from_ref(signer_public_key),
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,
signer_public_keys: &[PublicKeyBundle],
expected_recipient_id: u64,
policy: ProtectionPolicy,
) -> Result<VerifiedRelayContent, RelayError> {
open_relay_content_with_limits_without_replay(
metadata,
std::slice::from_ref(&keyring),
signer_public_keys,
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 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],
signer_public_keys: &[PublicKeyBundle],
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_and_limits(
keyrings,
MtpProtectionPurpose::RelayContentEncryption.into(),
options.decode_limits,
)?;
let signed = decrypted
.as_signed()
.ok_or(RelayError::InvalidLayout("content is not signed"))?;
if signed.signer_id != metadata.signer_id {
return Err(RelayError::InvalidLayout(
"metadata and content signer IDs differ",
));
}
// Content is a separately signed value and must not inherit a weaker
// metadata policy.
signed.verify_with_key_history_and_limits(
metadata.signer_id,
signer_public_keys,
MtpProtectionPurpose::RelayContentSignature.into(),
options.policy,
options.encode_limits,
)?;
let content = signed
.value
.container_entries()
.ok_or(RelayError::InvalidLayout("content is not a container"))?;
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(),
})
}
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.
pub fn forward_relay_frame(
frame: &CommunicationValue,
next_hop_receiver_id: u64,
) -> Result<CommunicationValue, RelayError> {
validate_relay_frame(frame)?;
Ok(frame.clone().with_receiver(next_hop_receiver_id))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InMemoryReplayGuard;
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")
}
fn content_open_options(
metadata: &VerifiedRelayMetadata,
policy: ProtectionPolicy,
) -> RelayOpenOptions {
RelayOpenOptions {
policy,
decode_limits: metadata.decode_limits,
encode_limits: metadata.encode_limits,
protected_limits: metadata.protected_limits,
}
}
// 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,
sender: &Keyring,
recipient: &Keyring,
) -> CommunicationValue {
let signer = ed_signer(sender);
let type_map = TypeMap::latest();
let mut fields = Vec::new();
if let Some(version) = version {
fields.push((
relay_field(DataType::RelayVersion, &type_map).expect("RelayVersion mapping"),
DataValue::UnsignedNumber(version as u128),
));
}
if include_v1_fields {
let content = DataValue::Container(vec![
(
relay_field(DataType::MessageType, &type_map).expect("MessageType mapping"),
DataValue::Str("ProtectedMessage".into()),
),
(
relay_field(DataType::Content, &type_map).expect("Content mapping"),
DataValue::Null,
),
])
.sign(
7,
MtpProtectionPurpose::RelayContentSignature.into(),
&signer,
)
.expect("sign content")
.encrypt_for(
&[recipient.public_key_bundle()],
MtpProtectionPurpose::RelayContentEncryption.into(),
)
.expect("encrypt content");
fields.extend([
(
relay_field(DataType::MessageId, &type_map).expect("MessageId mapping"),
DataValue::Str("version-test".into()),
),
(
relay_field(DataType::FinalRecipientId, &type_map)
.expect("FinalRecipientId mapping"),
DataValue::UnsignedNumber(42),
),
(
relay_field(DataType::CreatedAt, &type_map).expect("CreatedAt mapping"),
DataValue::UnsignedNumber(123),
),
(
relay_field(DataType::Metadata, &type_map).expect("Metadata mapping"),
DataValue::Null,
),
(
relay_field(DataType::Content, &type_map).expect("Content mapping"),
content,
),
]);
}
let payload = DataValue::Container(fields)
.sign(
7,
MtpProtectionPurpose::RelayMetadataSignature.into(),
&signer,
)
.expect("sign metadata")
.encrypt_for(
&[recipient.public_key_bundle()],
MtpProtectionPurpose::RelayMetadataEncryption.into(),
)
.expect("encrypt metadata");
CommunicationValue::new_with_type_map(CommunicationType::Relay, &type_map)
.without_sender()
.with_receiver(9)
.with_payload(payload)
}
#[test]
fn relay_version_one_round_trips() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let frame =
relay_frame_with_version(Some(CURRENT_RELAY_VERSION), true, &sender, &recipient);
let metadata = open_relay_metadata(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
)
.expect("version 1 metadata");
assert_eq!(metadata.relay_version(), CURRENT_RELAY_VERSION);
}
#[test]
fn missing_relay_version_is_rejected() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let frame = relay_frame_with_version(None, true, &sender, &recipient);
let result = open_relay_metadata(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
);
assert!(matches!(result, Err(RelayError::MissingRelayVersion)));
}
#[test]
fn unknown_relay_version_is_rejected_before_v1_fields_are_parsed() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let frame = relay_frame_with_version(Some(99), false, &sender, &recipient);
let result = open_relay_metadata(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
);
assert!(matches!(
result,
Err(RelayError::UnsupportedRelayVersion(99))
));
}
#[test]
fn relay_created_at_preserves_exact_milliseconds() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let created_at = 1_720_000_000_123;
let frame = SealedRelayBuilder::new(
"ProtectedMessage",
DataValue::Str("timestamp content".into()),
11,
42,
42,
&signer,
)
.message_id("relay-created-at-millis")
.created_at(created_at)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let type_map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest);
let decrypted = frame
.payload()
.decrypt(
&recipient,
MtpProtectionPurpose::RelayMetadataEncryption.into(),
)
.expect("metadata decryption");
let signed = decrypted.as_signed().expect("signed metadata");
let created_at_id = DataType::CreatedAt
.try_to_id(&type_map)
.expect("CreatedAt mapping");
assert_eq!(
signed.value.get_field(created_at_id),
Some(&DataValue::UnsignedNumber(created_at as u128))
);
let verified = open_relay_metadata(
&frame,
&recipient,
11,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
)
.expect("verified metadata");
assert_eq!(verified.created_at(), created_at);
}
#[test]
fn relay_metadata_policy_and_replay_guard_are_receiver_controls() {
let sender = Keyring::generate();
let metadata_recipient = Keyring::generate();
let final_recipient = Keyring::generate();
let signer = ed_signer(&sender);
let application_metadata = DataValue::Container(vec![
(DataTypeId(32), DataValue::Str("opaque-field".into())),
(DataTypeId(33), DataValue::UnsignedNumber(7)),
]);
let frame = SealedRelayBuilder::new(
"ProtectedMessage",
DataValue::Str("hello".into()),
7,
42,
9,
&signer,
)
.message_id("message-1")
.created_at(123)
.metadata(application_metadata.clone())
.metadata_recipients(vec![
metadata_recipient.public_key_bundle(),
final_recipient.public_key_bundle(),
])
.content_recipients(vec![final_recipient.public_key_bundle()])
.build()
.expect("relay frame");
let frame =
CommunicationValue::from_bytes(&frame.to_bytes().expect("relay frame encoding"))
.expect("relay frame decoding");
assert_eq!(frame.sender(), None);
assert_eq!(frame.receiver(), Some(9));
let policy = ProtectionPolicy::from(crate::SignaturePolicy::Ed25519);
let mut guard = InMemoryReplayGuard::default();
let metadata = open_relay_metadata_with(
&frame,
&[&metadata_recipient],
None,
|signer_id| (signer_id == 7).then(|| vec![sender.public_key_bundle()]),
policy,
Some(&mut guard),
)
.expect("metadata");
assert_eq!(metadata.signer_id(), 7);
assert_eq!(metadata.relay_version(), CURRENT_RELAY_VERSION);
assert_eq!(metadata.final_recipient_id(), 42);
assert_eq!(metadata.message_id(), "message-1");
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(
&frame,
&final_recipient,
7,
&sender.public_key_bundle(),
policy,
)
.expect("final recipient metadata");
assert_eq!(final_metadata.metadata(), Some(&application_metadata));
let final_content = open_relay_content_with_limits_without_replay(
&final_metadata,
&[&final_recipient],
std::slice::from_ref(&sender.public_key_bundle()),
Some(42),
content_open_options(&final_metadata, policy),
)
.expect("final recipient content");
assert_eq!(final_content.content, DataValue::Str("hello".into()));
assert!(matches!(
open_relay_metadata(
&frame,
&metadata_recipient,
8,
&sender.public_key_bundle(),
policy,
),
Err(RelayError::Protection(ProtectionError::SignerIdMismatch {
expected: 8,
actual: 7,
}))
));
let wrong_signer = Keyring::generate();
assert!(matches!(
open_relay_metadata(
&frame,
&metadata_recipient,
7,
&wrong_signer.public_key_bundle(),
policy,
),
Err(RelayError::Protection(_))
));
assert!(matches!(
open_relay_metadata(
&frame.clone().with_sender(99),
&metadata_recipient,
7,
&sender.public_key_bundle(),
policy,
),
Err(RelayError::OuterSenderPresent)
));
assert!(
open_relay_content_with_limits_without_replay(
&metadata,
&[&metadata_recipient],
std::slice::from_ref(&sender.public_key_bundle()),
Some(42),
content_open_options(&metadata, policy),
)
.is_err()
);
let content = open_relay_content_with_limits_without_replay(
&metadata,
&[&final_recipient],
std::slice::from_ref(&sender.public_key_bundle()),
Some(42),
content_open_options(&metadata, policy),
)
.expect("content");
assert_eq!(content.signer_id, 7);
assert_eq!(content.final_recipient_id, 42);
assert_eq!(content.message_type, "ProtectedMessage");
assert_eq!(content.content, DataValue::Str("hello".into()));
assert!(matches!(
open_relay_content_with_limits_without_replay(
&metadata,
&[&final_recipient],
std::slice::from_ref(&sender.public_key_bundle()),
Some(43),
content_open_options(&metadata, policy),
),
Err(RelayError::NotFinalRecipient)
));
let replay = open_relay_metadata_with(
&frame,
&[&metadata_recipient],
None,
|signer_id| (signer_id == 7).then(|| vec![sender.public_key_bundle()]),
policy,
Some(&mut guard),
);
assert!(matches!(replay, Err(RelayError::Replay)));
}
#[test]
fn relay_metadata_preserves_generic_values_and_absence() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let values = vec![
DataValue::Null,
DataValue::Bool(true),
DataValue::Str("scalar metadata".into()),
DataValue::Bytes(vec![1, 2, 3]),
DataValue::Array(vec![DataValue::UnsignedNumber(7), DataValue::BoolFalse]),
DataValue::Container(vec![(DataTypeId(32), DataValue::Str("typed".into()))]),
];
for (index, value) in values.into_iter().enumerate() {
let frame =
SealedRelayBuilder::new("ProtectedMessage", DataValue::Null, 7, 42, 9, &signer)
.message_id(format!("metadata-{index}"))
.created_at(123)
.metadata(value.clone())
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let opened = open_relay_metadata(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
)
.expect("relay metadata");
assert_eq!(opened.metadata(), Some(&value));
}
let absent =
SealedRelayBuilder::new("ProtectedMessage", DataValue::Null, 7, 42, 9, &signer)
.message_id("metadata-absent")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let opened = open_relay_metadata(
&absent,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
)
.expect("relay metadata");
assert_eq!(opened.metadata(), None);
}
#[test]
fn relay_keyring_boundary_helpers_open_verified_handles() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let frame = SealedRelayBuilder::new(
"ProtectedMessage",
DataValue::Str("hello".into()),
7,
42,
9,
&signer,
)
.message_id("boundary-message")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
assert_eq!(
relay_metadata_claimed_signer_id_with_limits(
&frame,
&[&recipient],
DecodeLimits::default(),
)
.expect("signer ID"),
7
);
let metadata = open_relay_metadata_with_keys(
&frame,
&[&recipient],
7,
&[sender.public_key_bundle()],
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
)
.expect("relay metadata");
let content = open_relay_content_with_limits_without_replay(
&metadata,
&[&recipient],
&[sender.public_key_bundle()],
Some(42),
content_open_options(
&metadata,
ProtectionPolicy::from(crate::SignaturePolicy::Ed25519),
),
)
.expect("relay content");
assert_eq!(content.signer_id, 7);
assert_eq!(content.final_recipient_id, 42);
assert_eq!(content.message_type, "ProtectedMessage");
assert_eq!(content.content, DataValue::Str("hello".into()));
}
#[test]
fn relay_content_opening_accepts_previous_recipient_key_history() {
let sender = Keyring::generate();
let metadata_recipient = Keyring::generate();
let current_content_recipient = Keyring::generate();
let previous_content_recipient = Keyring::generate();
let signer = ed_signer(&sender);
let frame = SealedRelayBuilder::new(
"ProtectedMessage",
DataValue::Str("opened with a previous recipient key".into()),
7,
42,
9,
&signer,
)
.message_id("recipient-rotation-1")
.created_at(123)
.metadata_recipients(vec![metadata_recipient.public_key_bundle()])
.content_recipients(vec![previous_content_recipient.public_key_bundle()])
.build()
.expect("relay frame");
let policy = ProtectionPolicy::from(crate::SignaturePolicy::Ed25519);
let metadata = open_relay_metadata_with(
&frame,
&[&metadata_recipient],
Some(7),
|signer_id| (signer_id == 7).then(|| vec![sender.public_key_bundle()]),
policy,
None,
)
.expect("relay metadata");
let content = open_relay_content_with_limits_without_replay(
&metadata,
&[&current_content_recipient, &previous_content_recipient],
&[sender.public_key_bundle()],
Some(42),
content_open_options(&metadata, policy),
)
.expect("previous content recipient key should decrypt");
assert_eq!(content.signer_id, 7);
assert_eq!(content.final_recipient_id, 42);
assert_eq!(content.message_type, "ProtectedMessage");
assert_eq!(
content.content,
DataValue::Str("opened with a previous recipient key".into())
);
}
#[test]
fn dual_policy_rejects_a_valid_ed25519_relay() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let frame = SealedRelayBuilder::new("ProtectedMessage", DataValue::Null, 7, 42, 9, &signer)
.message_id("message-2")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let result = open_relay_metadata(
&frame,
&recipient,
7,
&sender.public_key_bundle(),
ProtectionPolicy::from(crate::SignaturePolicy::Dual),
);
assert!(matches!(
result,
Err(RelayError::Protection(
ProtectionError::SignaturePolicyMismatch { .. }
))
));
}
#[test]
fn forwarding_changes_only_the_outer_next_hop() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let frame = SealedRelayBuilder::new("ProtectedMessage", DataValue::Null, 7, 42, 9, &signer)
.message_id("message-3")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let forwarded = forward_relay_frame(&frame, 10).expect("forward");
assert_eq!(frame.payload(), forwarded.payload());
assert_eq!(
frame.payload().to_bytes().expect("original payload bytes"),
forwarded
.payload()
.to_bytes()
.expect("forwarded payload bytes")
);
assert_eq!(forwarded.receiver(), Some(10));
assert_eq!(forwarded.sender(), None);
}
#[test]
fn relay_signer_key_rotation_accepts_previous_key_history() {
let old_signer_keyring = Keyring::generate();
let current_signer_keyring = Keyring::generate();
let recipient = Keyring::generate();
let old_signer = ed_signer(&old_signer_keyring);
let old_signer_public = old_signer_keyring.public_key_bundle();
let current_signer_public = current_signer_keyring.public_key_bundle();
let frame = SealedRelayBuilder::new(
"ProtectedMessage",
DataValue::Str("signed with the previous key".into()),
77,
42,
9,
&old_signer,
)
.message_id("rotation-1")
.created_at(456)
.metadata(DataValue::Str("rotation metadata".into()))
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build()
.expect("relay frame");
let policy = ProtectionPolicy::from(crate::SignaturePolicy::Ed25519);
let metadata = open_relay_metadata_with(
&frame,
&[&recipient],
Some(77),
|signer_id| {
(signer_id == 77)
.then(|| vec![current_signer_public.clone(), old_signer_public.clone()])
},
policy,
None,
)
.expect("metadata signed by a previous key should verify");
assert_eq!(metadata.matched_signer_key_index(), 1);
let content = open_relay_content_with_limits_without_replay(
&metadata,
&[&recipient],
&[current_signer_public, old_signer_public],
Some(42),
content_open_options(&metadata, policy),
)
.expect("content signed by a previous key should verify");
assert_eq!(content.message_type, "ProtectedMessage");
assert_eq!(
content.content,
DataValue::Str("signed with the previous key".into())
);
}
#[test]
fn builder_preserves_the_negotiated_type_map_context() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let type_map = TypeMap::latest();
let frame = SealedRelayBuilder::new("ProtectedMessage", DataValue::Null, 7, 42, 9, &signer)
.message_id("message-4")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.type_map(&type_map)
.build()
.expect("relay frame");
assert_eq!(frame.type_map(), Some(&type_map));
}
#[test]
fn builder_rejects_reserved_application_message_types() {
let sender = Keyring::generate();
let recipient = Keyring::generate();
let signer = ed_signer(&sender);
let result = SealedRelayBuilder::new("Ping", DataValue::Null, 7, 42, 9, &signer)
.message_id("reserved-message")
.created_at(123)
.metadata_recipients(vec![recipient.public_key_bundle()])
.content_recipients(vec![recipient.public_key_bundle()])
.build();
assert!(matches!(
result,
Err(RelayError::ReservedApplicationType(type_name)) if type_name == "Ping"
));
}
}