Original text:
The proposals included by value in an External Commit MUST meet the following conditions: <del> There MUST be a single Add proposal that adds the new issuing new member to the group </del> There MUST be a single ExternalInit proposal <del> There MUST NOT be any Update proposals </del>  If a Remove proposal is present, then the "credential" and "endpoint_id" of the removed leaf MUST be the same as the There MUST NOT be any ExternalInit proposals External Commits MUST contain a "path" field (and is therefore a <del> "full" Commit) </del>  External Commits MUST be signed by the new member.  In particular, the signature on the enclosing MLSPlaintext MUST verify using the

Reviewer comments:
In , we removed the requirement for an external Commit to have an Add proposal, under the theory that the required already has a leaf node for the new joiner. When I went to implement this, however, I noticed some ambiguities. First, the Commit processing section doesn't mention when the new joiner is added. The obvious approach is to do the add as part of the processing, but that processing presumes that the sender is already in the tree (for example, there is no provision for extending the tree). Second, the UpdatePath for an external commit needs to be done in a specific order in order to avoid a circular dependency. The TreeKEM encapsulation done by the joiner uses the GroupContext a context parameter, including the hash of the tree being used. But the LeafNode in the has to have a parent hash in it, which can only be computed after the encapsulation operation. This circularity can be resolved, but only by preparing the UpdatePath with the following specific steps: Compute the path secrets Compute the parent hash and the new leaf node Compute the new tree hash Encrypt the path secrets to the rest of the group That rules out some implementations that compute path secrets and encrypt them in one traversal of the direct path (this is what MLSpp does now). The options here are basically: Re-add the Add proposal, thus having a throw-away temporary LeafNode for the joiner Specify that the UpdatePath must be computed in the specific order above To me, (2) seems like a pretty clear winner.
Actually, it seems like there's a simpler option here: Extend the definition of an ExternalInit proposal to say that part of how it is applied is to ensure there is a blank leaf in the tree, extending the tree if necessary. (This implies that ExternalInit must be done after any Remove.) The leftmost blank leaf is the sender's leaf for purposes of UpdatePath processing. The provisional tree used in encrypting path secrets is still the provisional tree, in the sense that all proposals have been applied. This tree has a blank leaf for the UpdatePath sender. If we clarify these points, I think everything else works unmodified.
I agree with the first point. Even if the Add is not explicitly present, the external commit effectively emulates an Add anyway. To the second point: The solution is only simpler if that's how you want to implement it . I haven't tried it, but if you have chosen a different order of implementation, i.e. perform all changes to the provisional tree (including the update path and parent hash), then compute the tree hash and then encrypt the path, you now have to go and compute an additional tree hash before applying the update path, then apply the update path and keep the partial tree hash around for later encryption/decryption. This was not a part of MLS that I have looked at from a provable security perspective, but I'm wondering what the consequences are from a security perspective if we're including a parent hash for an incomplete provisional tree.
It seems like the main question here is what tree is provided to TreeKEM encryptions of path secrets. I'll call this the "context tree" below. It sounds like you have in mind that the tree provided to TreeKEM has the UpdatePath applied as well as the proposals, but that's not what the spec says right now: The starting point for this issue is that the "provisional" tree right now doesn't really faithfully capture the tree you're encrypting to, at least in the sense that it has a different number of leaves. That seemed wrong to me. It seems like we have the following options here: Do nothing. The context tree doesn't necessarily have a blank for the joiner. Maximal divergence between the new tree and the context tree (among these options) TreeKEM encryption operates on a different tree than the context tree Ensure the provisional tree has a blank leaf for the joiner (as up-thread). Context tree has a blank leaf for the joiner, but direct path doesn't match new tree TreeKEM encryption can operate on the context tree, because the structure is correct Use the "new" ratchet tree as the TreeKEM context. Context tree is exactly the new tree -- could probably eliminate provisional Requires separate steps for (1) updating tree nodes and (2) encrypting path secrets ... when these are currently described together as effectively one operation Personally, it seems like the answer is (2) or (3), and I probably lean toward (3). I don't think (1) is insecure, because the context tree does capture all of the nodes to which path secrets are encrypted. But the divergence is worrisome and it's cheap to do better. Between (2) and (3), there's a trade-off between fidelity and spec change size. The impact of (2) is more localized (just the ExternalInit section). Updating the spec to do (3) will require amending the description of update paths as well as the Commit creation / processing rules (to separate update/encrypt and remove provisional, respectively). But honestly, removing the provisional tree seems like a worthwhile simplification, so between that and the greater fidelity of the context tree, (3) seems worth the cost.
Actually, as I look into implementing this: We still need a provisional GroupContext, because the will differ. But we can at least have two trees instead of three.
When a new joiner sends an external Commit, within the current structure, they have to generate and send two KeyPackages: First in an Add proposal, and second in . Obviously, this is wasteful, since the second immediately overwrites the first. It also complicates receiver processing, since the receiver has to find where the appropriate Add happened so that it knows the sender's index in the new tree, so that it knows how to apply the update path in the Commit. We should remove the requirement for an Add commit in an external Commit, and instead assign the new member the leftmost free slot after all proposals have been applied, then apply the commit from there.
I agree that the key package in the Add proposal is redundant. I'm not so sure it will simplify implementations so much, because key packages from the update path of a Commit are usually treated like Update proposals (e.g. the constraint that the identity must be the same between the old and new credential). Tl;DR: I agree with the change, but I want to raise awareness about issues this introduces.
Approved, but see my comment in URL

Revised text:
The proposals included by value in an External Commit MUST meet the following conditions:  There MUST be a single ExternalInit proposal  <ins> There MUST NOT be any Add or Update proposals </ins> If a Remove proposal is present, then the "credential" and "endpoint_id" of the removed leaf MUST be the same as the There MUST NOT be any ExternalInit proposals External Commits MUST contain a "path" field (and is therefore a  <ins> "full" Commit).  The joiner is added at the leftmost free leaf node (just as if they were added with an Add proposal), and the path is calculated relative to that leaf node. </ins> External Commits MUST be signed by the new member.  In particular, the signature on the enclosing MLSPlaintext MUST verify using the