Skip to content

Installations publish a single one-time key package, so a member can only join one conversation #169

Description

@osmaczko

Problem

An installation publishes exactly one MLS key package, and the first conversation that adds the member consumes it. Every conversation after that fails to admit them, with welcome not addressed to this member.

The chain:

  • InboxV2::register publishes one key package, once, at Core bring-up (Core::register_keypackage). No pool, no replenishment.
  • The registry keeps it in a single slot per device_id, and retrieve is a non-consuming peek (EphemeralRegistry::retrieve clones; the HTTP registry is GET /v0/keypackage/{device_id}, no pop). Every peer who adds the member fetches the same package.
  • An openmls key package's init key is one-time-use: keys_for_welcome deletes it after the first Welcome consumes it, unless the package is last-resort. A later Welcome to that same, now-deleted package hits NoMatchingKeyPackage, which de-mls returns as Ok(None) and we surface as welcome not addressed to this member.

Reproduces cleanly: DM a peer (DirectV1), then invite the same peer to a group (GroupV2). The DM consumes their key package; the group Welcome fails. Two GroupV2 invites to the same peer fail the same way.

Current mitigation

InboxV2::create_keypackage marks the key package last-resort, so openmls retains the init key and one package admits the installation to any number of conversations. This unblocks group chat, but it reuses one init key for every join, so the Welcome and initial-epoch secrets of all conversations derive from a single key: no per-join forward secrecy. A TODO at the register site tracks the follow-up. It's a testnet-acceptable stopgap, not the end state.

Proposed fix: a one-time key-package pool with last-resort fallback

The standard MLS shape:

  1. libchat publishes a pool of N one-time key packages and replenishes it when it runs low, keeping one last-resort package as the exhaustion backstop.
  2. The keypackage-registry service makes retrieve pop (claim-and-remove) a one-time package per fetch, handing out the last-resort package only when the pool is empty.

The two halves are coupled: publishing many packages does nothing while retrieve is a peek, so the pop/claim change in the registry service is load-bearing (and lives outside this repo).

Result: a fresh init key per Welcome restores per-join forward secrecy, and the last-resort fallback keeps adds from failing once a member's pool is drained.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions