used for the Merkle trees in the Certificate Transparency protocol I- D.ietf-trans-rfc6962-bis.) <del> The _direct path_ of a root is the empty list, and of any other node is the concatenation of that node's parent along with the parent's direct path.  The _copath_ of a node is the node's sibling concatenated with the list of siblings of all the nodes in its direct path, excluding the root. For example, in the below tree: The direct path of C is (W, V, X) The copath of C is (D, U, Z) A tree with "n" leaves has "2*n - 1" nodes.  For example, the above tree has 7 leaves (A, B, C, D, E, F, G) and 13 nodes. Each leaf is given an _index_ (or _leaf index_), starting at "0" from the left to "n-1" at the right. Finally, a node in the tree may also be _blank_, indicating that no value is present at that node (i.e. no keying material).  This is often the case when a leaf was recently removed from the tree. </del> There are multiple ways that an implementation might represent a ratchet tree in memory.  For example, left-balanced binary trees can be represented as an array of nodes, with node relationships computed and associated algorithms, as long as they produce correct protocol messages.  5.2. We generally assume that each participant maintains a complete and Note how the tree invariant applies: Each member knows only their own leaf, and the private key AB is known only to A and B. <del> 5.3. A particular instance of a ratchet tree includes the same parameters that define an instance of HPKE, namely: A Key Encapsulation Mechanism (KEM), including a "DeriveKeyPair" function that creates a key pair for the KEM from a symmetric secret A Key Derivation Function (KDF), including "Extract" and "Expand" functions An AEAD encryption scheme Each non-blank node in a ratchet tree contains up to five values: A public key A private key (only within the member's direct path, see below) A credential (only for leaf nodes) An ordered list of "unmerged" leaves (see views) A hash of certain information about the node's parent, as of the last time the node was changed (see parent-hash). The _resolution_ of a node is an ordered list of non-blank nodes that collectively cover all non-blank descendants of the node.  The resolution of a non-blank node with no unmerged leaves is just the node itself.  More generally, the resolution of a node is effectively a depth-first, left-first enumeration of the nearest non-blank nodes below the node: The resolution of a non-blank node comprises the node itself, followed by its list of unmerged leaves, if any The resolution of a blank leaf node is the empty list The resolution of a blank intermediate node is the result of concatenating the resolution of its left child with the resolution of its right child, in that order For example, consider the following subtree, where the "_" character represents a blank node and unmerged leaves are indicated in square brackets: In this tree, we can see all of the above rules in play: The resolution of node Z is the list [Z, C] The resolution of leaf 1 is the empty list [] The resolution of top node is the list [A, Z, C] Every node, regardless of whether the node is blank or populated, has a corresponding _hash_ that summarizes the contents of the subtree below that node.  The rules for computing these hashes are described in tree-hashes. </del> 6. 6.1. that allows group members to efficiently communicate the fresh secret keys to other group members, as described in update-paths. <del> Recall the definition of resolution from ratchet-tree-nodes.  To begin with, the generator of the UpdatePath updates its leaf and its leaf's _filtered direct path_ with new key pairs.  The filtered direct path of a node is obtained from the node's direct path by removing all nodes whose child on the nodes' copath has an empty resolution (keeping in mind that any unmerged leaves of the copath child count towards its resolution).  Such a removed node does not need its own key pair since, if the copath child's resolution is blank, then encrypting any data to the node's key pair would be equivalent to encrypting only to the non-copath child. </del>  Blank all the nodes on the direct path from the leaf to the root. the nodes in the UpdatePath form a parent-hash chain from the committer's leaf to the root.  8.9. As described in commit, each Commit message may optionally contain an via a "sender ratchet".  Each sender has their own sender ratchet, and each step along the ratchet is called a "generation".  A sender ratchet starts from a per-sender base secret derived from a Secret Tree, as described in secret-tree.  The base secret initiates a symmetric hash ratchet which generates a sequence of keys and "application_ratchet_nonce_[L]_[j]" and "application_ratchet_key_[L]_[j]". <del> Concretely, suppose we have the following Secret Tree and ratchet for participant D: </del>  <del> Then if a client uses key K1 and nonce N1 during epoch n then it must consume (at least) values G, F, D, AR0, AR1, K1, N1 as well as the key schedule secrets used to derive G (the "encryption_secret"), namely "init_secret" of epoch n-1 and "commit_secret", "joiner_secret", "epoch_secret" of epoch n.  The client MAY retain (not consume) the values K0 and N0 to allow for out-of-order delivery, and SHOULD retain AR2 for processing future messages. </del>  11.

This PR reorganizes the exposition of tree concept to make them read more clearly. A new Paths through the Ratchet Tree section is introduced, which covers direct paths, copaths, and filtered direct paths The Ratchet Tree Nodes section is moved forward, so that the definition of filtered direct paths can refer to the notion of resolutions (see NAME note on ) The path example expanded so that there are blanks in the tree, and so that it shows all three path types (direct, copath, and filtered direct) for every leaf. The section on verifying parent hashes uses the example tree from the path section to illustrate the concepts noted in
Latest commit adds coverage for the last part of . In addition to moving the figure forward, it refactors the explanation to be even more explicit about what must be consumed and what may be kept.
Currently defined only briefly in "RatchetTree Evolution". While that's where it's primarily used, it would fit better in the terminology section, especially if we expand it a little and add an illustration.
The problem here is that the definition of "filtered direct path" depends on the definition of "resolution" which isn't defined until a later section.
Indeed, so proposes moving up the definition of resolution as well.
Add illustration of filtered direct path Add illustration of parent hash validation, including: Leaves with no parent hash Nodes with invalid parent hash Skipping blank nodes Add corresponding derivations to Appendix A Move figure in Section 10.2 up to Section 10 as an overview
NAME Note that I have taken a stab at parts of this in because it came along with some other tree stuff.
Duplicate
Regarding the Tree Hash Including the parentnode in the tree hash which is supposed to be computed from the leaves, makes the definition appear circular to a new reader. Since the situation when you have blank nodes in the middle of a tree but not at the root is a rather complicated, the situation when you could calculate a tree hash using the parentnode is non-obvious. In the description of Tree Hash the document says that the ParentNode object is optional "depending on whether the node is blank or not." This isn't clear specification language. Saying the parentnode object is present if the node is blank or present if the node is not blank would be clear. What if the parent node is also blank? What if the root is blank? I think an example with and without blank nodes would be appropriate. Explaining the reason why the parentnode is included will help the reader establish a better mental model. Regarding the Parent Hash I still find this section super confusing. A few points Using a tree hash (siblingtreehash) inside the parent hash is non-obvious. There should be some explanation about what property this is providing and that it is a tree hash and not a parent hash. (Honestly it was the third time I read the section that I realized that it was really the tree hash and not the parent hash of the sibling.) Using P, V, and S for Parent, target node, and sibling node is introduced here and never used anywhere else in the document. P for parent and S for sibling make sense. I can't tell what V is supposed to stand for, and it unfortunately could cause people to confuse the vector notation used throughout, but shortly after introducing V for the target node. I propose using "N" for node or "X" instead. There is one reference to the filtered direct path of U. Perhaps this is V, or a child of V? The calculations are sufficiently complicated that I think this warrants a complete, non-trivial, worked example (possibly in an appendix). At some point we should consider providing test vectors in a future document, and this would be an ideal candidate.
Regarding the Tree Hash To be clear, the that is included in the tree hash is the node whose subtree is being hashed. The tree hash doesn't include only the leaves, it includes all the nodes in the tree. If you mean , then yes, I agree the connection is a little subtle. I've tried to explain this a little better. Updated this to be clear that the ParentNode is omitted if and only if the node is blank. Not clear what situation you mean here. I think the specific computations are pretty clearly specified for these cases. Something like this was added in : URL I extended the example and put it in an appendix. Assuming you mean here, hopefully this is addressed by the above notes. Added a note to clarify that this is the tree hash and why. Changed to "D" for "direct path", and explained the semantics. Actually it's the leaf, which is now called "L". I think the example linked above does a pretty good job. Feedback obviously welcome. Agreed that a test vectors document will be a useful follow-on.
In order to use tree hashes to represent siblings in parent hashes, we had to compute a different tree hash on the right edge of the tree, pretending that the tree is full, in the sense of having leaves. Thus we actually have two tree hashes in play, one used for group agreement on the tree and one used for parent hash inputs. We should consider specifying that the logical tree used by MLS is always full. This change to the specification would not change the computations done by the protocol much, if at all. Thanks to the fact that we no longer generate redundant nodes (), there is never anything non-blank to the right of the rightmost non-blank leaf anyway. So the main impact is to insert some blank nodes between a parent node on the right edge of the tree and its right child -- and all of the algorithms in the specification already skip over such nodes. Likewise, implementations' internal represntations of the tree wouldn't have to change much, since the extra nodes could be "virtual". The only time they would be noticed is when computing tree hashes. We would have to update the description of the extension to say that it only covers the tree up to the rightmost non-blank leaf. It would simplify the specification in a few ways, for example: The "original sibling tree hash" would just entail blanking unmerged leaves, not structure changes. The description of extending and truncating the tree would be much simpler. The main cost would be extra hashes when joining, or when updating a member close to the right edge of the tree. If you have a tree with leaves, then you have to compute tree hashes over blank leaves and their blank parents.
Thanks to NAME and NAME for helping inspire and develop this idea in discussions around
Virtual interim 2022-05-19 NAME notes that the current tree hash is quite tricky to implement, so this would be a useful simplification Doubling the hashes on a fresh tree doesn't sound too intimidating Folks would find more concreteness useful here, interested in PR Could we phrase this as only a change to TreeKEM? Would be premised on the equivalence of the hashed tree an the tree you operate on
Discussed at 19 May interim: NAME to draft a PR.
Holding a PR until after lands. Some notes on what would need to change: {{ratchet-tree-terminology}} would change from "the unique left-balanced tree with N leaves" to "the unique balanced binary tree with depth , where is the smallest number such that ". {{adding-and-removing-leaves}} would either be removed, or updated so that it talks about increasing or decreasing the depth of the tree (equivalently, doubling or halving the number of leaves). {{add}} would talk about increasing depth instead of adding leaves. {{remove}} would talk about decreasing depth instead of removing leaves. {{ratchet-tree-extension}} would be explicit that it only carries nodes up to the rightmost non-blank leaf. The encoding/decoding algorithms would have to be updated. {{array-based-trees}} and {{link-based-trees}} could probably be simplified. We would need to make sure all figures showed full trees.
I went ahead and filed for this, based on top of . Once that merges, we can update the PR to be a little easier to read. The changes are basically as described above. One additional note: We have actually already incurred the cost to a new joiner of hashing all the extra nodes, because they have to do it for parent hash verification anyway. So this approach is actually a net reduction in the amount of hashing that has to be done, since they don't have to also compute the non-full-tree hashes.
In , we introduced the idea of a "filtered direct path". It would be helpful to have an illustration of how a filtered direct path differs from a direct path.

used for the Merkle trees in the Certificate Transparency protocol I- D.ietf-trans-rfc6962-bis.)  There are multiple ways that an implementation might represent a ratchet tree in memory.  For example, left-balanced binary trees can be represented as an array of nodes, with node relationships computed and associated algorithms, as long as they produce correct protocol messages. <ins> 5.1.1. Each leaf node in a ratchet tree is given an _index_ (or _leaf index_), starting at "0" from the left to "n-1" at the right (for a tree with "n" leaves).  A tree with "n" leaves has "2*n - 1" nodes, including parent nodes. Each node in a ratchet tree is either _blank_ (containing no value) or it holds an asymmetric key pair with some associated data: A public key (for the HPKE scheme in use, see ciphersuites) A private key (only within the member's direct path, see views) A credential (only for leaf nodes) An ordered list of "unmerged" leaves (see views) A hash of certain information about the node's parent, as of the last time the node was changed (see parent-hash). Every node, regardless of whether the node is blank or populated, has a corresponding _hash_ that summarizes the contents of the subtree below that node.  The rules for computing these hashes are described in tree-hashes. The _resolution_ of a node is an ordered list of non-blank nodes that collectively cover all non-blank descendants of the node.  The resolution of a node is effectively a depth-first, left-first enumeration of the nearest non-blank nodes below the node: The resolution of a non-blank node comprises the node itself, followed by its list of unmerged leaves, if any The resolution of a blank leaf node is the empty list The resolution of a blank intermediate node is the result of concatenating the resolution of its left child with the resolution of its right child, in that order For example, consider the following subtree, where the "_" character represents a blank node and unmerged leaves are indicated in square brackets: In this tree, we can see all of the above rules in play: The resolution of node Z is the list [Z, C] The resolution of leaf 1 is the empty list [] The resolution of top node is the list [A, Z, C] 5.1.2. The _direct path_ of a root is the empty list, and of any other node is the concatenation of that node's parent along with the parent's direct path. The _copath_ of a node is the node's sibling concatenated with the list of siblings of all the nodes in its direct path, excluding the root. The _filtered direct path_ of a leaf node L is the node's direct path, with any node removed whose child on the copath of L has an empty resolution (keeping in mind that any unmerged leaves of the copath child count toward its resolution).  The removed nodes do not need their own key pairs because encrypting to the nodes key pair would be equivalent to encrypting to its non-copath child. For example, consider the following tree (where blank nodes are indicated with "_", but also assigned a label for reference): In this tree, the direct paths, copaths, and filtered direct paths for the leaf nodes are as follows: </ins> 5.2. We generally assume that each participant maintains a complete and Note how the tree invariant applies: Each member knows only their own leaf, and the private key AB is known only to A and B.  6. 6.1. that allows group members to efficiently communicate the fresh secret keys to other group members, as described in update-paths.  <ins> A member updates the nodes along its direct path as follows: </ins> Blank all the nodes on the direct path from the leaf to the root. the nodes in the UpdatePath form a parent-hash chain from the committer's leaf to the root. <ins> For example, nodes the tree in full-tree might have been populated by full Commits from A, E, F, G, and B (in that order).  These Commits would create the following parent-hash validity relationship (where the value for a node is reset each time it is listed): Commit from A: A - U - X Commit from E: E - Y - Z - X Commit from F: F - Y - Z - X Commit from G: G - Z - X Commit from B: B - U - X After these commits, the tree will have the following parent-hash chains: A B - U - X E F - Y G - Z Since these chains collectively cover all non-blank parent nodes in the tree, the tree is parent-hash valid. Note that this tree, though valid, contains invalid parent-hash links.  If a client were checking parent hashes top-down from X, for example, they would find that Z has an invalid parent hash relative to X, but that U has valid parent hash.  Likewise, if the client were checking bottom-up, they would find that the chain from F ends in an invalid link from Y to Z.  These invalid links are the natural result of multiple clients having committed. </ins> 8.9. As described in commit, each Commit message may optionally contain an via a "sender ratchet".  Each sender has their own sender ratchet, and each step along the ratchet is called a "generation". <ins> The following figure shows a secret tree for a four-member group, with the handshake and application ratchets that member D will use for sending and the first two application keys and nonces. </ins> A sender ratchet starts from a per-sender base secret derived from a Secret Tree, as described in secret-tree.  The base secret initiates a symmetric hash ratchet which generates a sequence of keys and "application_ratchet_nonce_[L]_[j]" and "application_ratchet_key_[L]_[j]".  <ins> Concretely, consider the Secret Tree shown in secret-tree-example. Client A, B, or C would generate the illustrated values on receiving a message from D with generation equal to 1, having not received a message with generation 0 (e.g., due to out-of-order delivery).  In such a case, the following values would be consumed: The key K1 and nonce N1 used to decrypt the message The application ratchet secrets AR1 and AR0 The tree secrets D, F, G (recall that G is the "encryption_secret" for the epoch) The "epoch_secret", "commit_secret", "psk_secret", and "joiner_secret" for the current epoch Other values may be retained (not consumed): K0 and N0 for decryption of an out-of-order message with generation 0 AR2 for derivation of further message decryption keys and nonces HR0 for protection of handshake messages from D </ins>  <ins> E and C for deriving secrets used by senders A, B, and C </ins> 11.


The HPKECiphertext values are computed as where "node_public_key" is the public key of the node that the path <del> secret is being encrypted for, group_context is the current </del>  GroupContext object for the group, and the functions "SetupBaseS" and "Seal" are defined according to RFC9180. 13.2.1. <del> When creating or processing a Commit, three different ratchet trees and their associated GroupContexts are used: "Old" refers to the ratchet tree and GroupContext for the epoch before the commit.  The old GroupContext is used when signing the MLSPlainText so that existing group members can verify the signature before processing the commit. "Provisional" refers to the ratchet tree and GroupContext constructed after applying the proposals that are referenced by the Commit.  The provisional GroupContext uses the epoch number for the new epoch, and the old confirmed transcript hash.  This is used when creating the UpdatePath, if the UpdatePath is needed. "New" refers to the ratchet tree and GroupContext constructed after applying the proposals and the UpdatePath (if any).  The new GroupContext uses the epoch number for the new epoch, and the new confirmed transcript hash.  This is used when deriving the new epoch secrets, and is the only GroupContext that newly-added members will have. </del>  A member of the group creates a Commit message and the corresponding Welcome message at the same time, by taking the following steps: populated from Proposals received during the current epoch, and an empty "path" field. <del> Generate the provisional ratchet tree and GroupContext by applying the proposals referenced in the initial Commit object, as described in proposals.  Update proposals are applied first, followed by Remove proposals, and then finally Add proposals.  Add proposals are applied in the order listed in the "proposals" vector, and always to the leftmost unoccupied leaf in the tree, or the right edge of the tree if all leaves are occupied. </del>  Note that the order in which different types of proposals are applied should be updated by the implementation to include any above), then it MUST be populated.  Otherwise, the sender MAY omit the "path" field at its discretion. <del> If populating the "path" field: Create an UpdatePath using the provisional ratchet tree and GroupContext.  Any new member (from an Add proposal) MUST be excluded from the resolution during the computation of the UpdatePath.  The "leaf_node" for this UpdatePath MUST have "leaf_node_source" set to "commit".  Note that the LeafNode in the "UpdatePath" effectively updates an existing LeafNode in the group and thus MUST adhere to the same restrictions as LeafNodes used in "Update" proposals (aside from "leaf_node_source"). </del>  <del> Assign this UpdatePath to the "path" field in the Commit. </del>  <del> Apply the UpdatePath to the tree, as described in synchronizing-views-of-the-tree, creating the new ratchet tree. Define "commit_secret" as the value "path_secret[n+1]" derived from the "path_secret[n]" value assigned to the root node. </del>  If not populating the "path" field: Set the "path" field in the Commit to the null optional.  Define "commit_secret" as the all- zero vector of length "KDF.Nh" (the same length as a "path_secret" <del> value would be).  In this case, the new ratchet tree is the same as the provisional ratchet tree. </del>  Derive the "psk_secret" as specified in pre-shared-keys, where the order of PSKs in the derivation corresponds to the order of context. Use the MLSMessageContent to update the confirmed transcript <del> hash and generate the new GroupContext. </del>  Use the "init_secret" from the previous epoch, the "commit_secret" and the "psk_secret" as defined in the previous Verify that all PreSharedKey proposals in the "proposals" vector are available. <del> Generate the provisional ratchet tree and GroupContext by applying the proposals referenced in the initial Commit object, as described in proposals.  Update proposals are applied first, followed by Remove proposals, and then finally Add proposals.  Add proposals are applied in the order listed in the "proposals" vector, and always to the leftmost unoccupied leaf in the tree, or the right edge of the tree if all leaves are occupied. </del>  Note that the order in which different types of proposals are applied should be updated by the implementation to include any vector contains any Update or Remove proposals, or if it's empty. Otherwise, the "path" value MAY be omitted. <del> If the "path" value is populated: Process the "path" value using the provisional ratchet tree and GroupContext, to generate the new ratchet tree and the "commit_secret": </del>  Validate the LeafNode as specified in leaf-node-validation. The "leaf_node_source" field MUST be set to "commit". Verify that the "encryption_key" value in the LeafNode is different from the committer's current leaf node. <del> Apply the UpdatePath to the tree, as described in synchronizing-views-of-the-tree, and store "leaf_node" at the committer's leaf. </del>  <del> Verify that the LeafNode has a "parent_hash" field and that its value matches the new parent of the sender's leaf node. </del>  Define "commit_secret" as the value "path_secret[n+1]" derived from the "path_secret[n]" value assigned to the root node.

via option (3)
Bringing up the issue mentioned in the comments to : this can be inconvenient for implementations because this way the update path can't be generated and encrypted at the same time. From security perspective, I think it can't break anything, since we include strictly more in the context. On the other hand, it's not clear to me what it buys.
I sympathize with the idea that this constraints applications. MLSpp does exactly what you're talking about, updating and encrypting at the same time. But I'm not really seeing why that refactoring matters all that much. It's not any more net work, aside from maybe computing a filtered direct path twice.
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.

The HPKECiphertext values are computed as where "node_public_key" is the public key of the node that the path  <ins> secret is being encrypted for, group_context is the provisional </ins> GroupContext object for the group, and the functions "SetupBaseS" and "Seal" are defined according to RFC9180. 13.2.1.  <ins> When creating or processing a Commit, a client updates the ratchet tree and GroupContext for the group.  These values advance from an "old" state reflecting the current epoch to a "new" state reflecting the new epoch initiated by the Commit.  When the Commit includes an UpdatePath, a "provisional" group context is constructed that reflects changes due to the proposals and UpdatePath, but with the old confirmed transcript hash. </ins> A member of the group creates a Commit message and the corresponding Welcome message at the same time, by taking the following steps: populated from Proposals received during the current epoch, and an empty "path" field.  <ins> Initialize the new ratchet tree and GroupContext by applying the proposals referenced in the initial Commit object, as described in proposals.  Update proposals are applied first, followed by Remove proposals, and then finally Add proposals.  Add proposals are applied in the order listed in the "proposals" vector, and always to the leftmost unoccupied leaf in the tree, or the right edge of the tree if all leaves are occupied. </ins> Note that the order in which different types of proposals are applied should be updated by the implementation to include any above), then it MUST be populated.  Otherwise, the sender MAY omit the "path" field at its discretion.  <ins> If populating the "path" field: </ins>  <ins> If this is an external commit, assign the sender the leftmost blank leaf node in the new ratchet tree.  If there are no blank leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender. </ins>  <ins> Generate path secrets for the parent nodes along the sender's filtered direct path, as described in synchronizing-views-of- the-tree.  Define "commit_secret" as the value "path_secret[n+1]" derived from the "path_secret[n]" value assigned to the root node. Update the new ratchet tree by setting the parent nodes on the sender's filtered direct path based on the path secrets. Compute parent hashes for these nodes.  Set the sender's leaf node in the new tree to a new leaf node including the resulting parent hash for its nearest ancestor. The new leaf node MUST have "leaf_node_source" set to "commit". Since the new leaf node effectively updates an existing leaf node in the group, it MUST adhere to the same restrictions as LeafNodes used in "Update" proposals (aside from "leaf_node_source"). The application MAY specify other changes to the leaf node, e.g., providing a new signature key, updated capabilities, or different extensions. Construct a provisional GroupContext object containing the following values: "group_id": Same as the old GroupContext "epoch": The epoch number for the new epoch "tree_hash": The tree hash of the new ratchet tree "confirmed_transcript_hash": Same as the old GroupContext "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal) Create an UpdatePath that encrypts the path secrets for the nodes along the sender's filtered direct path to the new ratchet tree, using the provisional GroupContext as context. Any new member (from an Add proposal) MUST be excluded from the resolution during the computation of the UpdatePath. Assign this UpdatePath to the "path" field in the Commit. </ins> If not populating the "path" field: Set the "path" field in the Commit to the null optional.  Define "commit_secret" as the all- zero vector of length "KDF.Nh" (the same length as a "path_secret"  <ins> value would be). </ins> Derive the "psk_secret" as specified in pre-shared-keys, where the order of PSKs in the derivation corresponds to the order of context. Use the MLSMessageContent to update the confirmed transcript  <ins> hash and update the new GroupContext. </ins> Use the "init_secret" from the previous epoch, the "commit_secret" and the "psk_secret" as defined in the previous Verify that all PreSharedKey proposals in the "proposals" vector are available.  <ins> Initialize the new ratchet tree and GroupContext by applying the proposals referenced in the initial Commit object, as described in proposals.  Update proposals are applied first, followed by Remove proposals, and then finally Add proposals.  Add proposals are applied in the order listed in the "proposals" vector, and always to the leftmost unoccupied leaf in the tree, or the right edge of the tree if all leaves are occupied. </ins> Note that the order in which different types of proposals are applied should be updated by the implementation to include any vector contains any Update or Remove proposals, or if it's empty. Otherwise, the "path" value MAY be omitted.  <ins> If the "path" value is populated, validate it and apply it to the tree: If this is an external commit, assign the sender the leftmost blank leaf node in the new ratchet tree.  If there are no blank leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender. </ins> Validate the LeafNode as specified in leaf-node-validation. The "leaf_node_source" field MUST be set to "commit". Verify that the "encryption_key" value in the LeafNode is different from the committer's current leaf node.  <ins> Compute parent hashes for the parent nodes in the UpdatePath (relative to the new ratchet tree) and verify that the "parent_hash" field of the leaf node matches the parent hash for the first node its filtered direct path. </ins>  <ins> Construct a provisional GroupContext object containing the following values: "group_id": Same as the old GroupContext "epoch": The epoch number for the new epoch "tree_hash": The tree hash of the new ratchet tree "confirmed_transcript_hash": Same as the old GroupContext "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal) Apply the UpdatePath to the tree, as described in synchronizing-views-of-the-tree, using the provisional GroupContext when decrypting the path secret and storing "leaf_node" at the committer's leaf. </ins> Define "commit_secret" as the value "path_secret[n+1]" derived from the "path_secret[n]" value assigned to the root node.


message are included by hashing the MLSPlaintext in which they were sent. <del> The "confirmed_transcript_hash" is updated with an MLSMessageContent and MLSMessageAuth containing a Commit in two steps: Thus the "confirmed_transcript_hash" field in a GroupContext object represents a transcript over the whole history of MLSMessage Commit messages, up to the confirmation_tag field of the most recent Commit. The confirmation tag is then included in the transcript for the next epoch.  The interim transcript hash is computed by new members using the confirmation_tag of the GroupInfo struct, while existing members can compute it directly. As shown above, when a new group is created, the "interim_transcript_hash" field is set to the zero-length octet string. </del>  9.3.

Changes and .
The collection of structs could be less chatty. Suggest PreSharedKeys is only used once, can be inlined in GroupSecrets Use GroupContext for the internals of GroupInfo
Note: was fixed in In , we did: So only the following remain: Use GroupContext for the internals of GroupInfo
TODO at some point: Narrative text to briefly explain MLSMessageCommitContent would be useful. At the moment this appears as a struct only twice in the spec - its contents in MLSMessageContent are defined and used frequently, but MLSMessageCommitContent is not ever mentioned in discussion text. Something as simple as a narrative comment that MLSMessageCommitContent is used as a combiner for transcript hashes and will not be referred to elsewhere would be useful, giving some scoping for the introduced term.
Here's the Weekly Digest for : - - Last week 8 issues were created. Of these, 0 issues have been closed and 8 issues are still open. :greenheart: , by :greenheart: , by :greenheart: , by :greenheart: , by :greenheart: , by :greenheart: , by :greenheart: , by :greenheart: , by :speaker: , by It received 1 comments. - - Last week, 6 pull requests were created, updated or merged. Last week, 1 pull request was opened. :greenheart: , by Last week, 3 pull requests were updated. :yellowheart: , by :yellowheart: , by :yellowheart: , by Last week, 2 pull requests were merged. :purpleheart: , by :purpleheart: , by - - Last week there were 10 commits. :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by - - Last week there were 2 contributors. :bustinsilhouette: :bustin_silhouette: - - Last week there was 1 stargazer. :star: You are the star! :star2: - - Last week there were no releases. - - That's all for last week, please :eyes: Watch and :star: Star the repository to receive next weekly updates. :smiley: You can also .

message are included by hashing the MLSPlaintext in which they were sent.  <ins> The transcript hash comprises two individual hashes: A "confirmed_transcript_hash" that represents a transcript over the whole history of Commit messages, up to and including the signature of the most recent Commit. An "interim_transcript_hash" that covers the confirmed transcript hash plus the "confirmation_tag" of the most recent Commit. New members compute the interim transcript hash using the "confirmation_tag" field of the GroupInfo struct, while existing members can compute it directly. Each Commit message updates these hashes by way of its enclosing MLSMessageContentAuth.  The MLSMessageContentAuth struct is split into ConfirmedTranscriptHashInput and InterimTranscriptHashInput. The former is used to update the confirmed transcript hash and the latter to update the interim transcript hash. </ins> 9.3.


16. <del> The primary purpose of the Handshake protocol is to provide an </del>  authenticated group key exchange to clients.  In order to protect <del> Application messages sent among the members of a group, the Application secret provided by the Handshake key schedule is used to derive nonces and encryption keys for the Message Protection Layer according to the Application Key Schedule.  That is, each epoch is equipped with a fresh Application Key Schedule which consist of a tree of Application Secrets as well as one symmetric ratchet per group member. Each client maintains their own local copy of the Application Key Schedule for each epoch during which they are a group member.  They derive new keys, nonces and secrets as needed while deleting old ones as soon as they have been used. Application messages MUST be protected with the Authenticated- Encryption with Associated-Data (AEAD) encryption scheme associated with the MLS ciphersuite using the common framing mechanism.  Note that "Authenticated" in this context does not mean messages are known to be sent by a specific client but only from a legitimate member of the group.  To authenticate a message from a particular member, signatures are required.  Handshake messages MUST use asymmetric signatures to strongly authenticate the sender of a message. </del>  16.1. <del> The group members MUST use the AEAD algorithm associated with the negotiated MLS ciphersuite to AEAD encrypt and decrypt their Application messages according to the Message Framing section. The group identifier and epoch allow a recipient to know which group secrets should be used and from which Epoch secret to start computing other secrets and keys.  The sender identifier is used to identify the member's symmetric ratchet from the initial group Application secret.  The application generation field is used to determine how far into the ratchet to iterate in order to reproduce the required AEAD keys and nonce for performing decryption. Application messages SHOULD be padded to provide some resistance against traffic analysis techniques over encrypted traffic.  CLINIC HCJ16 While MLS might deliver the same payload less frequently across a lot of ciphertexts than traditional web servers, it might still provide the attacker enough information to mount an attack.  If Alice asks Bob: "When are we going to the movie ?" the answer "Wednesday" might be leaked to an adversary by the ciphertext length.  An attacker expecting Alice to answer Bob with a day of the week might find out the plaintext by correlation between the question and the length. </del>  The length of the "padding" field in "MLSCiphertextContent" can be chosen at the time of message encryption by the sender.  Senders may 16.2. <del> During each epoch senders MUST NOT encrypt more data than permitted </del>  by the security bounds of the AEAD scheme used I-D.irtf-cfrg-aead- limits. <del> Note that each change to the Group through a Handshake message will </del>  also set a new "encryption_secret".  Hence this change MUST be applied before encrypting any new application message.  This is required both to ensure that any users removed from the group can no 16.3. <del> Since each Application message contains the group identifier, the epoch and a message counter, a client can receive messages out of order.  If they are able to retrieve or recompute the correct AEAD decryption key from currently stored cryptographic material clients can decrypt these messages. For usability, MLS clients might be required to keep the AEAD key and nonce for a certain amount of time to retain the ability to decrypt delayed or out of order messages, possibly still in transit while a decryption is being done. </del>  17.

Interim 2022-05-26: Intended to be mostly editorial Leaving open for now Clear to merge in between meetings if folks are comfortable
Interim 2022-06-02: Still clear from last week
The generation numbers should be increased sequentially, gaps should be avoided to avoid unnecessary expensive operations on clients. Clients might also abort if the gap is too large to prevent attacks from malicious insiders.
This is stated in 10.1:
I suspect that what Raphael is worried about is that a malicious insider (someone in possession of the ) could make an MLSCiphertext with set to that would cause the receiver to do HKDF invocations. The attacker doesn't have to incur this cost because they don't need to generate the required key -- the ciphertext can be garbage because the key generation happens before the decryption does. It seems like we could use a paragraph at the end of covering this and the fact that if you want to allow for out-of-order messages, you need to keep the intermediate keys. Since big gaps would force you to store the keys as well as doing the hashing. A figure here might also be helpful.
Couple of friendly amendments, otherwise lgtm. I am going to hold this as slightly more than editorial, given the number of MUSTs that are affected. But I would note for others that the MUSTs that are removed here are all redundant with requirements above.Thanks for picking this up! Just one nit, but looks good otherwise.

16.  <ins> The primary purpose of handshake messages are to provide an </ins> authenticated group key exchange to clients.  In order to protect  <ins> application messages sent among the members of a group, the "encryption_secret" provided by the key schedule is used to derive a sequence of nonces and keys for message encryption.  Every epoch moves the key schedule forward which triggers the creation of a new secret tree, as described in secret-tree, along with a new set of symmetric ratchets of nonces and keys for each member. Each client maintains their own local copy of the key schedule for each epoch during which they are a group member.  They derive new keys, nonces, and secrets as needed while deleting old ones as soon as they have been used. The group identifier and epoch allow a recipient to know which group secrets should be used and from which "epoch_secret" to start computing other secrets.  The sender identifier and content type is used to identify which symmetric ratchet to use from the secret tree. The "generation" counter determines how far into the ratchet to iterate in order to produce the required nonce and key for encryption or decryption. </ins> 16.1.  <ins> Application messages MAY be padded to provide some resistance against traffic analysis techniques over encrypted traffic CLINIC HCJ16. While MLS might deliver the same payload less frequently across a lot of ciphertexts than traditional web servers, it might still provide the attacker enough information to mount an attack.  If Alice asks Bob "When are we going to the movie?", then the answer "Wednesday" could be leaked to an adversary solely by the ciphertext length. </ins> The length of the "padding" field in "MLSCiphertextContent" can be chosen at the time of message encryption by the sender.  Senders may 16.2.  <ins> During each epoch, senders MUST NOT encrypt more data than permitted </ins> by the security bounds of the AEAD scheme used I-D.irtf-cfrg-aead- limits.  <ins> Note that each change to the group through a handshake message will </ins> also set a new "encryption_secret".  Hence this change MUST be applied before encrypting any new application message.  This is required both to ensure that any users removed from the group can no 16.3.  <ins> Since each application message contains the group identifier, the epoch, and a generation counter, a client can receive messages out of order.  When messages are received out of order, the client moves the sender ratchet forward to match the received generation counter.  Any unused nonce and key pairs from the ratchet are potentially stored so that they can be used to decrypt the messages which were delayed or reordered. Applications SHOULD define a policy on how long to keep unused nonce and key pairs for a sender, and the maximum number to keep.  This is in addition to ensuring that these nonce and key pairs are promptly deleted when the epoch ends.  Applications SHOULD also define a policy limiting the maximum number of steps that clients will move the ratchet forward in response to a new message.  Messages received with a generation counter that's too much higher than the last message received would then be rejected.  This avoids causing a denial-of-service attack by requiring the recipient to perform an excessive number of key derivations.  For example, a malicious group member could send a message with "generation = 0xffffffff" at the beginning of a new epoch, forcing recipients to perform billions of key derivations. </ins> 17.


those devices' clients could all present the user's application-layer identifiers. <del> If needed, applications may add application-specific unique identifiers to the "extensions" field of KeyPackage or LeafNode objects. </del>  7. Verify that the value of "leaf_node.encryption_key" is different from the value of the "init_key" field. <del> 11.2. Within MLS, a KeyPackage is identified by its hash (see, e.g., joining-via-welcome-message).  The "external_key_id" extension allows applications to add an explicit, application-defined identifier to a KeyPackage. </del> 12. A group is always created with a single member, the "creator".  The

At first I was going to make an extension for both KeyPackages and LeafNodes. But I was thinking and couldn't come up with a reason for KeyPackages to need this extension, so did just LeafNode.
Interim 2022-05-26: Agreement to merge is getting used a lot, so maybe or ? NAME to change to
What's the use case for this?
Mentioned at the last interim, but the use-case for this feature is either: giving applications an easier way to lookup the private key corresponding to their LeafNode giving applications a place to put client UUIDs

those devices' clients could all present the user's application-layer identifiers.  <ins> If needed, applications may add application-specific identifiers to the "extensions" field of a LeafNode object with the "application_id" extension. However, applications SHOULD NOT rely on the data in an "application_id" extension as if it were authenticated by the Authentication Service, and SHOULD gracefully handle cases where the identifier presented is not unique. </ins> 7. Verify that the value of "leaf_node.encryption_key" is different from the value of the "init_key" field.  12. A group is always created with a single member, the "creator".  The


An Add proposal requests that a client with a specified KeyPackage be added to the group. <del> An Add proposal is invalid if any of the following is true: The KeyPackage is invalid according to keypackage-validation. The Credential in the KeyPackage represents a client already in the group according to the application. </del>  An Add is applied after being included in a Commit message.  The position of the Add in the list of proposals determines the leaf node It contains multiple Add proposals that contain KeyPackages that represent the same client according to the application (for <del> example, identical KeyPackages or KeyPackages sharing the same Credential). </del>  It contains multiple PreSharedKey proposals that reference the same PreSharedKeyID.

In , it was pointed out that we previously had a clause: But this was amplified in to say that we also can't send Add proposals for users that are already in the group. I think we should talk about if this is the actual desired behavior or not.
I agree with Brendan. Specifically I want to make sure an MLS client can replace itself. My other concern is that because we have no guidance yet on identity, we need to make sure that multiple instances/clients of the same user are allowed.
Interim 2022-05-26: We want to: Avoid having multiple appearances of the same device Allow a device to resync itself Text at issue here is "An Add proposal is invalid if ... The Credential in the KeyPackage represents a client already in the group according to the application" It sounds like changing to "... The KeyPackage represents a client already in the group according to the application" would address the immediate concern NAME to write a PR

An Add proposal requests that a client with a specified KeyPackage be added to the group.  <ins> An Add proposal is invalid if the KeyPackage is invalid according to keypackage-validation. </ins> An Add is applied after being included in a Commit message.  The position of the Add in the list of proposals determines the leaf node It contains multiple Add proposals that contain KeyPackages that represent the same client according to the application (for  <ins> example, identical signature keys). It contains an Add proposal with a KeyPackage that represents a client already in the group according to the application, unless there is a Remove proposal in the list removing the matching client from the group. </ins> It contains multiple PreSharedKey proposals that reference the same PreSharedKeyID.


Generate path secrets for the parent nodes along the sender's filtered direct path, as described in synchronizing-views-of- the-tree.  Define "commit_secret" as the value <del> "path_secret[n+1]" derived from the "path_secret[n]" value assigned to the root node. </del>  Update the new ratchet tree by setting the parent nodes on the sender's filtered direct path based on the path secrets. "leaf_node" at the committer's leaf. Define "commit_secret" as the value "path_secret[n+1]" derived <del> from the "path_secret[n]" value assigned to the root node. </del>  If the "path" value is not populated: Define "commit_secret" as the all-zero vector of length "KDF.Nh" (the same length as a

One argument for the other option (i.e. force the root to be in the filtered updatepath): it gives a parent-hash chain from the comitting leaf to up to the root, hence the whole tree is bound in the leaf signature (assuming ). It ensures you can't modify the empty subtree. I don't think that's a huge deal, because you can always do modifications like this: given a tree T1, you can create tree T2 and a new root R like this (with the parent-hash link of R coming from T2): But it's worth noting that this change is not purely a TreeKEM change.
Interim 2022-06-02 As NAME notes in his comments, tree malleability is not worth blocking this for because you won't block the tree-extension attack anyway NAME notes the exception case where the UpdatePath has no nodes in it NAME suggests we should just forbid that case NAME will file a new PR to forbid that case
As I was pondering pathological cases related to , it occurred to me that we already have a slightly odd situation that can arise as a result of the suppression of redundant nodes. When a Commit has an UpdatePath, the is defined as follows: However, there are situations where the root node is not assigned a value, namely when the left half of the tree is blank. For example, suppose A creates a group with B, C, D: Suppose that then C removes A and B, leaving C and D as the only leaves in the tree. After removing A and B, the tree on which C computes an UpdatePath is as follows: C's direct path is (Z, Y), but it's filtered direct path is just (Z) because the copath subtree of Y is all blank. So C will not generate a pathsecret for Y, which means that the commit secret is not really defined. It seems like there are two paths to fix this: Keep the current definition of and require that a path secret always be generated for the root node Presumably the root node path secret would be generated directly from the path secret in the UpdatePath This would be simple to implement by just forcing the root node to be in the filtered direct path Keep the current path secret generation and specify that the is derived from the last path secret in the UpdatePath In the above example, the last path secret from the path secret for Z I lean slightly toward (2) because it makes the computation the same for two neighbors regardless of where they are in the tree. It seems like the two options primarily differ in whether there are one or two invocations of the key derivation.
Interim 2022-05-26: NAME supports doing "end of UpdatePath plus one" NAME to write up a PR
First, both fixes are secure. I lean towards "end of path plus one", because it matches better my mental model: a) if any node (including root) has a child with a blank subtree, then it's blank, b) we generate from the last secret generated by the committer. NAME while you're at it, you could also address the empty-UpdatePath ambiguity: If a member is alone in the group and sends a commit that doesn't add anyone, his UpdatePath is empty and he doesn't know how to derive . This can happen even without filtered paths, if the tree has exactly 1 node. There are 2 solutions: Pick a random secret Disallow such commits Regarding 2, does anyone know any reason we may want such commits?

Generate path secrets for the parent nodes along the sender's filtered direct path, as described in synchronizing-views-of- the-tree.  Define "commit_secret" as the value  <ins> "path_secret[n+1]" derived from the last path secret value ("path_secret[n]") derived for the UpdatePath. </ins> Update the new ratchet tree by setting the parent nodes on the sender's filtered direct path based on the path secrets. "leaf_node" at the committer's leaf. Define "commit_secret" as the value "path_secret[n+1]" derived  <ins> from the last path secret value ("path_secret[n]") derived for the UpdatePath. </ins> If the "path" value is not populated: Define "commit_secret" as the all-zero vector of length "KDF.Nh" (the same length as a


Removed ART (*)  draft-01 Initial description of the Message Protection mechanism. (*) Verify that the "prior_epoch" field of the Handshake message is equal the "epoch" field of the current GroupState object. <del> Use the "operation" message to produce an updated GroupState object incorporating the proposed changes. </del>  Look up the public key for slot index "signer_index" from the roster in the current GroupState object (before the update). If the signature fails to verify, discard the updated GroupState object and consider the Handshake message invalid. <del> If the signature verifies successfully, consider the updated </del>  GroupState object as the current state of the group.  [[ OPEN ISSUE: The Add and Remove operations create a "double-join" situation, where a participants leaf key is also known to another participant.  When a participant A is double-joined to another B,

At the MLS interim, the major feedback regarding authentication was that it would be good practice to provide explicit key confirmation. This PR adds an explicit confirmation field to Handshake messages, containing a MAC over the proposed (updated) state of the group, together with the latest handshake message.
Reflecting on offline conversation here: This PR derives a new key, then calculates the MAC over both the HS message and the group state; that others verify. PR 72 derives a new key, then sends that to the others to verify they all got the same key. We assume that, as the key was derived from a tree with this group state, it must be correct. These appear to test basically the same property, but I probably prefer this PR unless we can formally verify that they have the same properties (or at least the relevant ones); in which case 72 is a little more efficient.
So one question would be under what circumstances would this verify something that won't be verified by failing AEAD on any associated messages that decrypt? Or is it more just that we've now decoupled updates and message sending, so without a failing AEAD we need this to confirm too?
Thanks, NAME Absolutely agree that this needs more discussion. I'll send a ping on the list shortly.
NAME To answer your question, yes, the idea here is to fail before you get to the AEAD stage. In the spec before this PR merged, group members that arrived at different states would have different keys, but they would never find out that was the case until there was an AEAD failure. With this PR (or ), a peer know that if processing of a Handshake message succeeds, then it has the same GroupState as the sender.
Agreed that we need further discussion with academics, and modelling of the properties, but this seems good to provisionally merge.

Removed ART (*) <ins> Added explicit key confirmation (*) </ins> draft-01 Initial description of the Message Protection mechanism. (*) Verify that the "prior_epoch" field of the Handshake message is equal the "epoch" field of the current GroupState object.  <ins> Use the "operation" message to produce an updated, provisional GroupState object incorporating the proposed changes. </ins> Look up the public key for slot index "signer_index" from the roster in the current GroupState object (before the update). If the signature fails to verify, discard the updated GroupState object and consider the Handshake message invalid.  <ins> Use the "confirmation_key" for the new group state to compute the finished MAC for this message, as described below, and verify that it is the same as the "finished_mac" field. If the the above checks are successful, consider the updated </ins> GroupState object as the current state of the group. <ins> The "finished_mac" value is computed over the provisional group state and the current handshake message (with the confirmation value set to zero): HMAC RFC2104 uses the Hash algorithm for the ciphersuite in use. </ins> [[ OPEN ISSUE: The Add and Remove operations create a "double-join" situation, where a participants leaf key is also known to another participant.  When a participant A is double-joined to another B,


path secret to each public key in the resolution of the non-updated child. <del> The recipient of an UpdatePath processes it with the following steps: </del>  <del> Compute the updated path secrets. </del>  <del> Identify a node in the filtered direct path for which the recipient is in the subtree of the non-updated child. </del>  <del> Identify a node in the resolution of the copath node for which the recipient has a private key. </del>  <del> Decrypt the path secret for the parent of the copath node using the private key from the resolution node. </del>  <del> Derive path secrets for ancestors of that node using the algorithm described above. </del>  <del> The recipient SHOULD verify that the received public keys agree with the public keys derived from the new path_secret values. </del>  <del> Merge the updated path secrets into the tree. </del>  <del> Blank all nodes on the direct path of the sender's leaf. </del>  <del> For all nodes on the filtered direct path of the sender's leaf, </del>  <del> Set the public key to the received public key. </del>  <del> Set the list of unmerged leaves to the empty list. </del>  <del> Store the updated hash of the next node on the filtered direct path (represented as a ParentNode struct), going from root to leaf, so that each hash incorporates all the non- blank nodes above it.  The root node always has a zero- length hash for this value. </del>  <del> For nodes where a path secret was recovered in step 1 ("Compute the updated path secrets"), compute and store the node's updated private key. </del>  For example, in order to communicate the example update described in <del> ratchet-tree-evolution, the sender would transmit the following values: </del>  In this table, the value node_pub[i] represents the public key derived from node_secret[i], pk(X) represents the current public key of node X, and E(K, S) represents the public-key encryption of the path secret S to the public key K (using HPKE).  After processing the update, each recipient MUST delete outdated key material, specifically: <del> The path secrets used to derive each updated node key pair. </del>  Each outdated node key pair that was replaced by the update. leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender. <del> Generate path secrets for the parent nodes along the sender's filtered direct path, as described in synchronizing-views-of- the-tree.  Define "commit_secret" as the value "path_secret[n+1]" derived from the last path secret value ("path_secret[n]") derived for the UpdatePath. Update the new ratchet tree by setting the parent nodes on the sender's filtered direct path based on the path secrets. Compute parent hashes for these nodes.  Set the sender's leaf node in the new tree to a new leaf node including the resulting parent hash for its nearest ancestor. The new leaf node MUST have "leaf_node_source" set to "commit". Since the new leaf node effectively updates an existing leaf node in the group, it MUST adhere to the same restrictions as LeafNodes used in "Update" proposals (aside from "leaf_node_source"). The application MAY specify other changes to the leaf node, e.g., providing a new signature key, updated capabilities, or different extensions. </del>  Construct a provisional GroupContext object containing the following values: "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal) <del> Create an UpdatePath that encrypts the path secrets for the nodes along the sender's filtered direct path to the new ratchet tree, using the provisional GroupContext as context. Any new member (from an Add proposal) MUST be excluded from the resolution during the computation of the UpdatePath. </del>  <del> Assign this UpdatePath to the "path" field in the Commit. </del>  If not populating the "path" field: Set the "path" field in the Commit to the null optional.  Define "commit_secret" as the all- Verify that the "encryption_key" value in the LeafNode is different from the committer's current leaf node. <del> Compute parent hashes for the parent nodes in the UpdatePath (relative to the new ratchet tree) and verify that the "parent_hash" field of the leaf node matches the parent hash for the first node its filtered direct path. </del>  Construct a provisional GroupContext object containing the following values: "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal) <del> Apply the UpdatePath to the tree, as described in </del>  synchronizing-views-of-the-tree, using the provisional <del> GroupContext when decrypting the path secret and storing "leaf_node" at the committer's leaf. </del>  Define "commit_secret" as the value "path_secret[n+1]" derived from the last path secret value ("path_secret[n]") derived for

Conflicts with (in derivation of ) This is intended just to move around the required steps and give them clearer labels. Please keep an eye out for things that might have accidentally been added or dropped.
Interim 2022-06-02: NAME to review, then clear to merge
Currently, we only have explicit algorithm steps for validating an UpdatePath. We should have a similar level of concreteness for the process of creating an UpdatePath.

path secret to each public key in the resolution of the non-updated child.  <ins> A member of the group _updates their direct path_ by computing new values for their leaf node and the nodes along their filtered direct path: </ins>  <ins> Blank all nodes along the direct path of the sender's leaf. </ins>  <ins> Compute updated path secrets and public keys for the nodes on the sender's filtered direct path. </ins>  <ins> Generate a sequence of path secrets of the same length as the filtered direct path, as defined in </ins>  <ins> For each node in the filtered direct path, replace the node's public key with the "node_pub[n]" value derived from the corresponding path secret "path_secret[n]". </ins>  <ins> Compute the new parent hashes for the nodes along the filtered direct path and the sender's leaf node. </ins>  <ins> Update the leaf node for the sender. </ins>  <ins> Set the "leaf_node_source" to "commit". </ins>  <ins> Set the "encryption_key" to the public key of a freshly sampled key pair </ins>  <ins> Set the parent hash to the parent hash for the leaf. </ins>  <ins> Re-sign the leaf node with its new contents </ins>  <ins> Since the new leaf node effectively updates an existing leaf node in the group, it MUST adhere to the same restrictions as LeafNodes used in "Update" proposals (aside from "leaf_node_source").  The application MAY specify other changes to the leaf node, e.g., providing a new signature key, updated capabilities, or different extensions. </ins>  <ins> The member then _encrypts path secrets to the group_.  For each node in the member's filtered direct path, the member takes the following steps: Compute the resolution of the node's child that is on the copath of the sender (the child that is not in the direct path of the sender).  Any new member (from an Add proposal) added in the same Commit MUST be excluded from this resolution. For each node in the resolution, encrypt the path secret for the direct path node using the public key of the resolution node, as defined in The recipient of an UpdatePath performs the corresponding steps. First, the recipient _merges UpdatePath into the tree_: Blank all nodes on the direct path of the sender's leaf. For all nodes on the filtered direct path of the sender's leaf, Set the public key to the public key in the UpdatePath. Set the list of unmerged leaves to the empty list. Compute parent hashes for the nodes in the sender's filtered direct path, and verify that the "parent_hash" field of the leaf node matches the parent hash for the first node in its filtered direct path. Note that these hashes are computed from root to leaf, so that each hash incorporates all the non-blank nodes above it.  The root node always has a zero-length hash for its parent hash. Second, the recipient _decrypts the path secrets_: Identify a node in the filtered direct path for which the recipient is in the subtree of the non-updated child. </ins>  <ins> Identify a node in the resolution of the copath node for which the recipient has a private key. Decrypt the path secret for the parent of the copath node using the private key from the resolution node. Derive path secrets for ancestors of that node in the sender's filtered direct path using the algorithm described above. Derive the node secrets and node key pairs from the path secrets. Verify that the derived public keys are the same as the corresponding public keys sent in the UpdatePath. Store the derived private keys in the corresponding ratchet tree nodes. </ins> For example, in order to communicate the example update described in  <ins> ratchet-tree-evolution, the member at node B would transmit the following values: </ins> In this table, the value node_pub[i] represents the public key derived from node_secret[i], pk(X) represents the current public key of node X, and E(K, S) represents the public-key encryption of the path secret S to the public key K (using HPKE). <ins> A recipient at node A would decrypt "E(pk(A), path_secret[0])" to obtain "path_secret[0]", then use it to derive "path_secret[1]" and the resulting node secrets and key pairs.  Thus A would have the private keys to nodes X' and Y', in accordance with the tree invariant. Similarly, a recipient at node D would decrypt "E(pk(Z), path_secret[1])" to obtain "path_secret[1]", then use it to derive the node secret and and key pair for the node Y'.  As required to maintain the tree invariant, node D does not receive the private key for the node X', since X' is not an ancestor of D. </ins> After processing the update, each recipient MUST delete outdated key material, specifically:  <ins> The path secrets and node secrets used to derive each updated node key pair. </ins> Each outdated node key pair that was replaced by the update. leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender.  <ins> Update the sender's direct path in the ratchet tree as described in synchronizing-views-of-the-tree.  Define "commit_secret" as the value "path_secret[n+1]" derived from the last path secret value ("path_secret[n]") derived for the UpdatePath. </ins> Construct a provisional GroupContext object containing the following values: "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal)  <ins> Encrypt the path secrets resulting from the tree update to the group as described in synchronizing-views-of-the-tree, using the provisional group context as the context for HPKE encryption. </ins>  <ins> Create an UpdatePath containing the sender's new leaf node and the new public keys and encrypted path secrets along the sender's filtered direct path.  Assign this UpdatePath to the "path" field in the Commit. </ins> If not populating the "path" field: Set the "path" field in the Commit to the null optional.  Define "commit_secret" as the all- Verify that the "encryption_key" value in the LeafNode is different from the committer's current leaf node.  <ins> Merge the UpdatePath into the new ratchet tree as described in synchronizing-views-of-the-tree. </ins> Construct a provisional GroupContext object containing the following values: "extensions": The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal)  <ins> Decrypt the path secrets for UpdatePath as described in </ins> synchronizing-views-of-the-tree, using the provisional  <ins> GroupContext as the context for HPKE decryption. </ins> Define "commit_secret" as the value "path_secret[n+1]" derived from the last path secret value ("path_secret[n]") derived for


The "parent_hash" field contains a hash of this node's parent node, as described in parent-hash.  The "unmerged_leaves" field lists the leaves under this parent node that are unmerged, according to their <del> indices among all the leaves in the tree. </del>  8.2.

While reviewing , I realized that we have a potential interop problem around . This struct feeds into the tree hash, so everyone in the group needs to have the entries in the same order or else they'll disagree on the tree hash. But the order isn't specified anywhere: All the document says right now is "add L's leaf index to the unmergedleaves list", which could be implemented as an insert anywhere in the list. This PR resolves the ambiguity by requiring the list be sorted.
There's a few implications of simply allowing forks: FS issues - clients would need to use initsecret twice, which means keeping around initsecret for a potential fork, which breaks FS. One resolution here is to use something from the ASTree for initsecret (maybe the nth app secret, or a new ratchet derived from astreenode[n]secret). Does detection yield any sort of meaningful resolution technique? Are we better off than we are now in terms of user experience? (What we have now, if the DS fails to order, is two (or N) groups of clients operating on different versions of the tree, which effectively partitions the group) I wrote about this here: URL suggesting merges as a resolution technique. The main downside is clients would need to keep around the list of proposals for a while. This isn't bad imo, it reduces complexity to some extent. I'm somewhat in favour of a non-pseudorandom tiebreaker if there's no other arguments against it. How many bits of sequence number? Have we considered using varints? Similar to the CTLS proposal - URL Other forking considerations, I think there's more involved than just changing the epoch identifier: We'll need merges as well, since forks can lead to inconsistent group membership: Suppose in epoch 1 a group has A, B, and C A removes C in epoch 2A B adds D in epoch 2B We now have two group states 2A with AB and 2B with ABCD - and it's unclear which group to use to send with. We could mandate that any client MUST send a "merge" commit on it's view of history before sending any message. This commit could just be the list of epochs that are being merged (+a usual DirectPath update?), and processing it would mean replaying all proposals up to the common ancestor for those epochs on the common ancestor. Forward secrecy issues. By allowing forks, devices must now keep the previous epoch around to derive possible future forks - but this means we'll be re-using initsecret. A possible solution could be to use the nth app secret from the ASTree instead of initsecret as the start for the new epoch secret. This ties nicely with using a non-pseudorandom tiebreaker. Ordering implications It's unclear what a sensible ordering strategy is for the DS if we allow forking. (simply ensuring that each commit is based on the "current" epoch isn't well defined since there can be multiple "current" epochs). By allowing forks the DS need not enforce ordering at all imo. This means that apps need to keep around old epochs, but this is already required per (2). A different question is how many we need to keep around - perhaps a TTL or the past N epochs after a topological sort would make sense. Generally I think getting rid of server ordering is good idea: Helps Matrix and general federation use cases A common consistent mutated state for group operations can cause server side DB contention in my experience Forks is one way to remove ordering, and if we need to support it anyways it's a nice benefit. Richard Barnes wrote: >

The "parent_hash" field contains a hash of this node's parent node, as described in parent-hash.  The "unmerged_leaves" field lists the leaves under this parent node that are unmerged, according to their  <ins> indices among all the leaves in the tree.  The entries in the "unmerged_leaves" vector MUST be sorted in increasing order. </ins> 8.2.


An ordered list of "unmerged" leaves (see views) A hash of certain information about the node's parent, as of the <del> last time the node was changed (see parent-hash). </del>  Every node, regardless of whether the node is blank or populated, has a corresponding _hash_ that summarizes the contents of the subtree The "encryption_key" field contains an HPKE public key whose private key is held only by the members at the leaves among its descendants. The "parent_hash" field contains a hash of this node's parent node, <del> as described in parent-hash.  The "unmerged_leaves" field lists the </del>  leaves under this parent node that are unmerged, according to their indices among all the leaves in the tree.  The entries in the "unmerged_leaves" vector MUST be sorted in increasing order. In the case where the leaf node was inserted into the tree via a Commit message, the "parent_hash" field contains the parent hash for <del> this leaf node (see parent-hash). </del>  The LeafNodeTBS structure covers the fields above the signature in the LeafNode.  In addition, when the leaf node was created in the 8.7. <del> To allow group members to verify that they agree on the public cryptographic state of the group, this section defines a scheme for generating a hash value (called the "tree hash") that represents the contents of the group's ratchet tree and the members' leaf nodes. </del>  The tree hash of an individual node is the hash of the node's "TreeHashInput" object, which may contain either a of node.  "LeafNodeHashInput" objects contain the "leaf_index" and the "LeafNode" (if any).  "ParentNodeHashInput" objects contain the "ParentNode" (if any) and the tree hash of the node's left and right <del> children. </del>  The tree hash of an entire tree corresponds to the tree hash of the root node, which is computed recursively by starting at the leaf 8.8. <del> The "parent_hash" field in ratchet tree nodes carries information to authenticate the information in the ratchet tree.  Parent hashes chain together so that the signature on a leaf node, by covering the leaf node's parent hash, indirectly includes information about the structure of the tree at the time the leaf node was last updated. Consider a ratchet tree with a non-blank parent node P and children V and S. </del>  The parent hash of P changes whenever an "UpdatePath" object is <del> applied to the ratchet tree along a path from a leaf U traversing node V (and hence also P).  The new "Parent hash of P (with copath </del>  child S)" is obtained by hashing P's "ParentHashInput" struct. The field "encryption_key" contains the HPKE public key of P.  If P is the root, then the "parent_hash" field is set to a zero-length octet string.  Otherwise, "parent_hash" is the Parent Hash of the <del> next node after P on the filtered direct path of U.  This way, P's Parent Hash fixes the new HPKE public key of each node V on the path from P to the root.  Note that the path from P to the root may contain some blank nodes that are not fixed by P's Parent Hash. However, for each node that has an HPKE key, this key is fixed by P's Parent Hash. </del>  Finally, "original_sibling_tree_hash" is the tree hash of S in the ratchet tree modified as follows: For each leaf L in updates of P.  This property is crucial for the correctness of the protocol.  For example, in the following tree: With P = W and S = Y, "original_sibling_tree_hash" is the tree hash 8.8.1. <del> The Parent Hash of P appears in three types of structs.  If V is itself a parent node then P's Parent Hash is stored in the "parent_hash" field of the structs "ParentHashInput" and "ParentNode" of the node before P on the filtered direct path of U.  (The "ParentNode" struct is used to encapsulate all public information about that node that must be conveyed to a new member joining the group as well as to define its Tree Hash.) If, on the other hand, V is the leaf U and its LeafNode has "leaf_node_source" set to "commit", then the Parent Hash of P (with V's sibling as copath child) is stored in the "parent_hash" field. This is true in particular of the LeafNode object sent in the "leaf_node" field of an UpdatePath.  The signature of such a LeafNode thus also attests to which keys the group member introduced into the ratchet tree and to whom the corresponding secret keys were sent. This helps prevent malicious insiders from constructing artificial ratchet trees with a node V whose HPKE secret key is known to the insider yet where the insider isn't assigned a leaf in the subtree rooted at V.  Indeed, such a ratchet tree would violate the tree invariant. </del>  8.8.2. Parent hashes are verified at two points in the protocol: When joining a group and when processing a Commit. <del> The parent hash in a node U is valid with respect to a parent node P </del>  if the following criteria hold: <del> U is a descendant of P in the tree </del>  <del> The nodes between U and P in the tree are all blank </del>  <del> The "parent_hash" field of U is equal to the parent hash of P with </del>  copath child S, where S is the child of P that is not on the path <del> from U to P. </del>  A parent node P is "parent-hash valid" if it can be chained back to a leaf node in this way.  That is, if there is leaf node L and a the nodes in the UpdatePath form a parent-hash chain from the committer's leaf to the root. <del> For example, nodes the tree in full-tree might have been populated by full Commits from A, E, F, G, and B (in that order).  These Commits would create the following parent-hash validity relationship (where the value for a node is reset each time it is listed): Commit from A: A - U - X Commit from E: E - Y - Z - X Commit from F: F - Y - Z - X Commit from G: G - Z - X Commit from B: B - U - X After these commits, the tree will have the following parent-hash chains: A B - U - X E F - Y G - Z Since these chains collectively cover all non-blank parent nodes in the tree, the tree is parent-hash valid. Note that this tree, though valid, contains invalid parent-hash links.  If a client were checking parent hashes top-down from X, for example, they would find that Z has an invalid parent hash relative to X, but that U has valid parent hash.  Likewise, if the client were checking bottom-up, they would find that the chain from F ends in an invalid link from Y to Z.  These invalid links are the natural result of multiple clients having committed. </del> 8.9. As described in commit, each Commit message may optionally contain an

Depends on Conflicts with The diff here is messy because it includes . The biggest change is the extended example, for which you can just scroll to the bottom.
Regarding the Tree Hash Including the parentnode in the tree hash which is supposed to be computed from the leaves, makes the definition appear circular to a new reader. Since the situation when you have blank nodes in the middle of a tree but not at the root is a rather complicated, the situation when you could calculate a tree hash using the parentnode is non-obvious. In the description of Tree Hash the document says that the ParentNode object is optional "depending on whether the node is blank or not." This isn't clear specification language. Saying the parentnode object is present if the node is blank or present if the node is not blank would be clear. What if the parent node is also blank? What if the root is blank? I think an example with and without blank nodes would be appropriate. Explaining the reason why the parentnode is included will help the reader establish a better mental model. Regarding the Parent Hash I still find this section super confusing. A few points Using a tree hash (siblingtreehash) inside the parent hash is non-obvious. There should be some explanation about what property this is providing and that it is a tree hash and not a parent hash. (Honestly it was the third time I read the section that I realized that it was really the tree hash and not the parent hash of the sibling.) Using P, V, and S for Parent, target node, and sibling node is introduced here and never used anywhere else in the document. P for parent and S for sibling make sense. I can't tell what V is supposed to stand for, and it unfortunately could cause people to confuse the vector notation used throughout, but shortly after introducing V for the target node. I propose using "N" for node or "X" instead. There is one reference to the filtered direct path of U. Perhaps this is V, or a child of V? The calculations are sufficiently complicated that I think this warrants a complete, non-trivial, worked example (possibly in an appendix). At some point we should consider providing test vectors in a future document, and this would be an ideal candidate.
Regarding the Tree Hash To be clear, the that is included in the tree hash is the node whose subtree is being hashed. The tree hash doesn't include only the leaves, it includes all the nodes in the tree. If you mean , then yes, I agree the connection is a little subtle. I've tried to explain this a little better. Updated this to be clear that the ParentNode is omitted if and only if the node is blank. Not clear what situation you mean here. I think the specific computations are pretty clearly specified for these cases. Something like this was added in : URL I extended the example and put it in an appendix. Assuming you mean here, hopefully this is addressed by the above notes. Added a note to clarify that this is the tree hash and why. Changed to "D" for "direct path", and explained the semantics. Actually it's the leaf, which is now called "L". I think the example linked above does a pretty good job. Feedback obviously welcome. Agreed that a test vectors document will be a useful follow-on.
This is a huge improvement. While I still prefer "path hash" to "parent hash", the new language in this PR is clear and I think will be straightforward for new implementers to read.

An ordered list of "unmerged" leaves (see views) A hash of certain information about the node's parent, as of the  <ins> last time the node was changed (see parent-hashes). </ins> Every node, regardless of whether the node is blank or populated, has a corresponding _hash_ that summarizes the contents of the subtree The "encryption_key" field contains an HPKE public key whose private key is held only by the members at the leaves among its descendants. The "parent_hash" field contains a hash of this node's parent node,  <ins> as described in parent-hashes.  The "unmerged_leaves" field lists the </ins> leaves under this parent node that are unmerged, according to their indices among all the leaves in the tree.  The entries in the "unmerged_leaves" vector MUST be sorted in increasing order. In the case where the leaf node was inserted into the tree via a Commit message, the "parent_hash" field contains the parent hash for  <ins> this leaf node (see parent-hashes). </ins> The LeafNodeTBS structure covers the fields above the signature in the LeafNode.  In addition, when the leaf node was created in the 8.7.  <ins> MLS hashes the contents of the tree in two ways to authenticate different properties of the tree.  This section defines _tree hashes_, and _parent hashes_ are defined in parent-hashes. Each node in a ratchet tree has a tree hash that summarizes the subtree below that node.  The tree hash of the root is used in the GroupContext to confirm that the group agrees on the whole tree. Tree hashes are computed recursively from the leaves up to the root. </ins> The tree hash of an individual node is the hash of the node's "TreeHashInput" object, which may contain either a of node.  "LeafNodeHashInput" objects contain the "leaf_index" and the "LeafNode" (if any).  "ParentNodeHashInput" objects contain the "ParentNode" (if any) and the tree hash of the node's left and right  <ins> children.  For both parent and leaf nodes, the optional node value MUST be absent if the node is blank and present if the node contains a value. </ins> The tree hash of an entire tree corresponds to the tree hash of the root node, which is computed recursively by starting at the leaf 8.8.  <ins> While tree hashes summarize the state of a tree at point in time, parent hashes capture information about how keys in the tree were populated. When a client sends a commit to change a group, it can include an UpdatePath to assign new keys to the nodes along its filtered direct path.  When a client computes an UpdatePath (as defined in synchronizing-views-of-the-tree), it computes and signs a parent hash that summarizes the state of the tree after the UpdatePath has been applied.  These summaries are constructed in a chain from the root to the member's leaf so that the part of the chain closer to the root can be overwritten as nodes set in one UpdatePath are reset by a later UpdatePath. As a result, the signature over the parent hash in each member's leaf effectively signs the subtree of the tree that hasn't been changed since that leaf was last changed in an UpdatePath.  A new member joining the group uses these parent hashes to verify that the parent nodes in the tree were set by members of the group, not chosen by an external attacker.  For an example of how this works, see ph- evolution. Consider a ratchet tree with a non-blank parent node P and children D and S (for "parent", "direct path", and "sibling"), with D and P in the direct path of a leaf node L (for "leaf"): </ins> The parent hash of P changes whenever an "UpdatePath" object is  <ins> applied to the ratchet tree along a path from a leaf L traversing node D (and hence also P).  The new "Parent hash of P (with copath </ins> child S)" is obtained by hashing P's "ParentHashInput" struct. The field "encryption_key" contains the HPKE public key of P.  If P is the root, then the "parent_hash" field is set to a zero-length octet string.  Otherwise, "parent_hash" is the Parent Hash of the  <ins> next node after P on the filtered direct path of the leaf L.  This way, P's Parent Hash fixes the new HPKE public key of each non-blank node on the path from P to the root.  Note that the path from P to the root may contain some blank nodes that are not fixed by P's Parent Hash.  However, for each node that has an HPKE key, this key is fixed by P's Parent Hash. </ins> Finally, "original_sibling_tree_hash" is the tree hash of S in the ratchet tree modified as follows: For each leaf L in updates of P.  This property is crucial for the correctness of the protocol. <ins> Note that "original_sibling_tree_hash" is the tree hash of S, not the parent hash.  The "parent_hash" field in ParentHashInput captures information about the nodes above P. the "original_sibling_tree_hash" captures information about the subtree under S that is not being updated (and thus the subtree to which a path secret for P would be encrypted according to synchronizing-views-of-the-tree). </ins> For example, in the following tree: With P = W and S = Y, "original_sibling_tree_hash" is the tree hash 8.8.1.  <ins> In ParentNode objects and LeafNode objects with "leaf_node_source" set to "commit", the value of the "parent_hash" field is the parent hash of the next non-blank parent node above the node in question (the next node in the filtered direct path).  Using the node labels in parent-hash-inputs, the "parent_hash" field of D is equal to the parent hash of P with copath child S.  This is the case even when the node D is a leaf node. The "parent_hash" field of a LeafNode is signed by the member.  The signature of such a LeafNode thus also attests to which keys the group member introduced into the ratchet tree and to whom the corresponding secret keys were sent.  This prevents malicious insiders from constructing artificial ratchet trees with a node D whose HPKE secret key is known to the insider yet where the insider isn't assigned a leaf in the subtree rooted at D.  Indeed, such a ratchet tree would violate the tree invariant. </ins> 8.8.2. Parent hashes are verified at two points in the protocol: When joining a group and when processing a Commit.  <ins> The parent hash in a node D is valid with respect to a parent node P </ins> if the following criteria hold:  <ins> D is a descendant of P in the tree </ins>  <ins> Any nodes between D and P in the tree are all blank </ins>  <ins> The "parent_hash" field of D is equal to the parent hash of P with </ins> copath child S, where S is the child of P that is not on the path  <ins> from D to P. </ins> A parent node P is "parent-hash valid" if it can be chained back to a leaf node in this way.  That is, if there is leaf node L and a the nodes in the UpdatePath form a parent-hash chain from the committer's leaf to the root.  8.9. As described in commit, each Commit message may optionally contain an