Files
quicproquo/docs/src/design-rationale/adr-005-single-use-keypackages.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

7.5 KiB

ADR-005: Single-Use KeyPackages

Status: Accepted


Context

MLS (RFC 9420) specifies that KeyPackages must be used at most once. A KeyPackage contains the client's HPKE init key, which is used during the add_members() operation to encrypt the Welcome message. If the same KeyPackage is used twice, the same HPKE shared secret is derived for both group additions, which destroys the forward secrecy of the initial key exchange.

The Authentication Service (AS) stores KeyPackages uploaded by clients and serves them to peers who want to add the client to a group. The design question is: how should the AS enforce single-use semantics?

Alternatives considered

  1. Mark-as-used. The AS could keep a "used" flag on each KeyPackage and reject subsequent fetch requests for packages already marked as used. This preserves the package on the server (for auditing or retry) but requires additional state tracking and introduces a race condition: if two peers fetch the same package concurrently, one of them will receive a "used" package unless the flag is set atomically with the first fetch.

  2. Reference counting. The AS could allow a KeyPackage to be fetched a configurable number of times. This would support use cases like "allow the same package to be used in N group additions." However, MLS requires strict single-use, making this approach non-compliant.

  3. Atomic removal on fetch. The AS removes the KeyPackage from storage in the same operation that returns it. The first fetch succeeds and returns the package; subsequent fetches for the same package find nothing. This is the simplest approach and provides the strongest guarantee.


Decision

The Authentication Service atomically removes a KeyPackage when it is fetched. The fetchKeyPackage method is destructive: it returns the package and deletes it in a single operation. If no packages are stored for the requested identity, an empty response is returned.

Implementation

The server stores KeyPackages in a per-identity queue (currently backed by DashMap with Vec<Vec<u8>> values). The fetchKeyPackage operation:

  1. Locks the entry for the requested identity key.
  2. Pops the first KeyPackage from the queue (FIFO order).
  3. Returns the popped package.
  4. The lock is released.

If the queue is empty (or no entry exists for the identity key), the method returns empty Data.

Before fetch:
  identity_key_0x1234 -> [KP_1, KP_2, KP_3]

After fetchKeyPackage(identity_key=0x1234):
  Returns: KP_1
  identity_key_0x1234 -> [KP_2, KP_3]

Client responsibilities

Because the AS consumes KeyPackages on fetch, clients must manage their KeyPackage supply:

  1. Pre-upload multiple KeyPackages. After generating their identity, a client should upload several KeyPackages (e.g., 10-100) so that multiple peers can add them to groups concurrently.

  2. Monitor supply. Clients should periodically check (via a future monitoring endpoint or heuristic) whether their KeyPackage supply on the server is running low, and replenish by uploading more.

  3. Handle empty responses. A client trying to add a peer whose KeyPackage supply is exhausted will receive an empty response from fetchKeyPackage. The client should handle this gracefully -- e.g., by notifying the user that the peer needs to upload more KeyPackages.

Fingerprint for tamper detection

The uploadKeyPackage method returns a SHA-256 fingerprint of the uploaded package. This fingerprint serves as a tamper-detection mechanism:

  1. The uploading client records the fingerprint.
  2. When a peer fetches the KeyPackage, they can compute the SHA-256 hash of the received package.
  3. If the fetched package's hash does not match the expected fingerprint (communicated out-of-band), the server may have tampered with the package.

This is a defense-in-depth measure. In practice, MLS's own signature verification on KeyPackages also detects tampering, since the KeyPackage includes a signature over its contents using the uploader's Ed25519 identity key.


Consequences

Benefits

  • Forward secrecy of initial key exchange. Each add_members() operation uses a fresh HPKE init key, so the shared secret derived from the Welcome message is unique. Compromising one group addition does not compromise others.

  • Simplicity. Atomic removal is the simplest possible implementation of single-use semantics. There is no "used" flag, no reference count, no expiration timer. The package is either in the store (available) or not (consumed).

  • No race conditions. Because removal is atomic with fetch, two concurrent fetches for the same identity key will each receive a different KeyPackage (or one will receive an empty response if only one package remains). There is no window where two fetchers could receive the same package.

  • Compliance with RFC 9420. The single-use semantics are a direct implementation of MLS's requirement that each KeyPackage's HPKE init key be used at most once.

Costs and trade-offs

  • Client must manage supply. Unlike a reusable credential, single-use KeyPackages are a consumable resource. Clients must proactively upload packages and monitor their supply. A client that goes offline for an extended period may exhaust its supply, becoming unreachable for new group additions.

  • No retry after fetch. If a client fetches a KeyPackage and then fails to complete the add_members() operation (e.g., due to a crash or network error), the KeyPackage is consumed and wasted. The client must fetch a new one and retry.

  • Storage scaling. If each client uploads N KeyPackages and there are M clients, the AS must store up to M * N packages. For reasonable values (e.g., 1000 clients, 100 packages each), this is 100,000 packages -- well within the capacity of an in-memory store. For larger deployments, persistent storage would be needed.

Residual risks

  • KeyPackage exhaustion attack. A malicious client could repeatedly fetch a target's KeyPackages without using them, draining the target's supply and preventing legitimate peers from adding the target to groups. Mitigation: rate limiting on fetchKeyPackage calls (planned for a future milestone) and the Auth struct for identifying and blocking abusive clients.

  • Server-side compromise. If the AS is compromised, the attacker could read stored KeyPackages and use the HPKE init keys to decrypt future Welcome messages. Mitigation: this is inherent to any prekey distribution service (Signal has the same risk with X3DH prekey bundles). MLS's post-compromise security means that even if the initial key exchange is compromised, subsequent epoch updates restore security.


Code references

File Relevance
schemas/auth.capnp AuthenticationService interface: uploadKeyPackage, fetchKeyPackage
schemas/node.capnp NodeService interface: same methods with Auth parameter
crates/quicprochat-server/src/storage.rs Server-side KeyPackage storage (DashMap-backed queue)
crates/quicprochat-server/src/main.rs RPC handler: fetchKeyPackage implementation with atomic removal

Further reading