# KeyPackage Exchange Flow

MLS KeyPackages are single-use tokens that enable a group creator to add a new
member. The KeyPackage contains the member's HPKE init public key, their MLS
credential (Ed25519 public key), and a signature proving ownership. The
quicproquo Authentication Service (AS) provides a simple upload/fetch
interface for distributing KeyPackages between clients.

**Expiry and refresh:** KeyPackages are consumed on fetch (single-use). The server may also enforce a TTL (e.g. 24h). Clients should upload a fresh KeyPackage periodically or on demand so they remain invitable. The CLI provides `refresh-keypackage`: load existing state, generate a new KeyPackage, upload to the AS. See [Running the Client](../getting-started/running-the-client.md#refresh-keypackage).

This page describes the end-to-end flow: from client-side generation through
server-side storage to peer-side retrieval and consumption.

**Sources:**
- `crates/quicproquo-core/src/group.rs` (client-side generation)
- `crates/quicproquo-server/src/main.rs` (server-side handlers)
- `crates/quicproquo-server/src/storage.rs` (server-side persistence)
- `crates/quicproquo-client/src/lib.rs` (client-side RPC calls)
- `schemas/node.capnp` (wire schema)

---

## Upload Flow

The upload flow moves a freshly generated KeyPackage from a client to the
server, where it is stored for later retrieval by a peer.

```text
Client                                      Server (AS)
  |                                            |
  |  1. GroupMember::generate_key_package()    |
  |     -> TLS-encoded KeyPackage bytes        |
  |     -> HPKE init key stored in backend     |
  |                                            |
  |  2. uploadKeyPackage RPC                   |
  |     identityKey = Ed25519 pub key (32 B)   |
  |     package     = TLS-encoded bytes        |
  |     auth        = Auth struct              |
  |  ----------------------------------------> |
  |                                            |  3. Validate inputs:
  |                                            |     - identityKey == 32 bytes
  |                                            |     - package non-empty
  |                                            |     - package < 1 MB
  |                                            |     - auth version valid
  |                                            |
  |                                            |  4. Compute SHA-256(package)
  |                                            |
  |                                            |  5. Store: push_back to
  |                                            |     DashMap<Vec<u8>, VecDeque<Vec<u8>>>
  |                                            |     keyed by identity_key
  |                                            |
  |  6. Response: fingerprint (SHA-256 hash)   |
  |  <---------------------------------------- |
  |                                            |
  |  7. Verify: local SHA-256 == server SHA-256|
  |                                            |
```

### Step-by-Step

1. **Client generates KeyPackage.** The client calls
   `GroupMember::generate_key_package()`, which internally:
   - Builds an MLS `CredentialWithKey` from the Ed25519 public key
     (`CredentialType::Basic`).
   - Calls `KeyPackage::builder().build()` with the ciphersuite
     `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`, the `StoreCrypto` backend,
     and the `IdentityKeypair` as the signer.
   - openmls generates an ephemeral HPKE key pair (X25519) and stores the
     private key in the backend's `DiskKeyStore`.
   - Returns the TLS-serialized KeyPackage bytes.

   See [GroupMember Lifecycle](group-member-lifecycle.md) for the critical
   invariant about backend identity.

2. **Client sends `uploadKeyPackage` RPC.** The request includes:
   - `identityKey`: The raw 32-byte Ed25519 public key.
   - `package`: The TLS-encoded KeyPackage bytes.
   - `auth`: An [Auth struct](../wire-format/auth-schema.md) with version and
     optional access token.

3. **Server validates inputs.** The server checks:
   - `identityKey` is exactly 32 bytes (Ed25519 public key size).
   - `package` is non-empty.
   - `package` does not exceed `MAX_KEYPACKAGE_BYTES` (1 MB).
   - The `Auth` struct version is acceptable (0 for legacy, 1 for token-based).

4. **Server computes fingerprint.** `SHA-256(package_bytes)` produces a 32-byte
   digest used as a tamper-detection fingerprint.

5. **Server stores the KeyPackage.** The package bytes are pushed to the back of
   a `VecDeque<Vec<u8>>` keyed by the identity key in the server's
   `FileBackedStore`. This allows multiple KeyPackages per identity (clients
   should upload several to handle concurrent invitations). The store flushes to
   disk after every mutation.

6. **Server returns the fingerprint.** The SHA-256 digest is sent back in the
   response's `fingerprint` field.

7. **Client verifies the fingerprint.** The client computes its own
   `SHA-256(package_bytes)` and compares it to the server-returned value. A
   mismatch indicates tampering (the server or a MITM modified the package in
   transit) and the client aborts with a `fingerprint mismatch` error.

---

## Fetch Flow

The fetch flow allows a peer to retrieve a stored KeyPackage for a target
identity, consuming it in the process (single-use per RFC 9420).

```text
Peer                                        Server (AS)
  |                                            |
  |  1. fetchKeyPackage RPC                    |
  |     identityKey = target's Ed25519 pub key |
  |     auth        = Auth struct              |
  |  ----------------------------------------> |
  |                                            |  2. Validate inputs:
  |                                            |     - identityKey == 32 bytes
  |                                            |     - auth version valid
  |                                            |
  |                                            |  3. Pop front of VecDeque
  |                                            |     (FIFO, single-use)
  |                                            |     Flush updated map to disk
  |                                            |
  |  4. Response: package bytes (or empty)     |
  |  <---------------------------------------- |
  |                                            |
  |  5. If non-empty:                          |
  |     KeyPackageIn::tls_deserialize()        |
  |     .validate(crypto, MLS10)               |
  |     -> trusted KeyPackage for add_member() |
  |                                            |
```

### Step-by-Step

1. **Peer sends `fetchKeyPackage` RPC.** The request includes the target's
   Ed25519 public key (32 bytes) and an Auth context.

2. **Server validates inputs.** Same identity key length check as upload (32
   bytes).

3. **Server pops from the front of the queue.** `VecDeque::pop_front()` returns
   the oldest uploaded KeyPackage. This enforces FIFO ordering and **single-use
   semantics**: once fetched, the KeyPackage is permanently removed from the
   server. This is a hard requirement of the MLS specification -- reusing a
   KeyPackage would allow an attacker to create conflicting group states.

   The store is flushed to disk after the pop, ensuring the removal survives
   server restarts.

4. **Server returns the package bytes.** If the queue was empty (no KeyPackages
   available), the response contains an empty `Data` field. The client checks
   for emptiness to distinguish "no packages available" from "package fetched."

5. **Peer deserializes and validates.** The peer uses `KeyPackageIn::tls_deserialize()`
   followed by `.validate(crypto, ProtocolVersion::Mls10)` to verify the
   KeyPackage signature. The validated `KeyPackage` can then be passed to
   `GroupMember::add_member()`.

---

## Fingerprint Verification

The fingerprint mechanism provides a simple tamper-detection check:

```text
Client                  Server                  Client
SHA-256(pkg) ---------> store pkg -----------> SHA-256(pkg)
     |                  SHA-256(pkg) -------->    |
     |                       |                    |
     +---- compare: local_fp == server_fp --------+
```

**What it detects:**
- A malicious server replacing the package bytes.
- A network-layer MITM modifying the package in transit (though QUIC/TLS
  already prevents this).

**What it does NOT detect:**
- A malicious server that simply returns the correct fingerprint for a package
  it has replaced (since the server computes the hash itself). True
  KeyPackage authenticity requires verifying the Ed25519 signature inside
  the KeyPackage, which openmls does during `validate()`.

The fingerprint is best understood as a transport-level integrity check, not a
cryptographic proof of authenticity. The real authenticity guarantee comes from
the MLS KeyPackage signature verified on the receiving side.

---

## Storage Model

On the server, KeyPackages are stored in a `FileBackedStore`:

```text
FileBackedStore
  +-- key_packages: Mutex<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>
  |                        ^                ^
  |                        |                |
  |                   identity_key     queue of TLS-encoded
  |                   (32 bytes)       KeyPackage bytes
  |
  +-- Persisted to: data/keypackages.bin (bincode serialized)
```

Each identity key maps to a FIFO queue of KeyPackage bytes. A client should
upload multiple KeyPackages so that peers can concurrently fetch them without
contention. If the queue is exhausted, fetches return empty until the client
uploads more.

The storage format uses the `QueueMapV1` wrapper for bincode serialization:

```rust
#[derive(Serialize, Deserialize, Default)]
struct QueueMapV1 {
    map: HashMap<Vec<u8>, VecDeque<Vec<u8>>>,
}
```

See [Storage Backend](storage-backend.md) for details on persistence,
flush-on-write semantics, and the V1/V2 delivery map migration.

---

## Input Validation Summary

| Field          | Constraint                | Error on Violation |
|----------------|---------------------------|--------------------|
| `identityKey`  | Exactly 32 bytes          | `"identityKey must be exactly 32 bytes, got {n}"` |
| `package`      | Non-empty                 | `"package must not be empty"` |
| `package`      | At most 1 MB (1,048,576)  | `"package exceeds max size (1048576 bytes)"` |
| `auth.version` | 0 (legacy) or 1 (current) | `"unsupported auth version {v}"` |
| `auth.token`   | Non-empty when version=1  | `"auth.version=1 requires non-empty accessToken"` |

---

## Wire Schema

From `schemas/node.capnp`:

```capnp
uploadKeyPackage @0 (identityKey :Data, package :Data, auth :Auth)
    -> (fingerprint :Data);

fetchKeyPackage  @1 (identityKey :Data, auth :Auth)
    -> (package :Data);
```

The `Auth` struct is shared across all RPC methods:

```capnp
struct Auth {
  version     @0 :UInt16;  # 0 = legacy/none, 1 = token-based
  accessToken @1 :Data;    # opaque bearer token
  deviceId    @2 :Data;    # optional UUID for auditing
}
```

See [NodeService Schema](../wire-format/node-service-schema.md) for the
complete schema reference.

---

## Client-Side Usage

The CLI exposes two commands for KeyPackage exchange:

### `register` / `register-state`

Generates a fresh KeyPackage and uploads it. `register` uses an ephemeral
identity; `register-state` loads from (or initializes) a persistent state file.

```bash
# Ephemeral registration (for testing)
qpq register --server 127.0.0.1:7000

# Persistent registration (production)
qpq register-state --state alice.bin --server 127.0.0.1:7000
```

Output:
```
identity_key : 7a3f...  (64 hex chars, 32 bytes)
fingerprint  : 9e1c...  (SHA-256 of KeyPackage)
KeyPackage uploaded successfully.
```

### `fetch-key`

Fetches a peer's KeyPackage by their hex-encoded Ed25519 public key:

```bash
qpq fetch-key --server 127.0.0.1:7000 7a3f...
```

---

## Security Considerations

1. **Single-use enforcement.** The server's `pop_front()` semantics ensure each
   KeyPackage is consumed exactly once, satisfying RFC 9420's requirement.
   However, a malicious server could duplicate KeyPackages before deletion. True
   single-use is enforced at the MLS protocol level: duplicate KeyPackage usage
   would be detected when processing the Welcome (mismatched group state).

2. **No authentication on fetch.** Currently, anyone can fetch any identity's
   KeyPackage. This is intentional for the MVP but means an attacker could
   exhaust a victim's KeyPackage supply. The
   [Auth, Devices, and Tokens](../roadmap/authz-plan.md) plan addresses this
   with token-based access control.

3. **HPKE init key lifetime.** The HPKE init private key lives in the
   `DiskKeyStore` from generation until the Welcome is processed. For persistent
   clients using `DiskKeyStore::persistent()`, this key survives process
   restarts. For ephemeral clients, the key exists only in memory and is lost if
   the process exits before `join_group()` is called.

---

## Related Pages

- [GroupMember Lifecycle](group-member-lifecycle.md) -- the MLS state machine that generates and consumes KeyPackages
- [Authentication Service Internals](authentication-service.md) -- server-side KeyPackage handling
- [Delivery Service Internals](delivery-service.md) -- how the Welcome message is relayed after `add_member()`
- [Storage Backend](storage-backend.md) -- `FileBackedStore` persistence model
- [NodeService Schema](../wire-format/node-service-schema.md) -- Cap'n Proto schema reference
