Files
quicproquo/docs/src/cryptography/post-compromise-security.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc
across documentation, Docker configuration, CI workflows, packaging
scripts, operational configs, and build tooling.

- Docker: crate paths, binary names, user/group, data dirs, env vars
- CI: workflow crate references, binary names, artifact names
- Docs: all markdown files under docs/, SDK READMEs, book.toml
- Packaging: OpenWrt Makefile, init script, UCI config (file renames)
- Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team
- Operations: Prometheus config, alert rules, Grafana dashboard
- Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths
- Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
2026-03-21 19:14:06 +01:00

9.5 KiB

Post-Compromise Security

Post-compromise security (PCS) is a property of a cryptographic protocol that guarantees: after an attacker compromises a participant's state, the protocol automatically heals so that future messages are protected. The attacker's window of access is limited to the current epoch; once the compromised member (or any other member) issues an Update or Commit, the group state is re-randomized and the attacker is locked out.

PCS is the complement of forward secrecy:

  • Forward secrecy protects the past from a future compromise.
  • Post-compromise security protects the future from a past compromise.

MLS (RFC 9420) is specifically designed to provide both properties simultaneously for group messaging. This is a key differentiator of quicprochat's design.

How MLS Provides PCS

The Ratchet Tree

At the heart of MLS's PCS mechanism is the ratchet tree, a binary tree where:

  • Each leaf represents a group member and contains their public key material.
  • Each internal node holds derived key material computed from its children.
  • The root of the tree determines the epoch's group key material.
             [Root]
            /      \
         [A,B]    [C,D]
         /   \    /   \
       [A]  [B] [C]  [D]

When a member updates their leaf (by generating fresh key material and issuing an Update proposal or Commit), the change propagates up the tree path from the leaf to the root:

             [Root]*        <- re-derived
            /      \
         [A,B]*   [C,D]    <- re-derived
         /   \    /   \
       [A]*  [B] [C]  [D]  <- A updated leaf

Every node marked with * receives new key material. This means the new epoch's group secrets depend on A's freshly generated randomness. An attacker who previously compromised A's state must now also compromise the new key material -- which they cannot do because it was generated after the compromise was detected (or automatically healed by the protocol).

Cost

The path from a leaf to the root has length O(log n) for a group of n members. This means:

  • An Update/Commit produces O(log n) encrypted path secrets.
  • Each group member processes the Update in O(log n) time.
  • This is dramatically more efficient than pairwise rekeying (O(n)) or broadcast encryption.

For a group of 1,000 members, the path length is approximately 10 nodes -- making PCS practical even for large groups.

Epoch Advancement in quicprochat

In the current implementation, epoch advancement occurs through the GroupMember methods in group.rs:

Adding a Member (Commit)

When add_member() is called, openmls creates a Commit that adds the new member and updates the ratchet tree:

// Alice adds Bob -- this creates a Commit + Welcome
let (commit_bytes, welcome_bytes) = alice.add_member(&bob_kp)?;

// Alice merges the pending Commit, advancing her epoch
// Internally, openmls re-derives the ratchet tree with Bob's leaf
group.merge_pending_commit(&self.backend)?;

After merge_pending_commit(), Alice's group is in a new epoch with fresh key material. Any attacker who had compromised Alice's state before this Commit must now also compromise the new epoch's keys.

Receiving a Commit

When a member receives and processes a Commit from another member:

ProcessedMessageContent::StagedCommitMessage(staged) => {
    // Merge the Commit into local state -- epoch advances
    group.merge_staged_commit(&self.backend, *staged)?;
    Ok(None)
}

This advances the receiver's epoch, incorporating the committer's fresh key material into the ratchet tree.

Future: Periodic Updates

The current implementation only advances epochs when members are added. A more robust PCS strategy involves periodic Update proposals, where members re-randomize their leaf key material on a regular schedule (e.g., every hour, or every N messages). This is planned for future milestones and will look like:

1. Member generates fresh leaf key material
2. Member creates an Update proposal
3. Any member (or the updater) creates a Commit including the Update
4. All members process the Commit and advance to the new epoch
5. The attacker's compromised state is now stale

PCS vs Forward Secrecy

These two properties are often confused but protect against different attack scenarios:

Property Protects Mechanism Threat
Forward Secrecy Past messages Delete old epoch keys Attacker compromises state now, tries to read past
Post-Compromise Security Future messages Re-randomize ratchet tree Attacker compromised state before, tries to read future

Together, they provide a strong guarantee: the attacker's window of access is limited to the current epoch only. Past epochs are protected by FS (old keys deleted), and future epochs are protected by PCS (new key material generated).

     Past epochs          Current epoch         Future epochs
  +-----------------+  +-----------------+  +-----------------+
  |  Protected by   |  |  Attacker has   |  |  Protected by   |
  |  Forward        |  |  access if they |  |  Post-Compromise|
  |  Secrecy        |  |  hold current   |  |  Security       |
  |  (keys deleted) |  |  epoch keys     |  |  (tree updated) |
  +-----------------+  +-----------------+  +-----------------+

Comparison with Signal Groups

Signal's group messaging uses Sender Keys, a fundamentally different mechanism from MLS's ratchet tree. The comparison is instructive because it highlights why MLS was chosen for quicprochat:

Signal Sender Keys

In Signal's group protocol:

  1. Each member generates a Sender Key -- a symmetric key used to encrypt all messages they send to the group.
  2. The Sender Key is distributed to all group members via pairwise Signal sessions.
  3. Each message from a sender is encrypted with their Sender Key.
  4. The Sender Key includes a symmetric ratchet (hash ratchet) that advances per message, providing forward secrecy within a sender's chain.

The critical limitation: Sender Keys do not provide post-compromise security. If an attacker compromises a member's Sender Key:

  • The attacker can derive all future message keys from that sender (the hash ratchet is one-way, but the attacker has the current state).
  • The key is only rotated when the member manually refreshes it or when group membership changes.
  • There is no automatic healing mechanism analogous to MLS's ratchet tree.

MLS Ratchet Tree (quicprochat)

In contrast, MLS's ratchet tree provides PCS because:

  1. Any member can issue an Update that re-randomizes their leaf.
  2. The Commit propagates new key material up the tree, affecting the group secret.
  3. The attacker, who holds old state, cannot predict the new randomness.
  4. The group automatically heals after at most one epoch advance.
Property Signal Sender Keys MLS Ratchet Tree
Forward secrecy (group) Per-sender hash ratchet Per-epoch (Commit)
Post-compromise security No -- compromised Sender Key reveals all future messages from that sender Yes -- any Commit/Update heals the tree
Key rotation Manual or on membership change Any Commit (add/remove/update)
Healing time Until manual rotation Next epoch (automatic)
Cost per update O(n) pairwise re-encryption O(log n) tree path

Practical Implications

What happens during a compromise?

Suppose an attacker compromises Member A's MLS state (including the current epoch keys):

  1. Current epoch: The attacker can decrypt all messages in the current epoch from all members (because epoch keys are shared group secrets).

  2. Past epochs: Protected by forward secrecy. Old epoch keys have been deleted by openmls.

  3. After the next Commit: Any member (including A, after recovering) can issue a Commit. The ratchet tree is updated with fresh key material. The attacker's stale state cannot derive the new epoch keys. The attacker is locked out.

How quickly does healing occur?

In the current implementation, healing occurs whenever:

  • A new member is added (add_member() issues a Commit).
  • A member is removed (not yet implemented, but will issue a Commit).
  • In the future: periodic Update proposals are issued on a timer.

The practical healing window is the time between Commits. For active groups with frequent membership changes, this window is small. For static groups, periodic Updates (planned) will bound the healing window.

Server compromise does not prevent PCS

The quicprochat server is MLS-unaware -- it stores and forwards encrypted MLS messages without access to the group state. A compromised server cannot:

  • Prevent PCS by blocking Commits (it could perform denial-of-service, but cannot selectively suppress Update proposals without being detected, because MLS epoch numbers must be sequential).
  • Inject fraudulent Commits (it lacks the signing key of any group member).

See Threat Model for the full analysis of a compromised server.