RFC 9420

The Messaging Layer Security (MLS) Protocol

๐Ÿ“… published July 2023, from ietf

RFC 9420 ๐Ÿ’– The Messaging Layer Security (MLS) Protocol

okay bestie, this is the MLS protocol doc, and it's giving group chat security done RIGHT ๐Ÿ”ฅ It's an IETF Standards Track document, dated July 2023, with ISSN 2070-1721. The authors are R. Barnes (Cisco), B. Beurdouche (Inria & Mozilla), R. Robert (Phoenix R&D), J. Millican (Meta Platforms), E. Omara, and K. Cohn-Gordon (University of Oxford) ๐Ÿ‘‘

The title block doesn't have an "Obsoletes" or "Updates" line, so this doc doesn't list any RFCs it replaces or changes ๐Ÿงš


Abstract โœจ

so messaging apps are getting really into end-to-end security, which means only the people actually talking can read the messages, not the servers passing them along ๐Ÿ” The tricky part is group chats, where more than two clients need to agree on a key but aren't always online at the same time ๐Ÿ˜ญ This doc specifies a key establishment protocol that gives efficient asynchronous group key establishment with forward secrecy (FS) and post-compromise security (PCS) for groups from two all the way up to thousands of members ๐ŸŒˆ


Status of This Memo ๐ŸŽ€

this is an Internet Standards Track document ๐Ÿ’…

it's a product of the Internet Engineering Task Force (IETF) and it represents the consensus of the IETF community. It got public review and was approved for publication by the Internet Engineering Steering Group (IESG) ๐Ÿซถ If you want more on Internet Standards, check Section 2 of RFC 7841 ๐Ÿฆ‹

for the current status, any errata, and how to send feedback, go to https://www.rfc-editor.org/info/rfc9420 ๐Ÿ“


Copyright (c) 2023 IETF Trust and the persons identified as the document authors. All rights reserved. ๐Ÿ’œ

this doc is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) as of the publication date. Please actually read those, bestie, because they explain your rights and restrictions for this doc ๐Ÿ™ˆ Any Code Components pulled out of this doc must include the Revised BSD License text as described in Section 4.e of the Trust Legal Provisions, and they come with no warranty, as the Revised BSD License describes โญ


Table of Contents ๐Ÿงš

   1.  Introduction
   2.  Terminology
     2.1.  Presentation Language
       2.1.1.  Optional Value
       2.1.2.  Variable-Size Vector Length Headers
   3.  Protocol Overview
     3.1.  Cryptographic State and Evolution
     3.2.  Example Protocol Execution
     3.3.  External Joins
     3.4.  Relationships between Epochs
   4.  Ratchet Tree Concepts
     4.1.  Ratchet Tree Terminology
       4.1.1.  Ratchet Tree Nodes
       4.1.2.  Paths through a Ratchet Tree
     4.2.  Views of a Ratchet Tree
   5.  Cryptographic Objects
     5.1.  Cipher Suites
       5.1.1.  Public Keys
       5.1.2.  Signing
       5.1.3.  Public Key Encryption
     5.2.  Hash-Based Identifiers
     5.3.  Credentials
       5.3.1.  Credential Validation
       5.3.2.  Credential Expiry and Revocation
       5.3.3.  Uniquely Identifying Clients
   6.  Message Framing
     6.1.  Content Authentication
     6.2.  Encoding and Decoding a Public Message
     6.3.  Encoding and Decoding a Private Message
       6.3.1.  Content Encryption
       6.3.2.  Sender Data Encryption
   7.  Ratchet Tree Operations
     7.1.  Parent Node Contents
     7.2.  Leaf Node Contents
     7.3.  Leaf Node Validation
     7.4.  Ratchet Tree Evolution
     7.5.  Synchronizing Views of the Tree
     7.6.  Update Paths
     7.7.  Adding and Removing Leaves
     7.8.  Tree Hashes
     7.9.  Parent Hashes
       7.9.1.  Using Parent Hashes
       7.9.2.  Verifying Parent Hashes
   8.  Key Schedule
     8.1.  Group Context
     8.2.  Transcript Hashes
     8.3.  External Initialization
     8.4.  Pre-Shared Keys
     8.5.  Exporters
     8.6.  Resumption PSK
     8.7.  Epoch Authenticators
   9.  Secret Tree
     9.1.  Encryption Keys
     9.2.  Deletion Schedule
   10. Key Packages
     10.1.  KeyPackage Validation
   11. Group Creation
     11.1.  Required Capabilities
     11.2.  Reinitialization
     11.3.  Subgroup Branching
   12. Group Evolution
     12.1.  Proposals
       12.1.1.  Add
       12.1.2.  Update
       12.1.3.  Remove
       12.1.4.  PreSharedKey
       12.1.5.  ReInit
       12.1.6.  ExternalInit
       12.1.7.  GroupContextExtensions
       12.1.8.  External Proposals
     12.2.  Proposal List Validation
     12.3.  Applying a Proposal List
     12.4.  Commit
       12.4.1.  Creating a Commit
       12.4.2.  Processing a Commit
       12.4.3.  Adding Members to the Group
   13. Extensibility
     13.1.  Additional Cipher Suites
     13.2.  Proposals
     13.3.  Credential Extensibility
     13.4.  Extensions
     13.5.  GREASE
   14. Sequencing of State Changes
   15. Application Messages
     15.1.  Padding
     15.2.  Restrictions
     15.3.  Delayed and Reordered Application Messages
   16. Security Considerations
     16.1.  Transport Security
     16.2.  Confidentiality of Group Secrets
     16.3.  Confidentiality of Sender Data
     16.4.  Confidentiality of Group Metadata
       16.4.1.  GroupID, Epoch, and Message Frequency
       16.4.2.  Group Extensions
       16.4.3.  Group Membership
     16.5.  Authentication
     16.6.  Forward Secrecy and Post-Compromise Security
     16.7.  Uniqueness of Ratchet Tree Key Pairs
     16.8.  KeyPackage Reuse
     16.9.  Delivery Service Compromise
     16.10. Authentication Service Compromise
     16.11. Additional Policy Enforcement
     16.12. Group Fragmentation by Malicious Insiders
   17. IANA Considerations
     17.1.  MLS Cipher Suites
     17.2.  MLS Wire Formats
     17.3.  MLS Extension Types
     17.4.  MLS Proposal Types
     17.5.  MLS Credential Types
     17.6.  MLS Signature Labels
     17.7.  MLS Public Key Encryption Labels
     17.8.  MLS Exporter Labels
     17.9.  MLS Designated Expert Pool
     17.10. The "message/mls" Media Type
   18. References
     18.1.  Normative References
     18.2.  Informative References
   Appendix A.  Protocol Origins of Example Trees
   Appendix B.  Evolution of Parent Hashes
   Appendix C.  Array-Based Trees
   Appendix D.  Link-Based Trees
   Contributors
   Authors' Addresses

1. Introduction ๐ŸŒท

okay so picture a group of users who want to send each other encrypted messages. They need a way to derive shared symmetric encryption keys, and honestly this problem is super well studied for two parties ๐Ÿง  The Double Ratchet is the main solution everyone uses, as described in [DoubleRatchet] [Signal] โœจ Channels built on the Double Ratchet get fine-grained forward secrecy AND post-compromise security, and they're still efficient enough for heavy use over low-bandwidth networks ๐Ÿ’…

now for a group of more than two, the usual move is to pass out symmetric "sender keys" over existing 1:1 secure channels. Then each member encrypts their messages to the group using their own sender key ๐Ÿซถ This is a big upgrade over sending every message pairwise, and it gives forward secrecy too (with a hash ratchet added on top) ๐Ÿฆ‹ BUT, and this is a big but ๐Ÿ˜ญ, getting post-compromise security with sender keys is hard. You need a number of key update messages that scales as the square of the group size. Also, an adversary who learns a sender key can often indefinitely and passively eavesdrop on that member's messages. Generating and distributing a fresh sender key does give a form of post-compromise security for that sender, but it needs computation and communication resources that scale linearly with the size of the group ๐Ÿ’€

so in this doc, we describe a protocol built on tree structures that gives asynchronous group keying with forward secrecy and post-compromise security ๐ŸŒณ It's based on earlier work on "asynchronous ratcheting trees" [ART]. The protocol uses an asynchronous key-encapsulation mechanism for tree structures, which lets the members of the group derive and update shared keys with costs that scale as the log of the group size ๐Ÿ‘‘ That's the whole glow-up, bestie โœจ


Part 1 of 30 ๐ŸŽ€ more parts coming, so stay tuned!

2. Terminology โœจ

Okay bestie, first things first: some of these capitalised words are secretly rules ๐Ÿ’… The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" are read the BCP 14 [RFC2119] [RFC8174] way, but ONLY when they show up in all caps like this. Lowercase is just normal chatting ๐Ÿซถ

Now the vocab list, because we need to be on the same page ๐Ÿงš

  • Client: An agent that uses this protocol to establish shared cryptographic state with other clients. A client is defined by the cryptographic keys it holds ๐Ÿ”‘
  • Group: A group is a logical collection of clients that share a common secret value at any given time. Its state is a linear sequence of epochs, where each epoch depends on the one before it ๐ŸŒˆ
  • Epoch: A state of a group in which a specific set of authenticated clients hold shared cryptographic state ๐Ÿ’œ
  • Member: A client that's included in the shared state of a group, and so has access to the group's secrets ๐Ÿ‘‘
  • Key Package: A signed object describing a client's identity and capabilities. It includes a hybrid public key encryption (HPKE) [RFC9180] public key that can be used to encrypt to that client. Other clients can use a client's KeyPackage to introduce them to a new group ๐ŸŽ€
  • Group Context: An object that summarises the shared, public state of the group. It's usually distributed in a signed GroupInfo message, which is handed to new members to help them join a group ๐Ÿฆ‹
  • Signature Key: A signing key pair used to authenticate the sender of a message โœจ
  • Proposal: A message that proposes a change to the group, like adding or removing a member ๐ŸŒธ
  • Commit: A message that puts into effect the changes proposed in a set of Proposals ๐Ÿ’…
  • PublicMessage: An MLS protocol message that is signed by its sender and authenticated as coming from a member of the group in a particular epoch, but it is not encrypted ๐Ÿ‘€
  • PrivateMessage: An MLS protocol message that is signed by its sender, authenticated as coming from a member of the group in a particular epoch, and encrypted so that only the members of the group in that epoch can read it ๐Ÿ”
  • Handshake Message: A PublicMessage or PrivateMessage carrying an MLS Proposal or Commit object, as opposed to application data ๐Ÿค
  • Application Message: A PrivateMessage carrying application data ๐Ÿ’–

Terminology specific to tree computations is described in Section 4.1 ๐ŸŒณ

Quick word on secrets: symmetric values get called "keys" or "secrets" interchangeably, and either way they are a value that MUST be kept confidential to a client ๐Ÿคซ When we label individual values, "secret" means a value used to derive further secret values, and "key" means a value used with an algorithm like Hashed Message Authentication Code (HMAC) or an Authenticated Encryption with Associated Data (AEAD) algorithm ๐Ÿง

The PublicMessage and PrivateMessage formats are defined in Section 6. Security notions like forward secrecy and post-compromise security are defined in Section 16 ๐Ÿ›ก๏ธ

As detailed in Section 13.5, MLS uses the "Generate Random Extensions And Sustain Extensibility" (GREASE) approach to keep things extensible. Senders insert random values into fields where receivers are required to ignore unknown values, which is a vibe-check for future-proofing ๐ŸŒ€ Specific "GREASE values" for this purpose are registered in the appropriate IANA registries ๐Ÿ“‹

2.1. Presentation Language

We use the TLS presentation language [RFC8446] to describe the structure of protocol messages ๐ŸŽ€ On top of the base syntax, we add two extra features: fields can be optional, and vectors can have variable-size length headers. Iconic upgrades ๐Ÿ’…

2.1.1. Optional Value

An optional value is encoded with a presence-signaling octet, followed by the value itself if it's present. When decoding, a presence octet with a value other than 0 or 1 MUST be rejected as malformed ๐Ÿ™…โ€โ™€๏ธ

   struct {
       uint8 present;
       select (present) {
           case 0: struct{};
           case 1: T value;
       };
   } optional<T>;

2.1.2. Variable-Size Vector Length Headers

In the TLS presentation language, vectors are encoded as a sequence of encoded elements, prefixed with a length. The length field has a fixed size, set by specifying the minimum and maximum lengths of the encoded sequence of elements ๐Ÿ“

In MLS, lots of vectors vary over huge ranges, so a fixed-size length field just won't cut it ๐Ÿ˜ญ Instead we use a variable-size length, based on the variable-length integer encoding from Section 16 of [RFC9000]. The only difference is that this one requires a minimum-size encoding. Instead of min and max values, the vector description just includes a V. For example:

   struct {
       uint32 fixed<0..255>;
       opaque variable<V>;
   } StructWithVectors;

A vector like that can hold values from 0 bytes all the way up to 2^30 bytes ๐ŸŒŸ The variable-length integer encoding uses the two most significant bits of the first byte to encode the base 2 logarithm of the integer encoding length in bytes. The integer value goes in the remaining bits, so the whole value is in network byte order ๐Ÿง  The encoded value MUST use the smallest number of bits required to represent the value. When decoding, values using more bits than necessary MUST be treated as malformed ๐Ÿšซ

So integers get encoded in 1, 2, or 4 bytes, and they can hold 6-, 14-, or 30-bit values, respectively ๐Ÿ“

          +========+=========+=============+=======+============+
          | Prefix | Length  | Usable Bits | Min   | Max        |
          +========+=========+=============+=======+============+
          | 00     | 1       | 6           | 0     | 63         |
          +--------+---------+-------------+-------+------------+
          | 01     | 2       | 14          | 64    | 16383      |
          +--------+---------+-------------+-------+------------+
          | 10     | 4       | 30          | 16384 | 1073741823 |
          +--------+---------+-------------+-------+------------+
          | 11     | invalid | -           | -     | -          |
          +--------+---------+-------------+-------+------------+

Table 1: Summary of Integer Encodings ๐Ÿ“Š

Vectors that start with the prefix "11" are invalid and MUST be rejected. No exceptions, no vibes-based parsing ๐Ÿ™…โ€โ™€๏ธ

Some examples, so you can see it in action ๐Ÿ‘€

  • The four-byte length value 0x9d7f3e7d decodes to 494878333. ๐ŸŒˆ
  • The two-byte length value 0x7bbd decodes to 15293. ๐Ÿฆ‹
  • The single-byte length value 0x25 decodes to 37. ๐ŸŒธ

The following figure adapts the pseudocode provided in [RFC9000] to add a check for minimum-length encoding ๐Ÿ”

   ReadVarint(data):
     // The length of variable-length integers is encoded in the
     // first two bits of the first byte.
     v = data.next_byte()
     prefix = v >> 6
     if prefix == 3:
       raise Exception('invalid variable length integer prefix')

     length = 1 << prefix

     // Once the length is known, remove these bits and read any
     // remaining bytes.
     v = v & 0x3f
     repeat length-1 times:
       v = (v << 8) + data.next_byte()

     // Check if the value would fit in half the provided length.
     if prefix >= 1 && v < (1 << (8*(length/2) - 2)):
       raise Exception('minimum encoding was not used')

     return v

Using variable-size integers for vector lengths means vectors can grow really, really big, up to 2^30 bytes ๐Ÿ˜ Implementations should take care not to let vectors overflow the storage that's available ๐Ÿงฏ To make interop bugs easier to debug, implementations SHOULD provide a clear error when such an overflow condition occurs. Future-you will thank present-you ๐Ÿซถ

Big vectors are a big deal. Overflow without a clear error is the kind of plot twist nobody wants ๐Ÿ’€

3. Protocol Overview ๐ŸŒธ

MLS is designed to operate in the context described in [MLS-ARCH] ๐Ÿ—บ๏ธ In particular, we assume that the following services are provided:

  • An Authentication Service (AS) that lets group members authenticate the credentials presented by other group members ๐Ÿชช
  • A Delivery Service (DS) that routes MLS messages among the participants in the protocol ๐Ÿ“ฌ

MLS assumes a trusted AS but a largely untrusted DS. Section 16.10 describes the impact of compromise or misbehavior of an AS. MLS is designed to protect the confidentiality and integrity of the group data even if the DS gets compromised. In general, the DS is only expected to reliably deliver messages, so it's basically the mail carrier ๐Ÿ“ฎ Section 16.9 describes the impact of compromise or misbehavior of a DS.

The core functionality of MLS is continuous group authenticated key exchange (AKE) ๐Ÿ”‘ Like other authenticated key exchange protocols (such as TLS), the participants agree on a common secret value, and each participant can verify the identity of the other participants ๐Ÿชž That secret can then protect messages sent from one participant in the group to the others, using the MLS framing layer, or it can be exported for use with other protocols ๐Ÿฆ‹ MLS provides group AKE in the sense that there can be more than two participants, and continuous group AKE in the sense that the set of participants can change over time ๐ŸŒˆ

The core organising principles of MLS are groups and epochs ๐Ÿ’œ A group represents a logical collection of clients that share a common secret value at any given time. The history of a group is divided into a linear sequence of epochs. In each epoch, a set of authenticated members agree on an epoch secret that is known only to the members of the group in that epoch ๐Ÿคซ The set of members can change from one epoch to the next, and MLS makes sure that only the members in the current epoch have access to the epoch secret ๐Ÿ” From the epoch secret, members derive further shared secrets for message encryption, group membership authentication, and so on ๐Ÿง

The creator of an MLS group creates the group's first epoch unilaterally, with no protocol interactions ๐ŸŒฑ After that, the members advance their shared cryptographic state from one epoch to another by exchanging MLS messages ๐Ÿ’Œ

  • A KeyPackage object describes a client's capabilities and provides keys that can be used to add the client to a group ๐ŸŽŸ๏ธ
  • A Proposal message proposes a change to be made in the next epoch, such as adding or removing a member โœ๏ธ
  • A Commit message starts a new epoch by telling the members of the group to implement a collection of proposals ๐Ÿš€
  • A Welcome message gives a new member the information they need to initialise their state for the epoch in which they were added, or in which they want to add themselves to the group ๐Ÿ‘‹

KeyPackage and Welcome messages are used to start a group or introduce new members, so they get exchanged between group members and clients who aren't in the group yet ๐Ÿ’ซ A client publishes a KeyPackage via the DS, which lets other clients add it to groups ๐Ÿ“ฃ When a group member wants to add a new member, they use the new member's KeyPackage to add them, and then construct a Welcome message so the new member can set up their local state ๐ŸŽ‰

Proposal and Commit messages are sent from one member of a group to the others. MLS provides a common framing layer for sending messages within a group. A PublicMessage gives sender authentication for unencrypted Proposal and Commit messages ๐Ÿ™Œ A PrivateMessage gives encryption and authentication for both Proposal and Commit messages, as well as any application data ๐Ÿ”’ Main character energy, honestly ๐ŸŒˆโœจ

3.1. Cryptographic State and Evolution ๐Ÿ”

okay so the cryptographic state at the heart of MLS is split into three areas of responsibility, and honestly it's giving organized ๐Ÿ’…

                          .-    ...    -.
                         |               |
                         |       |       |
                         |       |       | Key Schedule
                         |       V       |
                         |  epoch_secret |
.                        |       |       |                             .
|\ Ratchet               |       |       |                     Secret /|
| \ Tree                 |       |       |                      Tree / |
|  \                     |       |       |                          /  |
|   \                    |       V       |                         /   |
|    +--> commit_secret --> epoch_secret --> encryption_secret -->+    |
|   /                    |       |       |                         \   |
|  /                     |       |       |                          \  |
| /                      |       |       |                           \ |
|/                       |       |       |                            \|
'                        |       V       |                             '
                         |  epoch_secret |
                         |       |       |
                         |       |       |
                         |       V       |
                         |               |
                          '-    ...    -'

Figure 1: Overview of MLS Group Evolution โœจ

  • A ratchet tree ๐ŸŒณ that represents the membership of the group. It gives group members a way to authenticate each other and to efficiently encrypt messages to subsets of the group. Each epoch has a distinct ratchet tree, and it seeds the key schedule.
  • A key schedule ๐Ÿ—“๏ธ that describes the chain of key derivations used to move from epoch to epoch (mainly using the init_secret and epoch_secret), plus the derivation of a bunch of other secrets (see Table 4). For example:
    • An encryption secret that is used to initialize the secret tree for the epoch. ๐Ÿ”‘
    • An exporter secret that lets other protocols use MLS as a generic authenticated group key exchange. ๐Ÿฆ‹
    • A resumption secret that members can use to prove they're in the group, e.g., when creating a subgroup or a successor group. ๐ŸŽ€
  • A secret tree ๐ŸŒธ derived from the key schedule. It represents the shared secrets the group members use for encrypting and authenticating messages. Each epoch has a distinct secret tree.

Every member keeps a partial view of these components of the group's state. MLS messages are what set up those views and keep everyone synced as the group hops between epochs. bestie, that's the whole vibe ๐Ÿซถ

Each new epoch kicks off with a Commit message ๐Ÿ’œ. The Commit tells existing members to update their view of the ratchet tree by applying a set of Proposals, and it uses the updated ratchet tree to hand out fresh entropy to the group. That fresh entropy goes only to members in the new epoch, not to anyone who's been removed. So Commits keep the property that the epoch secret stays confidential to the members of the current epoch. iconic behaviour, no notes ๐Ÿ‘‘

For every Commit that adds one or more members, there are one or more matching Welcome messages ๐Ÿงš. Each Welcome gives the new members what they need to set up their views of the key schedule and ratchet tree, so their views line up with the ones the other members hold in this epoch. ngl this is the glue holding the whole thing together ๐ŸŒˆ

3.2. Example Protocol Execution โœจ

okay bestie, let's talk about the three big moves in a group's life ๐Ÿฆ‹ there are only three: adding a member (a current member starts it), updating the keys that represent a member in the tree, and removing a member ๐Ÿ’…

each of these gets "proposed" by sending a message of the matching type (Add / Update / Remove). but the group's state doesn't actually change until a Commit message shows up to bring in fresh entropy ๐ŸŽ€ so a proposal is kinda like a vibe check, and the Commit is the moment it becomes real ๐Ÿ’–

in these examples, every proposal gets committed right away ๐ŸŒธ in fancier real deployments, an app might collect a bunch of proposals and commit them all at once ๐Ÿงš also, the diagrams show Proposal and Commit messages directly, but in real life they'd be wrapped up in a PublicMessage or PrivateMessage object ๐Ÿ“ฆ

before a group even exists, clients publish their KeyPackages to a directory run by the DS (see Figure 2) ๐Ÿ“

                                                    Delivery Service
                                                            |
                                                  .--------' '--------.
                                                 |                     |
                                                                 Group
  A                B                C            Directory       Channel
  |                |                |                |              |
  | KeyPackageA    |                |                |              |
  +------------------------------------------------->|              |
  |                |                |                |              |
  |                | KeyPackageB    |                |              |
  |                +-------------------------------->|              |
  |                |                |                |              |
  |                |                | KeyPackageC    |              |
  |                |                +--------------->|              |
  |                |                |                |              |

    Figure 2: Clients A, B, and C publish KeyPackages to the directory

now Figure 3 is the glow-up ๐ŸŒˆ say client A wants a group with B and C. A starts out with a group state that's just itself, then downloads KeyPackages for B and C. For each member, A makes an Add proposal plus a Commit to add that person, and broadcasts both to the group ๐Ÿ“ข A also makes a Welcome message and sends it straight to the new member (no need to send it to the whole group, bestie ๐Ÿ™ˆ). And here's the important part: A only updates its own state to show the new member after its Commit comes back from the Delivery Service ๐Ÿ”ฅ

once A has updated, the new member has processed the Welcome, and everyone else has processed the Commit, they'll all have matching views of the group state ๐Ÿซถ that includes a group secret that only the members of the group know โœจ the new member can read and send new messages to the group, but anything sent before they joined stays hidden from them ๐Ÿ”’

                                                                  Group
   A              B              C          Directory            Channel
   |              |              |              |                   |
   |         KeyPackageB, KeyPackageC           |                   |
   |<-------------------------------------------+                   |
   |              |              |              |                   |
   |              |              |              | Add(A->AB)        |
   |              |              |              | Commit(Add)       |
   +--------------------------------------------------------------->|
   |              |              |              |                   |
   |  Welcome(B)  |              |              |                   |
   +------------->|              |              |                   |
   |              |              |              |                   |
   |              |              |              | Add(A->AB)        |
   |              |              |              | Commit(Add)       |
   |<---------------------------------------------------------------+
   |              |              |              |                   |
   |              |              |              |                   |
   |              |              |              | Add(AB->ABC)      |
   |              |              |              | Commit(Add)       |
   +--------------------------------------------------------------->|
   |              |              |              |                   |
   |              |  Welcome(C)  |              |                   |
   +---------------------------->|              |                   |
   |              |              |              |                   |
   |              |              |              | Add(AB->ABC)      |
   |              |              |              | Commit(Add)       |
   |<---------------------------------------------------------------+
   |              |<------------------------------------------------+
   |              |              |              |                   |

          Figure 3: Client A creates a group with clients B and C

adding more people later works the exact same way ๐Ÿฆ‹ any member can grab a KeyPackage for a new client, broadcast the Add and Commit that everyone else uses to update their state, and send a Welcome so the newcomer can set up and join ๐Ÿ’œ

now for the security glow-up ๐Ÿ’… to keep forward secrecy and post-compromise security going, each member regularly refreshes the keys that represent them to the group โœจ you can do that by sending a Commit (even one with no proposals!) or by sending an Update that another member commits for you (see Figure 4) ๐ŸŽ€ once the others have processed these messages, the group's secrets become unknown to an attacker who had compromised the secrets for the sender's leaf in the tree ๐Ÿ˜ญ by the end of Figure 4, the group has post-compromise security with respect to both A and B ๐Ÿ‘‘

  • Update messages SHOULD be sent at regular intervals of time as long as the group is active, and members that don't update SHOULD eventually be removed from the group.

the app gets to decide how much time goes between Updates โฐ the whole point is to shrink a compromise window, so the right rhythm is usually hours or days, not milliseconds ๐Ÿ™ˆ for example, an app could send an Update each time a member sends an application message after receiving a message from someone else, or once a day if nobody's chatting ๐ŸŒธ

the MLS architecture recommends running MLS over a secure transport (see Section 7.1 of [MLS-ARCH]) ๐Ÿ” those transports usually handle stuff like congestion control, which keeps an MLS app from hogging the network and wrecking everyone else's apps ๐ŸŒŠ so apps should be careful not to send MLS messages at a rate that causes trouble like network congestion, especially if they're not using that recommended transport (for example, sending MLS straight over UDP instead) ๐Ÿ’€

                                                             Group
   A              B     ...      Z          Directory        Channel
   |              |              |              |              |
   |              | Update(B)    |              |              |
   |              +------------------------------------------->|
   |              |              |              | Update(B)    |
   |<----------------------------------------------------------+
   |              |<-------------------------------------------+
   |              |              |<----------------------------+
   |              |              |              |              |
   | Commit(Upd)  |              |              |              |
   +---------------------------------------------------------->|
   |              |              |              | Commit(Upd)  |
   |<----------------------------------------------------------+
   |              |<-------------------------------------------+
   |              |              |<----------------------------+
   |              |              |              |              |

        Figure 4: Client B proposes to update its key, and client A
                            commits the proposal

removing someone works in a similar way, as Figure 5 shows ๐Ÿšซ any member can send a Remove proposal followed by a Commit ๐Ÿ’– the Commit brings new entropy to everyone except the person being removed โœจ that fresh entropy gets mixed into the epoch secret for the new epoch, so the removed member never learns it ๐Ÿ”’ but heads up, this doesn't mean any member is actually allowed to kick people out ๐Ÿ‘€ groups can layer their own access control policies on top of these basic mechanisms ๐Ÿ’…

                                                             Group
   A              B     ...      Z          Directory       Channel
   |              |              |              |              |
   |              |              | Remove(B)    |              |
   |              |              | Commit(Rem)  |              |
   |              |              +---------------------------->|
   |              |              |              |              |
   |              |              |              | Remove(B)    |
   |              |              |              | Commit(Rem)  |
   |<----------------------------------------------------------+
   |              |<-------------------------------------------+
   |              |              |<----------------------------+
   |              |              |              |              |

             Figure 5: Client Z removes client B from the group

quick reminder: these flows are just examples, and apps can arrange messages in other ways ๐Ÿงš for instance:

  • Welcome messages don't have to go straight to new joiners. They're encrypted to the new joiners, so they could be spread more widely, like if the app only had a broadcast channel for the group ๐Ÿ“ป
  • Proposal messages don't have to go out to every member right away. They just need to be available to the committer before it generates a Commit, and to the other members before they process that Commit ๐Ÿ“ฌ
  • The sender of a Commit doesn't necessarily have to wait for its own Commit to come back before moving its state forward. It only needs to know its Commit will be the next one the group applies, maybe based on a promise from an orchestration server ๐ŸŽ€

3.3. External Joins ๐Ÿฆ‹

okay so besides the Welcome-based flow (where existing members add someone), there's also a way to join by yourself using an "external Commit" ๐Ÿ’… This is useful when the existing members don't have a KeyPackage for the new person, like in an "open" group that anyone can hop into without asking permission first โœจ

Figure 6 shows a typical message flow for an external join. To let a new member join this way, a member of the group (A, B) publishes a GroupInfo object. It includes the GroupContext for the group and a public key that can be used to encrypt a secret to the existing members ๐Ÿ” When the new member Z wants in, they download the GroupInfo object and use it to form a Commit of a special form that adds Z to the group (as detailed in Section 12.4.3.2). The existing members process this external Commit in a similar way to a normal Commit, advancing to a new epoch where Z is now a member ๐ŸŽ€

                                                         Group
A              B              Z          Directory        Channel
|              |              |              |              |
| GroupInfo    |              |              |              |
+------------------------------------------->|              |
|              |              | GroupInfo    |              |
|              |              |<-------------+              |
|              |              |              |              |
|              |              | Commit(ExtZ) |              |
|              |              +---------------------------->|
|              |              |              | Commit(ExtZ) |
|<----------------------------------------------------------+
|              |<-------------------------------------------+
|              |              |<----------------------------+
|              |              |              |              |

    Figure 6: Client A publishes a GroupInfo object, and Client Z
                     uses it to join the group

3.4. Relationships between Epochs ๐ŸŒธ

okay this one is giving time travel ๐Ÿงš A group has a single linear sequence of epochs. Groups and epochs are usually independent of each other. BUT sometimes it's super useful to link epochs together cryptographically, either inside one group or across different groups ๐Ÿ”— MLS derives a resumption pre-shared key (PSK) from each epoch, so entropy pulled from one epoch can be injected into a future epoch. A group member who wants to inject a PSK issues a PreSharedKey proposal (Section 12.1.4) describing the PSK to be injected. When that proposal is committed, the matching PSK gets incorporated into the key schedule as described in Section 8.4 ๐Ÿ’œ

Linking epochs this way guarantees that members entering the new epoch agree on a key if and only if they were members of the group during the epoch the resumption key was extracted from. Iconic accountability energy ๐Ÿ‘‘

MLS has two ways to tie a new group to an existing one, shown in Figures 7 and 8. Reinitialization closes one group and creates a new group with the same members but different parameters ๐Ÿ”„ Branching starts a new group with a subset of the original group's participants, and the original group isn't affected at all ๐ŸŒฟ In both cases, the new group is linked to the old group via a resumption PSK.

epoch_A_[n-1]
     |
     |
     |<-- ReInit
     |
     V
epoch_A_[n]           epoch_B_[0]
     .                     |
     .  PSK(usage=reinit)  |
     .....................>|
                           |
                           V
                      epoch_B_[1]

                   Figure 7: Reinitializing a Group
epoch_A_[n]           epoch_B_[0]
     |                     |
     |  PSK(usage=branch)  |
     |....................>|
     |                     |
     V                     V
epoch_A_[n+1]         epoch_B_[1]

                     Figure 8: Branching a Group

Applications can also use resumption PSKs to link epochs in other ways ๐Ÿฆ„ For example, Figure 9 shows a resumption PSK from epoch n being injected into epoch n+k. This shows that the members of the group at epoch n+k were also members at epoch n, no matter what changes happened to their keys through Updates or Commits in between. Receipts secured ๐Ÿงพ

epoch_A_[n]
     |
     |  PSK(usage=application)
     |.....................
     |                    .
     |                    .
    ...                  ...
     |                    .
     |                    .
     V                    .
epoch_A_[n+k-1]           .
     |                    .
     |                    .
     |<....................
     |
     V
epoch_A_[n+k]

         Figure 9: Reinjecting Entropy from an Earlier Epoch

4. Ratchet Tree Concepts ๐ŸŒณ

okay the big idea here is "ratchet trees" ๐ŸŒฒ MLS uses them to derive shared secrets among a group of clients. A ratchet tree is an arrangement of secrets and key pairs spread across the members of a group, set up so secrets can be updated efficiently whenever the group changes ๐Ÿ’–

Ratchet trees let a group remove any member efficiently by encrypting new entropy to a subset of the group ๐Ÿ˜ค A ratchet tree assigns shared keys to subgroups of the overall group. So, for example, encrypting to all but one member only takes log(N) encryptions to subtrees, instead of the N-1 encryptions you'd need to encrypt to each participant one by one (where N is the number of members in the group). Efficiency is literally the whole personality ๐Ÿ™Œ

This remove operation is how MLS gets post-compromise security efficiently ๐Ÿ›ก๏ธ In an Update proposal or a full Commit message, an old (possibly compromised) representation of a member gets removed from the group and replaced with a freshly generated one. Fresh start, no drama ๐Ÿงผ

4.1. Ratchet Tree Terminology ๐Ÿ“

okay time for vocab, bestie ๐Ÿ“š Trees are made of nodes. A node is a leaf if it has no children. Otherwise, it's a parent. All parents in our trees have precisely two children, a left child and a right child ๐Ÿ‘ˆ๐Ÿ‘‰ A node is the root of a tree if it has no parent, and intermediate if it has both children and a parent. The descendants of a node are that node's children, plus the descendants of those children. We say a tree contains a node if that node is a descendant of the root of the tree, or if the node itself is the root. Nodes are siblings if they share the same parent ๐Ÿ‘ฏ

A subtree of a tree is the tree given by any node (the head of the subtree) and its descendants. The size of a tree or subtree is the number of leaf nodes it contains. For a given parent node, its left subtree is the subtree with its left child as head, and its right subtree is the subtree with its right child as head ๐ŸŒณ

Every tree used in this protocol is a perfect binary tree. In other words, it's a complete balanced binary tree with 2^d leaves, all at the same depth d. This structure is unique for a given depth d ๐Ÿ’…

There are multiple ways an implementation might represent a ratchet tree in memory ๐Ÿง  A super convenient property of left-balanced binary trees (including the complete trees used here) is that they can be represented as an array of nodes, with node relationships computed from the nodes' indices in the array. A more traditional representation based on linked node objects can also be used. Appendices C and D give some details on how to implement the tree operations MLS needs in these representations. MLS places no requirements on implementations' internal representations of ratchet trees. An implementation may use any tree representation and associated algorithms, as long as they produce correct protocol messages. Freedom of expression, but make it valid ๐Ÿฆ‹

4.1.1. Ratchet Tree Nodes ๐Ÿงฉ

Each leaf node in a ratchet tree gets an index (or leaf index), starting at 0 on the left and going up to 2^d - 1 on the right (for a tree with 2^d leaves). A tree with 2^d leaves has 2^(d+1) - 1 nodes, including parent nodes ๐Ÿค“

Each node in a ratchet tree is either blank (containing no value), or it holds an HPKE public key with some associated data:

  • A public key (for the HPKE scheme in use; see Section 5.1)
  • A credential (only for leaf nodes; see Section 5.3)
  • An ordered list of "unmerged" leaves (see Section 4.2)
  • A hash of certain information about the node's parent, as of the last time the node was changed (see Section 7.9)

As described in Section 4.2, different members know different subsets of the private keys corresponding to the public keys in the nodes of the tree. The private key for a parent node is only known to members at leaf nodes that are descendants of that node. The private key for a leaf node is only known to the member at that leaf node. A leaf node is unmerged relative to one of its ancestor nodes if the member at the leaf node doesn't know the private key for that ancestor node. Secrets stay need-to-know ๐Ÿคซ

Every node, whether blank or populated, has a corresponding hash that summarizes the contents of the subtree below that node. The rules for computing these hashes are described in Section 7.8 ๐Ÿ“

The resolution of a node is an ordered list of non-blank nodes that together cover all non-blank descendants of that node. The resolution of the root contains the set of keys that are collectively needed to encrypt to every node in the group. The resolution of a node is basically a depth-first, left-first walk through the nearest non-blank nodes below it:

  • The resolution of a non-blank node is the node itself, followed by its list of unmerged leaves, if any.
  • The resolution of a blank leaf node is the empty list.
  • The resolution of a blank intermediate node is the resolution of its left child concatenated with the resolution of its right child, in that order.

For example, check out this subtree, where the _ character stands for a blank node and unmerged leaves are shown in square brackets ๐Ÿ‘€

              ...
              /
             _
       ______|______
      /             \
     X[B]            _
   __|__           __|__
  /     \         /     \
 _       _       Y       _
/ \     / \     / \     / \
A   B   _   D   E   F   _   H

0   1   2   3   4   5   6   7

         Figure 10: A Tree with Blanks and Unmerged Leaves

In this tree, all of the rules above are in play โœจ

  • The resolution of node X is the list [X, B].
  • The resolution of leaf 2 or leaf 6 is the empty list [].
  • The resolution of the top node is the list [X, B, Y, H].

4.1.2. Paths through a Ratchet Tree ๐Ÿ›ค๏ธ

okay so the direct path of a root is just an empty list ๐Ÿ’… because the root has no parent to climb up to. For any other node, the direct path is its parent, followed by that parent's direct path. So it's basically the whole climb up to the top, step by step โœจ

The copath of a node is the node's sibling, plus the siblings of every node on its direct path, except the root ๐Ÿฆ‹

The filtered direct path of a leaf node L is its direct path, but with any node removed whose child on L's copath has an empty resolution. (And keep in mind that any unmerged leaves of that copath child count toward its resolution ๐Ÿ‘€) Those removed nodes don't need their own key pairs, because encrypting to that node's key pair would be the same as encrypting to its non-copath child. Iconic efficiency, honestly ๐Ÿ˜ญ๐Ÿซถ

Let's look at an example tree ๐ŸŒธ Blank nodes are marked with _, and I gave them labels so we can talk about them.

                 W = root
                 |
           .-----+-----.
          /             \
         _=U             Y
         |               |
       .-+-.           .-+-.
      /     \         /     \
     T       _=V     X       _=Z
    / \     / \     / \     / \
   A   B   _   _   E   F   G   _=H

   0   1   2   3   4   5   6   7

       Figure 11: A Complete Tree with Five Members, with Labels for
                             Blank Parent Nodes

With that tree, here are the direct paths, copaths, and filtered direct paths for each leaf ๐Ÿ’–

      +======+=============+=========+======================+
      | Node | Direct path | Copath  | Filtered Direct Path |
      +======+=============+=========+======================+
      | A    | T, U, W     | B, V, Y | T, W                 |
      +------+-------------+---------+----------------------+
      | B    | T, U, W     | A, V, Y | T, W                 |
      +------+-------------+---------+----------------------+
      | E    | X, Y, W     | F, Z, U | X, Y, W              |
      +------+-------------+---------+----------------------+
      | F    | X, Y, W     | E, Z, U | X, Y, W              |
      +------+-------------+---------+----------------------+
      | G    | Z, Y, W     | H, X, U | Y, W                 |
      +------+-------------+---------+----------------------+

                              Table 2

4.2. Views of a Ratchet Tree ๐Ÿ‘€

We generally assume that every participant keeps a complete and up-to-date view of the group's public ratchet tree. That includes the public keys for all nodes and the credentials attached to the leaf nodes ๐ŸŽ€

No participant in an MLS group knows the private key for every node in the tree ๐Ÿ™ˆ Instead, each member gets assigned to a leaf, and that leaf decides which subset of private keys they know. The credential stored at that leaf is the one the member provided.

MLS keeps everyone's view of the tree in a way that maintains the tree invariant ๐Ÿ’œ

The private key for a node in the tree is known to a member of the group only if the node's subtree contains that member's leaf.

In other words, if a node isn't blank, it holds a public key. The matching private key is known only to members sitting on leaves below that node โœจ

The reverse isn't true, though ๐Ÿ’… A member may not know the private key of an intermediate node above them. That member has an unmerged leaf at that intermediate node. Encrypting to an intermediate node means encrypting to that node's public key, plus the public keys of every unmerged leaf below it. A leaf is unmerged with regard to all of its ancestors when it's first added, because adding it doesn't give it access to the private keys of all the nodes above it. Leaves become "merged" as they receive the private keys for nodes, which is described in Section 7.4 ๐Ÿฆ‹

Let's walk through an example ๐Ÿงš Say a four-member group (A, B, C, D) exists, where the node above the right two members is blank. (This is what the tree would look like if A created a group with B, C, and D.) Here's the public state of the tree, plus what each participant can see of the tree's private keys. In these diagrams, _ means a blank node, ? means an unknown private key, and pk(X) means the public key matching the private key X ๐ŸŒˆ

            Public Tree
   ============================
               pk(ABCD)
             /          \
       pk(AB)            _
        / \             / \
   pk(A)   pk(B)   pk(C)   pk(D)


    Private @ A       Private @ B       Private @ C       Private @ D
   =============     =============     =============     =============
        ABCD              ABCD              ABCD              ABCD
       /   \             /   \             /   \             /   \
     AB      _         AB      _         ?       _         ?       _
    / \     / \       / \     / \       / \     / \       / \     / \
   A   ?   ?   ?     ?   B   ?   ?     ?   ?   C   ?     ?   ?   ?   D

Notice how the tree invariant plays out here ๐Ÿซถ Each member only knows their own leaf. The AB private key is known only to A and B, and ABCD is known to all four members. There's also a bigger lesson: there can be "holes" on the path from a member's leaf up to the root, where the member knows the key both above and below a given node but not for that node itself. D is the example of this ๐Ÿ˜ญ


5. Cryptographic Objects ๐Ÿ”

5.1. Cipher Suites ๐ŸŽ€

Each MLS session uses a single cipher suite. That suite specifies the following primitives for group key computations:

  • HPKE parameters:
    • A Key Encapsulation Mechanism (KEM)
    • A Key Derivation Function (KDF)
    • An Authenticated Encryption with Associated Data (AEAD) encryption algorithm
  • A hash algorithm
  • A Message Authentication Code (MAC) algorithm
  • A signature algorithm

MLS uses HPKE for public key encryption [RFC9180] ๐Ÿ’– The DeriveKeyPair function tied to the cipher suite's KEM maps octet strings to HPKE key pairs. Just like HPKE, MLS assumes that an AEAD algorithm produces a single ciphertext output from AEAD encryption (lining up with [RFC5116]), instead of a separate ciphertext and tag ๐Ÿฆ‹

Cipher suites are represented with the CipherSuite type, and they're defined in Section 17.1 โœจ

5.1.1. Public Keys ๐Ÿ”‘

HPKE public keys are opaque values in a format defined by the underlying protocol. (Check out Section 4 of [RFC9180] for more info ๐Ÿ’œ)

opaque HPKEPublicKey<V>;

Signature public keys are also opaque values, just in a format defined by the cipher suite's signature scheme ๐ŸŒธ

opaque SignaturePublicKey<V>;

For cipher suites using the Edwards-curve Digital Signature Algorithm (EdDSA) signature schemes (Ed25519 or Ed448), the public key is in the format specified in [RFC8032] ๐ŸŒŠ

For cipher suites using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the NIST curves (P-256, P-384, or P-521), the public key is represented as an encoded UncompressedPointRepresentation struct, as defined in [RFC8446] ๐Ÿ’…

5.1.2. Signing โœ๏ธ

The signature algorithm in a group's cipher suite is the mandatory algorithm for signing messages within the group. It MUST be the same as the signature algorithm specified in the credentials in the leaves of the tree (this includes the leaf node information in KeyPackages used to add new members) ๐Ÿซถ

The signatures in this document are encoded as specified in [RFC8446]. To be specific, ECDSA signatures are DER encoded, and EdDSA signatures are the concatenation of R and S, as specified in [RFC8032] ๐Ÿ”ฅ

To tell different MLS signatures apart, every signed value gets prefixed with a label, like this ๐Ÿท๏ธ

SignWithLabel(SignatureKey, Label, Content) =
    Signature.Sign(SignatureKey, SignContent)

VerifyWithLabel(VerificationKey, Label, Content, SignatureValue) =
    Signature.Verify(VerificationKey, SignContent, SignatureValue)

Where SignContent is specified as:

struct {
    opaque label<V>;
    opaque content<V>;
} SignContent;

And its fields are set to:

label = "MLS 1.0 " + Label;
content = Content;

The Signature.Sign and Signature.Verify functions are defined by the signature algorithm. If MLS extensions need signatures from group members, they should reuse the SignWithLabel construction, using a distinct label. To avoid collisions in these labels, there's an IANA registry defined in Section 17.6 ๐Ÿ“‹โœจ

5.1.3. Public Key Encryption ๐Ÿ”’

Just like with signing, MLS includes a label and context in encryption operations, so ciphertexts made for different purposes don't get mixed up ๐Ÿ™ˆ Encryption and decryption with this label and context work like this ๐Ÿ’–

EncryptWithLabel(PublicKey, Label, Context, Plaintext) =
  SealBase(PublicKey, EncryptContext, "", Plaintext)

DecryptWithLabel(PrivateKey, Label, Context, KEMOutput, Ciphertext) =
  OpenBase(KEMOutput, PrivateKey, EncryptContext, "", Ciphertext)

Where EncryptContext is specified as:

struct {
  opaque label<V>;
  opaque context<V>;
} EncryptContext;

And its fields are set to:

label = "MLS 1.0 " + Label;
context = Context;

The SealBase and OpenBase functions are defined in Section 6.1 of [RFC9180] (with "Base" as the MODE), and they use the HPKE algorithms from the group's cipher suite. If MLS extensions need HPKE encryption operations, they should reuse the EncryptWithLabel construction, using a distinct label. To avoid collisions in these labels, there's an IANA registry defined in Section 17.7 ๐Ÿง

5.2. Hash-Based Identifiers ๐Ÿงฉ

Some MLS messages refer to other MLS objects by hash ๐Ÿ‘‘ For example, Welcome messages refer to the KeyPackages of the members being welcomed, and Commits refer to the Proposals they cover. These identifiers are computed like this ๐ŸŒˆ

opaque HashReference<V>;

HashReference KeyPackageRef;
HashReference ProposalRef;

MakeKeyPackageRef(value)
  = RefHash("MLS 1.0 KeyPackage Reference", value)

MakeProposalRef(value)
  = RefHash("MLS 1.0 Proposal Reference", value)

RefHash(label, value) = Hash(RefHashInput)

Where RefHashInput is defined as:

struct {
  opaque label<V>;
  opaque value<V>;
} RefHashInput;

And its fields are set to:

label = label;
value = value;

For a KeyPackageRef, the value input is the encoded KeyPackage, and the cipher suite specified in the KeyPackage decides which KDF gets used. For a ProposalRef, the value input is the AuthenticatedContent carrying the Proposal. In both of those cases, the KDF is decided by the group's cipher suite ๐ŸŽ€โœจ

5.3. Credentials

okay so every member of a group shows up with a credential ๐ŸŽ€. It's basically an ID card that gives the member one or more identities and ties them to the member's signing key โœจ. The Authentication Service in use for the group is the one that verifies those identities and that signing key ๐Ÿ”.

It's totally up to the app which identifiers to use at the app level ๐Ÿ’…. For example, a certificate in an X509Credential can vouch for a bunch of domain names or email addresses in its subjectAltName extension. The app can show all of them to the user, or if it already knows the "desired" domain name or email address, it can check that the desired one is on the list ๐Ÿฆ‹. Borrowing the terms from [RFC6125], a credential gives you "presented identifiers," and the app has to supply a "reference identifier" for the authenticated client, if there is one ๐Ÿ’œ.

// See the "MLS Credential Types" IANA registry for values
uint16 CredentialType;

struct {
    opaque cert_data<V>;
} Certificate;

struct {
    CredentialType credential_type;
    select (Credential.credential_type) {
        case basic:
            opaque identity<V>;

        case x509:
            Certificate certificates<V>;
    };
} Credential;

A "basic" credential is just a bare assertion of an identity, with no extra info attached ๐ŸŒธ. The app decides how the encoded identity is formatted.

For an X.509 credential, each entry in the certificates field is one DER-encoded X.509 certificate ๐Ÿ“œ. The chain is ordered so that certificates[0] is the end-entity certificate. The public key in the subjectPublicKeyInfo of that end-entity certificate MUST be identical to the signature_key in the LeafNode holding this credential, no exceptions ๐Ÿ™…โ€โ™€๏ธ. Also, a chain MAY leave out any non-leaf certificates that supported peers are already known to have, so it's a little space-saver ๐Ÿงš.

5.3.1. Credential Validation

The app using MLS is responsible for deciding which identifiers it finds acceptable for each member in a group ๐Ÿ‘‘. So following the model that [RFC6125] describes for TLS, the app keeps a list of "reference identifiers" for the members, and the credentials bring the "presented identifiers." A member gets authenticated by first checking that their credential legit represents some presented identifiers, and then making sure the member's reference identifiers are authenticated by those presented identifiers ๐Ÿ”.

The parts of the system doing these jobs are collectively called the Authentication Service (AS) [MLS-ARCH] ๐Ÿ›ก๏ธ. A member's credential is said to be validated with the AS when the AS confirms that the credential's presented identifiers are correctly tied to the signature_key field in the member's LeafNode, and that those identifiers match the member's reference identifiers ๐Ÿ’–.

Whenever a new credential shows up in the group, it MUST be validated with the AS ๐Ÿšจ. Specifically, this happens at these moments in the protocol:

  • When a member receives a KeyPackage that it will use in an Add proposal to add a new member to the group
  • When a member receives a GroupInfo object that it will use to join a group, either via a Welcome or via an external Commit
  • When a member receives an Add proposal adding a member to the group
  • When a member receives an Update proposal whose LeafNode has a new credential for the member
  • When a member receives a Commit with an UpdatePath whose LeafNode has a new credential for the committer
  • When an external_senders extension is added to the group
  • When an existing external_senders extension is updated

When a member's credential is being replaced, like in the Update and Commit cases above, the AS MUST also verify that the set of presented identifiers in the new credential is valid as a successor to the set in the old credential, according to the app's policy ๐Ÿฆ‹. Main character energy for policy checks, honestly ๐ŸŽฌ.

5.3.2. Credential Expiry and Revocation

In some credential schemes, a valid credential can "expire" or become invalid after a certain point in time โฐ. For example, every X.509 certificate has a notAfter field, which is a time after which the certificate is not valid anymore ๐Ÿ’€.

Expired credentials can cause operational problems given the validation requirements of Section 5.3.1 ๐Ÿ˜ญ. Apps can use some operational practices and tweaks to their Authentication Service policies to soften these impacts ๐Ÿซถ.

In general, to avoid problems like new joiners rejecting expired credentials in a group, apps that use these credentials should ensure, to the extent practical, that all of the credentials in use in a group are valid at all times โœจ.

If a member finds that its credential has expired (or will soon), it should issue an Update or Commit that replaces it with a valid credential ๐ŸŒˆ. For this reason, members SHOULD accept Update proposals and Commits issued by members with expired credentials, as long as the credential in the Update or Commit is valid ๐Ÿ’….

Similarly, when a client is processing messages that were sent a while ago (like when it's catching up with a group after being offline), the client SHOULD accept signatures from members with expired credentials ๐Ÿ•ฐ๏ธ. The credential might have been valid back when the message was sent, so it's fair game ๐Ÿ™ˆ.

If a member finds that another member's credential has expired, they may issue a Remove that removes that member ๐Ÿงน. For example, an app could require a member who's about to issue a Commit to check the tree for expired credentials and include Remove proposals for those members in the Commit ๐Ÿ“‹. When the group tree is known to the DS, the DS could also keep an eye on the tree for expired credentials and issue external Remove proposals ๐Ÿ‘€.

Some credential schemes also let credentials get revoked ๐Ÿšซ. Revocation is kind of like expiry in that a previously valid credential becomes invalid, so most of the stuff above also applies to revoked credentials ๐Ÿ”. But apps may want to treat revoked credentials differently, for example by removing members with revoked credentials while still giving members with expired credentials time to update ๐ŸŽ€.

5.3.3. Uniquely Identifying Clients

MLS implementations will presumably give apps a way to request protocol operations on other clients (like removing clients) ๐Ÿงšโ€โ™€๏ธ. Those functions need to refer to the other clients using some kind of identifier. MLS clients have a few types of identifiers, and they behave differently operationally ๐Ÿ“.

Inside the protocol, group members are uniquely identified by their leaf index ๐Ÿ”ข. BUT a leaf index is only valid for referring to members in a given epoch. In a later epoch, that same leaf index might point to a different member, or to nobody at all ๐Ÿ˜ต.

The Credentials presented by the clients in a group authenticate application-level identifiers for those clients ๐Ÿชช. These identifiers might not be unique, though. For example, if a user has multiple devices all in the same MLS group, those devices' clients could all present the user's application-layer identifiers ๐Ÿ“ฑ๐Ÿ’ป.

If needed, apps can add their own application-specific identifiers to the extensions field of a LeafNode object, using the application_id extension ๐Ÿท๏ธ.

opaque application_id<V>;
Okay, real talk, this part is so important ๐Ÿšจ: applications MUST NOT rely on the data in an application_id extension as if the Authentication Service had authenticated it. Also, apps SHOULD gracefully handle cases where the identifier presented is not unique. Be nice to your future self and handle duplicates! ๐Ÿซถ

6. Message Framing ๐ŸŽ€

okay bestie, handshake messages and application messages all wear the same outfit ๐Ÿ‘—โœจ That outfit is the framing structure! It gives you encryption so only the group can read the stuff inside (confidentiality ๐Ÿ”’), plus signing so everyone knows who actually sent it ๐Ÿซถ

In most of the protocol, messages get handled as AuthenticatedContent objects ๐Ÿ’… These hold the message content itself plus the info needed to prove who sent it (see Section 6.1). Then, before the message can travel over an untrusted channel, you wrap it up as a PublicMessage or PrivateMessage, which is what gets sent as an MLSMessage ๐Ÿ“ฆ The extra protections for that trip, group membership authentication or AEAD encryption, get added at that step. Going the other way, the receiver checks those protections (membership verification or AEAD decryption) when they unwrap a PublicMessage or PrivateMessage back into an AuthenticatedContent ๐Ÿฆ‹

PrivateMessage is the signed AND encrypted one, protecting both the content and the metadata around it ๐Ÿคซ PublicMessage is only signed, not encrypted ๐Ÿ‘€ Applications MUST use PrivateMessage to encrypt application messages, and they SHOULD use PrivateMessage for handshake messages too. The exception: they MAY send handshake messages as PublicMessage objects when the Delivery Service needs to be able to look at them ๐ŸŒธ

enum {
    reserved(0),
    mls10(1),
    (65535)
} ProtocolVersion;

enum {
    reserved(0),
    application(1),
    proposal(2),
    commit(3),
    (255)
} ContentType;

enum {
    reserved(0),
    member(1),
    external(2),
    new_member_proposal(3),
    new_member_commit(4),
    (255)
} SenderType;

struct {
    SenderType sender_type;
    select (Sender.sender_type) {
        case member:
            uint32 leaf_index;
        case external:
            uint32 sender_index;
        case new_member_commit:
        case new_member_proposal:
            struct{};
    };
} Sender;

// See the "MLS Wire Formats" IANA registry for values
uint16 WireFormat;

struct {
    opaque group_id<V>;
    uint64 epoch;
    Sender sender;
    opaque authenticated_data<V>;

    ContentType content_type;
    select (FramedContent.content_type) {
        case application:
          opaque application_data<V>;
        case proposal:
          Proposal proposal;
        case commit:
          Commit commit;
    };
} FramedContent;

struct {
    ProtocolVersion version = mls10;
    WireFormat wire_format;
    select (MLSMessage.wire_format) {
        case mls_public_message:
            PublicMessage public_message;
        case mls_private_message:
            PrivateMessage private_message;
        case mls_welcome:
            Welcome welcome;
        case mls_group_info:
            GroupInfo group_info;
        case mls_key_package:
            KeyPackage key_package;
    };
} MLSMessage;

Messages from senders who aren't in the group get sent as PublicMessage ๐Ÿ™‹โ€โ™€๏ธ Check out Sections 12.1.8 and 12.4.3.2 for the full tea โ˜•

Here's the structure that fully describes the data sent in plaintexts or ciphertexts, so no mystery bits ๐Ÿงš

struct {
    WireFormat wire_format;
    FramedContent content;
    FramedContentAuthData auth;
} AuthenticatedContent;

The figure below shows how all these structures connect to each other, plus the high-level operations that make and read them. It's giving flowchart energy ๐Ÿ“Š

    Proposal        Commit     Application Data
       |              |              |
       +--------------+--------------+
                      |
                      V
               FramedContent
                   |  |                -.
          +--------+  |                  |
          |           |                  |
          V           |                  +-- Asymmetric
FramedContentAuthData |                  |   Sign / Verify
          |           |                  |
          +--------+  |                  |
                   |  |                  |
                   V  V                -'
             AuthenticatedContent
                      |                -.
             +--------+--------+         |
             |                 |         +-- Symmetric
             V                 V         |   Protect / Unprotect
       PublicMessage    PrivateMessage -'
             |                 |
             |                 |  Welcome  KeyPackage  GroupInfo
             |                 |     |          |          |
             +-----------------+-----+----------+----------+
                               |
                               V
                           MLSMessage

              Figure 12: Relationships among MLS Objects

6.1. Content Authentication ๐Ÿ”

FramedContent gets authenticated using the FramedContentAuthData structure. Let's see what's inside ๐Ÿ‘€

struct {
    ProtocolVersion version = mls10;
    WireFormat wire_format;
    FramedContent content;
    select (FramedContentTBS.content.sender.sender_type) {
        case member:
        case new_member_commit:
            GroupContext context;
        case external:
        case new_member_proposal:
            struct{};
    };
} FramedContentTBS;

opaque MAC<V>;

struct {
    /* SignWithLabel(., "FramedContentTBS", FramedContentTBS) */
    opaque signature<V>;
    select (FramedContent.content_type) {
        case commit:
            /*
              MAC(confirmation_key,
                  GroupContext.confirmed_transcript_hash)
            */
            MAC confirmation_tag;
        case application:
        case proposal:
            struct{};
    };
} FramedContentAuthData;

The signature is made with SignWithLabel, using the label "FramedContentTBS" ๐Ÿ–Š๏ธ It covers the message content AND the wire format used for that message. If the sender's sender_type is member, the signature also covers the GroupContext for the current epoch, so signatures are specific to one group and one epoch. Trรจs iconic ๐Ÿ’œ

The sender MUST use the private key that matches the signature key for their sender_type:

  • member: The signature key contained in the LeafNode at the index indicated by leaf_index in the ratchet tree.
  • external: The signature key at the index indicated by sender_index in the external_senders group context extension (see Section 12.1.8.1). The content_type of the message MUST be proposal and the proposal_type MUST be a value that is allowed for external senders.
  • new_member_commit: The signature key in the LeafNode in the Commit's path (see Section 12.4.3.2). The content_type of the message MUST be commit.
  • new_member_proposal: The signature key in the LeafNode in the KeyPackage embedded in an external Add proposal. The content_type of the message MUST be proposal and the proposal_type of the Proposal MUST be add.

Recipients of an MLSMessage MUST verify the signature using the key that matches the sender's sender_type, as described above ๐Ÿง No skipping this step, babe!

The confirmation tag is the proof that all the members of the group ended up in the same state of the group โœจ A FramedContentAuthData is called valid when both its signature and its confirmation_tag fields are valid. Main character energy for those two fields ๐Ÿ‘‘

6.2. Encoding and Decoding a Public Message ๐ŸŒˆ

Messages that are authenticated but NOT encrypted get encoded using the PublicMessage structure ๐Ÿ“ฃ

struct {
    FramedContent content;
    FramedContentAuthData auth;
    select (PublicMessage.content.sender.sender_type) {
        case member:
            MAC membership_tag;
        case external:
        case new_member_commit:
        case new_member_proposal:
            struct{};
    };
} PublicMessage;

The membership_tag field in a PublicMessage authenticates the sender's membership in the group ๐Ÿชช For messages sent by members, it MUST be set to the following value:

struct {
  FramedContentTBS content_tbs;
  FramedContentAuthData auth;
} AuthenticatedContentTBM;

membership_tag = MAC(membership_key, AuthenticatedContentTBM)

When decoding a PublicMessage into an AuthenticatedContent, the application MUST check membership_tag and MUST check that the FramedContentAuthData is valid. Double checks, no exceptions ๐Ÿ™ˆ

6.3. Encoding and Decoding a Private Message ๐Ÿคซ

Authenticated AND encrypted messages get encoded using the PrivateMessage structure ๐Ÿ”’โœจ

struct {
    opaque group_id<V>;
    uint64 epoch;
    ContentType content_type;
    opaque authenticated_data<V>;
    opaque encrypted_sender_data<V>;
    opaque ciphertext<V>;
} PrivateMessage;

encrypted_sender_data and ciphertext both get encrypted with the AEAD function from the cipher suite in use ๐Ÿ’– The inputs are the SenderData and PrivateMessageContent structures. Lowkey the best-kept secrets in the whole protocol ๐Ÿงš

6.3.1. Content Encryption ๐ŸŽ€

okay so first things first, the content you wanna encrypt gets wrapped up in a PrivateMessageContent struct ๐Ÿ’…

struct {
    select (PrivateMessage.content_type) {
        case application:
          opaque application_data<V>;

        case proposal:
          Proposal proposal;

        case commit:
          Commit commit;
    };

    FramedContentAuthData auth;
    opaque padding[length_of_padding];
} PrivateMessageContent;

so the padding ๐Ÿซง gets set by the sender. they encode the content (via the select) and the auth field first, and then tack on however many zero bytes they picked. on the receiving end, you decode the content and the auth field out of the plaintext from PrivateMessage.ciphertext, and whatever bytes are left over? that's your padding. the padding field MUST be filled with all zero bytes. a receiver MUST verify that there are no non-zero bytes in the padding field, and if that check fails, the enclosing PrivateMessage MUST be rejected as malformed.

zero bytes only, or the whole message gets tossed as malformed ๐Ÿ™…โ€โ™€๏ธ

why so strict? bc it keeps the padding process deterministic, so padding can't be used as a covert channel. that's a whole vibe check ๐Ÿ•ต๏ธโ€โ™€๏ธ

now the key schedule ๐Ÿ”‘ in MLS, the sender makes two separate key ratchets for every member of the group: one for handshake messages and one for application messages. when encrypting, the sender looks at the ratchets they derived for their own member and picks an unused generation from either the handshake ratchet or the application ratchet, depending on the content type. that generation gets used to derive a provisional nonce and key โœจ

before the nonce gets used in the encryption operation, it's XORed with a fresh random value to guard against reuse. and here's the tea: because the key schedule makes nonces deterministically, a client MUST keep persistent state about where it is in the key schedule. if that persistent state gets lost or corrupted, a client might reuse a generation that's already been used, which means reusing a key/nonce pair. yikes ๐Ÿ˜ญ

so to avoid that disaster, the sender of a message MUST generate a fresh random four-byte "reuse guard" value and XOR it with the first four bytes of the nonce from the key schedule before using the nonce for encryption. the sender MUST include the reuse guard in the reuse_guard field of the sender data object, so the recipient can use it to compute the nonce for decryption. bestie, that's the whole trick ๐Ÿซถ

+-+-+-+-+---------...---+
|   Key Schedule Nonce  |
+-+-+-+-+---------...---+
           XOR
+-+-+-+-+---------...---+
| Guard |       0       |
+-+-+-+-+---------...---+
           ===
+-+-+-+-+---------...---+
| Encrypt/Decrypt Nonce |
+-+-+-+-+---------...---+

next up, the Additional Authenticated Data (AAD) input to the encryption holds an object shaped like this, with the values used to identify the key and nonce ๐Ÿฆ‹

struct {
    opaque group_id<V>;
    uint64 epoch;
    ContentType content_type;
    opaque authenticated_data<V>;
} PrivateContentAAD;

when decoding a PrivateMessageContent, the application MUST check that the FramedContentAuthData is valid. no skipping that step, periodt ๐Ÿ’…

it's up to the application to decide what authenticated_data to provide and how much padding to add to a given message (if any). and the overall size of the AAD and ciphertext MUST fit within the limits set for the group's AEAD algorithm in [CFRG-AEAD-LIMITS]. size matters, even in crypto ๐Ÿ“๐ŸŒธ

6.3.2. Sender Data Encryption ๐Ÿงš

the "sender data" that tells you which key to use for content encryption gets encrypted with the cipher suite's AEAD. the key and nonce come from both the sender_data_secret and a sample of the encrypted content ๐Ÿ” before encryption, the sender data gets encoded like this:

struct {
    uint32 leaf_index;
    uint32 generation;
    opaque reuse_guard[4];
} SenderData;

when building a SenderData object from a Sender object, the sender MUST verify that Sender.sender_type is member, and then use Sender.leaf_index for SenderData.leaf_index. the main character here is the leaf index ๐Ÿ‘‘

the reuse_guard field holds that fresh random value that helps avoid nonce reuse when state gets lost or corrupted, as described in Section 6.3.1. callback moment ๐Ÿ”

the key and nonce for the AEAD are computed as the KDF of the first KDF.Nh bytes of the ciphertext generated in the previous section. if the ciphertext is shorter than KDF.Nh, the whole ciphertext gets used. in pseudocode, it goes like this:

ciphertext_sample = ciphertext[0..KDF.Nh-1]

sender_data_key = ExpandWithLabel(sender_data_secret, "key",
                      ciphertext_sample, AEAD.Nk)
sender_data_nonce = ExpandWithLabel(sender_data_secret, "nonce",
                      ciphertext_sample, AEAD.Nn)

the AAD for the SenderData ciphertext is the first three fields of PrivateMessage ๐ŸŽ€

struct {
    opaque group_id<V>;
    uint64 epoch;
    ContentType content_type;
} SenderDataAAD;

when parsing a SenderData struct as part of message decryption, the recipient MUST verify that the leaf index in the leaf_index field identifies a non-blank node. no ghost leaves allowed ๐Ÿ‘ป


7. Ratchet Tree Operations ๐ŸŒณ

okay now we're in the ratchet tree era ๐ŸŒˆ the ratchet tree for an epoch describes the membership of a group in that epoch. it gives you the public key encryption (HPKE) keys that let you encrypt to subsets of the group, plus the info needed to authenticate the members. whenever the membership changes from one epoch to the next, the tree changes to match ๐Ÿฆ‹ this section walks through what's in the tree and which operations you're allowed to do on it.

7.1. Parent Node Contents ๐Ÿ’œ

remember from Section 4.1.1 that the nodes of a ratchet tree hold a few different kinds of data. leaf nodes describe individual members, and parent nodes describe subgroups of the group. parent nodes are simpler, so here's their shape:

struct {
    HPKEPublicKey encryption_key;
    opaque parent_hash<V>;
    uint32 unmerged_leaves<V>;
} ParentNode;

the encryption_key field holds an HPKE public key. its private key is only held by the members at the leaves among that node's descendants ๐Ÿ”‘ the parent_hash field holds a hash of this node's parent node, as described in Section 7.9 ๐Ÿงฉ and the unmerged_leaves field lists the leaves under this parent node that are unmerged, going by their indices among all the leaves in the tree. the entries in the unmerged_leaves vector MUST be sorted in increasing order. sorted, always, no exceptions ๐Ÿ“ˆโœจ

7.2. Leaf Node Contents โœจ

okay so a leaf node is basically a client's whole look in the group, and it's signed by that client ๐Ÿ–Š๏ธ๐Ÿ’– it also does double duty inside client KeyPackage objects, where it stores everything needed to add someone to a group ๐ŸŽ€

   enum {
       reserved(0),
       key_package(1),
       update(2),
       commit(3),
       (255)
   } LeafNodeSource;

   struct {
       ProtocolVersion versions<V>;
       CipherSuite cipher_suites<V>;
       ExtensionType extensions<V>;
       ProposalType proposals<V>;
       CredentialType credentials<V>;
   } Capabilities;

   struct {
       uint64 not_before;
       uint64 not_after;
   } Lifetime;

   // See the "MLS Extension Types" IANA registry for values
   uint16 ExtensionType;

   struct {
       ExtensionType extension_type;
       opaque extension_data<V>;
   } Extension;

   struct {
       HPKEPublicKey encryption_key;
       SignaturePublicKey signature_key;
       Credential credential;
       Capabilities capabilities;

       LeafNodeSource leaf_node_source;
       select (LeafNode.leaf_node_source) {
           case key_package:
               Lifetime lifetime;

           case update:
               struct{};

           case commit:
               opaque parent_hash<V>;
       };

       Extension extensions<V>;
       /* SignWithLabel(., "LeafNodeTBS", LeafNodeTBS) */
       opaque signature<V>;
   } LeafNode;

   struct {
       HPKEPublicKey encryption_key;
       SignaturePublicKey signature_key;
       Credential credential;
       Capabilities capabilities;

       LeafNodeSource leaf_node_source;
       select (LeafNodeTBS.leaf_node_source) {
           case key_package:
               Lifetime lifetime;

           case update:
               struct{};

           case commit:
               opaque parent_hash<V>;
       };

       Extension extensions<V>;

       select (LeafNodeTBS.leaf_node_source) {
           case key_package:
               struct{};

           case update:
               opaque group_id<V>;
               uint32 leaf_index;

           case commit:
               opaque group_id<V>;
               uint32 leaf_index;
       };
   } LeafNodeTBS;

The encryption_key field holds an HPKE public key whose private key lives only with the member sitting in this leaf (or, for a LeafNode inside a KeyPackage, with whoever issued that KeyPackage) ๐Ÿ” The signature_key field holds the member's public signing key โœ๏ธ And the credential field carries info that authenticates both the member's identity and that signing key, see Section 5.3 ๐Ÿ’…

The capabilities field is the client's feature menu ๐Ÿงš It lists the protocol versions, cipher suites, credential types, non-default proposal types, and non-default extension types the client supports. The following proposal and extension types count as "default" and MUST NOT be listed ๐Ÿ™…โ€โ™€๏ธ

  • Proposal types:
    • 0x0001 - add
    • 0x0002 - update
    • 0x0003 - remove
    • 0x0004 - psk
    • 0x0005 - reinit
    • 0x0006 - external_init
    • 0x0007 - group_context_extensions
  • Extension types:
    • 0x0001 - application_id
    • 0x0002 - ratchet_tree
    • 0x0003 - required_capabilities
    • 0x0004 - external_pub
    • 0x0005 - external_senders

There are no defaults for the other fields of a capabilities object, so the client MUST list every single value it supports for each respective parameter ๐Ÿ’ฏ

Any non-default extension showing up in a LeafNode's extensions field MUST also be listed in the capabilities extensions field, and the credential type the LeafNode uses MUST be in the capabilities credentials field ๐Ÿซถ

Like Section 13 says, unknown values in capabilities MUST be ignored ๐Ÿ™ˆ And whoever creates a capabilities field SHOULD toss in some random GREASE values so other clients actually learn to ignore unknown stuff ๐Ÿงช

The leaf_node_source field tells everyone how this LeafNode ended up in the tree ๐ŸŒณ It signals whether the leaf needs a lifetime or parent_hash, and whether the group_id gets added as context to the signature. These fields are included selectively, because the creator of a LeafNode can't always compute all of them. For example, a KeyPackage gets made before the client knows which group it'll be used in, so its signature can't bind to a group_id ๐Ÿ˜ญ

When a leaf was added based on a pre-published KeyPackage, the lifetime field is the window during which clients consider the LeafNode valid โฐ These times are absolute, measured in seconds since the Unix epoch (1970-01-01T00:00:00Z). Applications MUST define a maximum total lifetime that's acceptable for a LeafNode, and reject any LeafNode whose total lifetime is longer than that duration ๐Ÿšซ To avoid disagreements about whether a LeafNode's lifetime is valid, the clients in a group SHOULD keep their time synchronized (e.g., using the Network Time Protocol [RFC5905]) โฑ๏ธ

When a leaf node got inserted into the tree via a Commit message, the parent_hash field holds the parent hash for that leaf (see Section 7.9) ๐ŸŒฟ

LeafNodeTBS covers the fields above the signature in the LeafNode. When the leaf node was created in the context of a group (the update and commit cases), the group ID gets added as context to the signature too ๐Ÿ”

LeafNode objects stored in the group's ratchet tree get updated as the tree evolves. Each modification of LeafNode content MUST be reflected by a change in its signature ๐Ÿ’ซ That lets other members verify the LeafNode's validity at any time, especially when a newcomer joins the group ๐Ÿ‘ฏ


7.3. Leaf Node Validation ๐Ÿ’…

The validity of a LeafNode needs to be checked at these moments ๐Ÿ‘€

  • When a LeafNode is downloaded in a KeyPackage, before it is used to add the client to the group
  • When a LeafNode is received by a group member in an Add, Update, or Commit message
  • When a client validates a ratchet tree, e.g., when joining a group or after processing a Commit

The client runs through these steps to verify a LeafNode ๐Ÿ“‹

  • Verify that the credential in the LeafNode is valid, as described in Section 5.3.1.
  • Verify that the signature on the LeafNode is valid using signature_key.
  • Verify that the LeafNode is compatible with the group's parameters. If the GroupContext has a required_capabilities extension, then the required extensions, proposals, and credential types MUST be listed in the LeafNode's capabilities field.
  • Verify that the credential type is supported by all members of the group, as specified by the capabilities field of each member's LeafNode, and that the capabilities field of this LeafNode indicates support for all the credential types currently in use by other members.
  • Verify the lifetime field:
    • If the LeafNode appears in a message being sent by the client, e.g., a Proposal or a Commit, then the client MUST verify that the current time is within the range of the lifetime field.
    • If instead the LeafNode appears in a message being received by the client, e.g., a Proposal, a Commit, or a ratchet tree of the group the client is joining, it is RECOMMENDED that the client verifies that the current time is within the range of the lifetime field. (This check is not mandatory because the LeafNode might have expired in the time between when the message was sent and when it was received.)
  • Verify that the extensions in the LeafNode are supported by checking that the ID for each extension in the extensions field is listed in the capabilities.extensions field of the LeafNode.
  • Verify the leaf_node_source field:
    • If the LeafNode appears in a KeyPackage, verify that leaf_node_source is set to key_package.
    • If the LeafNode appears in an Update proposal, verify that leaf_node_source is set to update and that encryption_key represents a different public key than the encryption_key in the leaf node being replaced by the Update proposal.
    • If the LeafNode appears in the leaf_node value of the UpdatePath in a Commit, verify that leaf_node_source is set to commit.
  • Verify that the following fields are unique among the members of the group:
    • signature_key
    • encryption_key

7.4. Ratchet Tree Evolution โœจ

okay so whenever a member starts an epoch change (aka they commit, see Section 12.4 ๐ŸŽ€), they may need to refresh the key pairs for their leaf and for the nodes on their leaf's direct path. that's how we keep forward secrecy and post-compromise security ๐Ÿ”๐Ÿ’…

the member starting the change makes the fresh key pairs with the procedure below. it's built so group members can efficiently pass the fresh secret keys to each other, as described in Section 7.6 ๐Ÿซถ

a member updates the nodes along its direct path like this:

  • Blank all the nodes on the direct path from the leaf to the root.
  • Generate a fresh HPKE key pair for the leaf.
  • Generate a sequence of path secrets, one for each node on the leaf's filtered direct path, as follows. In this setting, path_secret[0] refers to the first parent node in the filtered direct path, path_secret[1] to the second parent node, and so on.
path_secret[0] is sampled at random
path_secret[n] = DeriveSecret(path_secret[n-1], "path")
  • Compute the sequence of HPKE key pairs (node_priv,node_pub), one for each node on the leaf's direct path, as follows.
node_secret[n] = DeriveSecret(path_secret[n], "node")
node_priv[n], node_pub[n] = KEM.DeriveKeyPair(node_secret[n])

the node secret is derived as a temporary intermediate secret so that each secret only gets used with one algorithm ๐Ÿฆ‹ the path secret goes into DeriveSecret, and the node secret goes into DeriveKeyPair. no mixing, no cap.

for example, say there's a group with four members, with C an unmerged leaf at Z:

      Y
      |
    .-+-.
   /     \
  X       Z[C]
 / \     / \
A   B   C   D

0   1   2   3

            Figure 13: A Full Tree with One Unmerged Leaf

if member B then generates an UpdatePath based on a secret "leaf_secret", it would generate this sequence of path secrets ๐Ÿ‘‘

path_secret[1] ---> node_secret[1] -------> node_priv[1], node_pub[1]

     ^
     |
     |
path_secret[0] ---> node_secret[0] -------> node_priv[0], node_pub[0]

     ^
     |
     |
leaf_secret ------> leaf_node_secret --+--> leaf_priv, leaf_pub
                                           |                   |
                                            '-------. .-------'
                                                     |
                                                 leaf_node

    Figure 14: Derivation of Ratchet Tree Keys along a Direct Path

after applying the UpdatePath, the tree looks like this ๐ŸŒˆ

node_priv[1] --------> Y'
                       |
                     .-+-.
                    /     \
node_priv[0] ----> X'      Z[C]
                  / \     / \
                 A   B   C   D
                     ^
leaf_priv -----------+
                 0   1   2   3

            Figure 15: Placement of Keys in a Ratchet Tree

7.5. Synchronizing Views of the Tree

after generating fresh key material and applying it to their local tree state (as described in Section 7.4 ๐Ÿ’–), the generator broadcasts the update to the other members in a Commit message. everyone applies it to keep their local views of the tree in sync with the sender's โœจ more specifically, when a member commits a change to the tree (like adding or removing someone), they send an UpdatePath containing a set of public keys and encrypted path secrets for the intermediate nodes in the filtered direct path of their leaf. the other members use these values to update their view of the tree, lining up their copy with the sender's ๐Ÿงš

an UpdatePath contains the following info for each node in the filtered direct path of the sender's leaf, including the root:

  • The public key for the node
  • One or more encrypted copies of the path secret corresponding to the node

the path secret for a given node gets encrypted to the subtree rooted at the parent's non-updated child, aka the child on the copath of the sender's leaf node. there's one encryption of the path secret to each public key in the resolution of the non-updated child ๐Ÿ“

a member of the group updates their direct path by computing new values for their leaf node and the nodes along their filtered direct path, like so:

  1. Blank all nodes along the direct path of the sender's leaf.
  2. Compute updated path secrets and public keys for the nodes on the sender's filtered direct path.
    • Generate a sequence of path secrets of the same length as the filtered direct path, as defined in Section 7.4.
    • For each node in the filtered direct path, replace the node's public key with the node_pub[n] value derived from the corresponding path secret path_secret[n].
  3. Compute the new parent hashes for the nodes along the filtered direct path and the sender's leaf node.
  4. Update the leaf node for the sender.
    • Set the leaf_node_source to commit.
    • Set the encryption_key to the public key of a freshly sampled key pair.
    • Set the parent hash to the parent hash for the leaf.
    • Re-sign the leaf node with its new contents.

since the new leaf node basically updates an existing leaf node in the group, it MUST adhere to the same restrictions as LeafNodes used in Update proposals (aside from leaf_node_source). the application MAY specify other changes to the leaf node, like providing a new signature key, updated capabilities, or different extensions ๐ŸŽ€

then the member encrypts path secrets to the group. for each node in the member's filtered direct path, they do the following:

  1. Compute the resolution of the node's child that is on the copath of the sender (the child that is not in the direct path of the sender). Any new member (from an Add proposal) added in the same Commit MUST be excluded from this resolution.
  2. For each node in the resolution, encrypt the path secret for the direct path node using the public key of the resolution node, as defined in Section 7.6 ๐Ÿ”ฅ

the recipient of an UpdatePath does the matching steps. first, the recipient merges UpdatePath into the tree:

  1. Blank all nodes on the direct path of the sender's leaf.
  2. For all nodes on the filtered direct path of the sender's leaf:
    • Set the public key to the public key in the UpdatePath.
    • Set the list of unmerged leaves to the empty list.
  3. Compute parent hashes for the nodes in the sender's filtered direct path, and verify that the parent_hash field of the leaf node matches the parent hash for the first node in its filtered direct path.
    • Note that these hashes are computed from root to leaf, so that each hash incorporates all the non-blank nodes above it. The root node always has a zero-length hash for its parent hash.

second, the recipient decrypts the path secrets:

  1. Identify a node in the filtered direct path for which the recipient is in the subtree of the non-updated child.
  2. Identify a node in the resolution of the copath node for which the recipient has a private key.
  3. Decrypt the path secret for the parent of the copath node using the private key from the resolution node.
  4. Derive path secrets for ancestors of that node in the sender's filtered direct path using the algorithm described above.
  5. Derive the node secrets and node key pairs from the path secrets.
  6. Verify that the derived public keys are the same as the corresponding public keys sent in the UpdatePath.
  7. Store the derived private keys in the corresponding ratchet tree nodes.

for example, to pass along the example update from Section 7.4, the member at node B would send these values ๐Ÿ’…

+=============+====================================================+
| Public Key  | Ciphertext(s)                                      |
+=============+====================================================+
| node_pub[1] | E(pk(Z), path_secret[1]), E(pk(C), path_secret[1]) |
+-------------+----------------------------------------------------+
| node_pub[0] | E(pk(A), path_secret[0])                           |
+-------------+----------------------------------------------------+

                              Table 3

in this table, node_pub[i] is the public key derived from node_secret[i], pk(X) is the current public key of node X, and E(K, S) is the public key encryption of the path secret S to the public key K (using HPKE) ๐Ÿฆ‹

a recipient at node A decrypts E(pk(A), path_secret[0]) to get path_secret[0], then uses it to derive path_secret[1] and the resulting node secrets and key pairs. so A ends up with the private keys to nodes X' and Y', matching the tree invariant ๐Ÿงš

a recipient at node D decrypts E(pk(Z), path_secret[1]) to get path_secret[1], then uses it to derive the node secret and key pair for node Y'. and yes, as the tree invariant requires, node D does not get the private key for X', since X' isn't an ancestor of D ๐Ÿ™ˆ

after processing the update, each recipient MUST delete outdated key material, specifically: - The path secrets and node secrets used to derive each updated node key pair. - Each outdated node key pair that was replaced by the update.

7.6. Update Paths ๐ŸŽ€

as described in Section 12.4, each Commit message may optionally contain an UpdatePath, with a new LeafNode and a set of parent nodes for the sender's filtered direct path. for each parent node, the UpdatePath has a new public key and an encrypted path secret. the parent nodes stay in the same order as the filtered direct path ๐ŸŒธ

struct {
    opaque kem_output<V>;
    opaque ciphertext<V>;
} HPKECiphertext;

struct {
    HPKEPublicKey encryption_key;
    HPKECiphertext encrypted_path_secret<V>;
} UpdatePathNode;

struct {
    LeafNode leaf_node;
    UpdatePathNode nodes<V>;
} UpdatePath;

for each UpdatePathNode, the resolution of the corresponding copath node MUST exclude all new leaf nodes added as part of the current Commit. the length of the encrypted_path_secret vector MUST be equal to the length of the resolution of the copath node (excluding new leaf nodes), with each ciphertext being the encryption to the respective resolution node ๐Ÿ”

the HPKECiphertext values get encrypted and decrypted like this:

(kem_output, ciphertext) =
  EncryptWithLabel(node_public_key, "UpdatePathNode",
                   group_context, path_secret)

path_secret =
  DecryptWithLabel(node_private_key, "UpdatePathNode",
                   group_context, kem_output, ciphertext)

here node_public_key is the public key of the node the path secret is encrypted for, group_context is the provisional GroupContext object for the group, and the EncryptWithLabel function is as defined in Section 5.1.3 ๐Ÿ’œ

7.7. Adding and Removing Leaves โœจ

okay so besties, the tree doesn't just update along paths ๐ŸŒณ sometimes we need to add or remove leaves too, because members join and leave the group (see Sections 12.1.1 and 12.1.3) ๐Ÿ’– and since the tree is always full, adding or removing leaves means the tree gets deeper or shallower, so the number of leaves doubles or halves ๐Ÿซถ these operations are also called _extending_ and _truncating_ the tree ๐Ÿฆ‹

leaves always get added and removed on the right edge of the tree ๐Ÿ‘‘ when the tree needs to grow, a brand new blank root gets added, with the old tree on its left and a new all-blank subtree on its right ๐ŸŒธ this usually happens when a new member joins the group ๐Ÿงš

_ <-- new blank root _ __|__ __|__ / \ / \ X ===> X _ <-- new blank subtree ===> X _ / \ / \ / \ / \ / \ A B A B _ _ A B C _ ^

new member --+ </pre>

Figure 16: Extending the Tree to Make Room for a Third Member

Okay wait, the other way around ๐Ÿ˜ญ when the right subtree of the tree has no non-blank nodes left, it can be safely removed ๐Ÿ’… the root and that right subtree get thrown out (whether or not the root is blank) ๐Ÿ’€ the left child of the root becomes the new root, and the left subtree becomes the whole new tree ๐Ÿ“ this usually happens after a member gets removed from the group ๐Ÿ™ˆ

Y Y __|__ __|__ / \ / \ X _ ===> X _ ==> X <-- new root / \ / \ / \ / \ / \ A B C _ A B _ _ A B ^

removed member --+

Figure 17: Cleaning Up after Removing Member C

ngl the concrete algorithms for these operations on array-based and link-based trees live in Appendices C and D ๐ŸŽ€ those algorithms are non-normative, so an implementation can use any algorithm it likes as long as it ends up with the correct tree in its internal representation ๐ŸŒˆ iykyk ๐Ÿ’œ

7.8. Tree Hashes ๐ŸŒธ

okay so MLS hashes the tree contents in two different ways to check different properties of the tree ๐Ÿ”ฅ _tree hashes_ are defined right here, and _parent hashes_ get their own spot in Section 7.9 ๐Ÿฆ‹

each node in a ratchet tree has a tree hash that summarizes the subtree under that node ๐Ÿ’– the root's tree hash is what the GroupContext uses to confirm the whole group agrees on the whole tree ๐Ÿ‘‘ tree hashes get computed recursively, starting from the leaves and building up to the root ๐Ÿซถ

P --> th(P) ^ ^ / \ / \ th(L) th(R)

Figure 18: Composition of the Tree Hash

the tree hash of a single node is the hash of that node's TreeHashInput object ๐ŸŽ€ that object can hold either a LeafNodeHashInput or a ParentNodeHashInput, depending on what kind of node it is ๐Ÿ’… LeafNodeHashInput objects hold the leaf_index and the LeafNode (if there is one) ๐Ÿงš ParentNodeHashInput objects hold the ParentNode (if there is one) plus the tree hashes of the node's left and right children ๐ŸŒˆ for both parent and leaf nodes, the optional node value MUST be absent if the node is blank, and MUST be present if the node has a value ๐Ÿšจ๐Ÿ’œ

enum {
    reserved(0),
    leaf(1),
    parent(2),
    (255)
} NodeType;

struct {
  NodeType node_type;
  select (TreeHashInput.node_type) {
    case leaf:   LeafNodeHashInput leaf_node;
    case parent: ParentNodeHashInput parent_node;
  };
} TreeHashInput;

struct {
    uint32 leaf_index;
    optional<LeafNode> leaf_node;
} LeafNodeHashInput;

struct {
    optional<ParentNode> parent_node;
    opaque left_hash<V>;
    opaque right_hash<V>;
} ParentNodeHashInput;

the tree hash of a whole tree is just the tree hash of the root node ๐Ÿ“ and it gets computed recursively, starting at the leaves and building up ๐Ÿซง โœจ

7.9. Parent Hashes ๐Ÿ’œ

okay so while tree hashes summarize what the tree looks like at one moment in time, parent hashes capture info about how the keys in the tree got filled in ๐Ÿ•ฐ๏ธโœจ

when a client sends a Commit to change the group, it can include an UpdatePath to assign new keys to the nodes along its filtered direct path ๐Ÿฆ‹ when a client computes an UpdatePath (as defined in Section 7.5), it computes and signs a parent hash that summarizes the tree after the UpdatePath has been applied ๐Ÿ’… these summaries get built in a chain from the root down to the member's leaf ๐Ÿ”— so the part of the chain closer to the root can get overwritten when nodes set by one UpdatePath are reset by a later UpdatePath ๐ŸŒธ

                    ph(Q)
                    /
                   /
                  V
P.public_key --> ph(P)
                / ^
               /   \
              V     \
  N.parent_hash     th(S)

Figure 19: Inputs to a Parent Hash

so the signature over the parent hash in each member's leaf basically signs the subtree that hasn't changed since that leaf was last changed in an UpdatePath ๐Ÿซถ a new member joining the group uses these parent hashes to check that the parent nodes were set by members of the group, and NOT by some sneaky external attacker ๐Ÿ˜ค๐Ÿšซ for an example of how this works, check out Appendix B ๐ŸŽ€

Consider a ratchet tree with a non-blank parent node P and children D and S (for "parent", "direct path", and "sibling"), with D and P in the direct path of a leaf node L (for "leaf") ๐Ÿงš

        ...
        /
       P
     __|__
    /     \
   D       S
  / \     / \
... ... ... ...
/
L

Figure 20: Nodes Involved in a Parent Hash Computation

the parent hash of P changes whenever an UpdatePath object gets applied to the ratchet tree along a path from a leaf L that goes through node D (and so also through P) ๐ŸŒˆ the new "Parent hash of P (with copath child S)" comes from hashing P's ParentHashInput struct ๐Ÿ’–

struct {
    HPKEPublicKey encryption_key;
    opaque parent_hash<V>;
    opaque original_sibling_tree_hash<V>;
} ParentHashInput;

the field encryption_key holds the HPKE public key of P ๐Ÿ”‘ if P is the root, the parent_hash field is set to a zero-length octet string ๐Ÿซง otherwise, parent_hash is the parent hash of the next node after P on the filtered direct path of leaf L ๐Ÿฆ‹ this way, P's parent hash fixes the new HPKE public key of each non-blank node on the path from P up to the root ๐Ÿ’œ the path from P to the root may contain some blank nodes that P's parent hash does NOT fix ๐Ÿ˜ฌ but for every node that has an HPKE key, that key IS fixed by P's parent hash ๐Ÿ”’โœจ

last but not least, original_sibling_tree_hash is the tree hash of S in the ratchet tree after being modified like this: for each leaf L in P.unmerged_leaves, blank L and remove it from the unmerged_leaves sets of all parent nodes ๐Ÿงน

observe this, bestie: original_sibling_tree_hash does NOT change between updates of P ๐Ÿ’… this property is crucial for the correctness of the protocol ๐Ÿ”ฅ

note that original_sibling_tree_hash is the tree hash of S, not the parent hash ๐Ÿ™ƒ the parent_hash field in ParentHashInput captures info about the nodes above P. the original_sibling_tree_hash captures info about the subtree under S that is not being updated (so it's the subtree that a path secret for P would be encrypted to, according to Section 7.5) ๐ŸŽ€

for example, in the following tree:

             W [F]
       ______|_____
      /             \
     U               Y [F]
   __|__           __|__
  /     \         /     \
 T       _       _       _
/ \     / \     / \     / \
A   B   C   D   E   F   G   _

Figure 21: A Tree Illustrating Parent Hash Computations

with P = W and S = Y, original_sibling_tree_hash is the tree hash of the following tree:

      Y
    __|__
   /     \
  _       _
 / \     / \
E   _   G   _

because W.unmerged_leaves includes F, F gets blanked and removed from Y.unmerged_leaves ๐Ÿ’€

note that no recomputation is needed if the tree hash of S hasn't changed since the last time P was updated ๐Ÿ™Œ that's the case when computing or processing a Commit whose UpdatePath goes through P, since the Commit itself resets P ๐Ÿ’– (in other words, you only need to recompute the original sibling tree hash when validating a group's tree on joining ๐Ÿง) more generally, if none of the entries in P.unmerged_leaves are in the subtree under S (so no leaves got blanked), then the original tree hash at S is just the tree hash of S in the current tree โœจ

if you do need to recompute the original tree hash of a node, you can make it faster by caching intermediate tree hashes ๐Ÿš€ that way you don't have to recompute the subtree every time it shows up in multiple parent hashes ๐Ÿซถ a subtree hash can be reused as long as the intersection of the parent's unmerged leaves with the subtree is the same as in the earlier computation ๐ŸŒธ

7.9.1. Using Parent Hashes ๐Ÿฆ‹

in ParentNode objects and in LeafNode objects with leaf_node_source set to commit, the parent_hash field holds the parent hash of the next non-blank parent node above the node in question (the next node in the filtered direct path) ๐Ÿ’… using the node labels from Figure 20, the parent_hash field of D equals the parent hash of P with copath child S ๐Ÿงš and that's true even when D is a leaf node ๐Ÿซง

the parent_hash field of a LeafNode is signed by the member ๐Ÿ” so that signature attests to which keys the group member put into the ratchet tree and to whom the matching secret keys were sent, along with the rest of the LeafNode's contents ๐Ÿ’œ this stops malicious insiders from building fake ratchet trees with a node D whose HPKE secret key the insider knows, while the insider isn't assigned a leaf in the subtree rooted at D ๐Ÿ˜ˆ๐Ÿšซ and honestly, a tree like that would break the tree invariant ๐Ÿ’€๐Ÿ”ฅ

7.9.2. Verifying Parent Hashes ๐Ÿ”

Okay bestie, parent hashes get checked at two moments: when you join a group, and when a Commit gets processed. It's giving security check ๐Ÿ’…

The parent hash in a node D is valid with respect to a parent node P if all of these criteria hold. Here C and S are the children of P (for "child" and "sibling"). C is the child that's on the direct path of D (possibly D itself), and S is the other child. โœจ

  • D is a descendant of P in the tree.
  • The parent_hash field of D is equal to the parent hash of P with copath child S.
  • D is in the resolution of C, and the intersection of P's unmerged_leaves with the subtree under C is equal to the resolution of C with D removed.

These checks confirm that D and P got updated at the same time (in the same UpdatePath) ๐Ÿซถ. They were also neighbors in the UpdatePath, because the nodes in between them would have been left out of the filtered direct path. Lowkey genius.

A parent node P is "parent-hash valid" if it can be chained back to a leaf node this way. So basically, if there's a leaf node L and a sequence of parent nodes P_1, ..., P_N where P_N = P, and every step in the chain is authenticated by a parent hash, then L's parent hash is valid with respect to P_1, P_1's parent hash is valid with respect to P_2, and so on ๐Ÿ”—.

When joining a group, the new member MUST authenticate that each non-blank parent node P is parent-hash valid. You can do this bottom up by building chains up from the leaves and checking that every non-blank parent node is covered by exactly one of those chains. Or you can do it top down by checking that there's exactly one descendant of each non-blank parent node for which that parent node is parent-hash valid. Both work, pick your fave ๐ŸŒธ

When processing a Commit message that includes an UpdatePath, clients MUST recompute the expected value of parent_hash for the committer's new leaf and verify that it matches the parent_hash value in the supplied leaf_node. Once the UpdatePath nodes are merged into the tree, they form a parent-hash chain from the committer's leaf all the way up to the root. Iconic chain behavior ๐Ÿ‘‘

8. Key Schedule ๐Ÿ”‘

Group keys are derived using the Extract and Expand functions from the KDF for the group's cipher suite, plus the functions defined below. Let's gooo ๐Ÿงš

ExpandWithLabel(Secret, Label, Context, Length) =
    KDF.Expand(Secret, KDFLabel, Length)

DeriveSecret(Secret, Label) =
    ExpandWithLabel(Secret, Label, "", KDF.Nh)

Where KDFLabel is specified as:

struct {
    uint16 length;
    opaque label<V>;
    opaque context<V>;
} KDFLabel;

And its fields get set like this:

length = Length;
label = "MLS 1.0 " + Label;
context = Context;

The value KDF.Nh is the size of an output from KDF.Extract, in bytes. In the diagram below:

  • KDF.Extract takes its salt argument from the top and its Input Keying Material (IKM) argument from the left.
  • DeriveSecret takes its Secret argument from the incoming arrow.
  • 0 represents an all-zero byte string of length KDF.Nh.

When processing a handshake message, a client combines these three things to derive new epoch secrets ๐Ÿ’–:

  • The init secret from the previous epoch
  • The commit secret for the current epoch
  • The GroupContext object for the current epoch

Given these inputs, the derivation of secrets for an epoch goes like this ๐ŸŽ€:

                    init_secret_[n-1]
                          |
                          |
                          V
    commit_secret --> KDF.Extract
                          |
                          |
                          V
                  ExpandWithLabel(., "joiner", GroupContext_[n], KDF.Nh)
                          |
                          |
                          V
                     joiner_secret
                          |
                          |
                          V
psk_secret (or 0) --> KDF.Extract
                          |
                          |
                          +--> DeriveSecret(., "welcome")
                          |    = welcome_secret
                          |
                          V
                  ExpandWithLabel(., "epoch", GroupContext_[n], KDF.Nh)
                          |
                          |
                          V
                     epoch_secret
                          |
                          |
                          +--> DeriveSecret(., <label>)
                          |    = <secret>
                          |
                          V
                    DeriveSecret(., "init")
                          |
                          |
                          V
                    init_secret_[n]

                   Figure 22: The MLS Key Schedule

A bunch of values get derived from the epoch secret for different jobs ๐Ÿฆ‹:

+==================+=====================+=======================+
| Label            | Secret              | Purpose               |
+==================+=====================+=======================+
| "sender data"    | sender_data_secret  | Deriving keys to      |
|                  |                     | encrypt sender data   |
+------------------+---------------------+-----------------------+
| "encryption"     | encryption_secret   | Deriving message      |
|                  |                     | encryption keys (via  |
|                  |                     | the secret tree)      |
+------------------+---------------------+-----------------------+
| "exporter"       | exporter_secret     | Deriving exported     |
|                  |                     | secrets               |
+------------------+---------------------+-----------------------+
| "external"       | external_secret     | Deriving the external |
|                  |                     | init key              |
+------------------+---------------------+-----------------------+
| "confirm"        | confirmation_key    | Computing the         |
|                  |                     | confirmation MAC for  |
|                  |                     | an epoch              |
+------------------+---------------------+-----------------------+
| "membership"     | membership_key      | Computing the         |
|                  |                     | membership MAC for a  |
|                  |                     | PublicMessage         |
+------------------+---------------------+-----------------------+
| "resumption"     | resumption_psk      | Proving membership in |
|                  |                     | this epoch (via a PSK |
|                  |                     | injected later)       |
+------------------+---------------------+-----------------------+
| "authentication" | epoch_authenticator | Confirming that two   |
|                  |                     | clients have the same |
|                  |                     | view of the group     |
+------------------+---------------------+-----------------------+

                      Table 4: Epoch-Derived Secrets

The external_secret is used to derive an HPKE key pair whose private key is held by the entire group ๐ŸŒˆ:

external_priv, external_pub = KEM.DeriveKeyPair(external_secret)

The public key external_pub can be published as part of the GroupInfo struct, so non-members can join the group using an external Commit. Main character entrance energy ๐Ÿšชโœจ


8.1. Group Context ๐ŸŒธ

Each member of the group keeps a GroupContext object that summarizes the state of the group:

struct {
    ProtocolVersion version = mls10;
    CipherSuite cipher_suite;
    opaque group_id<V>;
    uint64 epoch;
    opaque tree_hash<V>;
    opaque confirmed_transcript_hash<V>;
    Extension extensions<V>;
} GroupContext;

The fields in this state mean the following ๐Ÿ’œ:

  • The cipher_suite is the cipher suite used by the group.
  • The group_id field is an application-defined identifier for the group.
  • The epoch field represents the current version of the group.
  • The tree_hash field contains a commitment to the contents of the group's ratchet tree and the credentials for the group's members, as described in Section 7.8.
  • The confirmed_transcript_hash field contains a running hash over the messages that led to this state.
  • The extensions field contains the details of any protocol extensions that apply to the group.

When a new member gets added to the group, an existing member provides them with a Welcome message. The Welcome message has all the info the new member needs to initialize its GroupContext. Hype ๐ŸŽ‰

Different changes to the group have different effects on the group state. Those effects are covered in their own subsections of Section 12.1. These general rules apply, no cap ๐Ÿง :

  • The group_id field is constant.
  • The epoch field goes up by one for each Commit message that gets processed.
  • The tree_hash gets updated to represent the current tree and credentials.
  • The confirmed_transcript_hash field gets updated with the data for an AuthenticatedContent encoding a Commit message, as described below.
  • The extensions field changes when a GroupContextExtensions proposal gets committed.

Okay that's the key schedule and group context done, we ate and left no crumbs ๐Ÿ’…๐ŸŒˆ

8.2. Transcript Hashes ๐Ÿงพโœจ

okay so the transcript hashes are basically a running hash over every Proposal and Commit that's ever been sent in a group ๐Ÿ’…. Commits get included directly, and Proposals sneak in indirectly through the Commit that applied them ๐Ÿฆ‹. Both types get hashed by way of the AuthenticatedContent object they were sent in. Iconic. ๐ŸŒธ

The transcript hash is made of two individual hashes ๐ŸŽ€:

  • A confirmed_transcript_hash that represents a transcript over the whole history of Commit messages, up to and including the signature of the most recent Commit.
  • An interim_transcript_hash that covers the confirmed transcript hash plus the confirmation_tag of the most recent Commit.

New members compute the interim transcript hash using the confirmation_tag field of the GroupInfo struct, while existing members can compute it directly ๐Ÿซถ.

Each Commit updates these hashes through its enclosing AuthenticatedContent. That struct gets split into ConfirmedTranscriptHashInput and InterimTranscriptHashInput. The first one updates the confirmed transcript hash, and the second one updates the interim transcript hash. Main character energy ๐Ÿ‘‘.

struct {
    WireFormat wire_format;
    FramedContent content; /* with content_type == commit */
    opaque signature<V>;
} ConfirmedTranscriptHashInput;

struct {
    MAC confirmation_tag;
} InterimTranscriptHashInput;

confirmed_transcript_hash_[0] = ""; /* zero-length octet string */
interim_transcript_hash_[0] = ""; /* zero-length octet string */

confirmed_transcript_hash_[epoch] =
    Hash(interim_transcript_hash_[epoch - 1] ||
        ConfirmedTranscriptHashInput_[epoch]);

interim_transcript_hash_[epoch] =
    Hash(confirmed_transcript_hash_[epoch] ||
        InterimTranscriptHashInput_[epoch]);

Okay so in this notation, ConfirmedTranscriptHashInput_[epoch] and InterimTranscriptHashInput_[epoch] are based on the Commit that kicked off the epoch with epoch number epoch. Lowkey confusing, but here's the tea ๐Ÿ“: the epoch field in that Commit gets set to epoch - 1, since it's sent inside the previous epoch ๐Ÿ™ˆ.

The transcript hash ConfirmedTranscriptHashInput_[epoch] is used as the confirmed_transcript_hash input to the confirmation_tag field for this Commit. So each Commit confirms the whole transcript of Commits up to that point, except for the latest Commit's confirmation tag. It's giving full-history receipts ๐Ÿงพ๐Ÿ’œ.

                                                         ...

                                                          |
                                                          |
                                                          V
                                                 +-----------------+
                                                 |  interim_[N-1]  |
                                                 +--------+--------+
                                                          |
 .--------------.         +------------------+            |
|  Ratchet Tree  |        | wire_format      |            |
|  Key Schedule  |<-------+ content          |            |
 '-------+------'         |   epoch = N-1    +------------+
         |                |   commit         |            |
         V                | signature        |            V
 +------------------------+   +------------------+   +-----------------+
 |  confirmation_key_[N]  +-->| confirmation_tag |<--+  confirmed_[N]  |
 +------------------------+   +--------+---------+   +--------+--------+
                                       |                      |
                                       |                      V
                                       |             +-----------------+
                                       +------------>|   interim_[N]   |
                                                     +--------+--------+
                                                              |
 .--------------.         +------------------+            |
|  Ratchet Tree  |        | wire_format      |            |
|  Key Schedule  |<-------+ content          |            |
 '-------+------'         |   epoch = N      +------------+
         |                |   commit         |            |
         V                | signature        |            V
 +------------------------+   +------------------+   +-----------------+
 | confirmation_key_[N+1] +-->| confirmation_tag |<--+ confirmed_[N+1] |
 +------------------------+   +--------+---------+   +--------+--------+
                                       |                      |
                                       |                      V
                                       |             +-----------------+
                                       +------------>|  interim_[N+1]  |
                                                     +--------+--------+
                                                              |
                                                              V

                                                         ...

Figure 23: Evolution of the Transcript Hashes through Two Epoch Changes ๐Ÿฆ‹


8.3. External Initialization ๐Ÿšช๐Ÿ’ซ

So besides starting a new epoch through those KDF invocations we already covered, an MLS group can also start a new epoch through an asymmetric interaction using the external key pair from the previous epoch ๐Ÿ”‘. This happens when a new member joins via an external commit ๐Ÿ™Œ.

Here's the vibe: the joiner sends a new init_secret value to the group using the HPKE export method ๐Ÿคซ. Then the joiner uses that init_secret, along with the info from the GroupInfo and an external Commit, to set up their own copy of the key schedule for the new epoch ๐ŸŒˆ.

kem_output, context = SetupBaseS(external_pub, "")
init_secret = context.export("MLS 1.0 external init secret", KDF.Nh)

Then the members of the group receive the kem_output in an ExternalInit proposal and do the matching calculation to get the init_secret value ๐Ÿ’œ.

context = SetupBaseR(kem_output, external_priv, "")
init_secret = context.export("MLS 1.0 external init secret", KDF.Nh)

8.4. Pre-Shared Keys ๐Ÿ—๏ธ๐ŸŒท

Groups that already have an out-of-band way to generate shared group secrets can inject them into the MLS key schedule ๐Ÿง. That brings in extra external entropy for the computation of the MLS group secrets. Super smart, honestly ๐Ÿง โœจ.

Injecting an external PSK can level up security when doing a full run of Updates across all members is too expensive ๐Ÿ’ธ. It also helps if the external group key establishment mechanism gives stronger security against classical or quantum adversaries ๐Ÿ›ก๏ธ.

ngl this part is important: a PSK might have a different lifetime than an Update, so it doesn't automatically give the same forward secrecy or post-compromise security guarantees as a Commit message ๐Ÿ˜ญ. Unlike the key pairs put in the tree by an Update or Commit, which are always freshly generated, PSKs can be pre-distributed and stored. That means a PSK could get compromised during distribution or storage ๐Ÿ’€. So the security the group gets from injecting a PSK depends on both the entropy of the PSK and the risk of compromise. These factors are outside the scope of this document, but application designers relying on PSKs should definitely consider them ๐Ÿ™ˆ.

Each PSK in MLS has a type that says how it was provisioned ๐Ÿ“ฆ. External PSKs are provided by the application, while resumption PSKs are derived from the MLS key schedule and used when you need to authenticate a member's participation in a prior epoch ๐Ÿ•ฐ๏ธ.

The injection of one or more PSKs into the key schedule is signaled in two ways. Existing members get told through PreSharedKey proposals covered by a Commit, and new members added in that Commit get told through the GroupSecrets object in the Welcome message for that Commit ๐Ÿ’Œ. To make sure existing and new members compute the same PSK input to the key schedule, the Commit and GroupSecrets objects MUST indicate the same set of PSKs, in the same order ๐Ÿ’….

enum {
  reserved(0),
  external(1),
  resumption(2),
  (255)
} PSKType;

enum {
  reserved(0),
  application(1),
  reinit(2),
  branch(3),
  (255)
} ResumptionPSKUsage;

struct {
  PSKType psktype;
  select (PreSharedKeyID.psktype) {
    case external:
      opaque psk_id<V>;

    case resumption:
      ResumptionPSKUsage usage;
      opaque psk_group_id<V>;
      uint64 psk_epoch;
  };
  opaque psk_nonce<V>;
} PreSharedKeyID;

Each time a client injects a PSK into a group, the psk_nonce of its PreSharedKeyID MUST be set to a fresh random value of length KDF.Nh, where KDF is the KDF for the cipher suite of the group the PSK is going into ๐ŸŽฒ. This makes sure that even when a PSK gets used multiple times, the value that goes into the key schedule is different every single time โœจ.

When a receiving client gets a Commit with a PreSharedKey proposal, or a GroupSecrets object with the psks field set, it includes those PSKs in the key schedule. They go in the order listed in the Commit, or in the psks field, respectively ๐Ÿ“‹. For resumption PSKs, the PSK is defined as the resumption_psk of the group and epoch specified in the PreSharedKeyID object. Here's how psk_secret gets computed ๐Ÿงฎ:

struct {
    PreSharedKeyID id;
    uint16 index;
    uint16 count;
} PSKLabel;

psk_extracted_[i] = KDF.Extract(0, psk_[i])
psk_input_[i] = ExpandWithLabel(psk_extracted_[i], "derived psk",
                  PSKLabel, KDF.Nh)

psk_secret_[0] = 0
psk_secret_[i] = KDF.Extract(psk_input_[i-1], psk_secret_[i-1])
psk_secret     = psk_secret_[n]

Here 0 stands for the all-zero vector of length KDF.Nh 0๏ธโƒฃ. The index field in PSKLabel matches the PSK's position in the psk array, and the count field holds the total number of PSKs. Basically, the PSKs get chained together with KDF.Extract invocations (labeled "Extract" in the diagram, because we're lazy like that ๐Ÿ˜‚), like this:

                 0                               0    = psk_secret_[0]
                 |                               |
                 V                               V
psk_[0]   --> Extract --> ExpandWithLabel --> Extract = psk_secret_[1]
                                                 |
                 0                               |
                 |                               |
                 V                               V
psk_[1]   --> Extract --> ExpandWithLabel --> Extract = psk_secret_[2]
                                                 |
                 0                              ...
                 |                               |
                 V                               V
psk_[n-1] --> Extract --> ExpandWithLabel --> Extract = psk_secret_[n]

Figure 24: Computation of a PSK Secret from a Set of PSKs ๐ŸŒˆ

In particular, if there are no PreSharedKey proposals in a given Commit, then the resulting psk_secret is psk_secret_[0], the all-zero vector. Zero drama, zero PSKs, all vibes ๐Ÿซง๐Ÿ’–.

8.5. Exporters โœจ

okay bestie, the main MLS key schedule hands you an exporter_secret ๐Ÿฆ‹ and any application can use it to cook up brand new secrets for stuff that lives outside of MLS. so periodt ๐Ÿ’…

Here's the formula. MLS-Exporter takes a Label, a Context and a Length, and it's basically DeriveSecret on the exporter_secret with your label, then ExpandWithLabel with the fixed word "exported", the Hash of your Context, and the Length you asked for:

MLS-Exporter(Label, Context, Length) =
       ExpandWithLabel(DeriveSecret(exporter_secret, Label),
                         "exported", Hash(Context), Length)

Okay so, applications SHOULD give every MLS-Exporter call a unique label that says what the secret is for ๐ŸŽ€ This keeps the same secret from getting made and used in two different places, which would be a total mess. And to stop different apps from picking the same label, there's an IANA registry for these labels in Section 17.8 ๐ŸŒธ

The exported values are tied to the group epoch that the exporter_secret came from, so they basically reflect one exact vibe of the group at one moment in time โœจ

It is RECOMMENDED that the app generating exported values refreshes them after a Commit gets processed ๐Ÿ”ฅ (the girl just wants fresh secrets, no cap)

8.6. Resumption PSK ๐ŸŒˆ

The main MLS key schedule also gives you a resumption_psk ๐Ÿ’– It's a PSK that carries entropy from one epoch over into another. This is the move used in the reinitialization and branching processes in Sections 11.2 and 11.3, but apps are allowed to use it for other stuff too ๐Ÿงš

Some resumption PSK uses might want PSKs from older, historical epochs ๐Ÿ‘‘ The application SHOULD set an upper limit on how many past epochs the resumption_psk can be stored for. Keep it tidy bestie ๐Ÿซถ

8.7. Epoch Authenticators ๐Ÿ”

The main MLS key schedule gives every epoch its own epoch_authenticator ๐Ÿ’œ If an active attacker is impersonating one member of the group, the epoch_authenticator that member's client computes will be different from the ones everyone else computes. Lowkey iconic, right? ๐Ÿ˜ญ

This is a big deal because you can build defenses against impersonation that still work even if members' signature keys got compromised ๐Ÿ™ˆ Here's a super simple example: say the users in an MLS group meet up in person and reliably confirm their epoch authenticator values match, using some decent user interface. Then each person knows nobody else is being impersonated in the current epoch ๐ŸŽ€ BUT the second the epoch changes, they'd have to do the whole check again! The group's state changed, and an attacker could have snuck in ๐Ÿ’€

More generally, if the group wants real authentication protection from the epoch_authenticator, they have to use it inside some secondary protocol (like that face-to-face thing above) ๐Ÿฆ‹ That protocol is what decides the actual authentication protections the MLS group ends up with. It's giving teamwork ๐Ÿซถ

9. Secret Tree ๐ŸŒณ

For encryption keys and nonces, the key schedule starts with the encryption_secret at the root and derives a whole tree of secrets ๐Ÿ“ The tree has the same shape as the group's ratchet tree, and each leaf in the secret tree belongs to the same group member as the matching leaf in the ratchet tree. Iconic structure, bestie โœจ

If N is a parent node in the secret tree, the children's secrets are defined like this (left(N) and right(N) are N's children):

tree_node_[N]_secret
        |
        |
        +--> ExpandWithLabel(., "tree", "left", KDF.Nh)
        |    = tree_node_[left(N)]_secret
        |
        +--> ExpandWithLabel(., "tree", "right", KDF.Nh)
             = tree_node_[right(N)]_secret

  Figure 25: Derivation of Secrets from Parent to Children within a
                             Secret Tree

The secret sitting in a leaf of the secret tree kicks off two symmetric hash ratchets ๐Ÿ’… From those you get a stream of single-use keys and nonces, as described in Section 9.1. The root of each ratchet is computed like so:

tree_node_[N]_secret
        |
        |
        +--> ExpandWithLabel(., "handshake", "", KDF.Nh)
        |    = handshake_ratchet_secret_[N]_[0]
        |
        +--> ExpandWithLabel(., "application", "", KDF.Nh)
             = application_ratchet_secret_[N]_[0]

  Figure 26: Initialization of the Hash Ratchets from the Leaves of
                            a Secret Tree

9.1. Encryption Keys ๐Ÿ”‘

As described in Section 6, MLS encrypts three different kinds of info ๐ŸŽ€

  • Metadata (sender information)
  • Handshake messages (Proposal and Commit)
  • Application messages

The sender information used to look up the key for content encryption gets encrypted with an AEAD ๐Ÿ’œ The key and nonce come from both sender_data_secret and a sample of the encrypted message content. Wild, right? ๐Ÿ™ˆ

For handshake and application messages, a sequence of keys is derived through a "sender ratchet" ๐ŸŒธ Every sender has their own sender ratchet, and each step along it is called a "generation" โœจ

Here's a secret tree for a four-member group, showing the handshake and application ratchets member D uses for sending, plus the first two application keys and nonces ๐Ÿ‘‘

       G
       |
     .-+-.
    /     \
   E       F
  / \     / \
 A   B   C   D
            / \
          HR0  AR0--+--K0
                |   |
                |   +--N0
                |
               AR1--+--K1
                |   |
                |   +--N1
                |
               AR2
   Figure 27: Secret Tree for a Four-Member Group

A sender ratchet starts from a per-sender base secret that comes from a Secret Tree, as described in Section 9 ๐Ÿฆ‹ That base secret kicks off a symmetric hash ratchet, which spits out a sequence of keys and nonces. The sender uses the j-th key/nonce pair in that sequence to encrypt (using the AEAD) the j-th message they send during that epoch. Each key/nonce pair MUST NOT be used to encrypt more than one message ๐Ÿ›‘ (non-negotiable, bestie)

Keys, nonces, and the secrets inside ratchets all get derived using DeriveTreeSecret ๐Ÿ’– The context for any given call is the current position in the ratchet.

DeriveTreeSecret(Secret, Label, Generation, Length) =
    ExpandWithLabel(Secret, Label, Generation, Length)

Generation gets encoded as a big endian uint32 โœจ

ratchet_secret_[N]_[j]
      |
      +--> DeriveTreeSecret(., "nonce", j, AEAD.Nn)
      |    = ratchet_nonce_[N]_[j]
      |
      +--> DeriveTreeSecret(., "key", j,  AEAD.Nk)
      |    = ratchet_key_[N]_[j]
      |
      V
DeriveTreeSecret(., "secret", j, KDF.Nh)
= ratchet_secret_[N]_[j+1]

AEAD.Nn and AEAD.Nk are the lengths in bytes of the nonce and key for the AEAD scheme defined by the cipher suite ๐ŸŽ€

9.2. Deletion Schedule ๐Ÿ—‘๏ธ

It is important to delete all security-sensitive values as soon as they are _consumed_ ๐Ÿ”ฅ A sensitive value S counts as _consumed_ if:

  • S was used to encrypt or (successfully) decrypt a message, or
  • a key, nonce, or secret derived from S has been consumed. (This goes for values derived via DeriveSecret as well as ExpandWithLabel.)

S can be the init_secret, commit_secret, epoch_secret, or encryption_secret, or any secret in a secret tree or one of the ratchets ๐ŸŒˆ

As soon as a group member consumes a value, they MUST immediately delete (all representations of) that value ๐Ÿ’€ This is crucial for forward secrecy on past messages. Members MAY keep unconsumed values around for some reasonable amount of time to handle out-of-order message delivery ๐Ÿซถ

For example, say a group member encrypts or (successfully) decrypts an application message using the j-th key and nonce in the ratchet of leaf node L in some epoch n. Then, for that member, at least the following values have been consumed and MUST be deleted ๐Ÿ˜ญ

  • the commit_secret, joiner_secret, epoch_secret, and encryption_secret of that epoch n, as well as the init_secret of the previous epoch n-1,
  • all node secrets in the secret tree on the path from the root to the leaf with node L,
  • the first j secrets in the application data ratchet of node L, and
  • application_ratchet_nonce_[L]_[j] and application_ratchet_key_[L]_[j].

Concretely, look at the secret tree in Figure 27 ๐Ÿ’… Client A, B, or C would generate the illustrated values on receiving a message from D with generation equal to 1, without having received a message with generation 0 (for example, because of out-of-order delivery). In that case, these values get consumed:

  • The key K1 and nonce N1 used to decrypt the message
  • The application ratchet secrets AR1 and AR0
  • The tree secrets D, F, and G (recall that G is the encryption_secret for the epoch)
  • The epoch_secret, commit_secret, psk_secret, and joiner_secret for the current epoch

Other values may be retained (not consumed):

  • K0 and N0 for decryption of an out-of-order message with generation 0
  • AR2 for derivation of further message decryption keys and nonces
  • HR0 for protection of handshake messages from D
  • E and C for deriving secrets used by senders A, B, and C

10. Key Packages โœจ

Okay so, to let people get added to a group whenever they want (no waiting around for everyone to be online ๐Ÿ™ˆ), clients can pre-publish KeyPackage objects. These hold some public info about a user, and a KeyPackage specifies:

  1. a protocol version and cipher suite that the client supports,
  2. a public key that others can use to encrypt a Welcome message to this client (an "init key"), and
  3. the content of the leaf node that should be added to the tree to represent this client.

KeyPackages are meant to be used only once, and they SHOULD NOT be reused, except for a "last resort" KeyPackage (see Section 16.8) ๐Ÿซถ. Clients MAY generate and publish multiple KeyPackages so they can support multiple cipher suites. Main character energy, honestly ๐Ÿ’….

The value for init_key MUST be a public key for the asymmetric encryption scheme defined by cipher_suite, and it MUST be unique among the set of KeyPackages created by this client. Likewise, the leaf_node field MUST be valid for the cipher suite, including both the encryption_key and signature_key fields. The whole structure is signed with the client's signature key. A KeyPackage object with an invalid signature field MUST be considered malformed ๐Ÿšซ.

The signature is computed by the function SignWithLabel, using the label KeyPackageTBS and a Content input that contains all of the fields except for the signature field.

struct {
    ProtocolVersion version;
    CipherSuite cipher_suite;
    HPKEPublicKey init_key;
    LeafNode leaf_node;
    Extension extensions<V>;
    /* SignWithLabel(., "KeyPackageTBS", KeyPackageTBS) */
    opaque signature<V>;
} KeyPackage;

struct {
    ProtocolVersion version;
    CipherSuite cipher_suite;
    HPKEPublicKey init_key;
    LeafNode leaf_node;
    Extension extensions<V>;
} KeyPackageTBS;

If a client receives a KeyPackage inside an MLSMessage object, then it MUST verify that the version field of the KeyPackage has the same value as the version field of the MLSMessage. This matters because the KeyPackage's version is an explicit signal of the intended version to the other members of the group when they receive it in an Add proposal ๐Ÿ“ฃ.

The field leaf_node.capabilities tells everyone which protocol versions, cipher suites, credential types, and non-default proposal/extension types the client supports. (As Section 7.2 explains, some proposal and extension types in this document count as "default" and so aren't listed ๐Ÿค“.) This is the secret sauce that keeps MLS session establishment safe from downgrade attacks on those parameters (more on that in Section 11), while each KeyPackage still only advertises one version and one cipher suite. Iconic. ๐Ÿ‘‘

The field leaf_node.leaf_node_source of the LeafNode in a KeyPackage MUST be set to key_package.

Extensions included in the extensions or leaf_node.extensions fields MUST be included in the leaf_node.capabilities field. As Section 13 covers, unknown extensions in KeyPackage.extensions MUST be ignored, and the creator of a KeyPackage object SHOULD include some random GREASE extensions ๐Ÿงš. Those help make sure other clients correctly ignore unknown extensions, which is so smart ngl.

10.1. KeyPackage Validation

A KeyPackage's validity needs checking at a couple of stages:

  • When a KeyPackage is downloaded by a group member, before it gets used to add the client to the group
  • When a KeyPackage is received by a group member in an Add message

The client checks a KeyPackage's validity with these steps:

  • Verify that the cipher suite and protocol version of the KeyPackage match those in the GroupContext.
  • Verify that the leaf_node of the KeyPackage is valid for a KeyPackage according to Section 7.3.
  • Verify that the signature on the KeyPackage is valid using the public key in leaf_node.credential.
  • Verify that the value of leaf_node.encryption_key is different from the value of the init_key field.

11. Group Creation ๐ŸŒธ

A group always starts out with a single member, the "creator". Everyone else gets added later through the usual Add/Commit mechanism ๐ŸŽ€.

The creator is in charge of setting the group ID, cipher suite, and initial extensions for the group. If the creator plans to add other members right at creation time, then it SHOULD fetch KeyPackages for the members to be added, and pick a cipher suite and extensions based on the members' capabilities. To protect against downgrade attacks, the creator MUST use the capabilities information in those KeyPackages to check that the chosen version and cipher suite is the best option supported by all members. Lowkey the most important step here ๐Ÿ‘€.

Group IDs SHOULD be built so there's a super, super low chance that honest group creators end up generating the same group ID, even without help from the Delivery Service. One way to do this is to make the group ID a freshly generated random value of size KDF.Nh. The Delivery Service MAY try to keep group IDs globally unique by rejecting the creation of new groups that use a previously used ID.

To initialize a group, the creator of the group MUST take the following steps:

  • Initialize a one-member group with the following initial values:
    • Ratchet tree: A tree with a single node, a leaf node containing an HPKE public key and credential for the creator
    • Group ID: A value set by the creator
    • Epoch: 0
    • Tree hash: The root hash of the above ratchet tree
    • Confirmed transcript hash: The zero-length octet string
    • Epoch secret: A fresh random value of size KDF.Nh
    • Extensions: Any values of the creator's choosing
  • Calculate the interim transcript hash:
    • Derive the confirmation_key for the epoch as described in Section 8.
    • Compute a confirmation_tag over the empty confirmed_transcript_hash, using the confirmation_key, as described in Section 6.1.
    • Compute the updated interim_transcript_hash from the confirmed_transcript_hash and the confirmation_tag, as described in Section 8.2.

At this point, the creator's state represents a one-member group with a fully initialized key schedule, transcript hashes, and so on. Proposals and Commits can be generated for this group state just like for any other state of the group, such as Add proposals and Commits that add other members. A GroupInfo object for this group state can also be published to make external joins easier ๐Ÿฆ‹.

Members other than the creator join either by being sent a Welcome message (as described in Section 12.4.3.1) or by sending an external Commit (see Section 12.4.3.2).

In principle, the process above could be streamlined by having the creator directly create a tree and choose a random value for the first epoch's epoch secret. We follow the steps above anyway, because they remove unnecessary choices, which is how things like bad randomness could sneak in ๐Ÿ™…โ€โ™€๏ธ. The only choices the creator makes here are its own KeyPackage and the leaf secret that the Commit is built from.

11.1. Required Capabilities

A group's configuration puts certain requirements on the clients in it. At a minimum, all members of the group need to support the cipher suite and protocol version in use. Extra requirements can be added by including a required_capabilities extension in the GroupContext.

struct {
    ExtensionType extension_types<V>;
    ProposalType proposal_types<V>;
    CredentialType credential_types<V>;
} RequiredCapabilities;

This extension lists the extensions, proposals, and credential types that must be supported by all members of the group. The "default" proposal and extension types defined in this document are assumed to be implemented by all clients, so they don't need to be listed in RequiredCapabilities to be safely used. Note that this is not true for credential types, so don't skip those ๐Ÿ‘€.

For new members, existing members enforce support for required capabilities while they apply Add commits. Existing members should of course already be in compliance. To make sure this stays true even as the group's extensions get updated, a GroupContextExtensions proposal is deemed invalid if it contains a required_capabilities extension that requires non-default capabilities not supported by all current members ๐Ÿšจ.

11.2. Reinitialization

A group can be reinitialized by creating a new group with the same membership and different parameters, then linking it to the old group via a resumption PSK ๐Ÿ”. The members of a group reinitialize it with these steps:

  1. A member of the old group sends a ReInit proposal (see Section 12.1.5).
  2. A member of the old group sends a Commit covering the ReInit proposal.
  3. A member of the old group creates an initial Commit that sets up a new group matching the ReInit, and sends a Welcome message:
    • The version, cipher_suite, group_id, and extensions fields of the GroupContext object in the Welcome message MUST be the same as the corresponding fields in the ReInit proposal. The epoch in the Welcome message MUST be 1.
    • The Welcome message MUST specify a PreSharedKeyID of type resumption with usage reinit, where the group_id field matches the old group and the epoch field indicates the epoch after the Commit covering the ReInit.

Note that these three steps may be done by the same group member or by different members. For example, if a group member sends a Commit with an inline ReInit proposal (steps 1 and 2) and then goes offline, another group member may recreate the group instead. This flexibility avoids situations where a group gets stuck between steps 2 and 3 ๐Ÿ˜ฎโ€๐Ÿ’จ.

Resumption PSKs with usage reinit MUST NOT be used in other contexts. A PreSharedKey proposal with type resumption and usage reinit MUST be considered invalid. Hard boundary, bestie ๐Ÿšซ.

11.3. Subgroup Branching

A new group can be formed from a subset of an existing group's members, using the same parameters as the old group ๐ŸŒˆ.

A member can create a subgroup by performing these steps:

  1. Fetch a new KeyPackage for each group member that should be included in the subgroup.
  2. Create an initial Commit message that sets up the new group and contains a PreSharedKey proposal of type resumption with usage branch. To avoid key reuse, the psk_nonce included in the PreSharedKeyID object MUST be a randomly sampled nonce of length KDF.Nh ๐ŸŽฒ.
  3. Send the corresponding Welcome message to the subgroup members.

A client that receives a Welcome message including a PreSharedKey of type resumption with usage branch MUST verify that the new group reflects a subgroup branched from the referenced group. It checks that:

  • The version and cipher_suite values in the Welcome message are the same as those used by the old group.
  • The epoch in the Welcome message MUST be 1.
  • Each LeafNode in a new subgroup MUST match some LeafNode in the original group. In this context, a pair of LeafNodes is said to "match" if the identifiers presented by their respective credentials are considered equivalent by the application.
Resumption PSKs with usage branch MUST NOT be used in other contexts. A PreSharedKey proposal with type resumption and usage branch MUST be considered invalid. Same energy as before, no exceptions ๐Ÿ™…โ€โ™€๏ธ๐Ÿ’œ.

12. Group Evolution โœจ

okay bestie, groups are never static ๐Ÿ’… people join, people leave, and sometimes everyone wants fresh keys so post-compromise security stays strong ๐Ÿ”. In MLS every change happens in a two-step process ๐Ÿฆ‹:

  1. A proposal to make the change is broadcast to the group in a Proposal message.
  2. A member of the group or a new member broadcasts a Commit message that causes one or more proposed changes to enter into effect.

If the same member sends both the Proposal and the Commit, they can squish both steps together by sending the proposals in the commit. So efficient ๐Ÿ™ˆ

The group glows up from one cryptographic state to another every time a Commit message is sent and processed โœจ. These states are called "epochs", and each one is uniquely identified among the group's states by an eight-octet epoch value ๐ŸŽ€. When a brand-new group gets initialized, its starting epoch is 0x0000000000000000. Then every state transition bumps the epoch number up by one ๐ŸŒธ.

12.1. Proposals

Proposals ride along inside a FramedContent through a Proposal structure that says what kind of proposal it is ๐Ÿ’–:

// See the "MLS Proposal Types" IANA registry for values
uint16 ProposalType;

struct {
    ProposalType proposal_type;
    select (Proposal.proposal_type) {
        case add:                      Add;
        case update:                   Update;
        case remove:                   Remove;
        case psk:                      PreSharedKey;
        case reinit:                   ReInit;
        case external_init:            ExternalInit;
        case group_context_extensions: GroupContextExtensions;
    };
} Proposal;

When a client receives a FramedContent containing a Proposal, it MUST verify the signature inside FramedContentAuthData, and it MUST check that the epoch field of the enclosing FramedContent matches the epoch field of the current GroupContext object ๐Ÿ”. If that check passes, the Proposal should be cached so it can be pulled back out by hash (as a ProposalOrRef object) in a later Commit message ๐Ÿงš.

12.1.1. Add

An Add proposal asks for a client holding a specific KeyPackage to be added to the group ๐Ÿฅฐ.

struct {
    KeyPackage key_package;
} Add;

An Add proposal is invalid if its KeyPackage is invalid according to Section 10.1 ๐Ÿšซ.

An Add gets applied once it's included in a Commit message ๐ŸŽ‰. Its position in the list of proposals decides which leaf node the new member lands on. The first Add in the Commit goes into the leftmost empty leaf in the tree, the second Add goes into the next empty leaf to the right, and so on. If there's no empty leaf left, the tree gets extended to the right.

  • Identify the leaf L for the new member. If there are empty leaves in the tree, L is the leftmost empty leaf. Otherwise, the tree is extended to the right as described in Section 7.7, and L is assigned the leftmost new blank leaf.
  • For each non-blank intermediate node along the path from the leaf L to the root, add L's leaf index to the unmerged_leaves list for the node.
  • Set the leaf node L to a new node containing the LeafNode object carried in the leaf_node field of the KeyPackage in the Add.

12.1.2. Update

An Update proposal works a lot like an Add, except it swaps out the sender's LeafNode in the tree instead of adding a brand-new leaf ๐Ÿ”„.

struct {
    LeafNode leaf_node;
} Update;

An Update proposal is invalid if the LeafNode is invalid for an Update proposal according to Section 7.3 โŒ.

When a member of the group applies an Update, they take these steps:

  • Replace the sender's LeafNode with the one contained in the Update proposal.
  • Blank the intermediate nodes along the path from the sender's leaf to the root.

12.1.3. Remove

A Remove proposal asks for the member at the removed leaf index to be taken out of the group ๐Ÿ‘‹.

struct {
    uint32 removed;
} Remove;

A Remove proposal is invalid if the removed field doesn't identify a non-blank leaf node ๐Ÿ™….

When a member of the group applies a Remove, they follow these steps:

  • Identify the leaf node matching removed. Let L be this leaf node.
  • Replace the leaf node L with a blank node.
  • Blank the intermediate nodes along the path from L to the root.
  • Truncate the tree by removing the right subtree until there is at least one non-blank leaf node in the right subtree. If the rightmost non-blank leaf has index L, then this will result in the tree having 2^d leaves, where d is the smallest value such that 2^d > L. Tidy ๐Ÿงนโœจ

12.1.4. PreSharedKey

A PreSharedKey proposal can ask for a pre-shared key to be injected into the key schedule while the epoch is advancing ๐Ÿ”‘.

struct {
    PreSharedKeyID psk;
} PreSharedKey;

A PreSharedKey proposal is invalid if any of the following is true:

  • The PreSharedKey proposal is not being processed as part of a reinitialization of the group (see Section 11.2), and the PreSharedKeyID has psktype set to resumption and usage set to reinit.
  • The PreSharedKey proposal is not being processed as part of a subgroup branching operation (see Section 11.3), and the PreSharedKeyID has psktype set to resumption and usage set to branch.
  • The psk_nonce is not of length KDF.Nh.

The psk_nonce MUST be randomly sampled ๐ŸŽฒ. When a Commit message includes one or more PreSharedKey proposals, group members derive psk_secret as described in Section 8.4, and the order of the PSKs matches the order of the PreSharedKey proposals in the Commit ๐Ÿงฉ.

12.1.5. ReInit

A ReInit proposal is a request to reinitialize the group with different parameters, like bumping the version number or switching the cipher suite ๐Ÿ”. The reinit happens by creating a completely new group and shutting the old one down ๐Ÿ’€.

struct {
    opaque group_id<V>;
    ProtocolVersion version;
    CipherSuite cipher_suite;
    Extension extensions<V>;
} ReInit;

A ReInit proposal is invalid if its version field is less than the version for the current group ๐Ÿ™…โ€โ™€๏ธ.

When a member of the group applies a ReInit proposal, they wait for the committer to send the Welcome message that matches the ReInit, following the criteria in Section 11.2 ๐Ÿ“ฌ.

12.1.6. ExternalInit

An ExternalInit proposal is for new members who want to join a group through an external commit ๐Ÿšช. This proposal can only be used in that context.

struct {
  opaque kem_output<V>;
} ExternalInit;

When a member of the group applies an ExternalInit, they initialize the next epoch using an init secret computed as described in Section 8.3 โœจ. The kem_output field carries the required KEM output.

12.1.7. GroupContextExtensions

A GroupContextExtensions proposal updates the list of extensions in the GroupContext for the group ๐Ÿง.

struct {
  Extension extensions<V>;
} GroupContextExtensions;

A GroupContextExtensions proposal is invalid if it includes a required_capabilities extension and some members of the group don't support some of the required capabilities (counting capabilities added in the same Commit, and leaving out ones removed) ๐Ÿšซ.

When a member of the group applies a GroupContextExtensions proposal, they do this:

  • Remove all of the existing extensions from the GroupContext object for the group and replace them with the list of extensions in the proposal. (This is a wholesale replacement, not a merge. An extension is only carried over if the sender of the proposal includes it in the new list.)
Once the GroupContext is updated, its inclusion in the confirmation_tag through the key schedule confirms that all members of the group agree on the extensions in use ๐Ÿค. Main character confirmation energy ๐Ÿ‘‘.

12.1.8. External Proposals

Outsiders can build proposals and send them to the group in two situations ๐Ÿ‘€. The first one, marked by the external SenderType, lets an entity outside the group submit proposals. For example, an automated service might propose removing a member who's been inactive for a long time, or propose adding a newly hired staff member to a group that represents a real-world team ๐Ÿง‘โ€๐Ÿ’ผ. An external sender might also send a ReInit proposal to enforce a changed policy about MLS versions or cipher suites ๐Ÿ“œ.

The external SenderType needs its signers to be pre-provisioned to the clients within the group, and it can only be used when the external_senders extension is present in the group's GroupContext ๐Ÿ”.

The second case, marked by the new_member_proposal SenderType, is handy when existing members can independently verify that an Add proposal sent by the new joiner themselves (not an existing member) is authorized ๐Ÿฅน. External proposals that aren't authorized are considered invalid ๐Ÿšซ.

An external proposal MUST be sent as a PublicMessage object, because the sender won't have the keys needed to construct a PrivateMessage object ๐Ÿ”“.

Some proposal types can't come from an external sender ๐Ÿ™…. Among the proposal types defined in this document, only these types may be sent by an external sender:

  • add
  • remove
  • psk
  • reinit
  • group_context_extensions

Messages from external senders containing proposal types other than the above MUST be rejected as malformed ๐Ÿ’ฅ. New proposal types defined in the future MUST define whether they may be sent by external senders. The "Ext" column in the "MLS Proposal Types" registry (Section 17.4) reflects this property ๐Ÿ“‹.

12.1.8.1. External Senders Extension

The external_senders extension is a group context extension that holds the credentials and signature keys of the senders who are allowed to send external proposals to the group ๐Ÿ—๏ธ.

struct {
  SignaturePublicKey signature_key;
  Credential credential;
} ExternalSender;

ExternalSender external_senders<V>;

12.2. Proposal List Validation โœจ

okay bestie, before a Commit gets to go anywhere, its list of proposals has to pass a vibe check ๐Ÿ’… A group member creating a Commit and a group member processing a Commit MUST verify that the list of committed proposals is valid, using one of the following procedures depending on whether the Commit is external or not. If the list is invalid, the Commit message MUST be rejected as invalid. No exceptions, periodt ๐Ÿ™…โ€โ™€๏ธ

For a regular Commit (aka not external) ๐ŸŒธ, the list is invalid if any of these things happen:

  • It contains an individual proposal that is invalid as specified in Section 12.1. ๐Ÿ’€
  • It contains an Update proposal generated by the committer. Nope, you can't update yourself in your own Commit ๐Ÿ™ˆ
  • It contains a Remove proposal that removes the committer. Literally cannot kick yourself out, bestie ๐Ÿ˜ญ
  • It contains multiple Update and/or Remove proposals that apply to the same leaf. If the committer has received multiple such proposals, they SHOULD prefer any Remove received, or the most recent Update if there are no Removes. Remove is the drama queen here ๐Ÿ‘‘
  • It contains multiple Add proposals that contain KeyPackages that represent the same client according to the application (for example, identical signature keys). Double-booking the same person is a no ๐Ÿ™…
  • It contains an Add proposal with a KeyPackage that represents a client already in the group according to the application, unless there is a Remove proposal in the list removing the matching client from the group. Unless you're swapping them out in the same Commit, you can't add someone who's already here ๐Ÿ“
  • It contains multiple PreSharedKey proposals that reference the same PreSharedKeyID. One PSK ID per vibe, please โœจ
  • It contains multiple GroupContextExtensions proposals. Too many cooks in the group settings ๐ŸŽ€
  • It contains a ReInit proposal together with any other proposal. If the committer has received other proposals during the epoch, they SHOULD prefer them over the ReInit proposal, which lets the ReInit get resent and applied in a later epoch. ReInit is a solo act, it goes last ๐Ÿฆ‹
  • It contains an ExternalInit proposal. ExternalInit doesn't belong in a regular Commit ๐Ÿ™ƒ
  • It contains a Proposal with a non-default proposal type that is not supported by some members of the group that will process the Commit. Members being added or removed by the Commit don't need to support the proposal type, though ๐Ÿงš
  • After processing the Commit, the ratchet tree is invalid. In particular, this happens if it contains any leaf node that is invalid according to Section 7.3. Broken tree = broken vibes ๐Ÿ’”

An application may extend the above procedure by additional rules, for example, requiring application-level permissions to add members, or rules concerning non-default proposal types. The app gets to add its own house rules too ๐Ÿ’–

For an external Commit, the list is valid if it contains only the following proposals (not necessarily in this order):

  • Exactly one ExternalInit
  • At most one Remove proposal, with which the joiner removes an old version of themselves. If a Remove proposal is present, then the LeafNode in the path field of the external Commit MUST meet the same criteria as would the LeafNode in an Update for the removed leaf (see Section 12.1.2). In particular, the credential in the LeafNode MUST present a set of identifiers that is acceptable to the application for the removed participant. So you can swap out your old self, but only if your new credential passes the app's check ๐Ÿฆ‹
  • Zero or more PreSharedKey proposals
  • No other proposals

Proposal types defined in the future may make updates to the above validation logic to incorporate considerations related to proposals of the new type. Future proposal types get a seat at the table too ๐ŸŒˆ


12.3. Applying a Proposal List โœจ

okay so the sections above explain how each individual proposal gets applied ๐ŸŽ€ When creating or processing a Commit, a client applies a whole list of proposals to the ratchet tree and the GroupContext. The client MUST apply the proposals in the list in the following order:

  • If there is a GroupContextExtensions proposal, replace the extensions field of the GroupContext for the group with the contents of the proposal. The new extensions MUST be used when evaluating other proposals in this list. For example, if a GroupContextExtensions proposal adds a required_capabilities extension, then any Add proposals need to indicate support for those capabilities. Settings go first, so everyone else has to match the new vibe โš™๏ธ
  • Apply any Update proposals to the ratchet tree, in any order. ๐Ÿ”„
  • Apply any Remove proposals to the ratchet tree, in any order. ๐Ÿ‘‹
  • Apply any Add proposals to the ratchet tree, in the order they appear in the list. โž•
  • Look up the PSK secrets for any PreSharedKey proposals, in the order they appear in the list. These secrets are then used to advance the key schedule later in Commit processing. ๐Ÿ”‘
  • If there is an ExternalInit proposal, use it to derive the init_secret for use later in Commit processing. ๐Ÿงช
  • If there is a ReInit proposal, note its parameters for application later in Commit processing. ๐Ÿ“

Proposal types defined in the future MUST specify how the above steps are to be adjusted to accommodate the application of proposals of the new type. New kids on the block have to explain where they fit in the line-up ๐Ÿ’œ


12.4. Commit โœจ

okay this is the main event ๐Ÿ‘‘ A Commit message initiates a new epoch for the group, based on a collection of Proposals. It tells group members to update their picture of the group's state by applying the proposals and advancing the key schedule. Main character energy, literally ๐ŸŒŸ

Each proposal covered by the Commit is included by a ProposalOrRef value, which identifies the proposal to be applied either by value or by reference. Commits that refer to new Proposals from the committer can include them by value. Commits for previously sent proposals, from anyone (including the committer), can be sent by reference. Proposals sent by reference are specified by including the hash of the AuthenticatedContent object in which the proposal was sent (see Section 5.2). So it's either the whole proposal, or a receipt for it ๐Ÿงพ

enum {
  reserved(0),
  proposal(1),
  reference(2),
  (255)
} ProposalOrRefType;

struct {
  ProposalOrRefType type;
  select (ProposalOrRef.type) {
    case proposal:  Proposal proposal;
    case reference: ProposalRef reference;
  };
} ProposalOrRef;

struct {
    ProposalOrRef proposals<V>;
    optional<UpdatePath> path;
} Commit;

A group member that has observed one or more valid proposals within an epoch MUST send a Commit message before sending application data. This ensures, for example, that any members whose removal was proposed during the epoch are actually removed before any application data is transmitted. Proposals don't just chill forever, they get committed before the party gets going ๐ŸŽ‰

A sender and a receiver of a Commit MUST verify that the committed list of proposals is valid as specified in Section 12.2. A list is invalid if, for example, it includes an Update and a Remove for the same member, or an Add when the sender does not have the application-level permission to add new users. Both sides run the vibe check, every time ๐Ÿ’…

The sender of a Commit SHOULD include all proposals that it has received during the current epoch that are valid according to the rules for their proposal types and according to application policy, as long as this results in a valid proposal list. Bring everyone you've got to the party ๐Ÿฅณ

Due to the asynchronous nature of proposals, receivers of a Commit SHOULD NOT enforce that all valid proposals sent within the current epoch are referenced by the next Commit. In the event that a valid proposal is omitted from the next Commit, and that proposal is still valid in the current epoch, the sender of the proposal MAY resend it after updating it to reflect the current epoch. Left on read? No worries, it can be resent with a fresh epoch ๐Ÿซถ

A member of the group MAY send a Commit that references no proposals at all, which would thus have an empty proposals vector. Such a Commit resets the sender's leaf and the nodes along its direct path, and provides forward secrecy and post-compromise security with regard to the sender of the Commit. An Update proposal can be regarded as a "lazy" version of this operation, where only the leaf changes and intermediate nodes are blanked out. A glow-up with no proposals attached, iconic โœจ

By default, the path field of a Commit MUST be populated. The path field MAY be omitted if (a) it covers at least one proposal and (b) none of the proposals covered by the Commit are of "path required" types. A proposal type requires a path if it cannot change the group membership in a way that requires the forward secrecy and post-compromise security guarantees that an UpdatePath provides. The only proposal types defined in this document that do not require a path are:

  • add
  • psk
  • reinit

New proposal types MUST state whether they require a path. If any instance of a proposal type requires a path, then the proposal type requires a path. This attribute of a proposal type is reflected in the "Path Required" field of the "MLS Proposal Types" registry defined in Section 17.4. Each new proposal type has to spill whether it needs a path ๐Ÿ“‹

Update and Remove proposals are the clearest examples of proposals that require a path. An UpdatePath is required to evict the removed member or the old appearance of the updated member. Basically, if someone's getting booted or re-keyed, the path is non-negotiable ๐Ÿ”

In pseudocode, the logic for validating the path field of a Commit is as follows:

pathRequiredTypes = [
    update,
    remove,
    external_init,
    group_context_extensions
]

pathRequired = false

for proposal in commit.proposals:
    pathRequired = pathRequired ||
                   (proposal.msg_type in pathRequiredTypes)

if len(commit.proposals) == 0 || pathRequired:
    assert(commit.path != null)

To summarize, a Commit can have three different configurations, each with its own use case ๐Ÿง

  1. An "empty" Commit that references no proposals, which updates the committer's contribution to the group and provides PCS with regard to the committer. Solo glow-up ๐ŸŒธ
  2. A "partial" Commit that references proposals that do not require a path, and where the path is empty. Such a Commit doesn't provide PCS with regard to the committer. Light and breezy ๐Ÿฆ‹
  3. A "full" Commit that references proposals of any type, which provides FS with regard to any removed members and PCS for the committer and any updated members. The whole squad, all the way, absolutely iconic ๐ŸŒˆ

12.4.1. Creating a Commit โœจ

okay so bestie, this is where the committer gets to be the main character ๐Ÿ’… when a Commit gets made or processed, the client updates the ratchet tree and the GroupContext, and they go from an "old" state (the current epoch) to a "new" state (the epoch that the Commit kicks off) ๐Ÿฆ‹ when the Commit has an UpdatePath, a "provisional" group context gets built that reflects the proposals and the UpdatePath, but it keeps the old confirmed transcript hash ๐ŸŒธ

a member of the group makes a Commit message and its matching Welcome message at the same time, and the steps go like this ๐ŸŽ€

  • Verify that the list of proposals being committed is valid, as specified in Section 12.2. ๐Ÿซถ
  • Build an initial Commit object with the proposals field filled from the Proposals received during the current epoch, and with the path field empty. ๐Ÿงš
  • Make the new ratchet tree and GroupContext by applying the list of proposals to the old ratchet tree and GroupContext, as defined in Section 12.3. ๐ŸŒˆ
  • Decide whether to fill in the path field. If the proposals in the Commit mean the path field is required (see above), then it MUST be populated. Otherwise, the sender MAY leave the path field out, totally at its discretion. ๐Ÿ‘‘
  • If populating the path field:
    • If this is an external Commit, give the sender the leftmost blank leaf node in the new ratchet tree. If there are no blank leaf nodes in the new ratchet tree, expand the tree to the right as defined in Section 7.7 and assign the leftmost new blank leaf to the sender. ๐Ÿ“
    • Update the sender's direct path in the ratchet tree as described in Section 7.5. Define commit_secret as the value path_secret[n+1], derived from the last path secret value (path_secret[n]) derived for the UpdatePath. ๐Ÿ’œ
    • Build a provisional GroupContext object containing these values:
      • group_id: Same as the old GroupContext
      • epoch: The epoch number for the new epoch
      • tree_hash: The tree hash of the new ratchet tree
      • confirmed_transcript_hash: Same as the old GroupContext
      • extensions: The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal)
    • Encrypt the path secrets from the tree update to the group as described in Section 7.5, using the provisional group context as the context for HPKE encryption. ๐Ÿ”ฅ
    • Create an UpdatePath holding the sender's new leaf node and the new public keys and encrypted path secrets along the sender's filtered direct path. Assign this UpdatePath to the path field in the Commit. โœจ
  • If not populating the path field: set the path field in the Commit to the null optional. Define commit_secret as the all-zero vector of length KDF.Nh (the same length a path_secret value would be). ๐Ÿ™ˆ
  • Derive the psk_secret as specified in Section 8.4, where the order of PSKs in the derivation matches the order of PreSharedKey proposals in the proposals vector. ๐Ÿซง
  • Build a FramedContent object containing the Commit object. Sign the FramedContent using the old GroupContext as context.
    • Use the FramedContent to update the confirmed transcript hash and the new GroupContext. ๐ŸŒธ
    • Use the init_secret from the previous epoch, the commit_secret and psk_secret from the previous steps, and the new GroupContext to compute the new joiner_secret, welcome_secret, epoch_secret, and derived secrets for the new epoch. ๐Ÿฆ‹
    • Use the confirmation_key for the new epoch to compute the confirmation_tag value. ๐Ÿ’–
    • Calculate the interim transcript hash using the new confirmed transcript hash and the confirmation_tag from the FramedContentAuthData. ๐ŸŽ€
  • Protect the AuthenticatedContent object using keys from the old epoch:
    • If encoding as PublicMessage, compute the membership_tag value using the membership_key. ๐Ÿ”
    • If encoding as a PrivateMessage, encrypt the message using the sender_data_secret and the next (key, nonce) pair from the sender's handshake ratchet. ๐Ÿง
  • Build a GroupInfo reflecting the new state:
    • Set the group_id, epoch, tree, confirmed_transcript_hash, interim_transcript_hash, and group_context_extensions fields to reflect the new state.
    • Set the confirmation_tag field to the value of the corresponding field in the FramedContentAuthData object.
    • Add any other extensions as defined by the application.
    • Optionally derive an external key pair as described in Section 8. (required for external Commits, see Section 12.4.3.2). ๐ŸŒท
    • Sign the GroupInfo using the member's private signing key. โœ๏ธ
    • Encrypt the GroupInfo using the key and nonce derived from the joiner_secret. for the new epoch (see Section 12.4.3.1). ๐Ÿ”’
  • For each new member in the group:
    • Identify the lowest common ancestor in the tree of the new member's leaf node and the member sending the Commit. ๐ŸŒณ
    • If the path field was populated above: compute the path secret corresponding to the common ancestor node. ๐Ÿƒ
    • Compute an EncryptedGroupSecrets object that encapsulates the init_secret for the current epoch and the path secret (if present). ๐Ÿงš
  • Build one or more Welcome messages from the encrypted GroupInfo object, the encrypted key packages, and any PSKs for which a proposal was included in the Commit. The order of the psks MUST be the same as the order of PreSharedKey proposals in the proposals vector. As discussed in Section 12.4.3.1, the committer is free to choose how many Welcome messages to construct. However, the set of Welcome messages produced in this step MUST cover every new member added in the Commit. ๐Ÿ’Œ
  • If a ReInit proposal was part of the Commit, the committer MUST create a new group with the parameters specified in the ReInit proposal, and with the same members as the original group. The Welcome message MUST include a PreSharedKeyID with these parameters:
    • psktype: resumption
    • usage: reinit
    • group_id: The group ID for the current group
    • epoch: The epoch that the group will be in after this Commit

12.4.2. Processing a Commit ๐ŸŽ€

okay now the receiving side ๐Ÿ‘€ a member of the group applies a Commit message by going through these steps ๐Ÿ’…

  • Verify that the epoch field of the enclosing FramedContent is equal to the epoch field of the current GroupContext object. ๐Ÿฆ‹
  • Unprotect the Commit using the keys from the current epoch:
    • If the message is encoded as PublicMessage, verify the membership MAC using the membership_key. ๐Ÿ”
    • If the message is encoded as PrivateMessage, decrypt the message using the sender_data_secret and the (key, nonce) pair from the step on the sender's hash ratchet indicated by the generation field. ๐ŸŒˆ
  • Verify the signature on the FramedContent message as described in Section 6.1. โœ๏ธ
  • Verify that the proposals vector is valid according to the rules in Section 12.2. ๐Ÿ‘‘
  • Verify that all PreSharedKey proposals in the proposals vector are available. ๐ŸŒธ
  • Make the new ratchet tree and GroupContext by applying the list of proposals to the old ratchet tree and GroupContext, as defined in Section 12.3. ๐Ÿง
  • Verify that the path value is populated if the proposals vector contains any Update or Remove proposals, or if it's empty. Otherwise, the path value MAY be omitted. ๐Ÿ’–
  • If the path value is populated, validate it and apply it to the tree:
    • If this is an external Commit, give the sender the leftmost blank leaf node in the new ratchet tree. If there are no blank leaf nodes in the new ratchet tree, add a blank leaf to the right side of the new ratchet tree and assign it to the sender. ๐Ÿ“
    • Validate the LeafNode as specified in Section 7.3. The leaf_node_source field MUST be set to commit. ๐Ÿซถ
    • Verify that the encryption_key value in the LeafNode is different from the committer's current leaf node. ๐Ÿ™ˆ
    • Verify that none of the public keys in the UpdatePath appear in any node of the new ratchet tree. ๐Ÿ”Ž
    • Merge the UpdatePath into the new ratchet tree, as described in Section 7.5. ๐ŸŒณ
    • Build a provisional GroupContext object containing these values:
      • group_id: Same as the old GroupContext
      • epoch: The epoch number for the new epoch
      • tree_hash: The tree hash of the new ratchet tree
      • confirmed_transcript_hash: Same as the old GroupContext
      • extensions: The new GroupContext extensions (possibly updated by a GroupContextExtensions proposal)
    • Decrypt the path secrets for UpdatePath as described in Section 7.5, using the provisional GroupContext as the context for HPKE decryption. ๐Ÿ”“
    • Define commit_secret as the value path_secret[n+1], derived from the last path secret value (path_secret[n]) derived for the UpdatePath. ๐Ÿงš
  • If the path value is not populated, define commit_secret as the all-zero vector of length KDF.Nh (the same length a path_secret value would be). ๐ŸŒท
  • Update the confirmed and interim transcript hashes using the new Commit, and generate the new GroupContext. ๐Ÿฆ‹
  • Derive the psk_secret as specified in Section 8.4, where the order of PSKs in the derivation matches the order of PreSharedKey proposals in the proposals vector. ๐Ÿซง
  • Use the init_secret from the previous epoch, the commit_secret and psk_secret from the previous steps, and the new GroupContext to compute the new joiner_secret, welcome_secret, epoch_secret, and derived secrets for the new epoch. ๐Ÿ’œ
  • Use the confirmation_key for the new epoch to compute the confirmation tag for this message, as described below, and verify that it is the same as the confirmation_tag field in the FramedContentAuthData object. โœ…
  • If the above checks are successful, consider the new GroupContext object as the current state of the group. ๐Ÿ‘‘
  • If the Commit included a ReInit proposal, the client MUST NOT use the group to send messages anymore. Instead, it MUST wait for a Welcome message from the committer meeting the requirements of Section 11.2. ๐Ÿ˜ญ
Clients need to be prepared to receive a valid Commit message that removes them from the group. In this case, the client cannot send any more messages in the group and SHOULD promptly delete its group state and secret tree. (A client might keep the secret tree for a short time to decrypt late messages in the previous epoch.) ๐Ÿ’€

That's the Commit flow, bestie! ๐ŸŒธ

12.4.3. Adding Members to the Group ๐Ÿฆ‹

okay so new members can join the group in two ways ๐Ÿ’–: a group member adds them, or they add themselves through an external Commit ๐Ÿซถ. either way, the newbies need some info to bootstrap their local group state โœจ

struct {
    GroupContext group_context;
    Extension extensions<V>;
    MAC confirmation_tag;
    uint32 signer;
    /* SignWithLabel(., "GroupInfoTBS", GroupInfoTBS) */
    opaque signature<V>;
} GroupInfo;

the group_context field is the current state of the group ๐ŸŒธ. the extensions field lets the sender tuck in extra data that might help new joiners ๐ŸŽ€. the confirmation_tag is the one from the Commit that kicked off the current epoch, or for epoch 0, it's the one computed when the group was created (see Section 11) ๐Ÿงš. (either way, whoever makes a GroupInfo can recompute the confirmation tag as MAC(confirmation_key, confirmed_transcript_hash), so it's not a mystery ๐Ÿ’…)

as we talked about in Section 13, unknown extensions in GroupInfo.extensions MUST be ignored ๐Ÿ™ˆ, and the creator of a GroupInfo object SHOULD toss in some random GREASE extensions so other clients learn to ignore unknown stuff ๐ŸŒˆ. extensions in GroupInfo.group_context.extensions, though, MUST be supported by the new joiner ๐Ÿ‘‘. no skipping those, periodt ๐Ÿ˜ญ

new members MUST verify that group_id is unique among the groups they're already participating in ๐Ÿ“. they also MUST verify the signature, using the public key from the leaf node of the ratchet tree at leaf index signer ๐Ÿ”. the signature covers everything in GroupInfo above the signature field, so here's that structure ๐Ÿ‘‡

struct {
    GroupContext group_context;
    Extension extensions<V>;
    MAC confirmation_tag;
    uint32 signer;
} GroupInfoTBS;

12.4.3.1. Joining via Welcome Message

the committer (the person sending the Commit) is the one in charge of sending a Welcome message to every new member added through Add proposals ๐Ÿ’Œ. the Welcome format lets one message be encrypted for multiple new members at once, which is so smart ๐Ÿง . it's up to the committer how many Welcomes to make for a given Commit. they could make one Welcome encrypted for everyone, a separate one for each person, or Welcomes for batches of people based on whatever batching scheme works for the app ๐Ÿฆ‹. the process for creating and processing the Welcome is the same in every case, the only difference is which new members the Welcome is encrypted for โœจ

the Welcome gives the new members the current state of the group after the Commit gets applied ๐ŸŒŸ. the new members can't decrypt or verify the Commit itself, but they get the secrets they need to take part in the epoch that the Commit started ๐ŸŽ€

so the same Welcome can go to several new members, the group info is encrypted with a symmetric key and nonce derived from the joiner_secret for the new epoch ๐Ÿ”‘. the joiner_secret is then encrypted to each new member using HPKE ๐Ÿ’œ. inside that same encrypted package, the committer also sends the path secret for the lowest node (closest to the leaf) that sits in the direct paths of both the committer and the new member ๐ŸŒธ. this lets the new member compute private keys for the nodes in its direct path that the Commit is resetting ๐Ÿซถ

if the Welcome sender wants the receiver to bring a PSK into the derivation of the epoch_secret, they can fill in the psks field to say which PSK to use ๐Ÿงš

struct {
  opaque path_secret<V>;
} PathSecret;

struct {
  opaque joiner_secret<V>;
  optional<PathSecret> path_secret;
  PreSharedKeyID psks<V>;
} GroupSecrets;

struct {
  KeyPackageRef new_member;
  HPKECiphertext encrypted_group_secrets;
} EncryptedGroupSecrets;

struct {
  CipherSuite cipher_suite;
  EncryptedGroupSecrets secrets<V>;
  opaque encrypted_group_info<V>;
} Welcome;

the client processing a Welcome needs a copy of the group's ratchet tree ๐ŸŒณ. the tree can ride along in the Welcome message, in an extension of type ratchet_tree ๐ŸŽ. if it comes some other way (like a caching service on the Delivery Service), then the client MUST download the tree before processing the Welcome ๐Ÿ™

when a client gets a Welcome, here's the play-by-play ๐Ÿ“:

  • find an entry in the secrets array where the new_member value matches one of this client's KeyPackages, using the hash indicated by the cipher_suite field ๐Ÿ”. if there's no such entry, or the cipher suite in the KeyPackage doesn't match the one in the Welcome, return an error ๐Ÿšซ
  • decrypt the encrypted_group_secrets value with the algorithms from the cipher suite, using the private key init_key_priv that goes with init_key in the referenced KeyPackage ๐Ÿ”“
encrypted_group_secrets =
  EncryptWithLabel(init_key, "Welcome",
                   encrypted_group_info, group_secrets)

group_secrets =
  DecryptWithLabel(init_key_priv, "Welcome",
                   encrypted_group_info, kem_output, ciphertext)
  • if a PreSharedKeyID is part of the GroupSecrets and the client doesn't have the matching PSK, return an error ๐Ÿ’€. also, if a PreSharedKeyID has type resumption with usage reinit or branch, check that it's the only PSK like that ๐Ÿ‘€
  • from the joiner_secret in the decrypted GroupSecrets object and the PSKs listed in GroupSecrets, derive the welcome_secret, then the welcome_key and welcome_nonce ๐Ÿง. use that key and nonce to decrypt the encrypted_group_info field ๐Ÿ”
welcome_nonce = ExpandWithLabel(welcome_secret, "nonce", "", AEAD.Nn)
welcome_key = ExpandWithLabel(welcome_secret, "key", "", AEAD.Nk)
  • verify the signature on the GroupInfo object ๐Ÿ–Š๏ธ. the signature input is every field in GroupInfo except the signature field ๐Ÿ“œ. the public key comes from the LeafNode at leaf index signer in the ratchet tree. if that node is blank or the signature check fails, return an error โŒ
  • verify that the group_id is unique among the groups this client is currently in ๐Ÿ“
  • verify that the cipher_suite in the GroupInfo matches the cipher_suite in the KeyPackage ๐Ÿ’ซ
  • verify the integrity of the ratchet tree ๐ŸŒณโœ…
    • verify that the tree hash of the ratchet tree matches the tree_hash field in GroupInfo ๐Ÿงฉ
    • for each non-empty parent node, verify that it's "parent-hash valid", as described in Section 7.9.2 ๐Ÿ”—
    • for each non-empty leaf node, validate the LeafNode as described in Section 7.3 ๐Ÿ•ต๏ธ
    • for each non-empty parent node and each entry in that node's unmerged_leaves field:
      • verify that the entry is a non-blank leaf node that's a descendant of the parent node ๐ŸŒฟ
      • verify that every non-blank intermediate node between the leaf node and the parent node also has an entry for that leaf node in its unmerged_leaves ๐ŸŒฑ
      • verify that the encryption key in the parent node doesn't show up in any other node of the tree ๐Ÿšจ
  • find a leaf whose LeafNode is identical to the one in the KeyPackage. if there isn't one, return an error ๐Ÿ™…. call this leaf my_leaf ๐Ÿ’–
  • build a new group state from the information in the GroupInfo object ๐Ÿ—๏ธ
    • initialize the GroupContext from the group_context field of the GroupInfo object ๐ŸŒธ
    • update the leaf my_leaf with the private key that matches the public key in the node, since my_leaf is the new member's leaf node in the ratchet tree, as defined above ๐ŸŽ€
    • if the path_secret value is set in the GroupSecrets object: find the lowest common ancestor of the leaf node my_leaf and the node of the member at leaf index GroupInfo.signer. then set the private key for this node to the private key derived from the path_secret ๐Ÿ—๏ธ
    • for each parent of the common ancestor, up to the root of the tree, derive a new path secret and set the private key for that node to the private key derived from the path secret. the private key MUST be the one that corresponds to the public key in the node ๐Ÿ”‘
  • use the joiner_secret from the GroupSecrets object to generate the epoch secret and the other derived secrets for the current epoch ๐Ÿง
  • set the confirmed transcript hash in the new state to the confirmed_transcript_hash value in the GroupInfo โœ๏ธ
  • verify the confirmation tag in the GroupInfo using the derived confirmation key and the confirmed_transcript_hash from the GroupInfo ๐Ÿงพ
  • use the confirmed transcript hash and the confirmation tag to compute the interim transcript hash in the new state ๐Ÿช„
  • if a PreSharedKeyID was used that has type resumption with usage reinit or branch, verify that the epoch field in the GroupInfo is equal to 1 ๐Ÿ”ข
    • for usage reinit, verify that the last Commit to the referenced group contains a ReInit proposal, and that the group_id, version, cipher_suite, and group_context.extensions fields of the GroupInfo match the ReInit proposal ๐ŸŽฏ. also verify that all the members of the old group are also members of the new group, according to the application ๐Ÿ‘ฏ
    • for usage branch, verify that the version and cipher_suite of the new group match those of the old group, and that the members of the new group are a subset of the members of the old group, according to the application ๐ŸŒˆ

12.4.3.2. Joining via External Commits โœจ

okay so External Commits are how new members (outside parties who want in ๐Ÿซถ) add themselves to a group without an existing member having to come online and issue a Commit that references an Add proposal. it's giving independence, no cap ๐Ÿฆ‹

Whether existing members accept or reject an external Commit follows the same rules that get applied to other handshake messages ๐Ÿ’…

New members can create and issue an external Commit if they have access to the following info for the group's current epoch:

  • group ID
  • epoch ID
  • cipher suite
  • public tree hash
  • confirmed transcript hash
  • confirmation tag of the most recent Commit
  • group extensions
  • external public key

So, to join via an external Commit, a new member needs a GroupInfo with an external_pub extension sitting in its extensions field ๐ŸŽ€

struct {
    HPKEPublicKey external_pub;
} ExternalPub;

This means a member of the group can let new clients join by making a GroupInfo object available to them ๐ŸŒธ Heads up though: a GroupInfo object is specific to an epoch, so it has to get updated as the group advances. Each GroupInfo object can be used for one external join, since that join will cause the epoch to change ๐Ÿ˜ญ

The tree_hash field gets used the same way it does in the Welcome message. The full tree can be included via the ratchet_tree extension (see Section 12.4.3.3) ๐Ÿงš

The info in a GroupInfo isn't generally public, but applications can choose to make it available to new members so they can do External Commits ๐Ÿ”

In principle, external Commits work like regular Commits. BUT their content has to meet a specific set of requirements, bestie:

  • External Commits MUST contain a path field (and is therefore a "full" Commit). The joiner gets added at the leftmost free leaf node (just as if they were added with an Add proposal), and the path is calculated relative to that leaf node.
  • The Commit MUST NOT include any proposals by reference, since an external joiner can't determine the validity of proposals sent within the group ๐Ÿ™ˆ
  • External Commits MUST be signed by the new member. In particular, the signature on the enclosing AuthenticatedContent MUST verify using the public key for the credential in the leaf_node of the path field.
  • When processing a Commit, both existing and new members MUST use the external init secret as described in Section 8.3.
  • The sender type for the AuthenticatedContent encapsulating the external Commit MUST be new_member_commit.

External Commits come in two "flavors" ๐ŸŒˆ A "join" Commit adds the sender to the group, or a "resync" Commit replaces a member's prior appearance with a brand new one.

Now the tea โ˜• The "resync" operation lets an attacker who has compromised a member's signature private key introduce themselves into the group and remove the prior, legit member in a single Commit. ๐Ÿ’€ Without resync, this can still happen, but it takes two operations: the external Commit to join, and a second Commit to remove the old appearance. Applications for whom this distinction is salient can choose to disallow external commits that contain a Remove. They can also allow resync commits only if they contain a "reinit" PSK proposal that proves the joining member was present in a prior epoch of the group. With that latter approach, the attacker would need to compromise the PSK as well as the signing key. But the application will need to make sure that continuing, non-resynchronizing members have the required PSK ๐Ÿ‘‘

12.4.3.3. Ratchet Tree Extension ๐ŸŒณ

By default, a GroupInfo message only gives the joiner a hash of the group's ratchet tree. To process or generate handshake messages, the joiner will need a copy of the ratchet tree from some other source. (For example, the DS might provide a cached copy.) Because the tree hash is included in the GroupInfo message, the source of the ratchet tree doesn't need to be trusted to keep the tree's integrity intact ๐Ÿซง

When the application doesn't want an external source, the whole public state of the ratchet tree can be provided in an extension of type ratchet_tree, which holds a ratchet_tree object of this form:

struct {
    NodeType node_type;
    select (Node.node_type) {
        case leaf:   LeafNode leaf_node;
        case parent: ParentNode parent_node;
    };
} Node;

optional<Node> ratchet_tree<V>;

Each entry in the ratchet_tree vector gives the value for one node in the tree, or the null optional for a blank node ๐Ÿ“

The nodes are listed in the order of a left-to-right in-order traversal of the ratchet tree. Each node sits between its left subtree and its right subtree. (This is the same ordering as the array-based trees in Appendix C ๐Ÿง )

If the tree has 2^d leaves, then it has 2^(d+1) - 1 nodes. The ratchet_tree vector logically has that many entries, but the sender MUST NOT include blank nodes after the last non-blank node. The receiver MUST check that the last node in ratchet_tree is non-blank, and then extend the tree to the right until its length is of the form 2^(d+1) - 1, adding the minimum number of blank values possible. (You can totally do this "virtually" by synthesizing blank nodes when needed, instead of actually changing the structure in memory ๐Ÿงšโ€โ™€๏ธ)

The leaves of the tree are stored in the even-numbered entries of the array (the leaf with index L sits in array position 2*L). The root node is at position 2^d - 1 of the array. Intermediate parent nodes can be found by doing the same calculation on the subarrays to the left and right of the root, following something like this algorithm:

# Assuming a class Node that has left and right members
def subtree_root(nodes):
    # If there is only one node in the array, return it
    if len(nodes) == 1:
        return Node(nodes[0])

    # Otherwise, the length of the array MUST be odd
    if len(nodes) % 2 == 0:
        raise Exception("Malformed node array {}", len(nodes))

    # Identify the root of the subtree
    d = 0
    while (2**(d+1)) < len(nodes):
       d += 1
    R = 2**d - 1
    root = Node(nodes[R])
    root.left = subtree_root(nodes[:R])
    root.right = subtree_root(nodes[(R+1):])
    return root

(Note that this is the same ordering of nodes as in the array-based tree representation described in Appendix C. The algorithms in that section can be used to simplify decoding this extension into other representations ๐Ÿช„)

Here's an example: a tree with six non-blank leaves would be represented as an array of eleven elements, [A, W, B, X, C, _, D, Y, E, Z, F]. The decoding procedure above identifies the subtree roots like so (using R to represent a subtree root):

               Y
               |
         .-----+-----.
        /             \
       X               _
       |               |
     .-+-.           .-+-.
    /     \         /     \
   W       _       Z       _
  / \     / \     / \     / \
 A   B   C   D   E   F   _   _

                     1
 0 1 2 3 4 5 6 7 8 9 0
 <-----------> R <----------->
 <---> R <--->   <---> R <--->
 - R -   - R -   - R -   - R -

    Figure 28: Left-to-Right In-Order Traversal of a Six-Member Tree

The presence of a ratchet_tree extension in a GroupInfo message doesn't change the GroupContext extensions for the group at all ๐Ÿ™…โ€โ™€๏ธ The client just stores the ratchet tree it was given and uses it for MLS operations.

If this extension isn't provided in a Welcome message, the client will need to fetch the ratchet tree over some other channel before it can generate or process Commit messages. Applications should ensure that this out-of-band channel has security protections equivalent to the ones afforded to Proposal and Commit messages. For example, an application that encrypts Proposal and Commit messages might distribute ratchet trees encrypted using a key exchanged over the MLS channel ๐Ÿ”’

Regardless of how the client gets the tree, the client MUST verify that the root hash of the ratchet tree matches the tree_hash of the GroupContext before using the tree for MLS operations. No skipping this, periodt ๐Ÿ’œ


13. Extensibility ๐Ÿงฉ

The base MLS protocol can be extended in a few ways. New cipher suites can be added to enable new cryptographic algorithms. New types of proposals can perform new actions within an epoch. Extension fields can add extra information to the protocol. In this section we talk about some constraints on these extensibility mechanisms, which are needed to keep broad interoperability intact ๐ŸŒธ

13.1. Additional Cipher Suites

As discussed in Section 5.1, MLS lets the participants in a group negotiate the cryptographic algorithms used within the group. This extensibility matters for keeping the protocol secure over time [RFC7696]. It also creates a risk of interoperability failure when clients don't support a common cipher suite ๐Ÿ˜ฌ

The cipher suite registry defined in Section 17.1 tries to strike a balance here. On one hand, the base policy for the registry is Specification Required, a pretty low bar meant to avoid needing standards work when different ciphers are needed for niche applications. On the other hand, there's a higher bar (Standards Action) for ciphers to set the Recommended field in the registry. That higher bar exists partly to make sure the interoperability implications of new cipher suites get considered ๐Ÿง

MLS cipher suites are defined independently of MLS versions, so in principle the same cipher suite can be used across versions. Standards work defining new MLS versions should consider whether it's desirable for the new version to be compatible with existing cipher suites, or whether the new version should rule out some cipher suites. For example, a new version could follow the example of HTTP/2, which restricted the set of allowed TLS ciphers (see Section 9.2.2 of [RFC9113]) ๐Ÿฆ‹

13.2. Proposals

Commit messages don't have an extension field, because the set of proposals is extensible. As discussed in Section 12.4, Proposals with a non-default proposal type MUST NOT be included in a commit unless the proposal type is supported by all the members of the group that will process the Commit. Not the vibe of sneaking in unsupported proposal types ๐Ÿ™…โ€โ™€๏ธ๐Ÿ’…

13.3. Credential Extensibility ๐ŸŽ€

Okay bestie, so MLS only gives you meaningful authentication if every member can authenticate some identity info for every other member ๐Ÿซถ. That identity info lives in Credentials, so the whole trick is making sure everyone uses compatible credential types ๐Ÿ’…. The only types of credential that may be used in a group are the ones all members support, as shown by the capabilities field on each LeafNode in the ratchet tree โœจ. An app can introduce a brand-new credential type by grabbing an unallocated identifier from the registry in Section 17.5 and flagging support for it in published LeafNodes ๐ŸŒˆ. That can happen in Update proposals for existing groups or in KeyPackages that get added to new groups. Once every member in the group has indicated support, members can start using LeafNodes with the new credential ๐Ÿฆ‹. An app can also lock certain credential types in place by adding a required_capabilities extension to the group's GroupContext ๐Ÿ”’. That way nobody can be added to the group unless they support them. Iconic behaviour, honestly ๐Ÿ‘‘.

Looking ahead, future MLS extensions might let a member present more than one credential ๐Ÿ‘€. Those credentials could show different attributes that different authorities attest to. To stay consistent with the principle from the top of this section, any extension like that would need to make sure each member can still authenticate some identity for each other member ๐Ÿ’–. So for every pair of members (Alice, Bob), Alice would need to present at least one credential of a type that Bob supports ๐Ÿงš.

13.4. Extensions ๐ŸŒธ

This protocol has a way to negotiate extension parameters that's kinda like the one in TLS [RFC8446] ๐Ÿ’œ. In TLS, negotiation is one-to-one: the client offers extensions in its ClientHello message, and the server shares its choices for the session with extensions in its ServerHello and EncryptedExtensions messages. In MLS, though, extensions show up in four places ๐Ÿ˜ญ:

  • In KeyPackages, to describe additional information related to the client
  • In LeafNodes, to describe additional information about the client or its participation in the group (once in the ratchet tree)
  • In the GroupInfo, to tell new members what parameters the group is using, and to provide any additional details needed to join the group
  • In the GroupContext object, to make sure all members of the group have the same view of the parameters in use

So basically, an app can use GroupContext extensions to make sure everyone in the group agrees on a set of parameters ๐Ÿค. Clients show their support for parameters in the capabilities field of their LeafNode ๐ŸŽ€. New members find out about the group's GroupContext extensions through the extensions field inside the group_context field of the GroupInfo object ๐Ÿ“ฌ. The extensions field of a GroupInfo object (outside of group_context) can carry extra parameters for new joiners to use when they join the group โœจ.

This extension mechanism is built for secure and forward-compatible negotiation of extensions ๐Ÿฆ‹. For that to actually work, implementations MUST correctly handle extensible fields:

  • A client that posts a KeyPackage MUST support all parameters advertised in it. Otherwise, another client might fail to interoperate by selecting one of those parameters.
  • A client processing a KeyPackage object MUST ignore all unrecognized values in the capabilities field of the LeafNode and all unknown extensions in the extensions and leaf_node.extensions fields. Otherwise, it could fail to interoperate with newer clients.
  • A client processing a GroupInfo object MUST ignore all unrecognized extensions in the extensions field.
  • Any field containing a list of extensions MUST NOT have more than one extension of any given type.
  • A client adding a new member to a group MUST verify that the LeafNode for the new member is compatible with the group's extensions. The capabilities field MUST indicate support for each extension in the GroupContext.
  • A client joining a group MUST verify that it supports every extension in the GroupContext for the group. Otherwise, it MUST treat the enclosing GroupInfo message as invalid and not join the group.
Heads up: those last two requirements mean every MLS GroupContext extension is mandatory ๐Ÿšจ. Any extension the group is using MUST be supported by every member of the group. No exceptions, no cap ๐Ÿ’€.

The parameters of a group can change in two ways ๐Ÿ’…. You can send a GroupContextExtensions proposal to turn on additional extensions (Section 12.1.7), or you can reinitialize the group (Section 11.2) ๐ŸŒˆ.

13.5. GREASE ๐Ÿงš

Remember how clients are supposed to ignore unknown values for certain parameters? ๐Ÿ™ˆ To make sure other clients actually do that, a client can follow the "Generate Random Extensions And Sustain Extensibility" approach, or GREASE, from [RFC8701] ๐Ÿซง. In MLS, that means a client generating a KeyPackage, LeafNode, or GroupInfo object throws random values into certain fields. A correctly implemented client just ignores them. A client that wrongly rejects unknown code points will choke on the message, and that's a big flashing sign that it needs fixing ๐Ÿšฉ.

When generating the following fields, an MLS client SHOULD include a random selection of values chosen from these GREASE values:

  • LeafNode.capabilities.cipher_suites
  • LeafNode.capabilities.extensions
  • LeafNode.capabilities.proposals
  • LeafNode.capabilities.credentials
  • LeafNode.extensions
  • KeyPackage.extensions
  • GroupInfo.extensions

For the KeyPackage and GroupInfo extensions, the extension_data of GREASE extensions MAY have any contents the sender picks, since a correctly implemented receiver will ignore it ๐Ÿ“. For example, a sender could fill these extensions with a randomly sized chunk of random data ๐ŸŽฒ.

Heads up: any GREASE values added to LeafNode.extensions need to be mirrored in LeafNode.capabilities.extensions too ๐Ÿชž. That's because the LeafNode validation process in Section 7.3 requires those two fields to match.

GREASE values MUST NOT be sent in the following fields ๐Ÿšซ. An unsupported value in any of these, GREASE included, will get the whole enclosing message rejected:

  • Proposal.proposal_type
  • Credential.credential_type
  • GroupContext.extensions
  • GroupContextExtensions.extensions

GREASE values have been registered across the registries in Section 17, so they can't collide with real future values ๐Ÿ’œ. These values are reserved in each registry: 0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A, 0x6A6A, 0x7A7A, 0x8A8A, 0x9A9A, 0xAAAA, 0xBABA, 0xCACA, 0xDADA, and 0xEAEA. (0xFAFA falls in the private use range ๐ŸŒธ.) These values MUST only appear in the fields listed above, and never somewhere like the proposal_type field of a Proposal. Clients MUST NOT implement any special processing rules for them when they receive them, because special-casing would wreck their whole purpose of catching extensibility bugs ๐Ÿ™…โ€โ™€๏ธ.

GREASE values MUST be handled using the normal logic for processing unsupported values ๐Ÿง . When clients compare capability lists to find what they both support, clients MUST represent their own capabilities with a list that only contains what they actually support, with no GREASE values ๐Ÿช„. Basically, lists with GREASE values only get sent to other clients. Representations of a client's own capabilities MUST NOT contain GREASE values ๐Ÿซถ.

14. Sequencing of State Changes โณ

Every Commit message is based on a starting state, which is indicated by the epoch field of the enclosing FramedContent ๐Ÿ“. If you apply the changes from a Commit starting from a different state, the results will be wrong ๐Ÿ˜ฌ.

Sequencing wouldn't be a problem at all if every group member sent a Commit based on the most current state of the group ๐Ÿ™Œ. In practice, though, there's a real risk that two members generate Commit messages at the same time, both based on the same state ๐Ÿ‘ฏ.

Applications MUST have an established way to resolve conflicting Commit messages for the same epoch ๐ŸฅŠ. They can do that by stopping conflicting messages from happening in the first place, or by coming up with rules for deciding which Commit out of several sent in an epoch becomes the canonical one ๐Ÿ‘‘. Whatever approach they pick MUST minimize how long forked or previous group states stay in memory, and it MUST promptly delete them once they're no longer needed for forward secrecy ๐Ÿงน.

Generating a Commit message MUST NOT modify a client's state ๐Ÿ™…โ€โ™€๏ธ. The client can't know yet whether the Commit's changes will conflict with another Commit. Similarly, the Welcome message for a Commit MUST NOT be delivered to a new joiner until it's clear that the Commit has been accepted ๐Ÿ’Œ.

Even with sequencing sorted, there's a risk that in a busy enough group, a given member never gets to send a Commit because they keep losing to everyone else ๐Ÿ˜ญ. How big of a problem that is depends on the app's dynamics ๐ŸŒˆ.

15. Application Messages ๐Ÿ’ฌ

Handshake messages are mainly there to give clients an authenticated group key exchange ๐Ÿ”. To protect application messages sent among group members, the key schedule's encryption_secret is used to derive a sequence of nonces and keys for encryption ๐Ÿ—๏ธ. Every epoch moves the key schedule forward, which kicks off a new secret tree (see Section 9) along with a fresh set of symmetric ratchets of nonces and keys for each member ๐ŸŒณ.

Each client keeps its own local copy of the key schedule for every epoch it's a group member in ๐Ÿ—‚๏ธ. They derive new keys, nonces, and secrets as needed, and delete the old ones as soon as they've been used ๐Ÿงฝ.

The group identifier and epoch let a recipient figure out which group secrets to use and which epoch_secret to start computing other secrets from โœจ. The sender identifier and content type tell the recipient which symmetric ratchet to pull from the secret tree ๐ŸŽฏ. The generation counter decides how far to iterate along the ratchet to produce the nonce and key needed for encryption or decryption ๐Ÿ”‘.

15.1. Padding ๐ŸŽ€

Okay so, application messages MAY be padded to help resist traffic analysis over encrypted traffic [CLINIC] [HCJ16] ๐Ÿ•ต๏ธโ€โ™€๏ธ. MLS might deliver the same payload way less often across a lot of ciphertexts than a traditional web server does, but that still might give an attacker enough info to pull off an attack ๐Ÿ’€. Like, if Alice asks Bob "When are we going to the movie?", the answer "Wednesday" could leak to an adversary purely from the ciphertext length. Not okay!! ๐Ÿ™ˆ

The length of the padding field in PrivateMessageContent is chosen by the sender at the time of encryption โœจ. Senders may use padding to make it harder for attackers outside the group to infer the size of the encrypted content ๐Ÿซถ. Just a heads up though: the transports that carry MLS messages may have maximum message sizes, so padding schemes SHOULD avoid making messages bigger than those limits in a given deployment ๐Ÿ“.

15.2. Restrictions ๐Ÿ”’

During each epoch, senders MUST NOT encrypt more data than the security bounds of the AEAD scheme in use allow [CFRG-AEAD-LIMITS] ๐Ÿ˜ญ. Bestie, this one is serious.

Also, every change to the group through a handshake message sets a new encryption_secret ๐Ÿ”‘. So that change MUST be applied before encrypting any new application message. This is required both so that anyone removed from the group can no longer receive messages, and so that confidentiality and authenticity for future messages can (potentially) recover after a past state compromise ๐Ÿ‘‘.

15.3. Delayed and Reordered Application Messages ๐Ÿฆ‹

Since each application message contains the group identifier, the epoch, and a generation counter, a client can receive messages out of order ๐ŸŒˆ. When that happens, the client moves the sender ratchet forward to match the received generation counter. Any unused nonce and key pairs from the ratchet are potentially stored, so they can decrypt the messages that got delayed or reordered ๐Ÿงš.

Applications SHOULD define a policy on how long to keep unused nonce and key pairs for a sender, and the maximum number to keep. This is on top of making sure those secrets get deleted according to the deletion schedule in Section 9.2 ๐Ÿ—‘๏ธ. Applications SHOULD also define a policy limiting the maximum number of steps clients will move the ratchet forward in response to a new message. Messages with a generation counter that is too much higher than the last message received would then get rejected. This avoids a denial-of-service attack where the recipient has to do an excessive number of key derivations ๐Ÿ˜ค.

For example, a malicious group member could send a message with generation = 0xffffffff at the beginning of a new epoch, forcing recipients to perform billions of key derivations unless they apply limits like the ones discussed above ๐Ÿ’€๐Ÿ’€.

16. Security Considerations โœจ

The security goals of MLS are described in [MLS-ARCH] ๐Ÿ“š. Here we describe, at a high level, how the protocol achieves those goals. A complete security analysis is outside the scope of this document. The Security Considerations section of [MLS-ARCH] has some citations to detailed security analyses ๐Ÿ”.

16.1. Transport Security ๐Ÿšš

Because MLS messages are protected at the message level, the confidentiality and integrity of the group state do not depend on those messages being protected in transit ๐Ÿ’…. However, an attacker who can observe those messages in transit will be able to learn about the group state, including potentially the group membership (see Section 16.4.3 below) ๐Ÿ‘€. Such an attacker might also be able to mount denial-of-service attacks on the group, or exclude new members by selectively removing messages in transit ๐Ÿ˜ฌ.

To prevent this form of attack, it is RECOMMENDED that all MLS messages be carried over a secure transport such as TLS [RFC8446] or QUIC [RFC9000] ๐Ÿ”๐Ÿซถ.

16.2. Confidentiality of Group Secrets ๐Ÿคซ

Group secrets are partly derived from the output of a ratchet tree ๐ŸŒณ. Ratchet trees work by assigning each member of the group to a leaf in the tree, and maintaining this property: the private key of a node in the tree is known only to members of the group that are assigned a leaf in that node's subtree. This is called the _tree invariant_, and it makes it possible to encrypt to all group members except one, with a number of ciphertexts that is logarithmic in the number of group members ๐Ÿง โœจ.

The ability to efficiently encrypt to all members except one is what lets members be securely removed from a group ๐Ÿšช. It also lets a member rotate their key pair, so the old private key can no longer be used to decrypt new messages ๐Ÿ”„๐Ÿ’–.

16.3. Confidentiality of Sender Data ๐Ÿ•ถ๏ธ

The PrivateMessage framing encrypts "sender data" that identifies which group member sent an encrypted message, as described in Section 6.3.2 ๐ŸŽญ. Just like the QUIC header protection scheme [RFC9001], Section 5.4, this scheme is a variant of the HN1 construction analyzed in [NAN]. A sample of the ciphertext is combined with a sender_data_secret to derive a key and nonce, which are then used for AEAD encryption of the sender data ๐Ÿง.

(key, nonce) = PRF(sender_data_secret, sample)
encrypted_sender_data =
  AEAD.Seal(key, nonce, sender_data_aad, sender_data)

The only differences between this construction and HN1 as described in [NAN] are that it (1) uses authenticated encryption instead of unauthenticated encryption, and (2) protects information used to derive a nonce instead of the nonce itself ๐Ÿค“.

Since the sender_data_secret is distinct from the content encryption key, the sender data encryption scheme achieves AE2 security as defined in [NAN], and therefore guarantees the confidentiality of the sender data ๐Ÿฅฐ.

โš ๏ธ Bestie, listen up! Using the same sender_data_secret and ciphertext sample more than once risks compromising sender data protection, because it reuses an AEAD (key, nonce) pair. For example, in many AEAD schemes, reusing a key and nonce reveals the exclusive OR of the two plaintexts ๐Ÿ’€. Assuming the ciphertext output of the AEAD algorithm is indistinguishable from random data (i.e., the AEAD is AE1-secure in the phrasing of [NAN]), the odds of two ciphertext samples being identical are roughly 2^(-L/2), i.e., the birthday bound ๐ŸŽ‚.

The AEAD algorithms for the cipher suites defined in this document all provide this property. The size of the sample depends on the cipher suite's hash function, but in all cases, the probability of collision is no more than 2^-128 ๐ŸŒธ. Any future cipher suite MUST use an AE1-secure AEAD algorithm ๐Ÿ“œ.

16.4. Confidentiality of Group Metadata ๐Ÿซฃ

Okay, so MLS does not give confidentiality protection to some messages and fields within messages:

  • KeyPackage messages
  • GroupInfo messages
  • The unencrypted portion of a Welcome message
  • Any Proposal or Commit messages sent as PublicMessage messages
  • The unencrypted header fields in PrivateMessage messages
  • The lengths of encrypted Welcome and PrivateMessage messages

The only mechanism MLS provides for confidentially distributing a group's ratchet tree to new members is to send it in a Welcome message as a ratchet_tree extension ๐ŸŒฒ. If an application distributes the tree some other way, its security will depend on that application mechanism ๐Ÿคทโ€โ™€๏ธ.

A party observing these fields might be able to infer certain properties of the group:

  • Group ID
  • Current epoch and frequency of epoch changes
  • Frequency of messages within an epoch
  • Group extensions
  • Group membership

How much metadata is exposed to parties outside the group, and so how much they can infer about the group's properties, depends on several aspects of the DS design, such as:

  • How KeyPackages are distributed
  • How the ratchet tree is distributed
  • How prospective external joiners get a GroupInfo object for the group
  • Whether Proposal and Commit messages are sent as PublicMessage or PrivateMessage

In the rest of this section, we note the ways the above properties of the group show up in unprotected group messages, as a guide to understanding how they might be exposed or protected in a given application ๐Ÿ”Ž๐Ÿ’œ.

16.4.1. GroupID, Epoch, and Message Frequency โฑ๏ธ

MLS provides no mechanism to protect the group ID and epoch of a message from the DS. So the group ID and the frequency of messages and epoch changes are not protected against inspection by the DS ๐Ÿ‘๏ธ. However, any modifications to these will cause decryption failure ๐Ÿ™…โ€โ™€๏ธ.

16.4.2. Group Extensions ๐Ÿงฉ

A group's extensions are first set by the group's creator, and then updated by GroupContextExtensions proposals. A GroupContextExtensions proposal sent as a PublicMessage leaks the group's extensions ๐Ÿ“ข.

A new member learns the group's extensions via a GroupInfo object. When the new member joins via a Welcome message, the Welcome message's encryption protects the GroupInfo message ๐ŸŽ. When the new member joins via an external join, they must be provided with a GroupInfo object. Protecting this GroupInfo object is up to the application. If it is transmitted over a channel that is not confidential to the group and the new joiner, it will leak the group's extensions ๐Ÿซ .

16.4.3. Group Membership ๐Ÿ‘ฏโ€โ™€๏ธ

The group's membership is represented directly by its ratchet tree, since each member's LeafNode contains the member's cryptographic keys, a credential that holds information about the member's identity, and possibly other identifiers ๐Ÿชช. Applications that expose the group's ratchet tree outside the group also leak the group's membership ๐Ÿšจ.

Changes to the group's membership are made by means of Add and Remove proposals. If these proposals are sent as PublicMessage, information about the corresponding membership changes will leak. A party that sees all of these changes can reconstruct the group membership ๐Ÿงฉโœจ.

Welcome messages contain a hash of each KeyPackage the Welcome message is encrypted for. If a party has access to a pool of KeyPackages and observes a Welcome message, they can identify the KeyPackage representing the new member. If the party can also associate the Welcome with a group, they can infer that the identified new member was added to that group ๐Ÿ•ต๏ธ.

These information leaks only reveal the group's membership to the degree that membership is revealed by the contents of a member's LeafNode in the ratchet tree. In some cases this may be pretty direct, e.g., due to credentials attesting to identifiers such as email addresses ๐Ÿ“ง. An application could construct a member's leaf node to be less identifying, e.g., by using a pseudonymous credential and frequently rotating encryption and signature keys ๐Ÿฆ‹๐Ÿ’ซ.

16.5. Authentication ๐Ÿ”

okay so the first kind of authentication is that group members can check a message came from someone in the group ๐Ÿ’–. For encrypted messages this is locked in, because they're encrypted with an AEAD under a key derived from the group secrets. For plaintext messages, the membership_tag does the job. It's a MAC over the message, under a key derived from the group secrets โœจ.

The second kind is more specific: group members can check that a message came from a particular member ๐ŸŽฏ. That's guaranteed by a digital signature on each message, made with the sender's signature key ๐Ÿซถ.

Okay, the signature keys are honestly the MVP for MLS against active attacks ๐Ÿ˜ญ. If a member's signature key gets compromised, an attacker can make LeafNodes and KeyPackages pretending to be that member. Depending on the app, that can let the attacker join the group as the compromised member ๐Ÿ’€. Like, if the group lets outside parties join via external commits, an attacker with a stolen signature key could use an external Commit to slide right in. Even a "resync"-style external Commit could swap out the compromised member ๐Ÿ™ˆ.

Applications can lower this risk with pre-shared keys (PSKs) ๐Ÿ”‘. If a group requires joiners to know a PSK on top of authenticating with a credential, then an impersonation attack needs both the relevant PSK and the victim's signature key. The catch? The app needs some outside arrangement that makes sure the legit members actually have the required PSKs ๐ŸŒธ.


16.6. Forward Secrecy and Post-Compromise Security ๐Ÿฆ‹

these two are the big deals for long-lived MLS groups ๐Ÿ‘‘. Forward secrecy means messages sent at a certain point stay secure even if a group member gets compromised later. Post-compromise security means messages stay secure even if a member was compromised at some point in the past.

                      Compromise
                          |
                          |
                     |    V    |
   ------------------|---------|------------------------->
                     |         |                     Time
   <-----------------|         |---------------->
     Forward Secrecy |         | Post-Compromise
                     |         |   Security

          Figure 29: Forward Secrecy and Post-Compromise Security

Between epochs, PCS comes from members regularly updating their leaf key in the ratchet tree ๐ŸŒˆ. That stops group secrets from still being encrypted to public keys whose private keys were already compromised. Heads up: sending an Update proposal doesn't get you PCS until another member includes it in a Commit ๐Ÿ‘€. Members can get immediate PCS by sending their own Commit and populating the path field, as described in Section 12.4 ๐Ÿš€. To be clear, in all these cases the PCS guarantees kick in when the members of the group process the relevant Commit, not when the sender creates it ๐Ÿ’….

Forward secrecy between epochs comes from deleting private keys from past versions of the ratchet tree, which stops old group secrets from being re-derived ๐Ÿ”’. Forward secrecy within an epoch comes from deleting message encryption keys once they've been used to encrypt or decrypt a message โœจ. Group secrets and message encryption keys are shared by the group, so there's a risk to forward secrecy for as long as any member hasn't deleted them ๐Ÿ˜ฌ. That's a particular risk if someone's offline for a long time. Applications SHOULD have mechanisms for evicting group members that are offline for too long (i.e., members who haven't changed their key within some period) โณ.

New groups have their own risk too. They can end up using previously compromised keys (like with PCS) if a member gets added via an old KeyPackage whose private key was compromised ๐Ÿฅฒ. The fix is having clients regularly generate new KeyPackages and upload them to the Delivery Service. That way, the key material used to add someone is more likely to be fresh and less likely to be compromised ๐Ÿงš.


16.7. Uniqueness of Ratchet Tree Key Pairs ๐Ÿ’œ

The encryption and signature keys stored in the encryption_key and signature_key fields of ratchet tree nodes MUST be distinct from one another. If two members' leaf nodes have the same signature key, for example, the data origin authentication properties afforded by signatures within the group get degraded ๐Ÿ˜ฉ.

Uniqueness of keys in leaf nodes is assured by explicitly checking each leaf node as it's added to the tree, whether that's in an Add proposal, an Update proposal, or the path field of a Commit ๐Ÿ”. The details live in Sections 7.3, 12.2, and 12.4.2. Uniqueness of encryption keys in parent nodes is assured by checking that the keys in an UpdatePath are not found elsewhere in the tree (see Section 12.4.2) ๐ŸŒŸ.


16.8. KeyPackage Reuse ๐ŸŽŸ๏ธ

KeyPackages are meant to be used only once ๐Ÿ™…โ€โ™€๏ธ. Once a KeyPackage has introduced the corresponding client to a group, it SHOULD be deleted from the KeyPackage publication system. Reusing KeyPackages can lead to replay attacks ๐Ÿ˜ฑ.

An application MAY allow reuse of a "last resort" KeyPackage to prevent denial-of-service attacks ๐Ÿ›Ÿ. Since a KeyPackage is needed to add a client to a new group, an attacker could stop a client from being added to new groups by exhausting all the available KeyPackages ๐Ÿฅบ. To prevent that denial-of-service attack, the KeyPackage publication system SHOULD rate-limit KeyPackage requests, especially if not authenticated ๐Ÿšฆ.


16.9. Delivery Service Compromise ๐Ÿ•ต๏ธโ€โ™€๏ธ

MLS is designed to protect the confidentiality and integrity of group data even in the face of a compromised DS ๐Ÿ›ก๏ธ. But a compromised DS can still pull some stunts. It can't forge messages, but it can selectively delay or remove them ๐Ÿ™ƒ. Sometimes you can spot this by detecting gaps in the per-sender generation counter, though it's not always possible to tell an attack apart from ordinary message loss. The DS can also permanently block messages to and from a group member, and that won't always be detectable by the other members ๐Ÿ˜ถ. If an application uses the DS to resolve conflicts between simultaneous Commits (see Section 14), the DS can also influence which Commit gets applied, even to the point of stopping a member from ever having its Commits applied ๐Ÿ’€.

Put together, these abilities could let a DS team up with an attacker who has compromised a member's state to defeat PCS ๐Ÿ˜ค. The DS suppresses the valid Update and Commit messages from that member, which would otherwise lock the attacker out and update the member's leaf to a new, uncompromised state. Aside from the SenderData.generation value, MLS leaves loss detection up to the application ๐Ÿซ .


16.10. Authentication Service Compromise ๐Ÿšจ

AS compromise is way more serious than DS compromise, bestie ๐Ÿ˜ญ. A compromised AS can assert a binding for a signature key and identity pair of its choice, which allows impersonation of a given user. That's enough for the AS to join new groups as if it were that user. Depending on the application architecture, it may also be enough for the compromised AS to join the group as an existing user, like as if it were a new device associated with the same user ๐Ÿ“ฑ.

If the application uses a transparency mechanism such as CONIKS [CONIKS] or Key Transparency [KT], end users may be able to detect this kind of misbehavior by the AS ๐Ÿ”Ž. It's also possible to construct schemes where a user's various clients vouch for each other, e.g., by signing each other's keys ๐Ÿค.


16.11. Additional Policy Enforcement ๐Ÿ“œ

The DS and AS may also apply additional policies to MLS operations to get additional security properties โœจ. For example, MLS enables any participant to add or remove members of a group, but a DS could enforce a policy that only certain members are allowed to do that ๐Ÿ‘‘. MLS authenticates all members of a group, and a DS could help ensure that only clients with certain types of credentials are admitted ๐Ÿชช. MLS provides no inherent protection against denial of service, so a DS could also enforce rate limits to mitigate those risks ๐Ÿšง.


16.12. Group Fragmentation by Malicious Insiders ๐Ÿช“

It's possible for a malicious member of a group to "fragment" the group by crafting an invalid UpdatePath ๐Ÿ˜ˆ. Recall that an UpdatePath encrypts a sequence of path secrets to different subtrees of the group's ratchet trees. Those path secrets should be derived in a sequence, as described in Section 7.4, but the UpdatePath syntax lets the sender encrypt arbitrary, unrelated secrets. The syntax also doesn't guarantee that the encrypted path secret for a given node corresponds to the public key provided for that node ๐Ÿ™ƒ.

Both of these types of corruption will make processing of a Commit fail for some members of the group. If the public key for a node doesn't match the path secret, the members who decrypt that path secret will reject the Commit based on this mismatch ๐Ÿ™…. If the path secret sequence is incorrect at some point, members who can decrypt nodes before that point will compute a different public key for the mismatched node than the one in the UpdatePath, which also makes the Commit fail ๐Ÿ’”. Applications SHOULD provide mechanisms for failed commits to be reported, so that group members who weren't able to recognize the error themselves can reinitialize the group if necessary ๐Ÿ“ฃ.

Even with an error reporting mechanism in place, though, members can still get locked out of the group by a malformed Commit ๐Ÿ˜ฉ. Malformed Commits can only be recognized by certain members of the group. So in an asynchronous application, it may be the case that all the members who could detect a fault in a Commit are offline. In that case, the Commit will be accepted by the group, and the resulting state may become the basis for further Commits. When the affected members come back online, they'll reject the first Commit and be unable to catch up with the group ๐Ÿฅ€. They'll need to either add themselves back with an external Commit or reinitialize the group from scratch.

Applications can address this risk by requiring certain members of the group to acknowledge successful processing of a Commit before the group regards the Commit as accepted โœ…. The minimum set of acknowledgements needed to verify that a Commit is well-formed is one acknowledgement from one member per node in the UpdatePath. That means one member from each subtree rooted in the copath node corresponding to the node in the UpdatePath ๐Ÿงฉ. MLS does not provide a built-in mechanism for such acknowledgements, but they can be added at the application layer ๐Ÿ’œ.

17. IANA Considerations โœจ

okay so IANA has set up a bunch of registries ๐ŸŽ€ all of them live under the "Messaging Layer Security" group registry heading ๐Ÿ’… and new entries get added through the Specification Required policy [RFC8126]. here's the lineup:

  • MLS Cipher Suites (Section 17.1)
  • MLS Wire Formats (Section 17.2)
  • MLS Extension Types (Section 17.3)
  • MLS Proposal Types (Section 17.4)
  • MLS Credential Types (Section 17.5)
  • MLS Signature Labels (Section 17.6)
  • MLS Public Key Encryption Labels (Section 17.7)
  • MLS Exporter Labels (Section 17.8)

the MLS Designated Experts (DEs) get their own spotlight in Section 17.9 ๐Ÿฆ‹ so check there for the deets on them.

17.1. MLS Cipher Suites ๐Ÿ”

okay bestie, a cipher suite is basically a combo of a protocol version and the set of crypto algorithms you're supposed to use together ๐Ÿ’– it's giving "pick your whole outfit at once" energy.

the name follows this naming convention:

CipherSuite MLS_LVL_KEM_AEAD_HASH_SIG = VALUE;

and VALUE is a 16-bit integer:

uint16 CipherSuite;

each part of the name tells you something different ๐Ÿ‘‘

+===========+==================================+
| Component | Contents                         |
+===========+==================================+
| LVL       | The security level (in bits)     |
+-----------+----------------------------------+
| KEM       | The KEM algorithm used for HPKE  |
|           | in ratchet tree operations       |
+-----------+----------------------------------+
| AEAD      | The AEAD algorithm used for HPKE |
|           | and message protection           |
+-----------+----------------------------------+
| HASH      | The hash algorithm used for HPKE |
|           | and the MLS transcript hash      |
+-----------+----------------------------------+
| SIG       | The signature algorithm used for |
|           | message authentication           |
+-----------+----------------------------------+

Table 5

the registry has four columns, and here's the tea on each one ๐Ÿ“

  • Value: the numeric value of the cipher suite
  • Name: the name of the cipher suite
  • Recommended: whether the IETF recommends supporting this cipher suite. the valid values are "Y", "N", and "D", as explained below. the default value for this column is "N". setting the Recommended item to "Y" or "D", or changing an item that's currently "Y" or "D", requires Standards Action [RFC8126].
    • Y: the IETF has consensus that the item is RECOMMENDED. ๐Ÿ’… this only means the mechanism is fit for the purpose it was defined for. you still need to read the docs carefully to figure out where it applies. the IETF could recommend stuff with limited applicability, but they'll also write applicability statements that spell out any limitations or constraints on its use.
    • N: the item hasn't been evaluated by the IETF, and the IETF hasn't said anything about whether the mechanism is suitable. this doesn't mean it's flawed, just that there's no consensus yet ๐Ÿ™ˆ the IETF might keep something marked "N" because it has limited applicability or usage constraints.
    • D: the item is discouraged and SHOULD NOT or MUST NOT be used ๐Ÿ’€ this marking can flag mechanisms that cause problems if you use them, like a weak crypto algorithm or something that could cause interoperability headaches in deployment.
  • Reference: the document where this cipher suite is defined

here are the initial contents of the registry ๐ŸŒˆ

+========+===================================================+=+====+
| Value  |Name                                               |R|Ref |
+========+===================================================+=+====+
| 0x0000 |RESERVED                                           |-|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0001 |MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519       |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0002 |MLS_128_DHKEMP256_AES128GCM_SHA256_P256            |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0003 |MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519|Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0004 |MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448           |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0005 |MLS_256_DHKEMP521_AES256GCM_SHA512_P521            |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0006 |MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448    |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0007 |MLS_256_DHKEMP384_AES256GCM_SHA384_P384            |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x0A0A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x1A1A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x2A2A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x3A3A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x4A4A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x5A5A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x6A6A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x7A7A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x8A8A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0x9A9A |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xAAAA |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xBABA |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xCACA |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xDADA |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xEAEA |GREASE                                             |Y|RFC |
|        |                                                   | |9420|
+--------+---------------------------------------------------+-+----+
| 0xF000 |Reserved for Private Use                           |-|RFC |
| -      |                                                   | |9420|
| 0xFFFF |                                                   | |    |
+--------+---------------------------------------------------+-+----+

Table 6: MLS Extension Types Registry

all the non-GREASE cipher suites use HMAC [RFC2104] as their MAC function, with a different hash for each suite ๐Ÿฆ‹ here's how the cipher suites map onto the HPKE primitives [RFC9180], the HMAC hash functions, and the TLS signature schemes [RFC8446] ๐Ÿ‘‘

+======+======+========+========+========+========================+
|Value |KEM   | KDF    | AEAD   | Hash   | Signature              |
+======+======+========+========+========+========================+
|0x0001|0x0020| 0x0001 | 0x0001 | SHA256 | ed25519                |
+------+------+--------+--------+--------+------------------------+
|0x0002|0x0010| 0x0001 | 0x0001 | SHA256 | ecdsa_secp256r1_sha256 |
+------+------+--------+--------+--------+------------------------+
|0x0003|0x0020| 0x0001 | 0x0003 | SHA256 | ed25519                |
+------+------+--------+--------+--------+------------------------+
|0x0004|0x0021| 0x0003 | 0x0002 | SHA512 | ed448                  |
+------+------+--------+--------+--------+------------------------+
|0x0005|0x0012| 0x0003 | 0x0002 | SHA512 | ecdsa_secp521r1_sha512 |
+------+------+--------+--------+--------+------------------------+
|0x0006|0x0021| 0x0003 | 0x0003 | SHA512 | ed448                  |
+------+------+--------+--------+--------+------------------------+
|0x0007|0x0011| 0x0002 | 0x0002 | SHA384 | ecdsa_secp384r1_sha384 |
+------+------+--------+--------+--------+------------------------+

Table 7

the transcript hash used for the MLS transcript hash is whichever one is referenced in the cipher suite name ๐Ÿงš for the suites defined above, "SHA256", "SHA384", and "SHA512" mean the SHA-256, SHA-384, and SHA-512 functions defined in [SHS] โœจ

and one last thing, bestie ๐Ÿซถ: in addition to the general requirements of Section 13.1, future cipher suites MUST meet the requirements of Section 16.3. no skipping that!! ๐Ÿ˜ญ

It's advisable to keep the number of cipher suites low, so clients are more likely to interoperate across a federated setup. โœจ That's why the suites only include modern algorithms that are also well-established. ๐ŸŽ€ Clients can pick between two security levels, roughly 128-bit and 256-bit. Within each level, they can choose between the faster X25519/X448 curves or curves that comply with FIPS 140-2 for Diffie-Hellman key negotiation. ๐Ÿฆ‹ Clients can also go with ChaCha20Poly1305 or AES-GCM, for example if they care about performance. ๐Ÿ’… ChaCha20Poly1305 isn't listed by FIPS 140-2, so it doesn't get paired with the FIPS-compliant curves. The security level of the symmetric encryption and hash functions always matches the security level of the curve. ๐ŸŒธ

The mandatory-to-implement cipher suite for MLS 1.0 is MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519. ๐Ÿ‘‘ It uses Curve25519 for key exchange, AES-128-GCM for HPKE, HKDF over SHA2-256, and Ed25519 for signatures. MLS clients MUST implement this cipher suite. ๐Ÿซถ

17.2. MLS Wire Formats

The "MLS Wire Formats" registry is where you'll find the identifiers for the types of messages that can be sent in MLS. ๐Ÿ“ฎ The wire format field is two bytes wide, so the valid wire format values run from 0x0000 to 0xFFFF. ๐Ÿ”ฅ

Template:

  • Value: The numeric value of the wire format
  • Name: The name of the wire format
  • Recommended: Same as in Section 17.1
  • Reference: The document where this wire format is defined

Initial contents:

+=================+==========================+===+==========+
| Value           | Name                     | R | Ref      |
+=================+==========================+===+==========+
| 0x0000          | RESERVED                 | - | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0x0001          | mls_public_message       | Y | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0x0002          | mls_private_message      | Y | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0x0003          | mls_welcome              | Y | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0x0004          | mls_group_info           | Y | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0x0005          | mls_key_package          | Y | RFC 9420 |
+-----------------+--------------------------+---+----------+
| 0xF000 - 0xFFFF | Reserved for Private Use | - | RFC 9420 |
+-----------------+--------------------------+---+----------+

Table 8: MLS Wire Formats Registry

17.3. MLS Extension Types

The "MLS Extension Types" registry is the home base for identifiers of extensions to the MLS protocol. ๐Ÿงš The extension type field is two bytes wide, so valid extension type values fall in the range 0x0000 to 0xFFFF. ๐Ÿ’–

Template:

  • Value: The numeric value of the extension type
  • Name: The name of the extension type
  • Message(s): The messages in which the extension may appear, drawn from the following list:
    • KP: KeyPackage objects
    • LN: LeafNode objects
    • GC: GroupContext objects
    • GI: GroupInfo objects
  • Recommended: Same as in Section 17.1
  • Reference: The document where this extension is defined

Initial contents:

+==========+=======================+============+===+==========+
| Value    | Name                  | Message(s) | R | Ref      |
+==========+=======================+============+===+==========+
| 0x0000   | RESERVED              | N/A        | - | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0001   | application_id        | LN         | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0002   | ratchet_tree          | GI         | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0003   | required_capabilities | GC         | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0004   | external_pub          | GI         | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0005   | external_senders      | GC         | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x0A0A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x1A1A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x2A2A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x3A3A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x4A4A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x5A5A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x6A6A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x7A7A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x8A8A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0x9A9A   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xAAAA   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xBABA   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xCACA   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xDADA   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xEAEA   | GREASE                | KP, GI, LN | Y | RFC 9420 |
+----------+-----------------------+------------+---+----------+
| 0xF000 - | Reserved for Private  | N/A        | - | RFC 9420 |
| 0xFFFF   | Use                   |            |   |          |
+----------+-----------------------+------------+---+----------+

Table 9: MLS Extension Types Registry

17.4. MLS Proposal Types

The "MLS Proposal Types" registry holds identifiers for the kinds of proposals that can be made to change an MLS group. ๐ŸŒˆ The extension type field is two bytes wide here too, so valid extension type values sit in the range 0x0000 to 0xFFFF. ๐Ÿ“

Template:

  • Value: The numeric value of the proposal type
  • Name: The name of the proposal type
  • Recommended: Same as in Section 17.1
  • External: Whether a proposal of this type may be sent by an external sender (see Section 12.1.8)
  • Path Required: Whether a Commit covering a proposal of this type is required to have its path field populated (see Section 12.4)
  • Reference: The document where this extension is defined

Initial contents:

+==========+==========================+===+=====+======+==========+
| Value    | Name                     | R | Ext | Path | Ref      |
+==========+==========================+===+=====+======+==========+
| 0x0000   | RESERVED                 | - | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0001   | add                      | Y | Y   | N    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0002   | update                   | Y | N   | Y    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0003   | remove                   | Y | Y   | Y    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0004   | psk                      | Y | Y   | N    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0005   | reinit                   | Y | Y   | N    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0006   | external_init            | Y | N   | Y    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0007   | group_context_extensions | Y | Y   | Y    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x0A0A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x1A1A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x2A2A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x3A3A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x4A4A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x5A5A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x6A6A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x7A7A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x8A8A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0x9A9A   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xAAAA   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xBABA   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xCACA   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xDADA   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xEAEA   | GREASE                   | Y | -   | -    | RFC 9420 |
+----------+--------------------------+---+-----+------+----------+
| 0xF000 - | Reserved for Private Use | - | -   | -    | RFC 9420 |
| 0xFFFF   |                          |   |     |      |          |
+----------+--------------------------+---+-----+------+----------+

Table 10: MLS Proposal Types Registry

17.5. MLS Credential Types ๐ŸŽ€

okay so the "MLS Credential Types" registry is where we keep the list of credential types that MLS can use for authentication ๐Ÿชชโœจ the credential type field is two bytes wide, so valid values run from 0x0000 all the way up to 0xFFFF. big range, bestie ๐Ÿ˜ญ

Template:

  • Value: the numeric value of the credential type
  • Name: the name of the credential type
  • Recommended: same as in Section 17.1
  • Reference: the document where this credential is defined

Initial contents (it's giving spreadsheet energy ๐Ÿ’…):

    +=================+==========================+===+==========+
    | Value           | Name                     | R | Ref      |
    +=================+==========================+===+==========+
    | 0x0000          | RESERVED                 | - | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x0001          | basic                    | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x0002          | x509                     | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x0A0A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x1A1A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x2A2A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x3A3A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x4A4A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x5A5A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x6A6A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x7A7A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x8A8A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0x9A9A          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xAAAA          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xBABA          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xCACA          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xDADA          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xEAEA          | GREASE                   | Y | RFC 9420 |
    +-----------------+--------------------------+---+----------+
    | 0xF000 - 0xFFFF | Reserved for Private Use | - | RFC 9420 |
    +-----------------+--------------------------+---+----------+

                   Table 11: MLS Credential Types Registry

17.6. MLS Signature Labels โœ๏ธ

so the SignWithLabel function from Section 5.1.2 is there to stop signatures from different contexts getting mixed up ๐Ÿ™…โ€โ™€๏ธ every context gets its own distinct label that gets baked into the signature. the "MLS Signature Labels" registry records the labels defined in this document, and it lets extra labels be registered too, in case extensions add other kinds of signatures using the same signature keys that MLS already uses elsewhere ๐Ÿ’œ

Template:

  • Label: the string to be used as the Label parameter to SignWithLabel
  • Recommended: same as in Section 17.1
  • Reference: the document where this label is defined

Initial contents:

                +====================+===+==========+
                | Label              | R | Ref      |
                +====================+===+==========+
                | "FramedContentTBS" | Y | RFC 9420 |
                +--------------------+---+----------+
                | "LeafNodeTBS"      | Y | RFC 9420 |
                +--------------------+---+----------+
                | "KeyPackageTBS"    | Y | RFC 9420 |
                +--------------------+---+----------+
                | "GroupInfoTBS"     | Y | RFC 9420 |
                +--------------------+---+----------+

                    Table 12: MLS Signature Labels
                               Registry

17.7. MLS Public Key Encryption Labels ๐Ÿ”

same energy as the signature labels, but for encryption ๐Ÿซถ the EncryptWithLabel function from Section 5.1.3 avoids mixing up ciphertexts that were made for different purposes in different contexts. each context gets a distinct label that is incorporated into the signature. the "MLS Public Key Encryption Labels" registry records the labels defined in this document and allows additional labels to be registered in case extensions add other types of public key encryption using the same HPKE keys used elsewhere in MLS ๐Ÿฆ‹

Template:

  • Label: the string to be used as the Label parameter to EncryptWithLabel
  • Recommended: same as in Section 17.1
  • Reference: the document where this label is defined

Initial contents:

                 +==================+===+==========+
                 | Label            | R | Ref      |
                 +==================+===+==========+
                 | "UpdatePathNode" | Y | RFC 9420 |
                 +------------------+---+----------+
                 | "Welcome"        | Y | RFC 9420 |
                 +------------------+---+----------+

                       Table 13: MLS Public Key
                      Encryption Labels Registry

17.8. MLS Exporter Labels ๐ŸŒธ

the exporter function from Section 8.5 lets applications pull key material out of the MLS key schedule ๐Ÿ”‘ just like the TLS exporter [RFC8446], the MLS exporter uses a label to keep different applications' uses of it separate. the "MLS Exporter Labels" registry lets applications register their usage so nobody collides ๐Ÿ™Œ

Template:

  • Label: the string to be used as the Label parameter to MLS-Exporter
  • Recommended: same as in Section 17.1
  • Reference: the document where this label is defined

This registry has no initial contents, since it's meant for applications to use, not the core protocol ๐Ÿ’… the table below only shows the column layout:

                 +=======+=============+===========+
                 | Label | Recommended | Reference |
                 +=======+=============+===========+
                 | (N/A) | (N/A)       | (N/A)     |
                 +-------+-------------+-----------+

                    Table 14: MLS Exporter Labels
                               Registry

17.9. MLS Designated Expert Pool ๐Ÿ‘‘

okay this is the review squad ๐Ÿงš Specification Required [RFC8126] registry requests get registered after a three-week review period on the MLS Designated Expert (DE) mailing list <mailto:mls-reg-review@ietf.org>, based on the advice of one or more of the MLS DEs. BUT, to allow values to be allocated before publication, the MLS DEs may approve registration once they're satisfied that such a specification will be published โœจ

Registration requests sent to the MLS DEs' mailing list for review SHOULD use an appropriate subject (for example, "Request to register value in MLS Bar registry") ๐Ÿ’Œ

Within the review period, the MLS DEs will either approve or deny the registration request, and they'll tell the MLS DEs' mailing list and IANA what they decided. Denials SHOULD include an explanation and, if it applies, suggestions for how to make the request work ๐ŸŒˆ If a request sits undetermined for longer than 21 days, it can be brought to the IESG's attention for resolution using the <mailto:iesg@ietf.org> mailing list ๐Ÿ™ˆ

The criteria that SHOULD be applied by the MLS DEs include checking whether the proposed registration duplicates existing functionality, whether it's likely to be of general applicability or only useful for a single application, and whether the registration description is clear ๐Ÿง For example, for cipher suite registrations, the MLS DEs will apply the advisory found in Section 17.1.

IANA MUST only accept registry updates from the MLS DEs, and IANA SHOULD direct all requests for registration to the MLS DEs' mailing list ๐Ÿšซ

It's suggested that multiple MLS DEs who can represent the perspectives of different applications using this specification be appointed, so the review is broadly informed ๐Ÿซถ And if a registration decision could look like a conflict of interest for a particular MLS DE, that MLS DE SHOULD defer to the judgment of the other MLS DEs ๐Ÿฆ‹

17.10. The "message/mls" Media Type ๐Ÿ’œ

this document registers the "message/mls" media type in the "message" registry so other protocols (for example, HTTP [RFC9113]) can carry MLS messages ๐Ÿ“จ here's the full registration, verbatim:

Type name:  message

Subtype name:  mls

Required parameters:  none

Optional parameters:  version

                      version:  The MLS protocol version expressed as
      a string <major>.<minor>.  If omitted, the version is "1.0",
      which corresponds to MLS ProtocolVersion mls10.  If for some
      reason the version number in the media type parameter differs
      from the ProtocolVersion embedded in the protocol, the protocol
      takes precedence.

Encoding considerations:  MLS messages are represented using the TLS
   presentation language [RFC8446].  Therefore, MLS messages need to
   be treated as binary data.

Security considerations:  MLS is an encrypted messaging layer
   designed to be transmitted over arbitrary lower-layer protocols.
   The security considerations in this document (RFC 9420) also
   apply.

Interoperability considerations:  N/A

Published specification:  RFC 9420

Applications that use this media type:  MLS-based messaging
   applications

Fragment identifier considerations:  N/A

Additional information:

   Deprecated alias names for this type:  N/A
   Magic number(s):  N/A
   File extension(s):  N/A
   Macintosh file type code(s):  N/A

Person & email address to contact for further information:  IETF MLS
   Working Group <mailto:mls@ietf.org>

Intended usage:  COMMON

Restrictions on usage:  N/A

Author:  IETF MLS Working Group

Change controller:  IETF

18. References โœจ

18.1. Normative References

okay so these are the references you need to read if you want to implement this, no cap ๐Ÿ“š๐Ÿ’– the spec leans on them for real, so they're not optional vibes.

[RFC2104]  Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-
           Hashing for Message Authentication", RFC 2104,
           DOI 10.17487/RFC2104, February 1997,
           <https://www.rfc-editor.org/info/rfc2104>.

[RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
           Requirement Levels", BCP 14, RFC 2119,
           DOI 10.17487/RFC2119, March 1997,
           <https://www.rfc-editor.org/info/rfc2119>.

[RFC8126]  Cotton, M., Leiba, B., and T. Narten, "Guidelines for
           Writing an IANA Considerations Section in RFCs", BCP 26,
           RFC 8126, DOI 10.17487/RFC8126, June 2017,
           <https://www.rfc-editor.org/info/rfc8126>.

[RFC8174]  Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
           2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
           May 2017, <https://www.rfc-editor.org/info/rfc8174>.

[RFC8446]  Rescorla, E., "The Transport Layer Security (TLS) Protocol
           Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018,
           <https://www.rfc-editor.org/info/rfc8446>.

[RFC9180]  Barnes, R., Bhargavan, K., Lipp, B., and C. Wood, "Hybrid
           Public Key Encryption", RFC 9180, DOI 10.17487/RFC9180,
           February 2022, <https://www.rfc-editor.org/info/rfc9180>.

18.2. Informative References

these ones are the extra reading, the deep-dive stuff that's super worth a look if you're obsessed with how this all works ๐Ÿ”๐ŸŒธ they're not required to implement it, but they're giving context and I love that for us ๐Ÿซถ

[ART]      Cohn-Gordon, K., Cremers, C., Garratt, L., Millican, J.,
           and K. Milner, "On Ends-to-Ends Encryption: Asynchronous
           Group Messaging with Strong Security Guarantees", Version
           2.3, DOI 10.1145/3243734.3243747, March 2020,
           <https://eprint.iacr.org/2017/666.pdf>.

[CFRG-AEAD-LIMITS]
           Gรผnther, F., Thomson, M., and C. A. Wood, "Usage Limits on
           AEAD Algorithms", Work in Progress, Internet-Draft, draft-
           irtf-cfrg-aead-limits-07, 31 May 2023,
           <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-
           aead-limits-07>.

[CLINIC]   Miller, B., Huang, L., Joseph, A., and J. Tygar, "I Know
           Why You Went to the Clinic: Risks and Realization of HTTPS
           Traffic Analysis", Privacy Enhancing Technologies, pp.
           143-163, DOI 10.1007/978-3-319-08506-7_8, 2014,
           <https://doi.org/10.1007/978-3-319-08506-7_8>.

[CONIKS]   Melara, M. S., Blankstein, A., Bonneau, J., Felten, E. W.,
           and M. J. Freedman, "CONIKS: Bringing Key Transparency to
           End Users", Proceedings of the 24th USENIX Security
           Symposium, ISBN 978-1-939133-11-3, August 2015,
           <https://www.usenix.org/system/files/conference/
           usenixsecurity15/sec15-paper-melara.pdf>.

[DoubleRatchet]
           Cohn-Gordon, K., Cremers, C., Dowling, B., Garratt, L.,
           and D. Stebila, "A Formal Security Analysis of the Signal
           Messaging Protocol", 2017 IEEE European Symposium on
           Security and Privacy (EuroS&P),
           DOI 10.1109/eurosp.2017.27, April 2017,
           <https://doi.org/10.1109/eurosp.2017.27>.

[HCJ16]    Husรกk, M., ฤŒermรกk, M., Jirsรญk, T., and P. ฤŒeleda, "HTTPS
           traffic analysis and client identification using passive
           SSL/TLS fingerprinting", EURASIP Journal on Information
           Security, Vol. 2016, Issue 1,
           DOI 10.1186/s13635-016-0030-7, February 2016,
           <https://doi.org/10.1186/s13635-016-0030-7>.

[KT]       "Key Transparency Design Doc", commit fb0f87f, June 2020,
           <https://github.com/google/keytransparency/blob/master/
           docs/design.md>.

[MLS-ARCH] Beurdouche, B., Rescorla, E., Omara, E., Inguva, S., and
           A. Duric, "The Messaging Layer Security (MLS)
           Architecture", Work in Progress, Internet-Draft, draft-
           ietf-mls-architecture-10, 16 December 2022,
           <https://datatracker.ietf.org/doc/html/draft-ietf-mls-
           architecture-10>.

[NAN]      Bellare, M., Ng, R., and B. Tackmann, "Nonces Are Noticed:
           AEAD Revisited", Advances in Cryptology - CRYPTO 2019, pp.
           235-265, DOI 10.1007/978-3-030-26948-7_9, August 2019,
           <https://doi.org/10.1007/978-3-030-26948-7_9>.

[RFC5116]  McGrew, D., "An Interface and Algorithms for Authenticated
           Encryption", RFC 5116, DOI 10.17487/RFC5116, January 2008,
           <https://www.rfc-editor.org/info/rfc5116>.

[RFC5905]  Mills, D., Martin, J., Ed., Burbank, J., and W. Kasch,
           "Network Time Protocol Version 4: Protocol and Algorithms
           Specification", RFC 5905, DOI 10.17487/RFC5905, June 2010,
           <https://www.rfc-editor.org/info/rfc5905>.

[RFC6125]  Saint-Andre, P. and J. Hodges, "Representation and
           Verification of Domain-Based Application Service Identity
           within Internet Public Key Infrastructure Using X.509
           (PKIX) Certificates in the Context of Transport Layer
           Security (TLS)", RFC 6125, DOI 10.17487/RFC6125, March
           2011, <https://www.rfc-editor.org/info/rfc6125>.

[RFC7696]  Housley, R., "Guidelines for Cryptographic Algorithm
           Agility and Selecting Mandatory-to-Implement Algorithms",
           BCP 201, RFC 7696, DOI 10.17487/RFC7696, November 2015,
           <https://www.rfc-editor.org/info/rfc7696>.

[RFC8032]  Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital
           Signature Algorithm (EdDSA)", RFC 8032,
           DOI 10.17487/RFC8032, January 2017,
           <https://www.rfc-editor.org/info/rfc8032>.

[RFC8701]  Benjamin, D., "Applying Generate Random Extensions And
           Sustain Extensibility (GREASE) to TLS Extensibility",
           RFC 8701, DOI 10.17487/RFC8701, January 2020,
           <https://www.rfc-editor.org/info/rfc8701>.

[RFC9000]  Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based
           Multiplexed and Secure Transport", RFC 9000,
           DOI 10.17487/RFC9000, May 2021,
           <https://www.rfc-editor.org/info/rfc9000>.

[RFC9001]  Thomson, M., Ed. and S. Turner, Ed., "Using TLS to Secure
           QUIC", RFC 9001, DOI 10.17487/RFC9001, May 2021,
           <https://www.rfc-editor.org/info/rfc9001>.

[RFC9113]  Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113,
           DOI 10.17487/RFC9113, June 2022,
           <https://www.rfc-editor.org/info/rfc9113>.

[SHS]      National Institute of Standards and Technology (NIST),
           "Secure Hash Standard (SHS)", FIPS PUB 180-4,
           DOI 10.6028/NIST.FIPS.180-4, August 2015,
           <https://doi.org/10.6028/NIST.FIPS.180-4>.

[Signal]   Perrin(ed), T. and M. Marlinspike, "The Double Ratchet
           Algorithm", Revision 1, November 2016,
           <https://www.signal.org/docs/specifications/
           doubleratchet/>.

Appendix A. Protocol Origins of Example Trees ๐ŸŒˆ

okay this appendix is a little treasure map ๐Ÿ—บ๏ธโœจ MLS operations are what create the specific ratchet tree shapes you see in the examples, and they usually touch a whole direct path at once ๐Ÿ’œ this section walks through which operations could have produced each example tree in the document, so you can literally trace the drama step by step ๐ŸŽ€

To construct the tree in Figure 11:

  • A creates a group with B, ..., G
  • F sends an empty Commit, setting X, Y, and W
  • G removes C and D, blanking V, U, and setting Y and W
  • B sends an empty Commit, setting T and W

To construct the tree in Figure 10:

  • A creates a group with B, ..., H, as well as some members outside this subtree
  • F sends an empty Commit, setting Y and its ancestors
  • D removes B and C, with the following effects:
    • Blank the direct paths of B and C
    • Set X, the top node, and any further nodes in the direct path of D
  • Someone outside this subtree removes G, blanking the direct path of G
  • A adds a new member at B with a partial Commit, adding B as unmerged at X

To construct the tree in Figure 13:

  • A creates a group with B, C, and D
  • B sends a full Commit, setting X and Y
  • D removes C, setting Z and Y
  • B adds a new member at C with a full Commit
    • The Add proposal adds C as unmerged at Z and Y
    • The path in the Commit resets X and Y, clearing Y's unmerged leaves

To construct the tree in Figure 21:

  • A creates a group with B, ..., G
  • A removes F in a full Commit, setting T, U, and W
  • E sends an empty Commit, setting Y and W
  • A adds a new member at F in a partial Commit, adding F as unmerged at Y and W

Appendix B. Evolution of Parent Hashes โœจ

okay bestie, let's watch parent hashes glow up in a small group ๐ŸŒธ we'll follow this sequence of operations:

  1. A initializes a new group
  2. A adds B to the group with a full Commit
  3. B adds C and D to the group with a full Commit
  4. C sends an empty Commit
                         Y                   Y'
                         |                   |
                       .-+-.               .-+-.
  ==>         ==>     /     \     ==>     /     \
         X           X'      _=Z         X'      Z'
        / \         / \     / \         / \     / \
   A   A   B       A   B   C   D       A   B   C   D

Figure 30: Building a Four-Member Tree to Illustrate Parent Hashes

Now the parent hashes on the nodes update like this ๐Ÿ’… (we're using ph for parent hash, th for tree hash, and osth for original sibling tree hash):

  1. A adds B: set X
    • A.parent_hash = ph(X) = H(X, ph="", osth=th(B))
  2. B adds C, D: set B', X', and Y
    • X'.parent_hash = ph(Y) = H(Y, ph="", osth=th(Z)), where th(Z) covers (C, _, D)
    • B'.parent_hash = ph(X') = H(X', ph=X'.parent_hash, osth=th(A))
  3. C sends empty Commit: set C', Z', Y'
    • Z'.parent_hash = ph(Y') = H(Y', ph="", osth=th(X')), where th(X') covers (A, X', B')
    • C'.parent_hash = ph(Z') = H(Z', ph=Z'.parent_hash, osth=th(D))

When a new member joins, they get a tree with these parent hash values and check the validity relationships below ๐Ÿงš

+======+======================================+=====================+
| Node | Parent Hash Value                    | Valid?              |
+======+======================================+=====================+
| A    | H(X, ph="", osth=th(B))              | No, B changed       |
+------+--------------------------------------+---------------------+
| B'   | H(X', ph=X'.parent_hash, osth=th(A)) | Yes                 |
+------+--------------------------------------+---------------------+
| C'   | H(Z', ph=Z'.parent_hash, osth=th(D)) | Yes                 |
+------+--------------------------------------+---------------------+
| D    | (none, never sent an UpdatePath)     | N/A                 |
+------+--------------------------------------+---------------------+
| X'   | H(Y, ph="", osth=th(Z))              | No, Y and Z         |
|      |                                      | changed             |
+------+--------------------------------------+---------------------+
| Z'   | H(Y', ph="", osth=th(X'))            | Yes                 |
+------+--------------------------------------+---------------------+

Table 15

So the joiner ends up with these path-hash links in the tree:

        Y'
        |
        +-.
           \
    X'      Z'
     \     /
  A   B'  C'  D

Figure 31: Parent-hash links connect all non-empty parent nodes to leaves

These chains together cover every non-blank parent node in the tree, so the tree is parent-hash valid ๐ŸŽ€

Here's the tea though: this tree is valid, but it still has invalid parent-hash links ๐Ÿ‘€ If a client checks parent hashes top-down from Y', they'd find that X' has an invalid parent hash relative to Y', but that Z' has a valid one. If the client checks bottom-up instead, the chain from B' ends in an invalid link from X' to Y'. These invalid links are just the natural result of multiple clients having committed ๐Ÿ’œ

Also, look at how the tree hash and the parent hash interact ๐Ÿฆ‹ The parent hash of node C' includes the tree hash of node D. The parent hash of node Z' includes the tree hash of X', which covers nodes A and B' (including the parent hash of B'). Even though the tree hash and the parent hash depend on each other, the dependencies are structured so there's never a circular dependency. iconic, honestly.

In the special case where a new member first receives the tree for a group (for example, in a ratchet tree GroupInfo extension Section 12.4.3.3), the parent hashes are expressed in the tree representation, but the tree hash doesn't have to be. Instead, the new member recomputes the tree hashes for all the nodes in the tree and checks that they match the tree hash in the GroupInfo object. If the tree is valid, the subtree hashes computed this way line up with the inputs needed for parent hash validation, except where recomputation is needed to account for unmerged leaves โœจ


Appendix C. Array-Based Trees ๐ŸŒˆ

One benefit of complete balanced trees is that they admit a simple flat array representation, and it's so clean ๐Ÿ˜ญ In this representation, leaf nodes are even-numbered nodes, with the n-th leaf at 2*n. Intermediate nodes live in the odd-numbered nodes. For example, the tree with 8 leaves looks like this:

                          X
                          |
                .---------+---------.
               /                     \
              X                       X
              |                       |
          .---+---.               .---+---.
         /         \             /         \
        X           X           X           X
       / \         / \         / \         / \
      /   \       /   \       /   \       /   \
     X     X     X     X     X     X     X     X

Node: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14

Leaf: 0     1     2     3     4     5     6     7

Figure 32: An Eight-Member Tree Represented as an Array

This lets us compute relationships between tree nodes just by manipulating indices, instead of maintaining complicated structures in memory ๐Ÿง  The basic rule is that the high-order bits of parent and child node indices have the following relation (where x is an arbitrary bit string):

parent=01x => left=00x, right=10x

Since node relationships are implicit, the algorithms for adding and removing nodes at the right edge of the tree are super simple. If there are N nodes in the array:

  • Add: Append N + 1 blank values to the end of the array.
  • Remove: Truncate the array to its first (N-1) / 2 entries.

The Python code below shows the tree computations needed to use an array-based tree for MLS. Let's go, bestie ๐Ÿซถ

# The exponent of the largest power of 2 less than x. Equivalent to:
#   int(math.floor(math.log(x, 2)))
def log2(x):
    if x == 0:
        return 0

    k = 0
    while (x >> k) > 0:
        k += 1
    return k-1

# The level of a node in the tree. Leaves are level 0, their parents
# are level 1, etc. If a node's children are at different levels,
# then its level is the max level of its children plus one.
def level(x):
    if x & 0x01 == 0:
        return 0

    k = 0
    while ((x >> k) & 0x01) == 1:
        k += 1
    return k

# The number of nodes needed to represent a tree with n leaves.
def node_width(n):
    if n == 0:
        return 0
    else:
        return 2*(n - 1) + 1

# The index of the root node of a tree with n leaves.
def root(n):
    w = node_width(n)
    return (1 << log2(w)) - 1

# The left child of an intermediate node.
def left(x):
    k = level(x)
    if k == 0:
        raise Exception('leaf node has no children')

    return x ^ (0x01 << (k - 1))

# The right child of an intermediate node.
def right(x):
    k = level(x)
    if k == 0:
        raise Exception('leaf node has no children')

    return x ^ (0x03 << (k - 1))

# The parent of a node.
def parent(x, n):
    if x == root(n):
        raise Exception('root node has no parent')

    k = level(x)
    b = (x >> (k + 1)) & 0x01
    return (x | (1 << k)) ^ (b << (k + 1))

# The other child of the node's parent.
def sibling(x, n):
    p = parent(x, n)
    if x < p:
        return right(p)
    else:
        return left(p)

# The direct path of a node, ordered from leaf to root.
def direct_path(x, n):
    r = root(n)
    if x == r:
        return []

    d = []
    while x != r:
        x = parent(x, n)
        d.append(x)
    return d

# The copath of a node, ordered from leaf to root.
def copath(x, n):
    if x == root(n):
        return []

    d = direct_path(x, n)
    d.insert(0, x)
    d.pop()
    return [sibling(y, n) for y in d]

# The common ancestor of two nodes is the lowest node that is in the
# direct paths of both leaves.
def common_ancestor_semantic(x, y, n):
    dx = set([x]) | set(direct_path(x, n))
    dy = set([y]) | set(direct_path(y, n))
    dxy = dx & dy
    if len(dxy) == 0:
        raise Exception('failed to find common ancestor')

    return min(dxy, key=level)

# The common ancestor of two nodes is the lowest node that is in the
# direct paths of both leaves.
def common_ancestor_direct(x, y, _):
    # Handle cases where one is an ancestor of the other
    lx, ly = level(x)+1, level(y)+1
    if (lx <= ly) and (x>>ly == y>>ly):
      return y
    elif (ly <= lx) and (x>>lx == y>>lx):
      return x

    # Handle other cases
    xn, yn = x, y
    k = 0
    while xn != yn:
       xn, yn = xn >> 1, yn >> 1
       k += 1
    return (xn << k) + (1 << (k-1)) - 1

An implementation can choose to store ratchet trees in a "link-based" representation, where each node stores references to its parents and/or children. This is different from the array-based representation above, where those relationships get computed from the relationships between nodes' indices in the array. An implementation like this needs to update those links to keep the tree balanced as it gets extended to add new members or truncated when members are removed ๐ŸŒธ

The code snippet below shows how these algorithms could be implemented in Python:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value    # Value of the node
        self.left = left      # Left child node
        self.right = right    # Right child node

    @staticmethod
    def blank_subtree(depth):
        if depth == 1:
            return Node(None)

        L = Node.blank_subtree(depth-1)
        R = Node.blank_subtree(depth-1)
        return Node(None, left=L, right=R)

    def empty(self):
        L_empty = (self.left == None) or self.left.empty()
        R_empty = (self.right == None) or self.right.empty()
        return (self.value == None) and L_empty and R_empty

class Tree:
    def __init__(self):
        self.depth = 0    # Depth of the tree
        self.root = None  # Root node of the tree, initially empty

    # Add a blank subtree to the right
    def extend(self):
        if self.depth == 0:
            self.depth = 1
            self.root = Node(None)

        L = self.root
        R = Node.blank_subtree(self.depth)
        self.root = Node(None, left=L, right=R)
        self.depth += 1

    # Truncate the right subtree
    def truncate(self):
        if self.root == None:
            return

        if not self.root.right.empty():
            raise Exception("Cannot truncate non-blank subtree")

        self.depth -= 1
        self.root = self.root.left

Contributors ๐ŸŒธ

okay bestie, these are the amazing humans who helped cook this spec and deserve all the flowers ๐Ÿ’๐Ÿซถ

  • Joel Alwen, Amazon, Email: alwenjo@amazon.com
  • Karthikeyan Bhargavan, Inria, Email: karthikeyan.bhargavan@inria.fr
  • Cas Cremers, CISPA, Email: cremers@cispa.de
  • Alan Duric, Wire, Email: alan@wire.com
  • Britta Hale, Naval Postgraduate School, Email: britta.hale@nps.edu
  • Srinivas Inguva, Email: singuva@yahoo.com
  • Konrad Kohbrok, Phoenix R&D, Email: konrad.kohbrok@datashrine.de
  • Albert Kwon, MIT, Email: kwonal@mit.edu
  • Tom Leavy, Amazon, Email: tomleavy@amazon.com
  • Brendan McMillion, Email: brendanmcmillion@gmail.com
  • Marta Mularczyk, Amazon, Email: mulmarta@amazon.com
  • Eric Rescorla, Mozilla, Email: ekr@rtfm.com
  • Michael Rosenberg, Trail of Bits, Email: michael.rosenberg@trailofbits.com
  • Thรฉophile Wallez, Inria, Email: theophile.wallez@inria.fr
  • Thyla van der Merwe, Royal Holloway, University of London, Email: tjvdmerwe@gmail.com

Authors' Addresses ๐Ÿฆ‹

and these are the authors, the main characters who brought the whole thing to life ๐Ÿ‘‘โœจ

  • Richard Barnes, Cisco, Email: rlb@ipv.sx
  • Benjamin Beurdouche, Inria & Mozilla, Email: ietf@beurdouche.com
  • Raphael Robert, Phoenix R&D, Email: ietf@raphaelrobert.com
  • Jon Millican, Meta Platforms, Email: jmillican@meta.com
  • Emad Omara, Email: emad.omara@gmail.com
  • Katriel Cohn-Gordon, University of Oxford, Email: me@katriel.co.uk

that's the whole spec, babe, we did it ๐ŸŒˆ๐Ÿ’–

๐Ÿ“ฆ where this page comes from