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)


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 .*


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.


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.


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


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


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


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.


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.


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 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.


This seems to be a layer of indirection that doesn't add any value to implementation.
The Credentials section claims there is an epoch id field in key packages. I don't think that actually exists. My apologies if this is a duplicate issue.
Addressed in but may need to be pulled out depending on the fate of that PR
contains all of the Proposals that were previously sent within the same epoch. This seems unnecessary, since all members should have them anyway. The list of in the actual message should be enough for members to parse and validate a message. We should discuss if we can de-duplicate this, since the list of Proposals might be quite long.
The current definition of is as follows; Maybe this issue is obsolete?
Seems it is now obsolete indeed.
I don't have a strong opinion on this question, but this change seems sensible to me. The use of LeafNodeRef requires either (a) a linear scan through the tree, which is time-expensive in large trees, or (b) keeping an index that maps LeafNodeRef values to indices, which is storage-intensive in large trees. IIRC, the change to LeafNodeRef was made in the context of NAME work to make the tree description less tied to the array representation. In general, that is the right thing to do, and it's good that we did it. But we still have leaf indices anyway, for . Since we still have the concept, we might as well use it where it makes sense. And it certainly seems to me like and make sense, since they are internal to the protocol. In case folks are worried about the application-facing API -- MLS stacks can certainly be more intelligent, e.g., exposing or or . But they can do that regardless of this PR.


Brendan to revise based on Rohan suggestion.
Interim 2022-05-26: Concerns around making the group_id totally random Malicious group creator can choose it malicious Federated systems get worse randomness Using AEAD.Nk depends on ciphersuite, could be awkward NAME will revise to say: Here are the requirements that group ID must satisfy If you don’t have an alternative plan, do it randomly
Group ids are signed to bind certain information to specific groups. But I can imagine a case where a user mis-uses the group id to be something like a "room title" which lets an adversary control the group id and introduces potential problems. If we can prove that it's fine for a group id to be reused, then should we get rid of group ids? If not (as I expect), then group ids need to be guaranteed to be unique [globally per signing key] and we need to specify that.
Discussed at 19 May interim. NAME to generate PR documenting fresh random value approach.
Virtual interim 2022-05-19: Agreement that some sort of uniqueness requirement is worthwhile Main question is what scope of uniqueness should be Globally? Within scope of DS? Fresh random value? Simple, but precludes application usage of the field Also could help avoid metadata leakage Client / DS should probably still check for collisions NAME to write up a PR on the fresh random value idea
Group IDs must be generated randomly by creator Clients and DS must check that group ids are unique as far as they know


Replaces This PR attempts to add precision to the credential validation requirements in the document, along the lines NAME suggests in , in the spirit of what NAME wrote up in and drawing inspiration from what RFC 6125 does in the context of TLS. It seems like this gives a clearer definition of (a) what the AS's job is and (b) when it needs to be done.
While this is spread out throughout the document, it would be beneficial to spell out in which scenarios a Credential MUST be verified (with the AS): When fetching a KeyPackage to add a new member When discovering a KeyPackage in an AddProposal from another member When joining a new group (verify all other members' Credentials), both when receiving a Welcome message and when joining through an External Commit When a member updates its leaf ... anywhere else?
Thanks for picking it up!


As NAME pointed out on the mailing list, this reference does not need to be normative.


NAME thanks, good catch. should be fixed now.
Comment by NAME Algorithm entries use two octets (65535) but only allow 255 Private Use entries. I think that might be a little low. Maybe allow 0xf000 - 0xffff or something ?
I agree this should be changed. CCing NAME who I believe initially proposed this.


As discussed in the virtual interim 2022-12-16, it is possible that extensions might be tempted to re-use HPKE public keys created for MLS. Even within MLS, we have two different public-key encryption cases, Welcome and Commit (though these currently use different keys). Currently, for Commit encryption, the HPKE parameter is set to the group context. The content of is actually undefined for Welcome encryption (!) This PR makes the following changes: Define general and functions that populte the HPKE parameter with an encoded label and context Apply these to Commit and Welcome encryption: Commit: label="UpdatePathNode", context= Welcome: label="Welcome", context= Add an IANA registry of public-key encryption labels (as for signing) There is also a bit of reorganization of the Ciphersuites section to split it into three subsections.
Note that any previous analysis of MLS had to take this particular reuse into account. This can't be the case for any arbitrary extensions reusing the key. So if MLS uses an extension that reuses the key, none of the existing analysis applies, which is a much bigger issue. This said, this reuse did decrease security -- it weakened FS in that a joiner had to hold on to their HPKE leaf key, which also allowed to decrypt the init secret from the welcome message, long after joining.
I think this captures what we discussed during the interim. FWIW, I'm wondering if we didn't have a domain separation issue in the past when we only had one HPKE public key in KeyPackages and used it for both the Welcome subsequently for UpdatePath as well. Either way, I think this is a good defense-in-depth mechanism.


This updates the language around how certificate chains are constructed: We allow certificates to be omitted if the client knows any recipients will already possess the omitted certificate. We allow certificate chains to be out-of-order and potentially contain unused certificates. This is nice because it means less data needs to be sent on the wire, and also provides a method for root certificates to be rotated gracefully (as clients can provide chains for both old and new roots).
NAME it might be useful to also add this language from RFC 8446. It would make the process exactly the same as TLS which makes implementation easier. >The sender's certificate MUST come in the first >CertificateEntry in the list. Each following certificate SHOULD >directly certify the one immediately preceding it. Because >certificate validation requires that trust anchors be distributed >independently, a certificate that specifies a trust anchor MAY be >omitted from the chain, provided that supported peers are known to >possess any omitted certificates.
I think that is fine and is consistent with RFC8446. Don't we already have a better mechanism for rotating certificates? As soon as a member has a new certificate chain, it can update its LeafNode in an UpdatePath. This semantics of X.509 certificate bundles are already a bit fuzzy. Such a change will make validation more complicated and more error prone, when this seems completely unnecessary.
The scenario that this would address is: say that a root CA is compromised and needs to be replaced. Distributing the new CA certificate likely requires a software release, which not everyone will install simultaneously. During the rollout, some users will know only the old compromised CA and some users will know only the new CA and not the compromised one. To preserve interop during the rollout, users need to include two intermediates in their certificate chain: an intermediate signed by the old root that signs their leaf, and an intermediate signed by the new root that signs their leaf. (In the exact scenario I'm thinking of, the intermediate private key is held by the user, so it would not be compromised with the root. Though there may be other arrangements where this flexibility helps.) In terms of implementation complexity: the X509 chain validation libraries I'm aware of already support path building because it's necessary for TLS, so I would expect those could be used
This is actually the reason why I think it makes sense to adopt the same mechanism. For example, if you use OpenSSL to validate the chain it does not take a chain as input. It takes a collection of certificates, a set of trusted root certs, and the leaf you want to validate. I believe not making this change has a higher complexity.
I'm sorry I missed this previously, but this requirement was a source of trouble in TLS and was removed in RFC 8446, which allows extra certificates in the middle. Was there a conscious decision to be more strict here.
I don't recall this ever being discussed. I think it was just added and nobody ever objected.
NAME can you elaborate a little bit? The way I read it the requirement is just that there is a valid signature chain, i.e. that one certificate signs the next and so on.
NAME - I deliberately reverted to the original TLS level of strictness here, in hopes that in starting over with a new ecosystem we could reset things to a cleaner state. Do you think there are legitimate reasons for violating the strict rule, or is it just a common misconfiguration in TLS servers? I would not really be inclined to accommodate the latter. NAME - I believe what NAME is envisioning are cases where there are multiple valid chains, say via old and new versions of an intermediate certificate. So you might do something like provide a "chain" of the form (leaf, oldintermediate, newintermediate), with the idea that the client could verify using whichever one makes it happy.
NAME take a look at the comment I left on the PR. I can see your point about starting over as being valid, but one thing that also caught my eye in the TLS RFC was language around allowing a root CA to be left out of the chain since those could potentially already exist in a local trust store.
I see. Thanks NAME for the clarification. Maybe we can phrase it in such a way that whatever the credential includes (added intermediary certificates or left-out root certificates) in the end the local client MUST be able to create a continuous certificate chain from the leaf to the root. Do we need to define how the client does that? Since we're already pushing everything else related to authentication into the AS, I'm inclined to say no.


…oundaries defined by the CDDL
We should add something like: " within the syntactical boundaries defined by the CDDL in Figure 1."


This derives from the discussions on issue
The commit applies the changes suggested in the previous reviews. With those changes, I think we have a good basis for discussing a unified solution in the working group.
Thanks NAME for the review. I have applied the editorial changes. You are asking for something more regarding the "specific logic" required when supporting zero-length CID. There are in fact two pieces to that: logic at the receiver, i.e., the node that chose to receive packets with zero-length CID; and logic at the sender, i.e., the node that accepts to send data on multiple paths towards a node that uses zero-length CID. This is explained in details in the section "Using Zero-Length connection ID", so I guess what we need is a reference to that section.
Comment by NAME (on PR ): I think we expand this section a bit as we found getting the delay right was non-trivial. QUIC time-stamp would work for sure. But if one decides not to use QUIC timestamp, we probably want to explain what is needed to be done here.
I have submitted PR for this one.
Closing as got merged.
In section 3.1, we wrote When the multipath option is negotiated, clients that want to use an additional path MUST first initiate the Address Validation procedure with PATHCHALLENGE and PATHRESPONSE frames described in of ]. After receiving packets from the client on the new paths, the servers MAY in turn attempt to validate these paths using the same mechanisms. With multipath, we cannot simply write that the server MAY validate the new path. There is a risk of a malicious client that operates as follows: client creates path from its own address, A. Path is validate by server client starts long download from server client creates a second path to server using a spoofed address, B client starts address validation for B but does not care about the response from the server client stops acknowledging packets over the first path to force the server to move packets to the second one and then opportunistically acknowledges packets that should have been sent by the server over that second packet (SPNS would probably make such an attack simpler than MPNS) We probably need to start working on the security considerations section of the draft.
This is already covered in the QUIC threat model. The server is supposed to validate new paths by sending a "path challenge" on that path, then waiting for a challenge response. If the client spoofs a path, it will never receive the challenge, and thus the server will never receive the response and will not validate the path. See section 8.2 of RFC 9000, path validation. Security section 21.1.3. Connection Migration states that "Path validation () establishes that a peer is both willing and able to receive packets sent on a particular path."
So yes, the current text should be "the servers MUST in turn attempt to validate these paths using the same mechanisms, as specified in {{ Section 8.2 of RFC9000 }}."
No, this should not be a MUST. It is true that the path MUST be validated before it can be used. However, just because the serve receives a new packet on a path, that doesn't automatically mean that the server has to start path validation. It could e.g. also decide to simply withdraw that packet.
Actually, this is also not right. This is what RFC9000 says (section 9): So I guess we should maybe not use normative language here but refer to section 9 in RFC9000 instead...?
Just submitted PR for this issue.
This draft initially originates from a merging effort of previous Multipath proposals. While many points were found to be common between them, there remains one design point that still requires consensus: the number of packet number spaces that a Multipath QUIC connection should support (i.e., one for the whole connection vs. one per path). The current draft enables experimentation with both variants, but in the final version we will certainly need to choose between one of the versions.
The main issues mentioned so far: Efficiency. One number space per path should be more efficient, because implementations can directly reuse the loss-recovery logic specified for QUIC ACK size. Single space leads to lots of out of order delivery, which causes ACK sizes to grow Simplicity of code. Single space can be implemented without adding mane new code paths to uni-path QUIC Shorter header. Single space works well with NULL length CID The picoquic implementation shows that "efficiency" and "ack size" issues of single space implementations can be mitigated. However, that required significant improvements in the code: to obtain good loss efficiency, picoquic remembers not just the path on which a packet was sent, but also the order of the packet on this path, i.e. a "virtual sequence number". The loss detection logic then operates on that virtual sequence number. to contain ACK size, picoquic implements a prioritization logic to select the most important ranges in an ACK, avoid acking the same range more than 3 or 4 times, and keep knowledge of already acknowledged ranges so range coalescing works. I think these improvements are good in general, and I will keep them in the implementations whether we go for single space or not. The virtual sequence number is for example useful if the CID changes for reasons not related to path changes in multiple number space variants. It is also useful in unipath variants to avoid interference between sequence numbers used in probes and the RACK logic. The ACK size improvements do reduce the size of ACKs in presence of out of order delivery, e.g., if the network is doing some kind of internal load balancing. On the other hand, the improvements are somewhat complex, would need to be described in separate drafts, and pretty much contradicts the "simplicity of code" argument. So we are left with the "Null length CID" issue. I see for cases: Client CID Priority ----------------------- long Used by many implementations NULL Preferred configuration of many big deployments long Rarely used, server load balancing does not work NULL Only mentioned in some P2P deployments The point here is that it is somewhat hard to deploy a large server with NULL CID and use server load balancing. This configuration is mostly favored by planned P2P deployments.
The big debate is for the configuration with NULL CID on client, long CID on server. The packets from server to client do not carry a CID, and only the last bytes of the sequence number. The client will need some logic to infer the full sequence number before decrypting the packet. The client could maybe use the incoming 5 tuple as part of the logic, but it is not obvious. It is much simpler to assume a direct map from destination CID to number space. That means, if a peer uses a NULL CID, all packets sent to that peer are in the same number space.
Revised table: Client CID What -- -- long Multiple number space NULL Multiple number spaces on client side (one per CID), single space on server side long Multiple number spaces on server side (one per CID), single space on client side NULL single number space on each side If a node advertises both NULL CID and multipath support, they SHOULD have logic to contain the size of ACK. If a node engages in multipath with a NULL CID peer, they SHOULD have special logic to make loss recovery work well.
I think the above points to a possible "unified solution": if multipath negotiated, tie number spaces to destination CID, use default (N=0) if NULL CID. use multipath ack; in multipath ACK, identify number space by sequence number of DCID in received packets. Default that number to zero if received with NULL CID. in ABANDON path, identify path either by DCID of received packets, SCID of sent packet, or "this path" (sending path) if CID is NULL.
I like the proposal of the "unified solution". I think the elegance lies in the fact that it allows us to automatically cover all four cases listed above. The previous dilemma for me was that on one hand we have some use cases where we need to support more than two paths and separate PN makes the job easier, but on the other hand, I think we should not ignore the NULL CID use cases as it is also important. Now, with this proposal, a big part of the problem is solved. The rest of challenge is to make sure single PN remains efficient in terms of ACK and loss recovery. On that part, we plan to do an A/B test and would love to share the results when we get them. There is one more problem as pointed in issue , when we want to take hardware offloads into account. In such a case, we may still need single PN for long server CID. However, if hardware supports nonce modification, this problem can be addressed with the proposed "unified solution".
If the APi does not support 96 bit sequence numbers, it should always be possible to create an encryption context per number space, using Key=current key and ID = current-ID + CID sequence number. Of course, that forces creation of multiple context, and that makes key rotation a bit harder to manage. But still, that should be possible.
Thanks for the summary NAME I think one point is missing in your list which is related to issue . Use of a single packet number space might not support ECN. Regarding the unified solution: I think what you actually say is that we would specify both solutions and everybody would need to implement both logics. At least regarding the "simplicity of code" argument, that would be the worst choice. If we can make the multiple packet number spaces solution work with one-sided CIDs, I'm tending toward such an approach. Use of multiple packet number spaces avoids ambiguity/needed "smartness" in ACK handling and packet scheduling which, as you say above, can make the implementation more complex and, moreover, wrong decisions may have large impact on efficient (both local processing and on-the-wire). I don't think we want to leave these things to each implementation individually.
The summary Christian made above about design comparison sounds indeed quite accurate. Besides ECN, my other concern about single packet number is that it require cleverness from the receiver side if you want to be performant in some scenarios. At the sender-side, you need to consider a path-relative packet number threshold instead of an absolute one to avoid spurious losses. Just a point I think we did not mentioned yet is that there can be some interactions between Multipath out-of-order number delivery and incoming packet number duplicate detection. This requires maintaining some state at the receiver side, as described by URL With single packet number space, the receiver should take extra care when updating the "minimum packet number below which all packets are immediately dropped". Otherwise, in presence of paths with very different latencies, the receiver might end up discarding packets from a (slower) path. I'm also preferring the multiple packet number spaces solution for the above reasons. I'm not against thinking for a "unified" solution (the proposal sounds interesting), but I wonder how much complexity this would add compared to requiring end hosts to use one-byte CIDs.
I think the issue is not really so much "single vs multiple number space" as "support for multipath and NULL CID". As noted by Mirja, there is an implementation cost there. My take is: If a node uses NULL CID and proposes support of multipath, then that node MUST implement code to deal with size of acknowledgements. If a node sees a peer using NULL CID and supporting multipath, then that node MUST either use only one path at a time or implement code to deal with impact on loss detection due to out of order arrivals, and impact on congestion control including ambiguity of ECN signals. Then add sections on what it means to deal with the side of acknowledgements, out of order arrivals, and congestion control. I think this approach ends up with the correct compromises: It keeps the main case simple. Nodes that rely on multipath just use long CID and one number space per CID. Nodes that use long CID never have to implement the ACK length minimization algorithms. Nodes that see a peer using NULL CID have a variety of implementation choices, e.g., sending on just one path at a time, sending on mostly one path at a time in "make or break" transitions, or implementing sophisticated algorithms. If we do that, we can get rid of the single vs multiple space discussion, and end up with a single solution addressing all use cases.
Looking at all the discussions here and in other issues such as ECN, I think that we should try to write two different versions of section 7: one covering all the aspects of single packet number space (RTT estimation, retransmissions, ECN, handling of CIDs, ...) one covering all the aspects of multiple packet number space (RTT estimation, retransmissions, ECN, handling of CIDs, ...) There would be some overlap between these two sections and also some differences that would become clear as we specify them. At the end of this writing, we'll know whether it is possible to support/unify both or we need to recommend a single one. The other parts of the document are almost independent of that and can evolve in parallel with these two sections. However, I don't think that such a change would be possible by Monday
I don't think we should rush changes before we have agreed on the final vision.
It might be helpful to have these options as PRs (without merging) them, so people can understand all details of each approach.
I agree with NAME that supporting multi-path with null CIDs is a more fundamental issue than the efficiency comparison between single PN and separate PN, as it would ultimately impact the application scope of multipath QUIC. But indeed, we might want to implement proposed solution first and then decide if we want to adopt such a unified approach.
Since NAME prodded me, we now have a PR for the "unified" proposal.
I totally agree with Christian that the issue is about "support for multipath and NULL CID", and the solution that Christian suggested looks really great! It both takes advantage of multiple spaces, and support NULL CID users without affect the efficiency of ACK arrangements. Besides, the solution is more convenient for implementations, because If both endpoints uses non-zero length cids, endpoints only need to support multiple spaces, and if one of the endpoints use NULL CID, it could use single pn space in one direction and could support NULL CID and multipath at the same time.
Very high level summary of IETF-113 discussion seems that there is interest and likely support for the unified solution (review minutes for further details).


This PR is to resolve issue . The description of the path-closing figure is rephrased for better clarity. The pathidtype field in the PATHABANDON frame is also added in the figure to give an idea how the type is used. Moreover, one case for single PN is added (where the client receives zero-length CID while the server receives long CID). According to previous discussion, in the single PN case, only the client needs to send out a PATHABANDON. The server SHOULD stop sending new data on the path indicated by the PATHABANDON frame after receiving it. However, The client may want to repeat the PATHABANDON frame if it sees the server continuing to send data. It is optional for the server to respond with a PATHABANDON after it receives a PATHABANDON frame from the client.
There is one part I am afraid people might feel a little bit hard to follow: the three types of path identifiers in the PATH_STATUS frame. Those three types of path identifiers were introduced in order to be compatible with zero-length CID use cases, so I think we might need two additional example figures (like figure 3 in the current draft), one for (non-zero length CID, zero-length CID) and the other one for (zero-length CID & zero-length CID). Do you think that is a good idea? I can work on those figures if it doesn't complicate the flow of the draft.
This might be indeed useful in the "Example" section.
Yes, please create a PR!
NAME I am editing the path closing figures with zero-length CID and single PN. Is it similar to what you have in mind? {: #fig-example-path-close2 title="Example of closing a path when the server chooses to use zero-length CID"} {: #fig-example-path-close3 title="Example of closing a path when the both client and server choose to use zero-length CIDs."}
In the case of single number space, we use ACK, not ACKMP. But in fact you do not need to draw the ACKs there, there is no special processing of ACKs required. Also, there is no need to wait for an idle timeout, which typically is 10 to 30 seconds. That's too large. Do we really need to send "abandon path" in both directions? If I think about it, the real concern is that client will send a PATHABANDON message, but some time later, due to differences in delay, etc., the server receive data on the same four tuples. How does the server know whether it should create a new path or not? In the single space case, the logical test may be to check the sequence numbers of the packet. If the sequence number of the new packet is greater than that of the path_abandon, then the client is reactivating a path that was abandoned previously. Maybe the client connected again to the same network, after some disconnected interval. The server can start sending on the reactivated path. If the packet number is lower than that of the path abandon, then the server should not reactivate the path. In the multiple space case, if the CID is defined, there is no ambiguity.
ACK works for me. I think we might also want to add a sentence in the Sec. of "one packet number space" explicitly specifying that "for single PN, use ACK instead of ACKMP", and change some texts in the ACKMP frame section that is related to zero-length CID (the current draft says "if the endpoint receives 1-RTT packets with 0-length Connection ID, it SHOULD use Packet Number Space Identifier 0 in ACKMP frames". ). I think in single PN, sending "abandon path" in both directions is helpful. When the client sends "PATHABANDON", it promises not to send non-probing packets in the current activity cycle. It could send probing packets to reactivate the path a moment later if the client thinks the network environment has changed. After sending out "PATHABANDON", the client should still be able to receive packets from the server for a while on that path because there may be the server's inflight packets on that path. It was not until the client sees the server's "PATHABANDON", the client then knows that no more server's data is to be expected except for some out-of-order packets. So after receiving "PATHABANDON", the endpoint can start preparing to free memories and clear path stats allocated for that path. The time interval from seeing "PATHABANDON" to actually releasing relevant resources is going to be an important parameter, and yes, 10-30 seconds is too long. What would be a more appropriate number for this interval? Does it imply that we need to track the packet number of the PATH_ABANDON for a path that is closed? When a path is in a closed state, the server may reject any newly received packets unless it is a probing packet (back to our path initialization process). In this way, it should be OK to forget any info related to that path in the last activity cycle.
In most similar situations, RFC 9000 uses a delay of 3xRTO. Note that in the "simple" case, the client can receive packets from the server on any tuple. There are no resource allocated per path for receiving packets. There will be local resource allocated for sending packets, congestion context for example. But not on for receiving: all packets can be decrypted and processed. If the client knows that it will not send, it can discard the resource allocated for sending without risking a side effect.
I changed the timeout to 3RTO. As the problem is similar to FIN-ACK&FIN-ACK when TCP closes, I kept the ACK in the graph (because the pathidtype=2 refers to the path where the packet is traveling, we want to make sure it is acked before we close the path.). Regarding the resource, I think if we keep a unified receive record, which is not per-path, yes, we can discard the sending resources. But if we have some path stats (e.g. how many packets received in total in this current activity cycle), we need to keep these data structures until we are sure that the peer is not going to send more packets on that path. Indeed, it depends on the implementation, but I feel it is generally helpful to have PATH_ABANDON sent in both directions. What do you think? {: #fig-example-path-close2 title="Example of closing a path when the server chooses to use zero-length CID"} {: #fig-example-path-close3 title="Example of closing a path when both client and server choose to use zero-length CIDs."}
I submitted PR . Let's first have sth that we can start modifying.
addressed by PR
I think there is still a mismatch between the proposed examples and the definition of the pathidtype in Section 11.1 (definition of PATH_ABANDON frame).


Fix issue
In section 3.1, we wrote When the multipath option is negotiated, clients that want to use an additional path MUST first initiate the Address Validation procedure with PATHCHALLENGE and PATHRESPONSE frames described in of ]. After receiving packets from the client on the new paths, the servers MAY in turn attempt to validate these paths using the same mechanisms. With multipath, we cannot simply write that the server MAY validate the new path. There is a risk of a malicious client that operates as follows: client creates path from its own address, A. Path is validate by server client starts long download from server client creates a second path to server using a spoofed address, B client starts address validation for B but does not care about the response from the server client stops acknowledging packets over the first path to force the server to move packets to the second one and then opportunistically acknowledges packets that should have been sent by the server over that second packet (SPNS would probably make such an attack simpler than MPNS) We probably need to start working on the security considerations section of the draft.
This is already covered in the QUIC threat model. The server is supposed to validate new paths by sending a "path challenge" on that path, then waiting for a challenge response. If the client spoofs a path, it will never receive the challenge, and thus the server will never receive the response and will not validate the path. See section 8.2 of RFC 9000, path validation. Security section 21.1.3. Connection Migration states that "Path validation () establishes that a peer is both willing and able to receive packets sent on a particular path."
So yes, the current text should be "the servers MUST in turn attempt to validate these paths using the same mechanisms, as specified in {{ Section 8.2 of RFC9000 }}."
No, this should not be a MUST. It is true that the path MUST be validated before it can be used. However, just because the serve receives a new packet on a path, that doesn't automatically mean that the server has to start path validation. It could e.g. also decide to simply withdraw that packet.
Actually, this is also not right. This is what RFC9000 says (section 9): So I guess we should maybe not use normative language here but refer to section 9 in RFC9000 instead...?
Just submitted PR for this issue.