fix(marmot): exclude newly-added leaves from UpdatePath resolution
RFC 9420 §12.4.1: when a Commit contains Add proposals, the committer MUST exclude the new leaves from the copath-resolution used for path- secret encryption. Joiners get the group state via the Welcome at epoch N+1; they neither need nor expect a path secret for epoch N. Quartz was including them. openmls (via mdk-core) then runs its own resolution-minus-exclusion-list when picking which ciphertext to decrypt, so its `resolution_position` landed one slot off from where quartz had put the ciphertext for the existing member. The AEAD tag naturally mismatched and the commit died with `InvalidCommit(UpdatePathError(UnableToDecrypt))` — the blocker behind every test that needs an existing openmls member to process a commit produced by quartz (add, rename, remove, leave, promote, catchup). Fix: in `MlsGroup.commit()`, capture `newLeafIndices` from the Add proposals applied in this epoch and `filterNot` them out of every copath node's resolution before HPKE-encrypting the path secret. https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
This commit is contained in:
@@ -445,13 +445,24 @@ class MlsGroup private constructor(
|
||||
val leafSecret = MlsCryptoProvider.randomBytes(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
||||
val pathSecrets = tree.derivePathSecrets(myLeafIndex, leafSecret)
|
||||
|
||||
// Build UpdatePath with HPKE-encrypted path secrets for each copath node
|
||||
// Build UpdatePath with HPKE-encrypted path secrets for each copath node.
|
||||
// RFC 9420 §12.4.1: newly-added leaves (from Add proposals in THIS commit)
|
||||
// MUST be excluded from the copath resolution — they join via the Welcome
|
||||
// at epoch N+1 and don't need the path secret. Keeping them in the list
|
||||
// shifts every other resolution index by one, so strict receivers
|
||||
// (openmls/mdk) pick the wrong ciphertext and fail with
|
||||
// UpdatePathError(UnableToDecrypt).
|
||||
val newLeafIndices = addedMembers.map { it.first }.toSet()
|
||||
val updatePath =
|
||||
if (needsPath && pathSecrets.isNotEmpty()) {
|
||||
val copath = BinaryTree.copath(myLeafIndex, tree.leafCount)
|
||||
val pathNodes =
|
||||
pathSecrets.zip(copath).map { (pathKey, copathNode) ->
|
||||
val resolution = tree.resolution(copathNode)
|
||||
val resolution =
|
||||
tree.resolution(copathNode).filterNot { resNode ->
|
||||
BinaryTree.isLeaf(resNode) &&
|
||||
BinaryTree.nodeToLeaf(resNode) in newLeafIndices
|
||||
}
|
||||
val encryptedSecrets =
|
||||
resolution.mapNotNull { resNode ->
|
||||
val node = tree.getNode(resNode) ?: return@mapNotNull null
|
||||
|
||||
Reference in New Issue
Block a user