use crate::{Result, SealedError};
use alloc::vec::Vec;
use offline_protocol_core::Message;
pub const CTRL_SIG_META_KEY: &str = "__ctrl_sig";
pub const CTRL_PK_META_KEY: &str = "__ctrl_pk";
pub const CTRL_SIGN_DOMAIN: &[u8] = b"offline-ctrl-v1";
pub const CTRL_SIGN_DOMAIN_V2: &[u8] = b"offline-ctrl-v2";
pub fn canonical_payload(domain: &[u8], fields: &[&[u8]]) -> Result<Vec<u8>> {
let mut buf =
Vec::with_capacity(domain.len() + fields.iter().map(|f| 4 + f.len()).sum::<usize>());
buf.extend_from_slice(domain);
for field in fields {
let len: u32 = field
.len()
.try_into()
.map_err(|_| SealedError::FieldTooLarge(field.len()))?;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(field);
}
Ok(buf)
}
pub fn control_signing_payload(message: &Message) -> Result<Vec<u8>> {
let id = message.id.as_str();
let fields: [&[u8]; 4] = [
message.sender.as_str().as_bytes(),
id.as_bytes(),
message.recipient.as_str().as_bytes(),
message.content.as_bytes(),
];
canonical_payload(CTRL_SIGN_DOMAIN, &fields)
}
pub fn control_signing_payload_v2(message: &Message) -> Result<Vec<u8>> {
let id = message.id.as_str();
let stamped = message.timestamp.as_millis().to_be_bytes();
let fields: [&[u8]; 5] = [
message.sender.as_str().as_bytes(),
id.as_bytes(),
message.recipient.as_str().as_bytes(),
message.content.as_bytes(),
&stamped,
];
canonical_payload(CTRL_SIGN_DOMAIN_V2, &fields)
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use offline_protocol_core::{AppId, MessageId, Timestamp, UserId};
#[test]
fn canonical_payload_length_prefixes_every_field() {
let payload = canonical_payload(b"dom", &[b"ab", b"c"]).expect("payload");
assert_eq!(payload, b"dom\x00\x00\x00\x02ab\x00\x00\x00\x01c".to_vec());
}
#[test]
fn canonical_payload_distinguishes_field_splits() {
let a = canonical_payload(b"dom", &[b"ab", b"c"]).expect("payload");
let b = canonical_payload(b"dom", &[b"a", b"bc"]).expect("payload");
assert_ne!(a, b);
}
#[test]
fn canonical_payload_distinguishes_domains() {
let a = canonical_payload(b"dom-a", &[b"x"]).expect("payload");
let b = canonical_payload(b"dom-b", &[b"x"]).expect("payload");
assert_ne!(a, b);
}
#[test]
fn control_domain_has_its_published_spelling() {
assert_eq!(CTRL_SIGN_DOMAIN, b"offline-ctrl-v1");
assert_eq!(CTRL_SIGN_DOMAIN_V2, b"offline-ctrl-v2");
assert_eq!(CTRL_SIG_META_KEY, "__ctrl_sig");
assert_eq!(CTRL_PK_META_KEY, "__ctrl_pk");
}
#[test]
fn the_two_control_domains_are_mutually_non_prefixing() {
assert!(!CTRL_SIGN_DOMAIN_V2.starts_with(CTRL_SIGN_DOMAIN));
assert!(!CTRL_SIGN_DOMAIN.starts_with(CTRL_SIGN_DOMAIN_V2));
}
fn control_frame(stamp_ms: i64) -> Message {
let mut message = Message::from_parts(
MessageId::from_bytes([7u8; 16]),
UserId::new("off1alice").expect("sender"),
UserId::new("off1bob").expect("recipient"),
AppId::new("test").expect("app"),
"__MLS_KEY_PKG__{}",
Timestamp::from_millis(stamp_ms),
);
message
.metadata
.insert("unsigned".into(), "rewritten in flight".into());
message
}
#[test]
fn the_two_payloads_differ_for_one_frame() {
let message = control_frame(1_700_000_000_000);
let v1 = control_signing_payload(&message).expect("v1");
let v2 = control_signing_payload_v2(&message).expect("v2");
assert_ne!(v1, v2);
}
#[test]
fn the_v2_payload_changes_with_the_timestamp() {
let early = control_signing_payload_v2(&control_frame(1_700_000_000_000)).expect("early");
let late = control_signing_payload_v2(&control_frame(1_700_000_000_001)).expect("late");
assert_ne!(early, late);
}
#[test]
fn the_v1_payload_is_blind_to_the_timestamp() {
let early = control_signing_payload(&control_frame(1_700_000_000_000)).expect("early");
let late = control_signing_payload(&control_frame(1_900_000_000_000)).expect("late");
assert_eq!(early, late);
}
#[test]
fn the_v2_payload_ends_with_the_stamp_in_network_order() {
let stamp: i64 = 1_700_000_000_000;
let payload = control_signing_payload_v2(&control_frame(stamp)).expect("v2");
let mut tail = [0u8; 12];
tail[..4].copy_from_slice(&8u32.to_be_bytes());
tail[4..].copy_from_slice(&stamp.to_be_bytes());
assert!(payload.ends_with(&tail));
assert!(payload.starts_with(CTRL_SIGN_DOMAIN_V2));
}
#[test]
fn a_negative_stamp_encodes() {
let payload = control_signing_payload_v2(&control_frame(-1)).expect("v2");
assert!(payload.ends_with(&(-1i64).to_be_bytes()));
}
}