Code change:
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)

Related discussion:
text: 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> <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: