Code change:
This PR adds a RequiredCapabilities extension that allows the members of a group to impose a requirement that new joiners support certain capabilities. The extension is carried in the GroupContext and verified at Add time (including on external commits). In the interim 2021-10-04, we briefly discussed whether this mechanism needed to be included in the core protocol. Because of its interactions with Add and GroupContextExtensions proposals, it seems worth having in the main protocol.
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
Many protocols which have an extensibility mechanism have a way to indicate that understanding an extension is mandatory for certain behaviors. For MLS, this would mean having a way to require a member understand an MLS extension before it can join a group. The three extension types defined currently are: MLS ciphersuites, MLS extension types, and MLS credential types. One logical way to add mandatory extensions would be to pick one of these types (the MLS extensions type) and use the high order bit or a specific range to indicate that a member must understand and implement the extension to join a group which uses that extension. The IANA values could be changed as follows: Values from 0x8000-0xffff indicate understanding the extension is required 0x7f00 - 0x7fff Reserved for Private Use (not required) 0xff00 - 0xfff Reserved for Private Use (required) If a required ciphersuite or credential becomes necessary, a new extension type could be created to add an extension in the KeyPackage or GroupInfo which would be required and would indicate support for the mandatory ciphersuites or credentials. In order to prevent some other entity from adding an MLS client that does not support a mandatory extension, we already have the KeyPackage Client Capabilities, which lists which extensions are supported by the client.
Note that though this might seem "now or never", I don't think it really is. Consider TLS by way of analogy -- it has no mechanism for specifying that extensions are required. But if a server gets a ClientHello that is missing some extension that it requires, it can just abort the handshake. Likewise in MLS, the member adding a new joiner to the group can examine that joiner's KeyPackage to see if they support the extensions needed for the group. So I think you could implement this as a non-mandatory GroupContext extension, which would state the extensions required by the group so that all members apply the same requirements for new joiners.
While you can implement this "out-of-band", that makes it implicit rather than explicit behavior.
Discussed at 20211004 interim, liked this functionality. But, instead of IANA registry use field (or extension) in group context to indicate which extensions are required. NAME to generate PR.
Discussed with NAME and incorporated this functionality into PR

Related discussion:
text: "struct { Extension extensions<0..2^32-1>; } GroupContextExtensions; " <del> A member of the group applies a GroupContextExtensions proposal by removing all of the existing extensions from the GroupContext object for the group and replacing them with the list of extensions in the proposal.  (This is a wholesale replacement, not a merge.  An extension is only carried over if the sender of the proposal includes it in the new list.)  Note that once the GroupContext is updated, its inclusion in the confirmation_tag by way of the key schedule will confirm that all members of the group agree on the extensions in use. </del> <ins> A member of the group applies a GroupContextExtensions proposal with the following steps: If the new extensions include a "required_capabilities" extension, verify that all members of the group support the required capabilities (including those added in the same commit, and excluding those removed). Remove all of the existing extensions from the GroupContext object for the group and replacing them with the list of extensions in the proposal.  (This is a wholesale replacement, not a merge.  An extension is only carried over if the sender of the proposal includes it in the new list.) Note that once the GroupContext is updated, its inclusion in the confirmation_tag by way of the key schedule will confirm that all members of the group agree on the extensions in use. </ins> 11.1.9.