//! Sealed sender: embed sender identity + Ed25519 signature inside the MLS //! application payload so recipients can verify the sender from decrypted //! content, independent of MLS framing. //! //! # Wire format //! //! ```text //! [magic: 1 byte (0x53 = 'S')] //! [sender_identity_key: 32 bytes (Ed25519 public key)] //! [signature: 64 bytes (Ed25519)] //! [inner_payload: variable (the original app_message bytes)] //! ``` //! //! The signature covers: `magic || sender_identity_key || inner_payload`. //! Total overhead: 1 + 32 + 64 = 97 bytes per message. use crate::error::CoreError; use crate::identity::IdentityKeypair; /// Magic byte identifying a sealed sender envelope. pub const SEALED_MAGIC: u8 = 0x53; // 'S' /// Fixed overhead: magic(1) + sender_key(32) + signature(64). const SEALED_OVERHEAD: usize = 1 + 32 + 64; /// Wrap an app_message payload in a sealed sender envelope. /// /// Signs `magic || sender_key || payload` with the sender's Ed25519 key. pub fn seal(identity: &IdentityKeypair, app_message_bytes: &[u8]) -> Vec { let sender_key = identity.public_key_bytes(); // Build signing input let mut sign_input = Vec::with_capacity(1 + 32 + app_message_bytes.len()); sign_input.push(SEALED_MAGIC); sign_input.extend_from_slice(&sender_key); sign_input.extend_from_slice(app_message_bytes); let signature = identity.sign_raw(&sign_input); let mut out = Vec::with_capacity(SEALED_OVERHEAD + app_message_bytes.len()); out.push(SEALED_MAGIC); out.extend_from_slice(&sender_key); out.extend_from_slice(&signature); out.extend_from_slice(app_message_bytes); out } /// Unseal: verify the Ed25519 signature, return `(sender_identity_key, inner_app_message_bytes)`. pub fn unseal(bytes: &[u8]) -> Result<([u8; 32], Vec), CoreError> { if bytes.len() < SEALED_OVERHEAD { return Err(CoreError::AppMessage( "sealed sender envelope too short".into(), )); } if bytes[0] != SEALED_MAGIC { return Err(CoreError::AppMessage(format!( "sealed sender: expected magic 0x{:02X}, got 0x{:02X}", SEALED_MAGIC, bytes[0] ))); } let mut sender_key = [0u8; 32]; sender_key.copy_from_slice(&bytes[1..33]); let mut signature = [0u8; 64]; signature.copy_from_slice(&bytes[33..97]); let inner_payload = &bytes[97..]; // Reconstruct signing input: magic || sender_key || inner_payload let mut sign_input = Vec::with_capacity(1 + 32 + inner_payload.len()); sign_input.push(SEALED_MAGIC); sign_input.extend_from_slice(&sender_key); sign_input.extend_from_slice(inner_payload); IdentityKeypair::verify_raw(&sender_key, &sign_input, &signature)?; Ok((sender_key, inner_payload.to_vec())) } /// Check if bytes start with the sealed sender magic byte. pub fn is_sealed(bytes: &[u8]) -> bool { bytes.first() == Some(&SEALED_MAGIC) } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; #[test] fn seal_unseal_round_trip() { let identity = IdentityKeypair::generate(); let payload = b"hello sealed sender"; let sealed = seal(&identity, payload); assert!(is_sealed(&sealed)); let (sender_key, inner) = unseal(&sealed).unwrap(); assert_eq!(sender_key, identity.public_key_bytes()); assert_eq!(inner, payload); } #[test] fn unseal_tampered_payload_fails() { let identity = IdentityKeypair::generate(); let payload = b"hello"; let mut sealed = seal(&identity, payload); // Tamper with the inner payload if let Some(last) = sealed.last_mut() { *last ^= 0xFF; } assert!(unseal(&sealed).is_err()); } #[test] fn unseal_wrong_sender_fails() { let alice = IdentityKeypair::generate(); let bob = IdentityKeypair::generate(); let payload = b"from alice"; let mut sealed = seal(&alice, payload); // Replace sender key with Bob's let bob_key = bob.public_key_bytes(); sealed[1..33].copy_from_slice(&bob_key); assert!(unseal(&sealed).is_err()); } #[test] fn unseal_too_short_fails() { assert!(unseal(&[SEALED_MAGIC; 10]).is_err()); } #[test] fn unseal_wrong_magic_fails() { let identity = IdentityKeypair::generate(); let mut sealed = seal(&identity, b"test"); sealed[0] = 0x00; assert!(unseal(&sealed).is_err()); } #[test] fn non_sealed_detected() { assert!(!is_sealed(b"\x01\x01hello")); assert!(is_sealed(&[SEALED_MAGIC, 0, 0])); } #[test] fn empty_payload_round_trip() { let identity = IdentityKeypair::generate(); let sealed = seal(&identity, b""); let (sender_key, inner) = unseal(&sealed).unwrap(); assert_eq!(sender_key, identity.public_key_bytes()); assert!(inner.is_empty()); } }