on PSKs. Each PSK in MLS has a type that designates how it was provisioned. <del> External PSKs are provided by the application, while recovery and re- init PSKs are derived from the MLS key schedule and used in cases where it is necessary to authenticate a member's participation in a prior group state.  In particular, in addition to external PSK types, a PSK derived from within MLS may be used in the following cases: Re-Initialization: If during the lifetime of a group, the group members decide to switch to a more secure ciphersuite or newer protocol version, a PSK can be used to carry entropy from the old group forward into a new group with the desired parameters. Branching: A PSK may be used to bootstrap a subset of current group members into a new group.  This applies if a subset of current group members wish to branch based on the current group state. </del>  The injection of one or more PSKs into the key schedule is signaled <del> in two ways: 1) as a "PreSharedKey" proposal, and 2) in the "GroupSecrets" object of a Welcome message sent to new members added in that epoch. </del>  On receiving a Commit with a "PreSharedKey" proposal or a GroupSecrets object with the "psks" field set, the receiving Client

This PR reflects further discussion on and replaces . The high-level impacts are: Add clearer, more prescriptive definitions of the re-init and branch operations Use a single syntax for all resumption PSKs, but also have a parameter to distinguish cases Allow an usage to cover any other cases defined by applications Once lands, we should also add some text to the protocol overview to describe these operations. Pasting some text here for usage in that future PR...
One question regarding the diagram in the PR description. In the re-init case, shouldn't the "B" group start at epoch ? At least that's what the PR specifies in the spec.
I think the re-init diagram is fine because the spec says The in the Welcome message MUST be 1 However in the branch case (and application case) diagram I don't think there should be a ReInit proposal? (At least it is not said in the spec). Also, it is not entirely clear how to construct a in the branch case (or application case): what values should have and ?
Ah, sorry I misread. You are right and the re-init diagram is fine the way it is.
Minor typo in the text above: "allow a entropy" -> "allow entropy".
From my reading, it looks like the creator of an epoch must send the exact same message to each new user. However, in some systems, it is more efficient to send a different message to each user, only containing the intended for them, rather than the for all new members. As far as I can tell, no changes are required on the recipient side. I'm running some performance testing, trying to add thousands of members at a time, and the messages are hundreds of kilobytes large. In a federated environment, this could be copied to multiple servers, with the vast majority of that being unnecessary data.
Good point NAME I filed . In the case, you would do something like send a Welcome per home server?
Thanks. With the way that Matrix currently works, it would probably be best to send a separate message per user, since the current endpoint for sending messages directly to devices requires specifying the message separately for each recipient, and implementations (AFAIK) currently don't do deduplication. In the future, it might be possible to do it differently.
s currently have two usages in the spec: Pre-published key packages that you can download and put in a leaf when adding a new member leaf node content that you update when doing a full Currently, things have been designed to use only one structure for both uses, leading to some weirdness such as: and are useful in the usage 1, but are parameters of the group in usage 2. is a good name in usage 1, but would better be named in usage 2. some data is important in usage 2 and have no meaning in usage 1, hence they were designed as "mandatory-when-updating extensions" (such as , and we were also discussing about adding the in to help prevent cross-group attacks) How about using different structures for these? // Essentially identical to KeyPackage struct { ProtocolVersion version; CipherSuite ciphersuite; HPKEPublicKey hpkeinitkey; opaque endpointid; Credential credential; Extension extensions; opaque signature; } PrePublishedKeyPackage; struct { HPKEPublicKey hpkekey; opaque endpointid; Credential credential; opaque parenthash; opaque groupid; Extension extensions; opaque signature; } LeafNodeContent; enum { reserved(0), prepublishedkeypackage(1), leafnodecontent(2), (255) } LeafNodeType; struct LeafNode { // This is not included in the signature, but with the "unambiguous signature" PR that's not a problem LeafNodeType type; select(URL) { case prepublishedkeypackage: PrePublishedKeyPackage data; case leafnode_content: LeafNodeContent data; } } What would you think about refactoring like this?
If we're doing this, I'd suggest including a dedicated public key to encrypt Welcome messages to for the . NAME suggested this at some point. It gives us better FS guarantees, because the receiver of the can delete the corresponding private key immediately after decryption, while they might have to keep the other key around for a bit until they can actually issue an update. This might also help clarify .
Ties into as well because is an example of an extension that is required and should be a field instead
NAME one concern I have is how is that field in created?
When sending a full , the participant includes a new in the (it replaces the field). It computes the signature with where: corresponds to all the fields of except (the usual thing) comes from
What about the scenario where Alice pulls down Bob's key package to add him to the group? In that case Alice can't do the conversion between formats on Bob's behalf.
Alice don't have to do the conversion, this is why either contain a (when adding someone to the group) or a (when updating your own key package).
Ah somehow I missed that part in your original post. Makes sense
I like this general direction. It solves a few things. Instead of having leaf nodes contain either a prepublished thing or a normal leaf node, how about having the prepublished thing wrap the leaf node thing? As a bonus, we get separation between the key pair used for Welcome encryption and the key pair that goes in the leaf for TreeKEM. Then when Alice adds Bob, she extracts his LeafNodeContent and sends that in the Add. That doesn't let the other members of the group verify that Alice did the right thing w.r.t. expiry, but I'm not heartbroken about that. We might also be able to remove the extensions field from the outer (KeyPackage) wrapping if we turn the extension into KeyPackage field. I expect we probably need leaf extensions, but would be open to making into a field.
Why not keep sending the KeyPackage in the Add? That wouldn't cost much and would allow everyone to validate the lifetime. Note, that the lifetime is crucial in achieving PCS for pre-published key material. If I compromise Bob and get hold of the private key material of a bunch of his KeyPackages, I can add "Bob" to any group I want as long as the Credential inside the LeafNodeContent remains valid and Bob doesn't revoke the LeafNodeContent somehow. Also, we'll have to do this if we want to enable any member to send the Welcome message as you proposed here: URL
I agree that including inside is quite clean, but something is bothering me with this approach: I'd like that the is signed by the participant occupying that leaf, to protect against cross-group attacks. In the first message of this issue I did this by including in , but in fact we don't need to store it in the leaf, we just need to include it in the . In any case, if we want to include in the signature, we need a way to differentiate between leaves that triggered a commit and the ones that are "fresh". And for that I think using a tagged-union struct is the cleanest way to do it.
It always feels a bit lazy to use an extension (and implementations have to be careful to check for their presence), but we could use a extension in the same way that we use a extension. You will find both of them only on LeafContents that were updated at least once. Another result of including GroupId in the KeyPackage is that the signature will explicitly sign the and I'm not sure if that doesn't completely destroy any deniability guarantees we can still get from MLS.
I don't think there's really a good way to get the into the leaf. Because of prepublication, it would have to be optional, but in order to prevent attacks, you need rules to decide whether it's required in a given situation. It seems like the best you can do is basically TOFU: When you first get the tree, you can't validate whether the groupid should be in a given leaf or not Once you're a member, you require that groupid is sent in Update and UpdatePath.leafkey_package I could live with a solution of that character, but given it's a bit complex, might prefer to leave it out unless we get a useful property from it. NAME - Re: Sending KeyPackage in Add - Yep, that makes sense. So you would send KeyPackage in Add, and LeafNode in Update.
I tried to worked through what would be necessary to implement this. It looks like it would require some fairly invasive changes to be made to rationalize the document's structure before making this change. But those changes should probably be made anyway. Proposal: Preparatory PR: Rationalize layout Move "Cryptograhic Objects" to before "Ratchet Trees" Update "Ratchet Tree Nodes" to define ParentNode and LeafNode structs, with LeafNode initially just equal to KeyPackage Move subsections of "Key Packages" to under "Ratchet Trees" Parent Hash Tree Hashes Update Paths Move Group State to its own top-level section Main PR: Change LeafNode to have the required content: HPKE key for TreeKEM Credential Parent hash Capabilities Extensions Signature Change KeyPackage to refer to LeafNode for internals, plus: HPKE key for Welcome Lifetime Extensions Signature Add continues to send KeyPackage Update and Commit send LeafNode instead of KeyPackage Remove and Sender refer to LeafNodeRef instead of KeyPackageRef We should probably do the preparatory PR regardless of this issue.
Discussion on working call: General agreement on the approach Should sign group_id when possible Include in TBS if we at least have a flag for pre-published / in-group Or we can store it Or we could have independent structs for pre-published / in-group leaves Do we need extensions at both levels? Capabilities / lifetime should probably be promoted to fields
Right now, we treat the re-init and branch PSK cases as distinct cases, as opposed to just having a general "export PSK from one group+epoch, insert it into another" function. What I would call a "resumption PSK", following TLS. Even though we have the distinction, the content of the PSK itself is the same: (group ID, epoch, resumption secret). So the only difference is that the PreSharedKeyID has a bit that signals the intent of injecting this resumption PSK. Do we really need that bit? If there's not a compelling need, it would streamline the API and allow for more flexibility if we just had the notion of a resumption PSK.
Key separation by domain is usually a good idea. If we derive a key, the purpose should be "baked in", i.e. included in the derivation to avoid potential collision across protocol functionalities/domains. I'm not sure I understand how that makes the API more complex. If it's really bad, we should consider the trade-off, but if it's not too much trouble, I'd prefer we keep the key separation. Can you elaborate a bit on the API complexity?
I'm mostly concerned about being too specific in the "purpose" definitions, and thus making applications make a choice that doesn't really make sense. For example, take the obvious case of TLS-like resumption, where I take a key from the last epoch of a prior instance of the group and use it as input to a new appearance of the group. Is that a re-init? There wasn't necessarily a ReInit proposal. Is it a branch? "Branch" isn't even defined in the spec. Better to just talk about "resumption" as the general act of exporting a secret from one group and inserting it into another.
The scenarios you describe seem to indicate problems in the spec that don't have anything to do with the derivation of the resumption secret. If you want to enable re-init secrets without a proposal, that's fine and we can define in the spec which key to use for that purpose (probably the re-init key). Similarly, if branch isn't well defined in the spec, we should probably define it better. In the end, for everything else that you want to do that's not (properly) defined in the spec, you can just use an exporter secret. I don't see that weakening the domain separation of the re-init and branch key derivation is the right way to solve these problems. Is the code complexity that bad?
We could go off and try to define all the possible uses of a resumption PSK, but (a) that's a lot more work, (b) it seems unlikely to succeed, (c) even if you do succeed, you'll end up with a more complicated API for applications (bc N names for the same technical act) and (d) it's not clear to me what the benefit is. When you talk about domain separation here, what is the failure case that this prevents?
What we're trying to prevent is that participants end up with the same keys after executing a different part/functionality of the protocol. If party A thinks, they are doing a branch, while party B thinks they're doing a Re-Init and they end up with the same key, then that's bad. If an implementer wants to do proprietary things, i.e. things that are not described in the spec, they have to use an exporter key and make sure their keys are properly separated.
It depends on what you mean by a "different functionality of the protocol". The only difference between the branch and reinit cases is that in the reinit case, you quit using the old group. You could also imagine injecting a resumption PSK into an independent group with its own history, whose membership overlaps with the group from which the PSK was drawn. Is the functionality "linking one group/epoch to a prior group/epoch" or is it "reinit / branch / ..."? The former seems more at the right level of granularity to me, and leads to the approach here and in . If you're really hard over here, how about a compromise: Unify the syntax, but add a distinguisher alongside the group ID and epoch, which has specified values for the branch and reinit cases, and which the application MAY set for other cases.
I see where you're coming from and if you're convinced it is the more useful approach, we can make the trade-off and go with (although we might as well rename the key from "resumption" to something like "group-link" or something). However, I do want to note that we have a trade-off here. Previously, designating a PSK "branch" or "re-init" signaled intent on the side of the sender and arriving at the same PSK meant agreement on said intent. For example, using a re-init key would imply that the new group is meant to replace the old group. As far as I understand that is no longer the case with .
I understand the concern from a cryptographic standpoint, it's nice to have a type that indicates that the group should be shut down. From an engineering perspective, this all feels pretty vague. It's not clear at all how exactly a group will be shut down and a new one will be created, the protocol doesn't give any specific guidance. I don't think this is a huge concern, as long as we don't paint ourselves in a corner when we want to upgrade groups to newer MLS versions in the future.
Even if we want to have everyone agree that the group should be shut down, the ReInit proposal provides that. Committing the ReInit puts it in the transcript, which means everyone agrees on it. So everyone who exports a PSK from that epoch should know it's a "dead epoch". In fact, the vs. distinction seems duplicative here, since you shouldn't be able to get a PSK from a "live" epoch or a PSK from a dead one. iff live; iff dead. Now, there may be other sub-cases within live or dead; as noted, not all resumption PSK cases map clearly to or . But it seems premature to put in stub functionality without some taxonomy and requirements. I would propose we do for now, and if there turns out to be a need to be PSK use cases that really need segregating, it should be straightforward extension to add another PSKType.
Fair enough. I see that the trade-off is probably ok. I'd be ok with merging . The proposal should indeed ensure that the streams don't get crossed, so that all that remains would be ease of provability at that point. I still feel uncomfortable about not properly separating keys, though. Regarding the last two arguments: the "for now" solution is a bit moot, since with the WGLC at hand, it's almost certainly not going to change until then the extension argument goes the other way as well, as you could just as easily have an extension that implements and gets rid of the need for the distinction between resumption PSKs. If anything, we should opt to be conservative in the spec and allow users to loosen the key separation via an extension at their own risk.
I understand that it's unclear how the linking of groups is going to take place in a practical setting, but I don't see that that's an argument to weaken the cryptographic basis of the mechanism. If we don't figure out how to do it in practice, then that's fine, as we can just ignore that part of the protocol. If we do, however, we will want to get proper security guarantees whenever we link two groups in this way. Again, I think is ok. But it's because we probably get good enough guarentees and not because the whole mechanism is a secondary concern due to there being unsolved higher level engineering challenges.
I have recently implemented some of this to get a better feel for it. While we already knew the "reinit" case was quite underspecified, I now also realized that there might be another issue with it: A member can shut down a group before a new one is created and there is no way to force the creation of the new group within the protocol. I think it would be safer to first create a new group (with whatever new parameters) and to tear down the old one in that process. Since this is too complex for the protocol it should be solved at the application layer. In other words, I think is indeed fine.
Commenting on here, to keep discussion from splitting to the PR. There are a few issues here that could use clarify, and some confusion in the interpretation of the proposals. First - as NAME pointed out, if anything is unclear/underspecified, then that should be clarified and cleaned up vs. removal as the default. From the confusion above, it is clear that some pieces are not sufficiently specified. Second - There is a core distinction between how PSKs are used in MLS vs. TLS that we need to be careful of. In TLS, these are used strictly for the same two members communicating again after a finished session. It is not used again within the same session nor outside of the protocol (exporter keys are for that). Namely, the uses are: Group-internal use (resumption by an identical member set of the two parties) In MLS, these assumptions change by the very fact that the group may have more than 2 members. For the protocol-external case we have exporter keys as well. For PSK use, we have the following: Group-internal use (resumption by an identical member set) Subgroup-internal use (resumption by a proper subset of members) Session-internal use (since continuous KE implies that keying material may be injected) While the session-internal use is not the focus of this discussion, it has been previously (i.e. proving knowledge of prior group state). This contrasts against TLS were the handshake takes place only once Point 2 is notable in that it is impossible for a proper subset of TLS session members to use session information in a separate session (i.e. that would imply only one communication party with no partner). In MLS, this is possible, and we need to be careful about mis-aligning TLS for precedent. To avoid potential forks, etc., and clarity of key source, any group-subset PSK use really ought to be denoted (currently a 'branch'). NAME is correct that there could be subtle attacks on this if key use is not properly separated. In addition to the above there seems to be confusion on what resumption is linked to (this is consequently something that needs more clarity in the spec.). To be precise in alignment of terms, using a PSK in TLS for communication in a new session is aligned to using a PSK in MLS for a new group, i.e. it being a continuous session. Thus we need to align discussion here between "alive" and "dead" sessions in TLS to "alive" and "dead" groups in MLS - not epochs. We can get a reinit PSK from a live group, i.e. if a member wants to force a restart for any reason and instantiates a new group with the reinit secret. This signals to all members joining that group that the prior group should be terminated and not considered secure/alive after the point of reinit use. It is an edge case, but possible. Similarly and more commonly, we could get a branch PSK from a dead group, i.e. if a new (sub-)group wants members to prove knowledge from some prior connection. Thus it follows that we can branch from both "alive" and "dead" groups. This may be helpful in terms of authentication. I am favorable to NAME 's PSKUsage suggestion, i.e. that if there are other desired uses apart from the above cases the application can provide for those. The "bleeding" of key separation across different uses is something that is concerning, and should involve closer scrutiny before inclusion in the specification. If anything, more separation is safer at this stage, vs consolidation.
NAME Good point about the differences from the two-party case. I think your taxonomy is not quite complete, though. You could also imagine cases where the epoch into which the resumption PSK is injected has members that were not in the group in the epoch in which it was extracted (and these members were provided the PSK some other way). For instance, in the session-internal case where there have been some Adds in the meantime. To try to factor this a little differently, it seems like you have at least three independent columns in the truth table here comparing the extraction epoch to the injection epoch: Same group or different group ("group" is the term that matches TLS "session"; linear sequence of epochs) Some members have been removed Some members have been added So you have at least 8 cases to cover to start with assuming you care about both of those axes (same/different group and membership). It's not clear to me why those are the two axes that one cares about. On the one hand, why is it not important to distinguish ciphersuite changes as well, or changes in extensions? On the other hand, why do we need to agree on membership changes here, given that the key schedule already confirms that we agree on the membership? Basically, I don't understand (a) what cases need to be distinguished, (b) why do these cases need to be distinguished, and (c) why the required separation properties aren't provided by some other mechanism already. I would also note that you don't get the benefits of key separation unless the recipients know how to verify that the usage is right for the injection context. (Otherwise, a malicious PSK sender can send the wrong value.) For the same/different group, that's easy enough to do based on key schedule continuity. But membership is problematic. For example, if you're changing the ciphersuite between extraction and injection, then all of the KeyPackages and Credentials will be different -- what do you compare to verify equality? I can update to add PSKUsage, but the cases we put there need to at least be enforceable, and ideally fit into some principled taxonomy (even if all the cases in the taxonomy aren't enforceable).
For the record, distinguishing on ciphersuite changes was brought up earlier in the WG - I am in the support if bringing it back on the discussion table is an option... Separately though, for most other changes during the lifespan of a group, proposals basically perform a similar action for distinguishing between keys and separating changes from epoch to epoch. The difference with the PSK is there is no "one way" to interpret a PSK's use (as compared to e.g. a ciphersuite change). This also means that it can be more challenging to provably separate out misuse. [BTW: I am assuming in this discussion you mean ciphersuite change in the context of the listed components, since reinit is actually used for ciphersuite key changes per line 1598.] You are quite correct that there is a group-external use, i.e. the "different group" case. However, anything outside of the group/session is an external/exporter key use. Namely, the MLS protocol spec is about a single session and anything that is used in other groups/sessions is an external key (similar to TLS export from session). The distinction between PSK usage and external/exporter keys (whether the latter is used for another application or injected into a different group entirely) is that PSK use is within the analysis bounds of the given session. To this point, there is a gray area on item 2 (subgroup-internal use / resumption by a proper subset of members). This is probably the only one I could see an argument for also pushing as an exporter key use. The issue of some members added between PSK usage is really reducible to two cases: a) a member was already in the set and the PSK use appears as item 1 or 3 (group-internal use or session-internal use) or b) a member was added and the PSK offers no security beyond that of an exporter key inject. The potential value proposition is for existing members, there is no loss for a new member, and the it is better to offer the value as possible than to deny it based on the weakest link. The issue of some members being removed between PSK usage is similarly reducible. If the ejected member does not have the current group state then knowing a PSK should not provide value. It is similar to knowing an exporter key but having no place to export it into. The goal of PSK usage is to strengthen an existent system vs. being the sole point of security for that. If our KDF is done correctly, then combing current group state and the PSK will not be problematic (former unknown but latter known to the ejected member).
So NAME and I had some offline discussion about this, and it led me to the conclusion that what I'm really grumpy about is a need for more clarity in the branch and reinit definitions. We don't need a full taxonomy of uses if (a) we can say "use this PSK usage for this specific protocol feature", and (b) we have some escape hatch that allows for usages of resumption PSKs outside those specific protocol features. I have filed reflecting this theory. Hopefully folks find it more agreeable.
That seems like a well-reasoned solution.
Fixed with and
Looks good! Although I'm not sure in the branching keys we want to have the exact same key packages as in the main group.I think this looks fine generally (just one comment)

on PSKs. Each PSK in MLS has a type that designates how it was provisioned.  <ins> External PSKs are provided by the application, while resumption PSKs are derived from the MLS key schedule and used in cases where it is necessary to authenticate a member's participation in a prior epoch. </ins> The injection of one or more PSKs into the key schedule is signaled  <ins> in two ways: Existing members are informed via PreSharedKey proposals covered by a Commit, and new members added in the Commit are informed via GroupSecrets object in the Welcome message corresponding to the Commit.  To ensure that existing and new members compute the same PSK input to the key schedule, the Commit and GroupSecrets objects MUST indicate the same set of PSKs, in the same order. </ins> On receiving a Commit with a "PreSharedKey" proposal or a GroupSecrets object with the "psks" field set, the receiving Client


8.7. <del> The main MLS key schedule provides a "resumption_secret" which can provide extra security in some cross-group operations. </del>  <del> The application SHOULD specify an upper limit on the number of past epochs for which the "resumption_secret" may be stored. There are two ways in which a "resumption_secret" can be used: to re- initialize the group with different parameters, or to create a sub- group of an existing group as detailed in pre-shared-keys. Resumption keys are distinguished from exporter keys in that they have specific use inside the MLS protocol, whereas the use of exporter secrets may be decided by an external application.  They are thus derived separately to avoid key material reuse. </del>  8.8.

8.7.  <ins> The main MLS key schedule provides a "resumption_secret" that is used as a PSK to inject entropy from one epoch into another.  This functionality is used in the reinitialization and branching processes described in reinitialization and sub-group-branching, but may be used by applications for other purposes. </ins>  <ins> Some uses of resumption PSKs might call for the use of PSKs from historical epochs.  The application SHOULD specify an upper limit on the number of past epochs for which the "resumption_secret" may be stored. </ins> 8.8.


Transmit the Welcome message to the other new members The recipient of a Welcome message processes it as described in <del> joining-via-welcome-message. </del>  In principle, the above process could be streamlined by having the creator directly create a tree and choose a random value for first epoch's epoch secret.  We follow the steps above because it removes unnecessary choices, by which, for example, bad randomness could be introduced.  The only choices the creator makes here are its own <del> KeyPackage, the leaf secret from which the Commit is built, and the intermediate key pairs along the direct path to the root. </del>  10.1.

Transmit the Welcome message to the other new members The recipient of a Welcome message processes it as described in  <ins> joining-via-welcome-message.  If application context informs the recipient that the Welcome should reflect the creation of a new group (for example, due to a branch or reinitialization), then the recipient MUST verify that the epoch value in the GroupInfo is equal to 1. </ins> In principle, the above process could be streamlined by having the creator directly create a tree and choose a random value for first epoch's epoch secret.  We follow the steps above because it removes unnecessary choices, by which, for example, bad randomness could be introduced.  The only choices the creator makes here are its own  <ins> KeyPackage and the leaf secret from which the Commit is built. </ins> 10.1.


10.2. <del> A new group may be tied to an already existing group for the purpose of re-initializing the existing group, or to branch into a sub-group. Re-initializing an existing group may be used, for example, to restart the group with a different ciphersuite or protocol version. Branching may be used to bootstrap a new group consisting of a subset of current group members, based on the current group state. </del>  <del> In both cases, the "psk_nonce" included in the "PreSharedKeyID" object must be a randomly sampled nonce of length "KDF.Nh" to avoid key re-use. </del>  <del> 10.2.1. </del>  <del> If a client wants to create a subgroup of an existing group, they MAY choose to include a "PreSharedKeyID" in the "GroupSecrets" object of the Welcome message choosing the "psktype" "branch", the "group_id" of the group from which a subgroup is to be branched, as well as an epoch within the number of epochs for which a "resumption_secret" is kept. </del>  11.

10.2.  <ins> A group may be reinitialized by creating a new group with the same membership and different parameters, and linking it to the old group via a resumption PSK.  The members of a group reinitialize it using the following steps: A member of the old group sends a ReInit proposal (see reinit) A member of the old group sends a Commit covering the ReInit proposal A member of the old group sends a Welcome message for the new group that matches the ReInit The "group_id", "version", and "cipher_suite" fields in the Welcome message MUST be the same as the corresponding fields in the ReInit proposal. The "epoch" in the Welcome message MUST be 1 The Welcome MUST specify a PreSharedKey of type "resumption" with usage "reinit".  The "group_id" must match the old group, and the "epoch" must indicate the epoch after the Commit covering the ReInit. The "psk_nonce" included in the "PreSharedKeyID" of the resumption PSK MUST be a randomly sampled nonce of length "KDF.Nh", for the KDF defined by the new grou's ciphersuite. Note that these three steps may be done by the same group member or different members.  For example, if a group member sends a commit with an inline ReInit proposal (steps 1 and 2), but then goes offline, another group member may send the corresponding Welcome. This flexibility avoids situations where a group gets stuck between steps 2 and 3. </ins>  <ins> Resumption PSKs with usage "reinit" MUST NOT be used in other contexts.  A PreSharedKey proposal with type "resumption" and usage "reinit" MUST be considered invalid. </ins>  <ins> 10.3. </ins>  <ins> A new group can be formed from a subset of an existing group's members, using the same parameters as the old group.  The creator of the group indicates this situation by including a PreSharedKey of type "resumption" with usage "branch" in the Welcome message that creates the branched subgroup. A client receiving a Welcome including a PreSharedKey of type "resumption" with usage "branch" MUST verify that the new group reflects a subgroup branched from the referenced group. The "version" and "ciphersuite" values in the Welcome MUST be the same as those used by the old group. Each KeyPackage in a leaf node of the new group's tree MUST be a leaf in the old group's tree at the epoch indicated in the PreSharedKey. In addition, to avoid key re-use, the "psk_nonce" included in the "PreSharedKeyID" object MUST be a randomly sampled nonce of length "KDF.Nh". Resumption PSKs with usage "branch" MUST NOT be used in other contexts.  A PreSharedKey proposal with type "resumption" and usage "branch" MUST be considered invalid. </ins> 11.


11.1.5. <del> A ReInit proposal represents a request to re-initialize the group with different parameters, for example, to increase the version number or to change the ciphersuite.  The re-initialization is done by creating a completely new group and shutting down the old one. </del>  A member of the group applies a ReInit proposal by waiting for the <del> committer to send the Welcome message and by checking that the "group_id" and the parameters of the new group corresponds to the ones specified in the proposal.  The Welcome message MUST specify exactly one pre-shared key with "psktype = reinit", and with "psk_group_id" and "psk_epoch" equal to the "group_id" and "epoch" of the existing group after the Commit containing the "reinit" Proposal was processed.  The Welcome message may specify the inclusion of other pre-shared keys with a "psktype" different from "reinit". </del>  If a ReInit proposal is included in a Commit, it MUST be the only proposal referenced by the Commit.  If other non-ReInit proposals

11.1.5.  <ins> A ReInit proposal represents a request to reinitialize the group with different parameters, for example, to increase the version number or to change the ciphersuite.  The reinitialization is done by creating a completely new group and shutting down the old one. </ins> A member of the group applies a ReInit proposal by waiting for the  <ins> committer to send the Welcome message that matches the ReInit, according to the criteria in reinitialization. </ins> If a ReInit proposal is included in a Commit, it MUST be the only proposal referenced by the Commit.  If other non-ReInit proposals


If a ReInit proposal was part of the Commit, the committer MUST create a new group with the parameters specified in the ReInit proposal, and with the same members as the original group.  The <del> Welcome message MUST include a "PreSharedKeyID" with "psktype" "reinit" and with "psk_group_id" and "psk_epoch" corresponding to the current group and the epoch after the commit was processed. </del>  A member of the group applies a Commit message by taking the following steps:

If a ReInit proposal was part of the Commit, the committer MUST create a new group with the parameters specified in the ReInit proposal, and with the same members as the original group.  The  <ins> Welcome message MUST include a "PreSharedKeyID" with the following parameters: "psktype": "resumption" "usage": "reinit" "group_id": The group ID for the current group "epoch": The epoch that the group will be in after this Commit </ins> A member of the group applies a Commit message by taking the following steps:


If the Commit included a ReInit proposal, the client MUST NOT use the group to send messages anymore.  Instead, it MUST wait for a <del> Welcome message from the committer and check that The "version", "cipher_suite" and "extensions" fields of the new group corresponds to the ones in the "ReInit" proposal, and that the "version" is greater than or equal to that of the original group. The "psks" field in the Welcome message includes a "PreSharedKeyID" with "psktype" = "reinit", and "psk_epoch" and "psk_group_id" equal to the epoch and group ID of the original group after processing the Commit. </del>  The confirmation tag value confirms that the members of the group have arrived at the same state of the group:

If the Commit included a ReInit proposal, the client MUST NOT use the group to send messages anymore.  Instead, it MUST wait for a  <ins> Welcome message from the committer meeting the requirements of reinitialization. </ins> The confirmation tag value confirms that the members of the group have arrived at the same state of the group:


in the credential field of the KeyPackage objects in the leaves of the tree (including the InitKeys used to add new members).  The ciphersuites are defined in section mls-ciphersuites. 6.2.

Signatures as specified in the MLS protocol document are ambiguous, in the sense that signatures produced for one purpose in the protocol can be (maliciously) used in a different place of the protocol. Indeed, we found that for example, a can have the same serialization as , which proves that one signature could be accepted for different structures. The collisions we have found so far are quite artificial, so it is not super clear how to exploit this, but at least it makes it impossible to have provable security. The solution is pretty simple, we simply prefix each signed value with a label, in a fashion similar to . That way, when calling you can give the structure name in the argument.
Something that is still bothering me is the structure: struct { select (URL) { case member: GroupContext context; case preconfigured: case newmember: struct{}; } WireFormat wireformat; opaque groupid; uint64 epoch; Sender sender; opaque authenticateddata; ContentType contenttype; select (MLSPlaintextTBS.contenttype) { case application: opaque applicationdata; case proposal: Proposal proposal; case commit: Commit commit; } } MLSPlaintextTBS; Since the first is done on a field defined after it, this structure is not parsable. We could say that's not a problem since we only need to serialize it, but for security purposes I would recommend to put it at the end, like this: struct { WireFormat wireformat; opaque groupid; uint64 epoch; Sender sender; opaque authenticateddata; ContentType contenttype; select (MLSPlaintextTBS.contenttype) { case application: opaque applicationdata; case proposal: Proposal proposal; case commit: Commit commit; } select (URL) { case member: GroupContext context; case preconfigured: case newmember: struct{}; } } MLSPlaintextTBS; What do you think about it?
Nice one! Crafting collisions is probably not straightforward, but eliminating the the risk altogether is the way to go.
It is actually quite easy to craft collisions, I made a script do find them: URL (I wanted to check whether there was accidentally a way do disambiguate the structures)
Introduced in .
s currently have two usages in the spec: Pre-published key packages that you can download and put in a leaf when adding a new member leaf node content that you update when doing a full Currently, things have been designed to use only one structure for both uses, leading to some weirdness such as: and are useful in the usage 1, but are parameters of the group in usage 2. is a good name in usage 1, but would better be named in usage 2. some data is important in usage 2 and have no meaning in usage 1, hence they were designed as "mandatory-when-updating extensions" (such as , and we were also discussing about adding the in to help prevent cross-group attacks) How about using different structures for these? // Essentially identical to KeyPackage struct { ProtocolVersion version; CipherSuite ciphersuite; HPKEPublicKey hpkeinitkey; opaque endpointid; Credential credential; Extension extensions; opaque signature; } PrePublishedKeyPackage; struct { HPKEPublicKey hpkekey; opaque endpointid; Credential credential; opaque parenthash; opaque groupid; Extension extensions; opaque signature; } LeafNodeContent; enum { reserved(0), prepublishedkeypackage(1), leafnodecontent(2), (255) } LeafNodeType; struct LeafNode { // This is not included in the signature, but with the "unambiguous signature" PR that's not a problem LeafNodeType type; select(URL) { case prepublishedkeypackage: PrePublishedKeyPackage data; case leafnode_content: LeafNodeContent data; } } What would you think about refactoring like this?
If we're doing this, I'd suggest including a dedicated public key to encrypt Welcome messages to for the . NAME suggested this at some point. It gives us better FS guarantees, because the receiver of the can delete the corresponding private key immediately after decryption, while they might have to keep the other key around for a bit until they can actually issue an update. This might also help clarify .
Ties into as well because is an example of an extension that is required and should be a field instead
NAME one concern I have is how is that field in created?
When sending a full , the participant includes a new in the (it replaces the field). It computes the signature with where: corresponds to all the fields of except (the usual thing) comes from
What about the scenario where Alice pulls down Bob's key package to add him to the group? In that case Alice can't do the conversion between formats on Bob's behalf.
Alice don't have to do the conversion, this is why either contain a (when adding someone to the group) or a (when updating your own key package).
Ah somehow I missed that part in your original post. Makes sense
I like this general direction. It solves a few things. Instead of having leaf nodes contain either a prepublished thing or a normal leaf node, how about having the prepublished thing wrap the leaf node thing? As a bonus, we get separation between the key pair used for Welcome encryption and the key pair that goes in the leaf for TreeKEM. Then when Alice adds Bob, she extracts his LeafNodeContent and sends that in the Add. That doesn't let the other members of the group verify that Alice did the right thing w.r.t. expiry, but I'm not heartbroken about that. We might also be able to remove the extensions field from the outer (KeyPackage) wrapping if we turn the extension into KeyPackage field. I expect we probably need leaf extensions, but would be open to making into a field.
Why not keep sending the KeyPackage in the Add? That wouldn't cost much and would allow everyone to validate the lifetime. Note, that the lifetime is crucial in achieving PCS for pre-published key material. If I compromise Bob and get hold of the private key material of a bunch of his KeyPackages, I can add "Bob" to any group I want as long as the Credential inside the LeafNodeContent remains valid and Bob doesn't revoke the LeafNodeContent somehow. Also, we'll have to do this if we want to enable any member to send the Welcome message as you proposed here: URL
I agree that including inside is quite clean, but something is bothering me with this approach: I'd like that the is signed by the participant occupying that leaf, to protect against cross-group attacks. In the first message of this issue I did this by including in , but in fact we don't need to store it in the leaf, we just need to include it in the . In any case, if we want to include in the signature, we need a way to differentiate between leaves that triggered a commit and the ones that are "fresh". And for that I think using a tagged-union struct is the cleanest way to do it.
It always feels a bit lazy to use an extension (and implementations have to be careful to check for their presence), but we could use a extension in the same way that we use a extension. You will find both of them only on LeafContents that were updated at least once. Another result of including GroupId in the KeyPackage is that the signature will explicitly sign the and I'm not sure if that doesn't completely destroy any deniability guarantees we can still get from MLS.
I don't think there's really a good way to get the into the leaf. Because of prepublication, it would have to be optional, but in order to prevent attacks, you need rules to decide whether it's required in a given situation. It seems like the best you can do is basically TOFU: When you first get the tree, you can't validate whether the groupid should be in a given leaf or not Once you're a member, you require that groupid is sent in Update and UpdatePath.leafkey_package I could live with a solution of that character, but given it's a bit complex, might prefer to leave it out unless we get a useful property from it. NAME - Re: Sending KeyPackage in Add - Yep, that makes sense. So you would send KeyPackage in Add, and LeafNode in Update.
I tried to worked through what would be necessary to implement this. It looks like it would require some fairly invasive changes to be made to rationalize the document's structure before making this change. But those changes should probably be made anyway. Proposal: Preparatory PR: Rationalize layout Move "Cryptograhic Objects" to before "Ratchet Trees" Update "Ratchet Tree Nodes" to define ParentNode and LeafNode structs, with LeafNode initially just equal to KeyPackage Move subsections of "Key Packages" to under "Ratchet Trees" Parent Hash Tree Hashes Update Paths Move Group State to its own top-level section Main PR: Change LeafNode to have the required content: HPKE key for TreeKEM Credential Parent hash Capabilities Extensions Signature Change KeyPackage to refer to LeafNode for internals, plus: HPKE key for Welcome Lifetime Extensions Signature Add continues to send KeyPackage Update and Commit send LeafNode instead of KeyPackage Remove and Sender refer to LeafNodeRef instead of KeyPackageRef We should probably do the preparatory PR regardless of this issue.
Discussion on working call: General agreement on the approach Should sign group_id when possible Include in TBS if we at least have a flag for pre-published / in-group Or we can store it Or we could have independent structs for pre-published / in-group leaves Do we need extensions at both levels? Capabilities / lifetime should probably be promoted to fields
From what I can see, there is no reason not to sign the context in the case of an external init. I'm not sure why it got taken out, but while implementing external inits, verification went through even when including the context.
As I commented in , it's not even clear that we need this context any more for members any more, given for MLSPlaintext and the AEAD of MLSCiphertext. So I would propose we just remove the context added to MLSPlaintextTBS altogether. NAME IIRC you were the one that proposed adding the context. WDYT?
This was fixed in
Here's the Weekly Digest for : - - Last week 9 issues were created. Of these, 6 issues have been closed and 3 issues are still open. :greenheart: , by :greenheart: , by :greenheart: , by :heart: , by :heart: , by :heart: , by :heart: , by :heart: , by :heart: , by :speaker: , by It received 2 comments. - - Last week, 13 pull requests were created, updated or merged. Last week, 3 pull requests were updated. :yellowheart: , by :yellowheart: , by :yellowheart: , by Last week, 10 pull requests were merged. :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by :purpleheart: , by - - Last week there were 38 commits. :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :hammerandwrench: by :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 .*

in the credential field of the KeyPackage objects in the leaves of the tree (including the InitKeys used to add new members). <ins> To disambiguate different signatures used in MLS, each signed value is prefixed by a label as shown below: Here, the functions "Signature.Sign" and "Signature.Verify" are defined by the signature algorithm. </ins> The ciphersuites are defined in section mls-ciphersuites. 6.2.


The value for hpke_init_key MUST be a public key for the asymmetric encryption scheme defined by cipher_suite.  The whole structure is signed using the client's signature key.  A KeyPackage object with an <del> invalid signature field MUST be considered malformed.  The input to the signature computation comprises all of the fields except for the signature field. </del>  KeyPackage objects MUST contain at least two extensions, one of type "capabilities", and one of type "lifetime".  The "capabilities"

The value for hpke_init_key MUST be a public key for the asymmetric encryption scheme defined by cipher_suite.  The whole structure is signed using the client's signature key.  A KeyPackage object with an  <ins> invalid signature field MUST be considered malformed. The signature is computed by the function "SignWithLabel" with a label "KeyPackage" and a content comprising of all of the fields except for the signature field. </ins> KeyPackage objects MUST contain at least two extensions, one of type "capabilities", and one of type "lifetime".  The "capabilities"


The "signature" field in an MLSPlaintext object is computed using the signing private key corresponding to the public key, which was authenticated by the credential at the leaf of the tree indicated by <del> the sender field.  The signature covers the plaintext metadata and message content, which is all of MLSPlaintext except for the "signature", the "confirmation_tag" and "membership_tag" fields.  If the sender is a member of the group, the signature also covers the GroupContext for the current epoch, so that signatures are specific to a given group and epoch. </del>  The "membership_tag" field in the MLSPlaintext object authenticates the sender's membership in the group.  For an MLSPlaintext with a

The "signature" field in an MLSPlaintext object is computed using the signing private key corresponding to the public key, which was authenticated by the credential at the leaf of the tree indicated by  <ins> the sender field.  The signature is computed using "SignWithLabel" with label ""MLSPlaintextTBS"" and with a content that covers the plaintext metadata and message content, which is all of MLSPlaintext except for the "signature", the "confirmation_tag" and "membership_tag" fields.  If the sender is a member of the group, the content also covers the GroupContext for the current epoch, so that signatures are specific to a given group and epoch. </ins> The "membership_tag" field in the MLSPlaintext object authenticates the sender's membership in the group.  For an MLSPlaintext with a


actually allowed to evict other members; groups can enforce access control policies on top of these basic mechanism.  5. The protocol uses "ratchet trees" for deriving shared secrets among a

Adds the overview text proposed in .
Thanks NAME !
Thanks for putting together the overview. The figures are great :-) Looks good to me with one proposition.

actually allowed to evict other members; groups can enforce access control policies on top of these basic mechanism. <ins> 4.3. A group comprises a single linear sequence of epochs and groups are generally independent of one-another.  However, it can sometimes be useful to link epochs cryptographically, either within a group or across groups.  MLS derives a resumption pre-shared key (PSK) from each epoch to allow entropy extracted from one epoch to be injected into a future epoch.  This link guarantees that members entering the new epoch agree on a key if and only if were members of the group during the epoch from which the resumption key was extracted. MLS supports two ways to tie a new group to an existing group.  Re- initialization closes one group and creates a new group comprising the same members with different parameters.  Branching starts a new group with a subset of the original group's participants (with no effect on the original group).  In both cases, the new group is linked to the old group via a resumption PSK. Applications may also choose to use resumption PSKs to link epochs in other ways.  For example, the following figure shows a case where a resumption PSK from epoch "n" is injected into epoch "n+k".  This demonstrates that the members of the group at epoch "n+k" were also members at epoch "n", irrespective of any changes to these members' keys due to Updates or Commits. </ins> 5. The protocol uses "ratchet trees" for deriving shared secrets among a


precisely two children, a _left_ child and a _right_ child.  A node is the _root_ of a tree if it has no parents, and _intermediate_ if it has both children and parents.  The _descendants_ of a node are <del> that node, its children, and the descendants of its children, and we say a tree _contains_ a node if that node is a descendant of the root of the tree.  Nodes are _siblings_ if they share the same parent. </del>  <del> A _subtree_ of a tree is the tree given by the descendants of any node, the _head_ of the subtree.  The _size_ of a tree or subtree is </del>  the number of leaf nodes it contains.  For a given parent node, its _left subtree_ is the subtree with its left child as head (respectively _right subtree_).

Make the definition of descendant match the common-sense definition (you are not your own descendant), adjust the text which uses the definition. Note: this allows for a much clearer definition of the tree invariant as well.

precisely two children, a _left_ child and a _right_ child.  A node is the _root_ of a tree if it has no parents, and _intermediate_ if it has both children and parents.  The _descendants_ of a node are  <ins> that node's children, and the descendants of its children, and we say a tree _contains_ a node if that node is a descendant of the root of the tree, or if the node itself is the root of the tree.  Nodes are _siblings_ if they share the same parent. </ins>  <ins> A _subtree_ of a tree is the tree given by any node (the _head_ of the subtree) and its descendants.  The _size_ of a tree or subtree is </ins> the number of leaf nodes it contains.  For a given parent node, its _left subtree_ is the subtree with its left child as head (respectively _right subtree_).


the GroupContext. This extension lists the extensions and proposal types that must be <del> supported by all members of the group.  For new members, it is enforced by existing members during the application of Add commits. Existing members should of course be in compliance already.  In order to ensure this continues to be the case even as the group's extensions can be updated, a GroupContextExtensions proposal is invalid if it contains a "required_capabilities" extension that requires capabilities not supported by all current members. </del>  10.2.

cc NAME
Looks good to me, thanks!
Comment by NAME Maybe add a sentence saying this means there is no repudiation of messages?
I would prefer not to say anything like that here, because the degree to which signature implies non-repudiation depends on a bunch of context for how the signature keys are managed. Clearly if you have long term or certified keys, then yes, there's a pretty high degree of non-repudiation. But people are also interested in configuring MLS to be deniable, e.g., by exchanging signature keys over a deniable channel. So I would propose closing this with no action.
Ah, that is a very good point. In my head I was thinking signature means proof of key and key is proven to person. But that might certainly not be the case here.
The description of is clear that proposals defined in the RFC are not to be included because they are considered standard, however there is no such clarification around extensions. In general, extension requirements are not all that clear because in several places (such as Lifetime) it is mentioned that an extension must be included, however they are all listed as recommended rather than required in the table. It also raises a question in that if an extension is required to be implemented as part of the specification, and required in every KeyPackage, should it just be a field rather than an extension?
Even that is not quite clear to me. At least to me it doesn't follow from Section 10.1. The last one at least, I can shed some light on, as we had this discussion before and I was arguing the same thing. From what I remember, the argument was the keeping it an extension rather than a field would force implementers to implement the Extension mechanism. I don't have a super strong opinion here, but I seem to remember that that's what it boiled down to. I generally agree, though, that some things around RequiredCapabilities (RC) and Capabilities in general is unclear. I discussed this with NAME and here are some examples/questions that came up: If a group doesn't have an RC extension, can we assume that all extensions/proposals in the spec are supported by everyone, or do we have to check all KeyPackages? Do we want all extensions/proposals in the spec to be MTI and thus implicitly supported by everyone? It might be useful to distinguish between basic (MTI) ones like Add, Remove, etc. and consider some optional such as ExternalInit or AppAck. Relatedly, should the extension in the KeyPackage list all the Spec proposals/extensions, too? Are they invalid if they do or if they don't? We might want to discuss the difference between RC and a potential group policy. The way I understand it, RC is not meant to be an allow list or a block list, but instead should specify a minimum requirement for all clients in the group. A question here would be: If it's not in the RC, but everyone's capabilities support it, can I use/send it?
Reacting to Konrad's points here: It seems like to to be sure, you always need to check. RC just helps ensure that the check will produce the right answer. I would propose the answer to this be "Yes, the proposals/extensions in the spec are MTI" If the answer to the prior question is as I propose, then you don't need to list spec proposals/extensions in Capabilities The difference between RC and checking everyone supports it is whether we expect it to be supported in the future. For Proposals, that might not matter; for GroupContext extensions, it matters more. It also seems like there's a fair argument that RequiredCapabilities should just be removed from the spec and replaced by some application-layer coordinated policy. I would be OK with that. To NAME point about mandatory extensions -- I would probably be OK with converting mandatory extensions to fields, especially with the better division of labor proposed in .
I kind of like the idea of RequiredCapabilities because it enforces the proper checks lower down. Application layer would need to proxy packets via a middleware type of layer which could potentially be messy.
Discussion on working call: Agreement to not list in-spec capabilities in RC/Capabilities, so all those things are MTI

the GroupContext. This extension lists the extensions and proposal types that must be  <ins> supported by all members of the group.  The "default" proposal and extension types defined in this document are assumed to be implemented by all clients, and need not be listed in RequiredCapabilities in order to be safely used. For new members, support for required capabilities is enforced by existing members during the application of Add commits.  Existing members should of course be in compliance already.  In order to ensure this continues to be the case even as the group's extensions can be updated, a GroupContextExtensions proposal is invalid if it contains a "required_capabilities" extension that requires non- default capabilities not supported by all current members. </ins> 10.2.


be perceived as creating a conflict of interest for a particular MLS DE, that MLS DE SHOULD defer to the judgment of the other MLS DEs.  19.  References 19.1.  URIs

This is a minimal section for MIME type registration of message/mls. It assumes that PR include wire_format in all MLS messages (making a format parameter unnecessary), and that all messages have the MLS version number encoded (making a version parameter unnecessary). These parameters can be added if needed.
When MLS messages are transmitted over a protocol which supports Content-Type / MIME Type negotiation, signal that the message is an MLS message. Because MLS messages are really a bag of formats today we should have a MIME type parameter which specifies the specific format included. message/mls;format={applicationgroupcontext}
This seems like a fine idea to me. Could go as a section in the protocol spec, or as a separate (small) spec.
Discussion on virtual interim: Section or separate doc? How do we handle versioning? Internal to the protocol (field in MLSMessage?) Field in the MIME type Different MIME type per version
I would prefer a short section in the protocol document. I think Internal to the protocol is cleanest. Likewise the format parameter becomes unnecessary if we add Welcome, PublicGroupState, and KeyPackage to wire_format (see issue )
NAME send a PR?
Asking for friend: would we want to define (or use) a structured syntax suffix ala ?
NAME If you mean for multiple formats, no, we have only one encoding. If someone wants to re-encode the protocol as XML or whatever, a MIME type will be the least of their troubles.
Fixed in

be perceived as creating a conflict of interest for a particular MLS DE, that MLS DE SHOULD defer to the judgment of the other MLS DEs. <ins> 18.6. This document registers the "message/mls" MIME media type in order to allow other protocols (ex: HTTP RFC7540) to convey MLS messages. </ins> 19.  References 19.1.  URIs


Terminology specific to tree computations is described in ratchet- tree-terminology.  We use the TLS presentation language RFC8446 to describe the <del> structure of protocol messages. </del>  3.

There are two changes in this PR: Pull the extension to the TLS syntax up into the Terminology section Add an extension that lets vectors have variable-length headers, allowing them to be efficient at small sizes while also accommodating large sizes All vectors are converted to the variable-length header representation. It seems like this should simplify implementation, since the encoder will no longer have to be told how many bytes of header to use for a given vector. This has been the source of several annoying interop bugs.
Looks great! I only have a small objection: there is now several valid ways to serialize a length, as the example shows, both and are valid ways to encode 37. This means that if you parse a structure, and later want to re-serialize it to check a signature for example, you need to remember how many bits were used to store the length. In short, we loose the property that . The fix is easy: can we add that "the length MUST be stored using the smallest amount of bits possible"?
Good point, updated!
I wasn't clear under which circumstances you would use a longer length with a number that would fit in a smaller range. If never, then the table should be:
DIscussion on virtual interim Some concern about being able to send gigantic stuff In adversarial cases, the 4-byte headers used for a lot of stuff might already be a problem Could also have length constraints decoupled from the syntax SIP experience shows that imposing arbitrary length limits isn't a good idea ... but applications should have a way to signal that things are too big TODO(NAME Advisory text "Beware, things can get big; consider imposing limits and clear failure cases for this" Consider streaming implementations, where you might not know that the whole message fits in memory Modulo the above TODO, clear to merge

Terminology specific to tree computations is described in ratchet- tree-terminology. <ins> 2.1. </ins> We use the TLS presentation language RFC8446 to describe the  <ins> structure of protocol messages.  In addition to the base syntax, we add two additional features, the ability for fields to be optional and the ability for vectors to have variable-size length headers. 2.1.1. An optional value is encoded with a presence-signaling octet, followed by the value itself if present.  When decoding, a presence octet with a value other than 0 or 1 MUST be rejected as malformed. 2.1.2. In the TLS presentation language, vectors are encoded as a sequence of encoded elements prefixed with a length.  The length field has a fixed size set by specifying the minimum and maximum lengths of the encoded sequence of elements. In MLS, there are several vectors whose sizes vary over significant ranges.  So instead of using a fixed-size length field, we use a variable-size length using a variable-length integer encoding based on the one in Section 16 of RFC9000.  (They differ only in that the one here requires a minimum-size encoding.)  Instead of presenting min and max values, the vector description simply includes a "V". For example: Such a vector can represent values with length from 0 bytes to 2^62 bytes.  The variable-length integer encoding reserves the two most significant bits of the first byte to encode the base 2 logarithm of the integer encoding length in bytes.  The integer value is encoded on the remaining bits, in network byte order.  The encoded value MUST use the smallest number of bits required to represent the value. When decoding, values using more bits than necessary MUST be treated as malformed. This means that integers are encoded on 1, 2, 4, or 8 bytes and can encode 6-, 14-, 30-, or 62-bit values respectively. For example, the eight-byte sequence c2 19 7c 5e ff 14 e8 8c (in hexadecimal) decodes to the decimal value 151288809941952652; the four byte sequence 9d 7f 3e 7d decodes to 494878333; the two byte sequence 7b bd decodes to 15293; and the single byte 25 decodes to 37 (as does the two byte sequence 40 25). The following figure adapts the pseudocode provided in RFC9000 to add a check for minimum-length encoding: The use of variable-size integers for vector lengths allows vectors to grow very large, up to 2^62 bytes.  Implementations should take care not to allow vectors to overflow available storage.  To facilitate debugging of potential interoperatbility problems, implementations should provide a clear error when such an overflow condition occurs. </ins> 3.


The tree hash of a tree is the tree hash of its root node, which we define recursively, starting with the leaves. <del> As some nodes may be blank while others contain data we use the following struct to include data if present. </del> The tree hash of a leaf node is the hash of leaf's "LeafNodeHashInput" object which might include a "LeafNode" object depending on whether or not it is blank.

The tree hash of a tree is the tree hash of its root node, which we define recursively, starting with the leaves.  The tree hash of a leaf node is the hash of leaf's "LeafNodeHashInput" object which might include a "LeafNode" object depending on whether or not it is blank.


A GroupContextExtensions proposal is used to update the list of extensions in the GroupContext for the group. <del> "struct { Extension extensions<0..2^32-1>; } GroupContextExtensions; " </del>  A member of the group applies a GroupContextExtensions proposal with the following steps:

A GroupContextExtensions proposal is used to update the list of extensions in the GroupContext for the group.  <ins> "struct { Extension extensions<V>; } GroupContextExtensions; " </ins> A member of the group applies a GroupContextExtensions proposal with the following steps:


Verify that the signature on the KeyPackage is valid using the public key in "leaf_node.credential".  11.2. Within MLS, a KeyPackage is identified by its hash (see, e.g.,

This change ensures that the "init" key pair associated to the key package is one-time-use, at least to the degree that KeyPackages are one-time-use. The init key pair is used to encrypt/decrypt the Welcome message, then never again. The main drawback is requiring the KeyPackage sender to generate an additional key pair, which requires more computation and more entropy.

Verify that the signature on the KeyPackage is valid using the public key in "leaf_node.credential". <ins> Verify that the value of "leaf_node.public_key" is different from the value of the "init_key" field. </ins> 11.2. Within MLS, a KeyPackage is identified by its hash (see, e.g.,


Given these inputs, the derivation of secrets for an epoch proceeds as shown in the following diagram: <del> A number of secrets are derived from the epoch secret for different </del>  purposes: <del> The "external secret" is used to derive an HPKE key pair whose </del>  private key is held by the entire group: The public key "external_pub" can be published as part of the

Looks good! I would maybe add 1 sentence explaining the rule for what is called a secret and what is called a key. The rule says that a secret will go through some key derivation to obtain a key/keys, is that right?
NAME Yep, that's exactly right. Added a sentence. Thanks for the feedback everyone!
Reopening due to incomplete discussion and inaccuracy. The claims presented are incorrect in response by NAME demonstrate a misunderstanding of the roles of these keys and uses, and constitutes a change to the fundamental uses thereof. The authenticationsecret is NOT a public value nor was it ever introduced as such. If an application is using it as a public value, that onus is on the developers, and I advise against using Cisco's implementation as a standard for MLS. As shown in the name and all original discussion, the authenticationsecret is intended as a secret value (or else 'public' in the same way that the group secret is 'public). It is used for further derivation to support authentication. This should be corrected, and such foundational changes without WG discussion should be avoided.
Clearly we have a disagreement here NAME but maybe let's hold off on attributing misunderstanding. It seems like you are claiming that in order for MLS to be secure, the needs to be protected to the same level as the . What is this based on? If that is true, then we should probably remove the altogether. The "out-of-band" protocol in which it would be confirmed would have to be part of the MLS security analysis, thus no longer out-of-band.
NAME that is not the claim at all - different uses imply different compromise risks. This should not be a new concept to understand. As demonstrated in the terminology, the authenticationsecret is linked to authentication, not confidentiality. MLS, per specification, outsources authentication (the AS is not explicitly defined and all entity authentication within MLS relies on it). Even though it is definitionally out-of-scope and therefore out-of-band (certificates, authentication verification are entirely out-of-band of the protocol spec), we do not say that signature secret keys can be "public" - yet you are asserting that an authenticationsecret which can support epoch-level authentication would be public. The authenticationsecret is private in the same way that the exportervalue is: based on key schedule derivation, compromise of these keys should not undermine MLS, but secrecy of them can be leveraged. So, unless you are now proposing removing mention of certificates, etc. from the core protocol, there really isn't a logical basis for removing 'out-of-band' authentication mechanisms.
What I'm trying to get at here is: What is the negative consequence if an is known by someone other than the members of a group? For example, leaking a signature private key would enable the attacker to impersonate the corresponding member. Leaking the would allow the attacker to decrypt all of the group's messages. What is the bad consequence of an becoming public?
NAME I am also assuming that this push to use epoch-level authentication values as public codes is based on Cisco's implementation choice. I would be curious to know why there is such insistence that all implementations also go about using authentication in the same way. If an implementation chooses to use it as a secret value to then derive an authentication code based on yet other information, why is that a problem?
Publishing the authenticationsecret as a public value has a similar effect as publishing the exportervalue as public - it is not an issue for MLS per protocol spec, but it would definitely be an issue for the application if it is then used for something secret thereafter. One reason not to specify these as public values is that they are derived following the conditions placed on keys and therefore could be used as secret values if desired by an application. Forcing or annotating them as immediately public, or as final 'codes', when such a use as a secret key is an out-of-scope potential is artificial and unnecessary.
I'm not pushing anything, I'm just trying to get the protocol to be clear. If this thing needs to be secret, then it needs to be secret, and we need to document that so that people keep it secret. Thanks for elaborating more on your concerns. I see what you're saying -- basically the same point that NAME and I discussed . I think the core of the disagreement here is what we mean when we say that a value is "public" or "secret". I mean something very specific: That the security guarantees of MLS will not be harmed if a public value is provided to the attacker, and they will be harmed if a secret value is provided to the attacker. Importantly, "public" does not mean that the value must be published, or that the security guarantees of an application using MLS will not be harmed if you publish the value. It seems to me that in that specific taxonomy, the is "public" and not "secret". Do you disagree?
BTW, the notion of something being "public" at one layer but "secret" at another is not new. For example, values exported from TLS aren't secret in the sense that publishing them would compromise TLS. You could authenticate your TLS connection by exporting a value and shouting it through megaphones across a crowd. But the main way people use them in practice is as secrets in some other protocol. Every WebRTC call uses SRTP keys that are exported from TLS, and many people would be very sad if these "public" values were published.
If it would help, I would be happy to add some text to the Security Considerations clarifying this distinction. Basically, draw a clear boundary around which values have to be kept confidential to the client in order to realize the security properties of the protocol. But I still think is a clearer name. Doesn't mean you have to use it in any particular way in your application; doesn't prevent it being used as a shared secret; but also doesn't imply that the MLS library has to worry about its secrecy.
Within the bounds of that particular taxonomy there is not an issue. However, while you may be following a given taxonomy, that is not a general one, one that would be widely understood by implementors, nor is it specified in the protocol spec. It is not an unreasonable taxonomy, but it certainly not a general one. If you are looking for MLS usage spec to align to your taxonomy then I do think that there should be a clarification section added. BTW as a point case on more standard terminology use, the TLS 1.3 spec uses the exact phrasing for exporters as "exportermastersecret", from which the actual exporter may be derived. This aligns very closely to the authenticationsecret, from which an authentication code later may be derived. TLS is generic in 'exporters' after an exportermastersecret is derived. Thus there is alignment of the term 'secret' to things that can be used for keys (have sufficient entropy), not as an alignment to public/secret at different layers. I am strongly opposed to the use of authenticationcode, and see no clear definition for that in your customized taxonomy. As such, it would follow the more standard interpretations, such as message authentication codes, etc. which are indeed explicitly public values. Codes are normally an end-state in themselves, not keys. Perhaps it is a clearer name for the Cisco implementation design, but it is not a clear name for something that can/should be used for further derivations as may be more generally applicable. Following the TLS norm for exporters, the name would be "authenticationmastersecret". The shortened "authenticationsecret" was a good name. If you are going to provide a write-up for the custom taxonomy, then an adjustment to "authenticationexporter" could perhaps be made.
The notion of a having boundary to the protocol and values that may or may not safely be exported across the boundary is not exotic or customized. You see it in FIPS 140-2, PKCS, and WebCrypto, and in tools like ProVerif and Tamarin. The only choice I'm making is in the specific words, in particular reserving "secret" for "something that should not be exposed outside the protocol implementation because it would compromise the protocol". I hope you agree that is different from say in this regard. It would be OK for an MLS implementation to have an API for an application to access the , but it would not be OK to have an API to access the . I'm not wedded to , I just want something other than . If you want to align with the exported values (to which I agree it is similar), we could call it an .
The distinction between those documents and tools, and the MLS spec is that those tools clearly define the boundaries and terminology, vs. just anyone making terminology assumptions without them being specified in the document. Not to be pedantic, but if you want to assume specific meanings for terms relative to a threat model for MLS only, versus a general usage ("exporter secrets" being secrets used in other things), then you really do need to specify those assumptions within the specification itself and not various side discussions. Such specification is part of what a standard is meant to do. Within other documents this is clearly defined - i.e., what is known as a threat model - such as one sees in ProVerif and Tamarin. Assuming that you are going to add such a taxonomy, and that it follows that described by you in , then the correct term would be "authenticationkey". To quote you what you said there, which correctly aligns to a reasonable usage of the authenticationkey. pointed out that there is a lack of consistency in the usage of "keys" vs "secrets" throughout the document - something that still has not been addressed and should not be dropped from the issue for edits. Regardless of what taxonomy is being used or even assumed, there is no consistency in it in the spec. You seem to be commenting on adjustments only for the "key" and "secret" uses that appear as variables, but the commentary within the spec interchangeably use these terms for the same thing. Whatever is used should be applied consistently. It is not reasonable to assume that developers will correctly guess reasons for terminology separations particularly when they are inconsistently enforced.
On your latter point about usage of "keys" vs "secrets" throughout the document — Sorry if I misinterpreted your intent here. If you're seeing places where there are inconsistencies, it would be helpful if you could highlight those more specifically, or ideally send a PR to fix them.
NAME had a call about this today and hashed out a couple of things: We should be clear in general about what we mean by secrets/keys, in that we generally use them interchangeably, but sometimes use one or the other to indicate how a specific value is used The need for secrecy of the value labeled depends on the context in which it is used. So we should provide some text to suggest that there are different ways to use it, and use a fairly neutral name. NAME suggested , which I like a lot. I have attempted to implement these in
The spec currently uses a mix of terminologies for keys/secrets. In the key schedule we have the following: senderdatasecret | "sender data" encryptionsecret | "encryption" exportersecret | "exporter" authenticationsecret | "authentication" externalsecret | "external" confirmationkey | "confirm" membershipkey | "membership" resumptionsecret | "resumption" It would be best to provide a coherency to how we use these terms and check for consistency throughout, to avoid confusion for readers: A simple solution would be to use "secret" for all cases, i.e., changing membershipkey to membershipsecret.Another alternative, which may take more refining but could be useful to readers is to separate out terms based on items that are for internal protocol use versus external protocol use. For example, encryption, confirmation, membership, etc., would use "key" and exporter, authentication, etc., could use "secret", or vice versa. --Break-- Ref the mailinglist on confirmation vs authentication secrets, perhaps the naming of these two are a little misleading - authentication normally being within bounds of a protocol while confirmation is external to it. This confusion probably sparked some of that discussion to begin with. It could_ be worth considering switching those terms for clarity.
I actually think we have a fairly coherent taxonomy here: "secret" is something that gets fed into HKDF to derive further things "key" is something that gets fed into another algorithm, e.g., HMAC Looking through the list, the ones that deviate from this pattern are: , which is exported to the application but expected to be kept private , which is exported to the application but not expected to be kept private Given that, I would propose we update the table in question to something like the following, using because that's how it's used, and to follow the "security code" terminology used by applications:
Things are easier to understand like this! The "Purpose" column is quite valuable I think, I remember I was quite overwhelmed when I first read it and had no idea what all those secret / keys were used for. This helps to understand the big picture.

Given these inputs, the derivation of secrets for an epoch proceeds as shown in the following diagram:  <ins> A number of values are derived from the epoch secret for different </ins> purposes:  <ins> (In general, we use "secret" to refer to a value that is used derive further secret values, and "key" to refer to a value that is used with an algorithm such as HMAC or an AEAD algorithm.) The "external_secret" is used to derive an HPKE key pair whose </ins> private key is held by the entire group: The public key "external_pub" can be published as part of the


GroupSecrets object with the "psks" field set, the receiving Client includes them in the key schedule in the order listed in the Commit, or in the "psks" field respectively.  For resumption PSKs, the PSK is <del> defined as the "resumption_secret" of the group and epoch specified in the "PreSharedKeyID" object.  Specifically, "psk_secret" is computed as follows: </del>  Here "0" represents the all-zero vector of length "KDF.Nh".  The "index" field in "PSKLabel" corresponds to the index of the PSK in

GroupSecrets object with the "psks" field set, the receiving Client includes them in the key schedule in the order listed in the Commit, or in the "psks" field respectively.  For resumption PSKs, the PSK is  <ins> defined as the "resumption_psk" of the group and epoch specified in the "PreSharedKeyID" object.  Specifically, "psk_secret" is computed as follows: </ins> Here "0" represents the all-zero vector of length "KDF.Nh".  The "index" field in "PSKLabel" corresponds to the index of the PSK in


9.6. <del> The main MLS key schedule provides a "resumption_secret" that is used as a PSK to inject entropy from one epoch into another.  This </del>  functionality is used in the reinitialization and branching processes described in reinitialization and sub-group-branching, but may be used by applications for other purposes. Some uses of resumption PSKs might call for the use of PSKs from historical epochs.  The application SHOULD specify an upper limit on <del> the number of past epochs for which the "resumption_secret" may be </del>  stored. 9.7. <del> The main MLS key schedule provides a per-epoch "authentication_secret".  If one of the parties is being actively impersonated by an attacker, their "authentication_secret" will differ from that of the other group members.  Thus, members of a group MAY use their "authentication_secrets" within an out-of-band authentication protocol to ensure that they share the same view of the group. </del>  10.

9.6.  <ins> The main MLS key schedule provides a "resumption_psk" that is used as a PSK to inject entropy from one epoch into another.  This </ins> functionality is used in the reinitialization and branching processes described in reinitialization and sub-group-branching, but may be used by applications for other purposes. Some uses of resumption PSKs might call for the use of PSKs from historical epochs.  The application SHOULD specify an upper limit on  <ins> the number of past epochs for which the "resumption_psk" may be </ins> stored. 9.7.  <ins> The main MLS key schedule provides a per-epoch "authentication_code". If one of the parties is being actively impersonated by an attacker, their "authentication_code" will differ from that of the other group members.  Thus, members of a group MAY verify out of band that their "authentication_code" values are all the same, in order to ensure that they share the same view of the group. </ins> 10.


The ciphertext content is encoded using the MLSCiphertextContent structure.  In the MLS key schedule, the sender creates two distinct key ratchets for handshake and application messages for each member of the group. When encrypting a message, the sender looks at the ratchets it

Also requires that padding be all-zero. Before, the sender was allowed to put arbitrary octets in there.
Looks good to me. NAME I think this covers your issue on the mailing list
Discussed at 19 May interim. Decision to merge.
As discussed , we should refactor so that the padding is not length-delimited, something like: In the MLSCiphertextContent struct we have a field for padding represented using a variable-size vector which helps hide information about the length of content; notably application messages. But because of the way we encode variable-size vectors it turns out you can't actually pad the rest of the content with certain number of bytes: namely 0, 65, 16386, 16387. (E.g. if you pad with a vector of 63 bytes the TLS encoding actually produces 64 bytes for padding: 63 bytes + 1 length byte. But if you pad with 64 bytes you end up with a 66 byte encoding as now you need 2 bytes for the length. Not being able to pad with 0 is OK. But 65 is kind of annoying. E.g. if you want to pad to a target content length X bytes then if the rest of the content ends up being X-65 bytes long you are out of luck. (Assuming I'm right about how this all works) it seems a bit silly to me for a padding scheme to have somewhat arbitrary gaps like that; especially at a not unreasonable number of bytes to want to pad with. If I didn't miss something, and you all agree that its better to avoid having gaps like that then how about represent padding as, say, a fixed length vector's? Joël Amazon Development Center Austria GmbH Brueckenkopfgasse 1 8020 Graz Oesterreich Sitz in Graz Firmenbuchnummer: FN 439453 f Firmenbuchgericht: Landesgericht fuer Zivilrechtssachen Graz

The ciphertext content is encoded using the MLSCiphertextContent structure. <ins> The "padding" field is set by the sender, by first encoding the content (via the "select") and the "auth" field, then appending the chosen number of zero bytes.  A receiver identifies the padding field in a plaintext decoded from "MLSCiphertext.ciphertext" by first decoding the content and the "auth" field; then the "padding" field comprises any remaining octets of plaintext.  The "padding" field MUST be filled with all zero bytes.  A receiver MUST verify that there are no non-zero bytes in the "padding" field, and if this check fails, the enclosing MLSCiphertext MUST be rejected as malformed. </ins> In the MLS key schedule, the sender creates two distinct key ratchets for handshake and application messages for each member of the group. When encrypting a message, the sender looks at the ratchets it


The key and nonce provided to the AEAD are computed as the KDF of the first "KDF.Nh" bytes of the ciphertext generated in the previous section.  If the length of the ciphertext is less than "KDF.Nh", the <del> whole ciphertext is used without padding.  In pseudocode, the key and nonce are derived as: </del>  The Additional Authenticated Data (AAD) for the SenderData ciphertext is the first three fields of MLSCiphertext:

The key and nonce provided to the AEAD are computed as the KDF of the first "KDF.Nh" bytes of the ciphertext generated in the previous section.  If the length of the ciphertext is less than "KDF.Nh", the  <ins> whole ciphertext is used.  In pseudocode, the key and nonce are derived as: </ins> The Additional Authenticated Data (AAD) for the SenderData ciphertext is the first three fields of MLSCiphertext:


find out the plaintext by correlation between the question and the length. <del> The content and length of the "padding" field in "MLSCiphertextContent" can be chosen at the time of message encryption by the sender.  It is recommended that padding data is comprised of zero-valued bytes and follows an established deterministic padding scheme. </del>  16.2.

find out the plaintext by correlation between the question and the length.  <ins> The length of the "padding" field in "MLSCiphertextContent" can be chosen at the time of message encryption by the sender.  Senders may use padding to reduce the ability of attackers outside the group to infer the size of the encrypted content. </ins> 16.2.


13.1.7. <del> An AppAck proposal is used to acknowledge receipt of application messages.  Though this information implies no change to the group, it is structured as a Proposal message so that it is included in the group's transcript by being included in Commit messages. An AppAck proposal represents a set of messages received by the sender in the current epoch.  Messages are represented by the "sender" and "generation" values in the MLSCiphertext for the message.  Each MessageRange represents receipt of a span of messages whose "generation" values form a continuous range from "first_generation" to "last_generation", inclusive. AppAck proposals are sent as a guard against the Delivery Service dropping application messages.  The sequential nature of the "generation" field provides a degree of loss detection, since gaps in the "generation" sequence indicate dropped messages.  AppAck completes this story by addressing the scenario where the Delivery Service drops all messages after a certain point, so that a later generation is never observed.  Obviously, there is a risk that AppAck messages could be suppressed as well, but their inclusion in the transcript means that if they are suppressed then the group cannot advance at all. The schedule on which sending AppAck proposals are sent is up to the application, and determines which cases of loss/suppression are detected.  For example: The application might have the committer include an AppAck proposal whenever a Commit is sent, so that other members could know when one of their messages did not reach the committer. The application could have a client send an AppAck whenever an application message is sent, covering all messages received since its last AppAck.  This would provide a complete view of any losses experienced by active members. The application could simply have clients send AppAck proposals on a timer, so that all participants' state would be known. An application using AppAck proposals to guard against loss/ suppression of application messages also needs to ensure that AppAck messages and the Commits that reference them are not dropped.  One way to do this is to always encrypt Proposal and Commit messages, to make it more difficult for the Delivery Service to recognize which messages contain AppAcks.  The application can also have clients enforce an AppAck schedule, reporting loss if an AppAck is not received at the expected time. 13.1.8. </del> A GroupContextExtensions proposal is used to update the list of extensions in the GroupContext for the group.

The way the spec is written now, clients need to implement all proposal types, and they must react to appropriately to all valid proposals, including AppAck. AppAck in a large, busy group could result in a situation where clients with longer latency connections to the DS (perhaps in a different country, using satellite, or using some wireless networks) might be effectively prevented from sending any messages, or worse could be prevented from sending AppAcks because their proposals are always a few hundred milliseconds late. I think AppAcks still have a place is relatively small groups with low message volumes, but I have a few suggestions: 1) Describe this issue either in the AppAck proposal section or in the security considerations section 2) I think it is reasonable for a group to consider some proposal types invalid. If there is consensus with this position, I'd like to make this clear with an extra sentence or two. If that is not the consensus, then I will propose that only Add, Remove, and Update are assumed to be supported by each group, and other proposal types have to be explicitly added.
My team had similar comments. I agree that this shouldn't be a 100% necessary thing to support.
I would differentiate between "support" in the sense of "if you receive one, do something sensible with it" vs. "send this under some circumstances". My understanding was that the requirement here was the former, and even that the definition of "sensible" was up to the implementation. The thinking being that if an application wanted to get some anti-suppression properties, it could send and verify these on some schedule, but when exactly they would be sent would be up to the application. Does that alleviate some of the concern here? If so, I would totally be open to clarifying that. To NAME point about latency, it seems like we could add an parameter to AppAck so that you could report on past epochs' messages in a future epoch. On considering proposal types invalid -- I think there has always been a notion that applications might have policies on which proposals are acceptable. Not just at the proposal type level, but at even finer granularities. Think about things like groups where only certain members are allowed to add/remove participants. I would also be fine laying this out more explicitly, though maybe it would be a more natural fit in the architecture document.
Sorry, meant to delete a comment, closed the issue instead.
In the past I've expressed reservations about AppAck being included in the spec. I don't think it's sufficiently general (it only handles a one specific pattern of message loss and won't work for others, per NAME comment), and I don't like that we don't specify how to handle dropped messages in any way. The AppAck functionality is likely to just be re-invented at the application-level because of this. For example, AppAck can't be used to implement read receipts It doesn't help detect the suppression of handshake messages It doesn't handle cases where some messages must be delivered and others may be dropped So instead I would propose removing AppAck from the spec.
I've opened a PR to that effect in .
Virtual interim 2022-05-19 Agreement that the bar for support is quite low ... but you don't get a security property unless members actually do something more than the minimum That sort of implies that you want the group to opt in to this mechanism So we should have a new proposal type in a separate I-D
(To be clear, AppAck should move from the main spec here to its own I-D.)
One comment that renumbering the groupcontextextensions proposal was probably a bad idea from a process perspective which we should avoid doing in the future. In this case it seems unlikely that many people have implemented it who could not make a breaking change.

13.1.7.  A GroupContextExtensions proposal is used to update the list of extensions in the GroupContext for the group.


confirmation_tag by way of the key schedule will confirm that all members of the group agree on the extensions in use. <del> 13.1.9. </del>  Add and Remove proposals can be constructed and sent to the group by a party that is outside the group.  For example, a Delivery Service

confirmation_tag by way of the key schedule will confirm that all members of the group agree on the extensions in use.  <ins> 13.1.8. </ins> Add and Remove proposals can be constructed and sent to the group by a party that is outside the group.  For example, a Delivery Service