156 lines
4.8 KiB
Rust
156 lines
4.8 KiB
Rust
//! 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<u8> {
|
|
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<u8>), 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());
|
|
}
|
|
}
|