use crate::{
crypto::{AeadCipher, CipherSuite, Hash, KeySchedule},
member::KeyPackage,
MlsError, Result,
};
use saorsa_pqc::api::{MlKem, MlKemCiphertext, MlKemPublicKey, MlKemSecretKey};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
pub const MAX_LEAVES: u32 = 1 << 16;
pub type SecretState = (Option<u32>, Option<Vec<u8>>, Vec<(u32, Vec<u8>)>);
pub mod treemath {
use crate::{MlsError, Result};
#[must_use]
pub fn log2(x: u32) -> u32 {
if x == 0 {
0
} else {
31 - x.leading_zeros()
}
}
#[must_use]
pub fn level(x: u32) -> u32 {
x.trailing_ones()
}
#[must_use]
pub fn is_leaf(x: u32) -> bool {
x & 1 == 0
}
#[must_use]
pub fn leaf_to_node(leaf: u32) -> u32 {
leaf.saturating_mul(2)
}
pub fn node_to_leaf(x: u32) -> Result<u32> {
if is_leaf(x) {
Ok(x / 2)
} else {
Err(MlsError::TreeKemError(format!(
"node {x} is not a leaf node"
)))
}
}
#[must_use]
pub fn width_for_leaves(leaves: u32) -> u32 {
let l = leaves.max(1).next_power_of_two();
2 * l - 1
}
#[must_use]
pub fn root(width: u32) -> u32 {
(1 << log2(width)) - 1
}
pub fn left(x: u32) -> Result<u32> {
let k = level(x);
if k == 0 {
return Err(MlsError::TreeKemError(format!(
"leaf node {x} has no children"
)));
}
Ok(x ^ (1 << (k - 1)))
}
pub fn right(x: u32) -> Result<u32> {
let k = level(x);
if k == 0 {
return Err(MlsError::TreeKemError(format!(
"leaf node {x} has no children"
)));
}
Ok(x ^ (3 << (k - 1)))
}
pub fn parent(x: u32, width: u32) -> Result<u32> {
if x == root(width) {
return Err(MlsError::TreeKemError(format!(
"root node {x} has no parent"
)));
}
let k = level(x);
let b = (x >> (k + 1)) & 1;
Ok((x | (1 << k)) ^ (b << (k + 1)))
}
pub fn sibling(x: u32, width: u32) -> Result<u32> {
let p = parent(x, width)?;
if x < p {
right(p)
} else {
left(p)
}
}
pub fn direct_path(x: u32, width: u32) -> Result<Vec<u32>> {
let r = root(width);
let mut path = Vec::new();
let mut cur = x;
while cur != r {
cur = parent(cur, width)?;
path.push(cur);
}
Ok(path)
}
pub fn copath(x: u32, width: u32) -> Result<Vec<u32>> {
if x == root(width) {
return Ok(Vec::new());
}
let mut nodes = vec![x];
nodes.extend(direct_path(x, width)?);
nodes.pop(); nodes.into_iter().map(|y| sibling(y, width)).collect()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Node {
Leaf(LeafNodeData),
Parent(ParentNodeData),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LeafNodeData {
pub encryption_key: Vec<u8>,
pub key_package: KeyPackage,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParentNodeData {
pub encryption_key: Vec<u8>,
pub parent_hash: Vec<u8>,
pub unmerged_leaves: Vec<u32>,
}
#[derive(Clone)]
pub struct RatchetTree {
suite: CipherSuite,
nodes: Vec<Option<Node>>,
own_leaf: Option<u32>,
own_leaf_secret: Option<MlKemSecretKey>,
path_secrets: std::collections::BTreeMap<u32, zeroize::Zeroizing<Vec<u8>>>,
}
impl std::fmt::Debug for RatchetTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RatchetTree")
.field("suite", &self.suite)
.field("width", &self.width())
.field("active_leaves", &self.active_leaf_count())
.field("own_leaf", &self.own_leaf)
.field("path_secrets", &"<redacted>")
.field("own_leaf_secret", &"<redacted>")
.finish_non_exhaustive()
}
}
impl RatchetTree {
pub fn new(creator: KeyPackage, suite: CipherSuite) -> Result<Self> {
if creator.cipher_suite != suite {
return Err(MlsError::TreeKemError(
"creator key package cipher suite does not match tree suite".to_string(),
));
}
let mut tree = Self {
suite,
nodes: vec![None],
own_leaf: None,
own_leaf_secret: None,
path_secrets: std::collections::BTreeMap::new(),
};
tree.set_leaf_node(0, creator);
Ok(tree)
}
pub fn attach_owner(&mut self, leaf: u32, leaf_kem_secret: MlKemSecretKey) -> Result<()> {
if self.leaf(leaf).is_none() {
return Err(MlsError::TreeKemError(format!(
"cannot own blank/out-of-range leaf {leaf}"
)));
}
self.own_leaf = Some(leaf);
self.own_leaf_secret = Some(leaf_kem_secret);
Ok(())
}
#[must_use]
pub fn own_leaf(&self) -> Option<u32> {
self.own_leaf
}
#[must_use]
pub fn export_public_nodes(&self) -> Vec<Option<Node>> {
self.nodes.clone()
}
pub fn from_public_nodes(suite: CipherSuite, nodes: Vec<Option<Node>>) -> Result<Self> {
let width = nodes.len() as u64;
if width == 0 || width > u64::from(u32::MAX) || (width + 1).count_ones() != 1 {
return Err(MlsError::TreeKemError(format!(
"invalid ratchet-tree node count {width}"
)));
}
let leaf_capacity = width.div_ceil(2) as u32;
for (idx, node) in nodes.iter().enumerate() {
let idx = idx as u32;
match node {
Some(Node::Leaf(data)) => {
if idx & 1 != 0 {
return Err(MlsError::TreeKemError(format!(
"leaf node at odd index {idx}"
)));
}
if data.key_package.cipher_suite != suite {
return Err(MlsError::TreeKemError(
"leaf key package cipher suite does not match tree".to_string(),
));
}
if !data.key_package.verify().unwrap_or(false) {
return Err(MlsError::TreeKemError(
"leaf key package self-signature is invalid".to_string(),
));
}
}
Some(Node::Parent(data)) => {
if idx & 1 == 0 {
return Err(MlsError::TreeKemError(format!(
"parent node at even index {idx}"
)));
}
if data.unmerged_leaves.iter().any(|&l| l >= leaf_capacity) {
return Err(MlsError::TreeKemError(
"parent unmerged_leaves index out of range".to_string(),
));
}
}
None => {}
}
}
Ok(Self {
suite,
nodes,
own_leaf: None,
own_leaf_secret: None,
path_secrets: std::collections::BTreeMap::new(),
})
}
#[must_use]
pub fn find_leaf(&self, key_package: &KeyPackage) -> Option<u32> {
(0..self.leaf_capacity()).find(|&leaf| {
self.leaf(leaf).is_some_and(|data| {
data.key_package.verifying_key == key_package.verifying_key
&& data.key_package.agreement_key == key_package.agreement_key
})
})
}
#[must_use]
pub fn leaf_verifying_key(&self, leaf: u32) -> Option<&[u8]> {
self.leaf(leaf)
.map(|data| data.key_package.verifying_key.as_slice())
}
#[must_use]
pub fn secret_state(&self) -> SecretState {
let leaf_secret = self.own_leaf_secret.as_ref().map(|k| k.to_bytes().to_vec());
let path = self
.path_secrets
.iter()
.map(|(idx, secret)| (*idx, secret.to_vec()))
.collect();
(self.own_leaf, leaf_secret, path)
}
pub fn restore_secret_state(
&mut self,
own_leaf: Option<u32>,
own_leaf_secret: Option<Vec<u8>>,
path_secrets: Vec<(u32, Vec<u8>)>,
) -> Result<()> {
let width = self.width();
if let Some(leaf) = own_leaf {
if leaf >= self.leaf_capacity() || self.leaf(leaf).is_none() {
return Err(MlsError::TreeKemError(format!(
"snapshot owner leaf {leaf} is out of range or blank"
)));
}
}
for (idx, _) in &path_secrets {
if *idx >= width || idx & 1 == 0 {
return Err(MlsError::TreeKemError(format!(
"snapshot path secret has invalid node index {idx}"
)));
}
}
self.own_leaf = own_leaf;
self.own_leaf_secret = match own_leaf_secret {
Some(bytes) => Some(
MlKemSecretKey::from_bytes(self.suite.ml_kem_variant(), &bytes).map_err(|e| {
MlsError::CryptoError(format!("invalid leaf KEM secret: {e:?}"))
})?,
),
None => None,
};
self.path_secrets = path_secrets
.into_iter()
.map(|(idx, secret)| (idx, zeroize::Zeroizing::new(secret)))
.collect();
Ok(())
}
#[must_use]
pub fn cipher_suite(&self) -> CipherSuite {
self.suite
}
#[must_use]
pub fn width(&self) -> u32 {
self.nodes.len() as u32
}
#[must_use]
pub fn leaf_capacity(&self) -> u32 {
self.width().div_ceil(2)
}
#[must_use]
pub fn active_leaf_count(&self) -> u32 {
(0..self.leaf_capacity())
.filter(|&leaf| matches!(self.node(treemath::leaf_to_node(leaf)), Some(Node::Leaf(_))))
.count() as u32
}
#[must_use]
pub fn node(&self, idx: u32) -> Option<&Node> {
self.nodes.get(idx as usize).and_then(Option::as_ref)
}
#[must_use]
pub fn leaf(&self, leaf: u32) -> Option<&LeafNodeData> {
if leaf >= self.leaf_capacity() {
return None;
}
match self.node(treemath::leaf_to_node(leaf)) {
Some(Node::Leaf(data)) => Some(data),
_ => None,
}
}
pub fn add_leaf(&mut self, key_package: KeyPackage) -> Result<u32> {
if key_package.cipher_suite != self.suite {
return Err(MlsError::TreeKemError(
"key package cipher suite does not match tree suite".to_string(),
));
}
let leaf_index = self
.first_blank_leaf()
.unwrap_or_else(|| self.leaf_capacity());
if leaf_index >= MAX_LEAVES {
return Err(MlsError::TreeKemError(format!(
"group would exceed MAX_LEAVES ({MAX_LEAVES})"
)));
}
self.set_leaf_node(leaf_index, key_package);
let width = self.width();
for ancestor in treemath::direct_path(treemath::leaf_to_node(leaf_index), width)? {
if let Some(Node::Parent(parent)) = self.nodes[ancestor as usize].as_mut() {
if let Err(pos) = parent.unmerged_leaves.binary_search(&leaf_index) {
parent.unmerged_leaves.insert(pos, leaf_index);
}
}
}
Ok(leaf_index)
}
pub fn blank_leaf(&mut self, leaf: u32) -> Result<()> {
if leaf >= self.leaf_capacity() {
return Err(MlsError::TreeKemError(format!(
"leaf {leaf} is out of range"
)));
}
let leaf_node = treemath::leaf_to_node(leaf);
if self.node(leaf_node).is_none() {
return Err(MlsError::TreeKemError(format!(
"leaf {leaf} is already blank or out of range"
)));
}
let width = self.width();
self.nodes[leaf_node as usize] = None;
for ancestor in treemath::direct_path(leaf_node, width)? {
self.nodes[ancestor as usize] = None;
}
Ok(())
}
pub fn tree_hash(&self) -> Result<Vec<u8>> {
self.tree_hash_node(treemath::root(self.width()))
}
fn tree_hash_node(&self, idx: u32) -> Result<Vec<u8>> {
let hasher = Hash::new(self.suite);
let mut input = Vec::new();
if treemath::is_leaf(idx) {
input.push(0x01); match self.node(idx) {
Some(Node::Leaf(data)) => {
input.push(0x01); push_len_prefixed(&mut input, &data.encryption_key);
let kp = postcard::to_stdvec(&data.key_package)
.map_err(|e| MlsError::SerializationError(e.to_string()))?;
push_len_prefixed(&mut input, &kp);
}
_ => input.push(0x00), }
} else {
input.push(0x02); match self.node(idx) {
Some(Node::Parent(data)) => {
input.push(0x01); push_len_prefixed(&mut input, &data.encryption_key);
push_len_prefixed(&mut input, &data.parent_hash);
let mut unmerged = Vec::new();
for leaf in &data.unmerged_leaves {
unmerged.extend_from_slice(&leaf.to_be_bytes());
}
push_len_prefixed(&mut input, &unmerged);
}
_ => input.push(0x00), }
let left_hash = self.tree_hash_node(treemath::left(idx)?)?;
let right_hash = self.tree_hash_node(treemath::right(idx)?)?;
push_len_prefixed(&mut input, &left_hash);
push_len_prefixed(&mut input, &right_hash);
}
Ok(hasher.hash(&input))
}
fn first_blank_leaf(&self) -> Option<u32> {
(0..self.leaf_capacity()).find(|&leaf| self.node(treemath::leaf_to_node(leaf)).is_none())
}
pub fn set_parent_hashes(&mut self, leaf: u32) -> Result<()> {
let width = self.width();
let leaf_node = treemath::leaf_to_node(leaf);
let direct = treemath::direct_path(leaf_node, width)?;
let copath = treemath::copath(leaf_node, width)?;
let Some(&root_idx) = direct.last() else {
return Ok(()); };
if let Some(Node::Parent(p)) = self.nodes[root_idx as usize].as_mut() {
p.parent_hash = Vec::new(); }
for i in (0..direct.len().saturating_sub(1)).rev() {
let parent_idx = direct[i + 1];
let sibling_idx = copath[i + 1];
let (parent_enc, parent_ph) = match self.node(parent_idx) {
Some(Node::Parent(p)) => (p.encryption_key.clone(), p.parent_hash.clone()),
_ => {
return Err(MlsError::TreeKemError(format!(
"parent-hash: node {parent_idx} is not a populated parent"
)))
}
};
let sibling_hash = self.tree_hash_node(sibling_idx)?;
let parent_hash = self.parent_hash_input(&parent_enc, &parent_ph, &sibling_hash);
if let Some(Node::Parent(p)) = self.nodes[direct[i] as usize].as_mut() {
p.parent_hash = parent_hash;
}
}
Ok(())
}
fn parent_hash_input(
&self,
encryption_key: &[u8],
parent_hash: &[u8],
sibling_tree_hash: &[u8],
) -> Vec<u8> {
let hasher = Hash::new(self.suite);
let mut input = Vec::new();
push_len_prefixed(&mut input, encryption_key);
push_len_prefixed(&mut input, parent_hash);
push_len_prefixed(&mut input, sibling_tree_hash);
hasher.hash(&input)
}
fn grow_to_fit(&mut self, leaf_index: u32) {
let required = treemath::width_for_leaves(leaf_index + 1) as usize;
if self.nodes.len() < required {
self.nodes.resize(required, None);
}
}
fn set_leaf_node(&mut self, leaf: u32, key_package: KeyPackage) {
self.grow_to_fit(leaf);
let idx = treemath::leaf_to_node(leaf) as usize;
self.nodes[idx] = Some(Node::Leaf(LeafNodeData {
encryption_key: key_package.agreement_key.clone(),
key_package,
}));
}
fn set_leaf_encryption_key(&mut self, leaf: u32, key: Vec<u8>) -> Result<()> {
let idx = treemath::leaf_to_node(leaf) as usize;
match self.nodes.get_mut(idx).and_then(Option::as_mut) {
Some(Node::Leaf(data)) => {
data.encryption_key = key;
Ok(())
}
_ => Err(MlsError::TreeKemError(format!(
"leaf {leaf} is blank; cannot set encryption key"
))),
}
}
fn node_public_key(&self, idx: u32) -> Result<Vec<u8>> {
match self.node(idx) {
Some(Node::Leaf(d)) => Ok(d.encryption_key.clone()),
Some(Node::Parent(d)) => Ok(d.encryption_key.clone()),
None => Err(MlsError::TreeKemError(format!("node {idx} is blank"))),
}
}
fn resolution(&self, node: u32) -> Result<Vec<u32>> {
match self.node(node) {
Some(Node::Leaf(_)) => Ok(vec![node]),
Some(Node::Parent(p)) => {
let mut out = vec![node];
for &leaf in &p.unmerged_leaves {
out.push(treemath::leaf_to_node(leaf));
}
Ok(out)
}
None => {
if treemath::is_leaf(node) {
Ok(Vec::new())
} else {
let mut out = self.resolution(treemath::left(node)?)?;
out.extend(self.resolution(treemath::right(node)?)?);
Ok(out)
}
}
}
}
fn derive_secret(&self, secret: &[u8], label: &str) -> Result<Vec<u8>> {
KeySchedule::new(self.suite).derive_secret(secret, label, &[])
}
fn is_ancestor_or_self(&self, node: u32, descendant: u32, width: u32) -> Result<bool> {
if node == descendant {
return Ok(true);
}
Ok(treemath::direct_path(descendant, width)?.contains(&node))
}
fn secret_for_node(&self, node: u32) -> Result<Option<MlKemSecretKey>> {
if Some(node) == self.own_leaf.map(treemath::leaf_to_node) {
return Ok(self.own_leaf_secret.clone());
}
if let Some(ps) = self.path_secrets.get(&node) {
let (_pub, sk) = derive_key_pair(self.suite, ps)?;
return Ok(Some(sk));
}
Ok(None)
}
pub fn generate_update_path(
&mut self,
group_context: &[u8],
leaf_secret_seed: Option<&[u8]>,
) -> Result<(UpdatePath, zeroize::Zeroizing<Vec<u8>>)> {
let leaf = self
.own_leaf
.ok_or_else(|| MlsError::TreeKemError("instance owns no leaf".to_string()))?;
let width = self.width();
let leaf_node = treemath::leaf_to_node(leaf);
let direct = treemath::direct_path(leaf_node, width)?;
let copath = treemath::copath(leaf_node, width)?;
let leaf_secret = zeroize::Zeroizing::new(
leaf_secret_seed
.map(<[u8]>::to_vec)
.unwrap_or_else(|| crate::crypto::random_bytes(self.suite.hash_size())),
);
let (leaf_pub, leaf_sk) = derive_key_pair(self.suite, &leaf_secret)?;
let mut prev = leaf_secret;
let mut new_parents: Vec<(u32, Vec<u8>, zeroize::Zeroizing<Vec<u8>>)> = Vec::new();
for &dn in &direct {
let ps = zeroize::Zeroizing::new(self.derive_secret(&prev, "path")?);
let (pub_i, _sk_i) = derive_key_pair(self.suite, &ps)?;
new_parents.push((dn, pub_i, ps.clone()));
prev = ps;
}
let commit_secret = zeroize::Zeroizing::new(self.derive_secret(&prev, "path")?);
let mut up_nodes = Vec::with_capacity(direct.len());
for ((_, pub_i, ps_i), &cn) in new_parents.iter().zip(copath.iter()) {
let recipients = self.resolution(cn)?;
let mut cts = Vec::with_capacity(recipients.len());
for r in recipients {
let rpub = self.node_public_key(r)?;
cts.push(seal_to(self.suite, &rpub, ps_i, group_context)?);
}
up_nodes.push(UpdatePathNode {
encryption_key: pub_i.clone(),
encrypted_path_secret: cts,
});
}
self.set_leaf_encryption_key(leaf, leaf_pub.clone())?;
self.own_leaf_secret = Some(leaf_sk);
self.path_secrets.clear();
for (dn, pub_i, ps) in new_parents {
self.nodes[dn as usize] = Some(Node::Parent(ParentNodeData {
encryption_key: pub_i,
parent_hash: Vec::new(),
unmerged_leaves: Vec::new(),
}));
self.path_secrets.insert(dn, ps);
}
self.set_parent_hashes(leaf)?;
Ok((
UpdatePath {
leaf_index: leaf,
leaf_encryption_key: leaf_pub,
nodes: up_nodes,
},
commit_secret,
))
}
pub fn process_update_path(
&mut self,
update_path: &UpdatePath,
group_context: &[u8],
) -> Result<zeroize::Zeroizing<Vec<u8>>> {
let my_leaf = self
.own_leaf
.ok_or_else(|| MlsError::TreeKemError("instance owns no leaf".to_string()))?;
if update_path.leaf_index == my_leaf {
return Err(MlsError::TreeKemError(
"cannot process own UpdatePath; use the commit_secret from generate".to_string(),
));
}
if update_path.leaf_index >= self.leaf_capacity()
|| self.leaf(update_path.leaf_index).is_none()
{
return Err(MlsError::TreeKemError(format!(
"UpdatePath references invalid committer leaf {}",
update_path.leaf_index
)));
}
let width = self.width();
let my_node = treemath::leaf_to_node(my_leaf);
let committer_node = treemath::leaf_to_node(update_path.leaf_index);
let direct = treemath::direct_path(committer_node, width)?;
let copath = treemath::copath(committer_node, width)?;
if update_path.nodes.len() != direct.len() {
return Err(MlsError::TreeKemError(
"UpdatePath length does not match direct path".to_string(),
));
}
let mut decrypted: Option<(usize, zeroize::Zeroizing<Vec<u8>>)> = None;
for (i, &cn) in copath.iter().enumerate() {
if !self.is_ancestor_or_self(cn, my_node, width)? {
continue;
}
let recipients = self.resolution(cn)?;
for (j, &r) in recipients.iter().enumerate() {
if let Some(sk) = self.secret_for_node(r)? {
let ct = update_path.nodes[i]
.encrypted_path_secret
.get(j)
.ok_or_else(|| {
MlsError::TreeKemError(
"missing ciphertext for resolution slot".to_string(),
)
})?;
let ps = open_from(self.suite, &sk, ct, group_context)?;
decrypted = Some((i, zeroize::Zeroizing::new(ps)));
break;
}
}
if decrypted.is_some() {
break;
}
}
let (start, ps_start) = decrypted.ok_or_else(|| {
MlsError::TreeKemError("no decryptable path secret for this member".to_string())
})?;
let mut prev = ps_start;
let mut derived: Vec<(u32, zeroize::Zeroizing<Vec<u8>>)> = Vec::new();
for (i, (&dn, node)) in direct
.iter()
.zip(update_path.nodes.iter())
.enumerate()
.skip(start)
{
let ps = if i == start {
prev.clone()
} else {
zeroize::Zeroizing::new(self.derive_secret(&prev, "path")?)
};
let (pub_i, _sk) = derive_key_pair(self.suite, &ps)?;
if pub_i != node.encryption_key {
return Err(MlsError::TreeKemError(format!(
"derived public key for node {dn} does not match UpdatePath"
)));
}
derived.push((dn, ps.clone()));
prev = ps;
}
let commit_secret = zeroize::Zeroizing::new(self.derive_secret(&prev, "path")?);
self.set_leaf_encryption_key(
update_path.leaf_index,
update_path.leaf_encryption_key.clone(),
)?;
for (&dn, node) in direct.iter().zip(update_path.nodes.iter()) {
self.nodes[dn as usize] = Some(Node::Parent(ParentNodeData {
encryption_key: node.encryption_key.clone(),
parent_hash: Vec::new(),
unmerged_leaves: Vec::new(),
}));
}
for (dn, ps) in derived {
self.path_secrets.insert(dn, ps);
}
self.set_parent_hashes(update_path.leaf_index)?;
Ok(commit_secret)
}
}
fn push_len_prefixed(buf: &mut Vec<u8>, data: &[u8]) {
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(data);
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UpdatePath {
pub leaf_index: u32,
pub leaf_encryption_key: Vec<u8>,
pub nodes: Vec<UpdatePathNode>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UpdatePathNode {
pub encryption_key: Vec<u8>,
pub encrypted_path_secret: Vec<HpkeCiphertext>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HpkeCiphertext {
pub kem_ct: Vec<u8>,
pub aead_ct: Vec<u8>,
}
fn path_aead(suite: CipherSuite, shared_secret: &[u8]) -> Result<(AeadCipher, Vec<u8>)> {
let ks = KeySchedule::new(suite);
let key = ks.derive_key(
&[],
shared_secret,
b"saorsa treekem path key",
suite.key_size(),
)?;
let nonce = ks.derive_key(
&[],
shared_secret,
b"saorsa treekem path nonce",
suite.nonce_size(),
)?;
Ok((AeadCipher::new(key, suite)?, nonce))
}
pub(crate) fn seal_to(
suite: CipherSuite,
recipient_pub: &[u8],
plaintext: &[u8],
aad: &[u8],
) -> Result<HpkeCiphertext> {
let pk = MlKemPublicKey::from_bytes(suite.ml_kem_variant(), recipient_pub)
.map_err(|e| MlsError::CryptoError(format!("invalid recipient KEM key: {e:?}")))?;
let kem = MlKem::new(suite.ml_kem_variant());
let (shared, ct) = kem
.encapsulate(&pk)
.map_err(|e| MlsError::CryptoError(format!("encapsulation failed: {e:?}")))?;
let (aead, nonce) = path_aead(suite, &shared.to_bytes())?;
let aead_ct = aead.encrypt(&nonce, plaintext, aad)?;
Ok(HpkeCiphertext {
kem_ct: ct.to_bytes(),
aead_ct,
})
}
pub(crate) fn open_from(
suite: CipherSuite,
my_sk: &MlKemSecretKey,
hc: &HpkeCiphertext,
aad: &[u8],
) -> Result<Vec<u8>> {
let ct = MlKemCiphertext::from_bytes(suite.ml_kem_variant(), &hc.kem_ct)
.map_err(|e| MlsError::CryptoError(format!("invalid KEM ciphertext: {e:?}")))?;
let kem = MlKem::new(suite.ml_kem_variant());
let shared = kem
.decapsulate(my_sk, &ct)
.map_err(|e| MlsError::CryptoError(format!("decapsulation failed: {e:?}")))?;
let (aead, nonce) = path_aead(suite, &shared.to_bytes())?;
aead.decrypt(&nonce, &hc.aead_ct, aad)
}
pub fn derive_key_pair(
suite: CipherSuite,
node_secret: &[u8],
) -> Result<(Vec<u8>, MlKemSecretKey)> {
let ks = KeySchedule::new(suite);
let mut seed = ks.derive_key(&[], node_secret, b"MLS 1.0 DeriveKeyPair", 64)?;
if seed.len() != 64 {
seed.zeroize();
return Err(MlsError::CryptoError(format!(
"DeriveKeyPair expected 64 seed bytes, got {}",
seed.len()
)));
}
let mut d = [0u8; 32];
let mut z = [0u8; 32];
d.copy_from_slice(&seed[..32]);
z.copy_from_slice(&seed[32..]);
seed.zeroize();
let kem = MlKem::new(suite.ml_kem_variant());
let (public, secret) = kem.generate_keypair_from_seed(&d, &z);
d.zeroize();
z.zeroize();
Ok((public.to_bytes().to_vec(), secret))
}
#[cfg(test)]
mod tests {
use super::treemath::*;
use super::*;
use crate::crypto::KeyPair;
use crate::member::{Credential, KeyPackage, MemberId, MemberIdentity};
#[test]
fn test_log2_level() {
assert_eq!(log2(0), 0);
assert_eq!(log2(1), 0);
assert_eq!(log2(2), 1);
assert_eq!(log2(3), 1);
assert_eq!(log2(4), 2);
assert_eq!(log2(7), 2);
assert_eq!(log2(8), 3);
assert_eq!(level(0), 0); assert_eq!(level(2), 0); assert_eq!(level(1), 1);
assert_eq!(level(3), 2);
assert_eq!(level(7), 3);
assert_eq!(level(5), 1);
}
#[test]
fn test_width_and_root() {
assert_eq!(width_for_leaves(1), 1);
assert_eq!(width_for_leaves(2), 3);
assert_eq!(width_for_leaves(3), 7); assert_eq!(width_for_leaves(4), 7);
assert_eq!(width_for_leaves(5), 15);
assert_eq!(root(1), 0);
assert_eq!(root(3), 1);
assert_eq!(root(7), 3);
assert_eq!(root(15), 7);
}
#[test]
fn test_children() {
assert_eq!(left(1).unwrap(), 0);
assert_eq!(right(1).unwrap(), 2);
assert_eq!(left(3).unwrap(), 1);
assert_eq!(right(3).unwrap(), 5);
assert_eq!(left(5).unwrap(), 4);
assert_eq!(right(5).unwrap(), 6);
assert_eq!(left(7).unwrap(), 3);
assert_eq!(right(7).unwrap(), 11);
assert!(left(0).is_err());
assert!(right(2).is_err());
}
#[test]
fn test_parent_kats() {
assert_eq!(parent(0, 3).unwrap(), 1);
assert_eq!(parent(2, 3).unwrap(), 1);
assert!(parent(1, 3).is_err());
assert_eq!(parent(0, 7).unwrap(), 1);
assert_eq!(parent(2, 7).unwrap(), 1);
assert_eq!(parent(1, 7).unwrap(), 3);
assert_eq!(parent(4, 7).unwrap(), 5);
assert_eq!(parent(6, 7).unwrap(), 5);
assert_eq!(parent(5, 7).unwrap(), 3);
assert!(parent(3, 7).is_err()); }
#[test]
fn test_sibling() {
assert_eq!(sibling(0, 3).unwrap(), 2);
assert_eq!(sibling(2, 3).unwrap(), 0);
assert_eq!(sibling(0, 7).unwrap(), 2);
assert_eq!(sibling(1, 7).unwrap(), 5);
assert_eq!(sibling(5, 7).unwrap(), 1);
assert_eq!(sibling(4, 7).unwrap(), 6);
}
#[test]
fn test_direct_path_and_copath() {
assert_eq!(direct_path(0, 1).unwrap(), Vec::<u32>::new());
assert_eq!(copath(0, 1).unwrap(), Vec::<u32>::new());
assert_eq!(direct_path(0, 3).unwrap(), vec![1]);
assert_eq!(copath(0, 3).unwrap(), vec![2]);
assert_eq!(direct_path(0, 7).unwrap(), vec![1, 3]);
assert_eq!(copath(0, 7).unwrap(), vec![2, 5]);
assert_eq!(direct_path(4, 7).unwrap(), vec![5, 3]);
assert_eq!(copath(4, 7).unwrap(), vec![6, 1]);
}
#[test]
fn test_tree_math_invariants() {
for &width in &[1u32, 3, 7, 15, 31, 63, 127] {
let r = root(width);
for x in 0..width {
if x != r {
let p = parent(x, width).unwrap();
assert!(p < width, "parent {p} of {x} out of range, width={width}");
let (l, rr) = (left(p).unwrap(), right(p).unwrap());
assert!(x == l || x == rr, "width={width}: {x} not a child of {p}");
let s = sibling(x, width).unwrap();
assert!(s == l || s == rr);
assert!(s < width, "sibling {s} of {x} out of range, width={width}");
assert_ne!(s, x);
}
let dp = direct_path(x, width).unwrap();
if let Some(&last) = dp.last() {
assert_eq!(
last, r,
"width={width}: direct path of {x} must end at root"
);
}
let cp = copath(x, width).unwrap();
assert_eq!(
cp.len(),
dp.len(),
"width={width}: copath/direct_path length"
);
for &c in &cp {
assert!(
c < width,
"copath node {c} of {x} out of range, width={width}"
);
}
}
}
}
fn make_key_package() -> KeyPackage {
let suite = CipherSuite::default();
let keypair = KeyPair::generate(suite);
let cred = Credential::new_basic(MemberId::generate(), None, &keypair, suite).unwrap();
KeyPackage::new(keypair, cred).unwrap()
}
#[test]
fn test_derive_key_pair_is_deterministic() {
let suite = CipherSuite::default();
let secret = vec![7u8; 32];
let (pk1, sk1) = derive_key_pair(suite, &secret).unwrap();
let (pk2, sk2) = derive_key_pair(suite, &secret).unwrap();
assert_eq!(pk1, pk2, "same secret must yield same public key");
assert_eq!(
sk1.to_bytes(),
sk2.to_bytes(),
"same secret must yield same secret key"
);
let (pk3, _) = derive_key_pair(suite, &[8u8; 32]).unwrap();
assert_ne!(pk1, pk3, "different secret must yield different key");
}
#[test]
fn test_derive_key_pair_roundtrip_kem() {
let suite = CipherSuite::default();
let secret = vec![42u8; 32];
let (pk_bytes, sk) = derive_key_pair(suite, &secret).unwrap();
let kem = MlKem::new(suite.ml_kem_variant());
let pk =
saorsa_pqc::api::MlKemPublicKey::from_bytes(suite.ml_kem_variant(), &pk_bytes).unwrap();
let (ss_send, ct) = kem.encapsulate(&pk).unwrap();
let ss_recv = kem.decapsulate(&sk, &ct).unwrap();
assert_eq!(ss_send.to_bytes(), ss_recv.to_bytes());
}
#[test]
fn test_tree_new_and_add() {
let suite = CipherSuite::default();
let mut tree = RatchetTree::new(make_key_package(), suite).unwrap();
assert_eq!(tree.active_leaf_count(), 1);
assert!(tree.leaf(0).is_some());
let idx = tree.add_leaf(make_key_package()).unwrap();
assert_eq!(idx, 1);
assert_eq!(tree.active_leaf_count(), 2);
assert!(tree.leaf(1).is_some());
let idx = tree.add_leaf(make_key_package()).unwrap();
assert_eq!(idx, 2);
assert_eq!(tree.active_leaf_count(), 3);
assert_eq!(tree.leaf_capacity(), 4);
assert!(tree.leaf(3).is_none());
}
#[test]
fn test_add_reuses_blank_leaf() {
let suite = CipherSuite::default();
let mut tree = RatchetTree::new(make_key_package(), suite).unwrap();
tree.add_leaf(make_key_package()).unwrap(); tree.add_leaf(make_key_package()).unwrap(); assert_eq!(tree.active_leaf_count(), 3);
tree.blank_leaf(1).unwrap();
assert_eq!(tree.active_leaf_count(), 2);
assert!(tree.leaf(1).is_none());
let idx = tree.add_leaf(make_key_package()).unwrap();
assert_eq!(idx, 1);
assert_eq!(tree.active_leaf_count(), 3);
}
#[test]
fn test_tree_hash_deterministic_and_sensitive() {
let suite = CipherSuite::default();
let kp_a = make_key_package();
let kp_b = make_key_package();
let mut tree1 = RatchetTree::new(kp_a.clone(), suite).unwrap();
tree1.add_leaf(kp_b.clone()).unwrap();
let mut tree2 = RatchetTree::new(kp_a, suite).unwrap();
tree2.add_leaf(kp_b).unwrap();
let h1 = tree1.tree_hash().unwrap();
let h2 = tree2.tree_hash().unwrap();
assert_eq!(h1, h2, "identical public trees must hash identically");
tree2.add_leaf(make_key_package()).unwrap();
assert_ne!(tree2.tree_hash().unwrap(), h1);
}
#[test]
fn test_tree_suite_mismatch_rejected() {
let other = CipherSuite::from_id(
crate::crypto::CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
)
.unwrap();
let kp = make_key_package(); assert!(RatchetTree::new(kp, other).is_err());
}
#[test]
fn test_leaf_node_index_conversions() {
assert!(is_leaf(0) && is_leaf(2) && is_leaf(4));
assert!(!is_leaf(1) && !is_leaf(3));
assert_eq!(leaf_to_node(0), 0);
assert_eq!(leaf_to_node(1), 2);
assert_eq!(leaf_to_node(3), 6);
assert_eq!(node_to_leaf(0).unwrap(), 0);
assert_eq!(node_to_leaf(4).unwrap(), 2);
assert!(node_to_leaf(1).is_err()); assert!(node_to_leaf(3).is_err());
}
#[test]
fn test_blank_leaf_error_paths() {
let suite = CipherSuite::default();
let mut tree = RatchetTree::new(make_key_package(), suite).unwrap();
assert!(tree.blank_leaf(99).is_err());
tree.blank_leaf(0).unwrap();
assert_eq!(tree.active_leaf_count(), 0);
assert!(tree.blank_leaf(0).is_err());
let h1 = tree.tree_hash().unwrap();
let h2 = tree.tree_hash().unwrap();
assert_eq!(h1, h2);
}
#[test]
fn test_add_leaf_suite_mismatch_rejected() {
let suite = CipherSuite::default();
let mut tree = RatchetTree::new(make_key_package(), suite).unwrap();
let other = CipherSuite::from_id(
crate::crypto::CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
)
.unwrap();
let kp_other = {
let keypair = KeyPair::generate(other);
let cred = Credential::new_basic(MemberId::generate(), None, &keypair, other).unwrap();
KeyPackage::new(keypair, cred).unwrap()
};
assert!(tree.add_leaf(kp_other).is_err());
}
#[test]
fn test_growth_boundary_beyond_capacity_four() {
let suite = CipherSuite::default();
let mut tree = RatchetTree::new(make_key_package(), suite).unwrap();
for expected_idx in 1..=6u32 {
let idx = tree.add_leaf(make_key_package()).unwrap();
assert_eq!(idx, expected_idx);
}
assert_eq!(tree.active_leaf_count(), 7);
assert_eq!(tree.leaf_capacity(), 8); assert_eq!(tree.width(), 15);
for leaf in 0..7u32 {
assert!(
tree.leaf(leaf).is_some(),
"leaf {leaf} missing after growth"
);
}
assert!(tree.leaf(7).is_none()); assert_eq!(tree.tree_hash().unwrap(), tree.tree_hash().unwrap());
}
#[test]
fn test_derive_key_pair_high_security_suite() {
let suite = CipherSuite::from_id(
crate::crypto::CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
)
.unwrap();
let secret = vec![3u8; 32];
let (pk1, sk1) = derive_key_pair(suite, &secret).unwrap();
let (pk2, _) = derive_key_pair(suite, &secret).unwrap();
assert_eq!(pk1, pk2, "deterministic for ML-KEM-1024 suite");
let kem = MlKem::new(suite.ml_kem_variant());
let pk = saorsa_pqc::api::MlKemPublicKey::from_bytes(suite.ml_kem_variant(), &pk1).unwrap();
let (ss_send, ct) = kem.encapsulate(&pk).unwrap();
let ss_recv = kem.decapsulate(&sk1, &ct).unwrap();
assert_eq!(ss_send.to_bytes(), ss_recv.to_bytes());
}
const CTX: &[u8] = b"saorsa-mls-test-group-context";
fn member_view(public_tree: &RatchetTree, leaf: u32, identity: &MemberIdentity) -> RatchetTree {
let mut view = public_tree.clone();
view.own_leaf = None;
view.own_leaf_secret = None;
view.path_secrets.clear();
view.attach_owner(leaf, identity.kem_secret().unwrap().clone())
.unwrap();
view
}
#[test]
fn test_update_path_two_members_converge() {
let suite = CipherSuite::default();
let alice = MemberIdentity::generate(MemberId::generate()).unwrap();
let bob = MemberIdentity::generate(MemberId::generate()).unwrap();
let mut tree_a = RatchetTree::new(alice.key_package.clone(), suite).unwrap();
tree_a
.attach_owner(0, alice.kem_secret().unwrap().clone())
.unwrap();
let bob_leaf = tree_a.add_leaf(bob.key_package.clone()).unwrap();
let mut tree_b = member_view(&tree_a, bob_leaf, &bob);
let (update_path, cs_alice) = tree_a.generate_update_path(CTX, None).unwrap();
let cs_bob = tree_b.process_update_path(&update_path, CTX).unwrap();
assert_eq!(
&*cs_alice, &*cs_bob,
"committer and member must reach the same commit secret"
);
assert_eq!(cs_alice.len(), suite.hash_size());
}
#[test]
fn test_update_path_three_members_converge() {
let suite = CipherSuite::default();
let alice = MemberIdentity::generate(MemberId::generate()).unwrap();
let bob = MemberIdentity::generate(MemberId::generate()).unwrap();
let carol = MemberIdentity::generate(MemberId::generate()).unwrap();
let mut tree_a = RatchetTree::new(alice.key_package.clone(), suite).unwrap();
tree_a
.attach_owner(0, alice.kem_secret().unwrap().clone())
.unwrap();
let bob_leaf = tree_a.add_leaf(bob.key_package.clone()).unwrap();
let carol_leaf = tree_a.add_leaf(carol.key_package.clone()).unwrap();
let mut tree_b = member_view(&tree_a, bob_leaf, &bob);
let mut tree_c = member_view(&tree_a, carol_leaf, &carol);
let (update_path, cs_alice) = tree_a.generate_update_path(CTX, None).unwrap();
let cs_bob = tree_b.process_update_path(&update_path, CTX).unwrap();
let cs_carol = tree_c.process_update_path(&update_path, CTX).unwrap();
assert_eq!(&*cs_alice, &*cs_bob, "Bob must converge with Alice");
assert_eq!(&*cs_alice, &*cs_carol, "Carol must converge with Alice");
}
#[test]
fn test_update_path_commit_secret_is_fresh() {
let suite = CipherSuite::default();
let alice = MemberIdentity::generate(MemberId::generate()).unwrap();
let bob = MemberIdentity::generate(MemberId::generate()).unwrap();
let mut tree_a = RatchetTree::new(alice.key_package.clone(), suite).unwrap();
tree_a
.attach_owner(0, alice.kem_secret().unwrap().clone())
.unwrap();
let bob_leaf = tree_a.add_leaf(bob.key_package.clone()).unwrap();
let mut tree_b = member_view(&tree_a, bob_leaf, &bob);
let (up1, cs1) = tree_a.generate_update_path(CTX, None).unwrap();
tree_b.process_update_path(&up1, CTX).unwrap();
let (up2, cs2) = tree_a.generate_update_path(CTX, None).unwrap();
let cs2_b = tree_b.process_update_path(&up2, CTX).unwrap();
assert_ne!(
&*cs1, &*cs2,
"successive commits must produce distinct secrets"
);
assert_eq!(&*cs2, &*cs2_b, "still converges after a second commit");
}
#[test]
fn test_process_own_update_path_rejected() {
let suite = CipherSuite::default();
let alice = MemberIdentity::generate(MemberId::generate()).unwrap();
let bob = MemberIdentity::generate(MemberId::generate()).unwrap();
let mut tree_a = RatchetTree::new(alice.key_package.clone(), suite).unwrap();
tree_a
.attach_owner(0, alice.kem_secret().unwrap().clone())
.unwrap();
tree_a.add_leaf(bob.key_package.clone()).unwrap();
let (up, _cs) = tree_a.generate_update_path(CTX, None).unwrap();
assert!(tree_a.process_update_path(&up, CTX).is_err());
}
#[test]
fn test_deterministic_update_path_with_seed() {
let suite = CipherSuite::default();
let alice = MemberIdentity::generate(MemberId::generate()).unwrap();
let bob = MemberIdentity::generate(MemberId::generate()).unwrap();
let build = || {
let mut t = RatchetTree::new(alice.key_package.clone(), suite).unwrap();
t.attach_owner(0, alice.kem_secret().unwrap().clone())
.unwrap();
t.add_leaf(bob.key_package.clone()).unwrap();
t
};
let seed = [9u8; 32];
let (_up1, cs1) = build().generate_update_path(CTX, Some(&seed)).unwrap();
let (_up2, cs2) = build().generate_update_path(CTX, Some(&seed)).unwrap();
assert_eq!(
&*cs1, &*cs2,
"fixed seed must yield deterministic commit secret"
);
}
fn build_group(n: u32) -> (RatchetTree, Vec<MemberIdentity>) {
let suite = CipherSuite::default();
let ids: Vec<MemberIdentity> = (0..n)
.map(|_| MemberIdentity::generate(MemberId::generate()).unwrap())
.collect();
let mut tree = RatchetTree::new(ids[0].key_package.clone(), suite).unwrap();
tree.attach_owner(0, ids[0].kem_secret().unwrap().clone())
.unwrap();
for id in &ids[1..] {
tree.add_leaf(id.key_package.clone()).unwrap();
}
(tree, ids)
}
#[test]
fn test_parent_hashes_set_on_update() {
let (mut tree, _ids) = build_group(4); tree.generate_update_path(CTX, None).unwrap();
match tree.node(1) {
Some(Node::Parent(p)) => assert!(
!p.parent_hash.is_empty(),
"non-root path node must have a parent hash"
),
other => panic!("expected populated parent at node 1, got {other:?}"),
}
match tree.node(3) {
Some(Node::Parent(p)) => {
assert!(p.parent_hash.is_empty(), "root parent hash must be empty")
}
other => panic!("expected root parent at node 3, got {other:?}"),
}
}
#[test]
fn test_parent_hash_binds_sibling_subtree() {
let suite = CipherSuite::default();
let a = MemberIdentity::generate(MemberId::generate()).unwrap();
let b = MemberIdentity::generate(MemberId::generate()).unwrap();
let c1 = MemberIdentity::generate(MemberId::generate()).unwrap();
let c2 = MemberIdentity::generate(MemberId::generate()).unwrap();
let build = |third: &MemberIdentity| {
let mut t = RatchetTree::new(a.key_package.clone(), suite).unwrap();
t.attach_owner(0, a.kem_secret().unwrap().clone()).unwrap();
t.add_leaf(b.key_package.clone()).unwrap(); t.add_leaf(third.key_package.clone()).unwrap(); t
};
let mut t1 = build(&c1);
let mut t2 = build(&c2);
let seed = [5u8; 32];
t1.generate_update_path(CTX, Some(&seed)).unwrap();
t2.generate_update_path(CTX, Some(&seed)).unwrap();
let ph1 = match t1.node(1) {
Some(Node::Parent(p)) => p.parent_hash.clone(),
_ => panic!(),
};
let ph2 = match t2.node(1) {
Some(Node::Parent(p)) => p.parent_hash.clone(),
_ => panic!(),
};
assert_ne!(
ph1, ph2,
"parent hash must change when the sibling subtree changes"
);
}
#[test]
fn test_update_path_eight_members_converge() {
let (mut tree_a, ids) = build_group(8);
let mut views: Vec<RatchetTree> = (1..8)
.map(|leaf| member_view(&tree_a, leaf, &ids[leaf as usize]))
.collect();
let (up, cs_a) = tree_a.generate_update_path(CTX, None).unwrap();
for (k, view) in views.iter_mut().enumerate() {
let cs = view.process_update_path(&up, CTX).unwrap();
assert_eq!(&*cs_a, &*cs, "member at leaf {} must converge", k + 1);
}
}
#[test]
fn test_update_path_different_committers_converge() {
let (mut tree_a, ids) = build_group(3);
let mut tree_b = member_view(&tree_a, 1, &ids[1]);
let mut tree_c = member_view(&tree_a, 2, &ids[2]);
let (up, cs_bob) = tree_b.generate_update_path(CTX, None).unwrap();
let cs_a = tree_a.process_update_path(&up, CTX).unwrap();
let cs_c = tree_c.process_update_path(&up, CTX).unwrap();
assert_eq!(&*cs_bob, &*cs_a, "Alice converges with Bob's commit");
assert_eq!(&*cs_bob, &*cs_c, "Carol converges with Bob's commit");
}
#[test]
fn test_removed_member_cannot_derive_next_epoch() {
let (mut tree_a, ids) = build_group(3); let mut carol_stale = member_view(&tree_a, 2, &ids[2]); let mut bob = member_view(&tree_a, 1, &ids[1]);
tree_a.blank_leaf(2).unwrap();
bob.blank_leaf(2).unwrap();
let (up, cs_committer) = tree_a.generate_update_path(CTX, None).unwrap();
let cs_bob = bob.process_update_path(&up, CTX).unwrap();
assert_eq!(
&*cs_committer, &*cs_bob,
"surviving member converges after a removal"
);
assert!(
carol_stale.process_update_path(&up, CTX).is_err(),
"removed member must NOT be able to derive the new commit secret"
);
}
#[test]
fn test_process_rejects_tampered_pubkey() {
let (mut tree_a, ids) = build_group(2);
let mut tree_b = member_view(&tree_a, 1, &ids[1]);
let (mut up, _cs) = tree_a.generate_update_path(CTX, None).unwrap();
if let Some(node) = up.nodes.last_mut() {
node.encryption_key[0] ^= 0xFF;
}
assert!(
tree_b.process_update_path(&up, CTX).is_err(),
"a tampered path public key must be rejected"
);
}
#[test]
fn test_process_rejects_wrong_context() {
let (mut tree_a, ids) = build_group(2);
let mut tree_b = member_view(&tree_a, 1, &ids[1]);
let (up, _cs) = tree_a.generate_update_path(CTX, None).unwrap();
assert!(
tree_b
.process_update_path(&up, b"different-context")
.is_err(),
"mismatched group context must be rejected"
);
}
#[test]
fn test_process_rejects_out_of_range_committer() {
let (mut tree_a, ids) = build_group(2);
let mut tree_b = member_view(&tree_a, 1, &ids[1]);
let (mut up, _cs) = tree_a.generate_update_path(CTX, None).unwrap();
up.leaf_index = u32::MAX;
assert!(tree_b.process_update_path(&up, CTX).is_err());
}
}