Files
quicproquo/docs/src/cryptography/key-lifecycle.md
Chris Nennemann 853ca4fec0 chore: rename project quicnprotochat -> quicproquo (binaries: qpq)
Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
  quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
  *.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated

HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:11:51 +01:00

14 KiB

Key Lifecycle and Zeroization

quicproquo uses multiple key types with different lifetimes, creation patterns, and destruction guarantees. This page provides a comprehensive lifecycle diagram for every key type in the system, from generation through zeroization.

Lifecycle Overview

Key Type              Creation            Distribution         Use                    Destruction
--------------------------------------------------------------------------------------------------------------
Ed25519 Identity      Once per client     AS registration      MLS signing,           Zeroizing<[u8;32]>
                      (OsRng)             + MLS credential     credential binding     on struct drop

HPKE Init Key         Per KeyPackage      Uploaded to AS       Decrypt Welcome        Consumed by openmls;
                      (openmls backend)   in KeyPackage        (join_group)           deleted from keystore

MLS Epoch Keys        Per Commit          Internal (ratchet    Encrypt/decrypt        Old epoch keys deleted
                      (openmls ratchet)   tree derivation)     application messages   after processing Commit

Hybrid KEM Keys       Per peer (future)   Public portion       X25519+ML-KEM-768      Ephemeral per encrypt;
                      (OsRng)             sent to peers        hybrid encryption      static part on drop

Ed25519 Identity Key

Source: crates/quicproquo-core/src/identity.rs

The Ed25519 identity key is the most long-lived secret in the system. It represents the client's cryptographic identity across all sessions and groups.

Lifecycle

                    +-----------------+
                    |    OsRng        |
                    |  (getrandom)    |
                    +--------+--------+
                             |
                    generate()
                             |
                    +--------v--------+
                    | IdentityKeypair |
                    | seed: Zeroizing |  <-- 32-byte Ed25519 seed
                    | verifying: Vk   |  <-- 32-byte public key
                    +--------+--------+
                             |
              +--------------+--------------+
              |              |              |
         Persist to     Register with   Embed in MLS
         state file     Auth Service    BasicCredential
         (seed_bytes)   (public_key)    (CredentialWithKey)
              |              |              |
              |              |         Sign KeyPackages,
              |              |         Commits, Proposals
              |              |              |
         Load on next   Server stores   Used for lifetime
         client start   public key      of client
         (from_seed)    as queue index        |
              |              |              |
              +------+-------+--------------+
                     |
            struct dropped
                     |
            +--------v--------+
            |  Zeroizing<T>   |
            |  overwrites     |
            |  seed with 0x00 |
            +-----------------+

Key Properties

  • Generation: SigningKey::generate(&mut OsRng) produces 32 bytes of entropy from the OS CSPRNG. The seed is immediately wrapped in Zeroizing.
  • Persistence: The seed_bytes() method returns a plain [u8; 32] for serialization to the client state file. The caller must handle this copy securely.
  • Reconstruction: from_seed(seed) re-derives the VerifyingKey from the seed deterministically. The seed is wrapped in Zeroizing upon construction.
  • Destruction: When the IdentityKeypair struct is dropped, the Zeroizing<[u8; 32]> wrapper overwrites the 32 seed bytes with zeros.
  • No Clone/Copy: The struct does not implement Clone or Copy, preventing accidental duplication of the secret seed.

Fingerprint

The fingerprint (SHA-256(public_key_bytes)) is derived from the public key and is used as a compact identifier in logs. It is not secret and does not require zeroization.

HPKE Init Keys

Source: crates/quicproquo-core/src/keystore.rs and crates/quicproquo-core/src/group.rs

HPKE init keys are generated by the openmls backend as part of MLS KeyPackage creation. They are single-use: each init key is consumed exactly once when processing a Welcome message.

Lifecycle

              +----------------------------+
              | generate_key_package()     |
              | (GroupMember method)        |
              +-------------+--------------+
                            |
              openmls internally generates
              HPKE init keypair (X25519)
                            |
              +-------------v--------------+
              | DiskKeyStore / StoreCrypto |
              | stores init private key    |
              | keyed by init key ref      |
              +-------------+--------------+
                            |
              KeyPackage (containing init
              public key) is TLS-encoded
              and uploaded to Auth Service
                            |
              +-------------v--------------+
              | Peer fetches KeyPackage    |
              | from AS; includes it in    |
              | Welcome message            |
              +-------------+--------------+
                            |
              +-------------v--------------+
              | join_group(welcome)        |
              | openmls calls              |
              | new_from_welcome()         |
              | reads init private key     |
              | from DiskKeyStore          |
              | decrypts Welcome's HPKE    |
              | ciphertext                 |
              +-------------+--------------+
                            |
              Init key is consumed
              (never reused per MLS spec)
                            |
              +-------------v--------------+
              | Key deleted from store     |
              | (openmls manages cleanup)  |
              +----------------------------+

Key Properties

  • Generation: Handled internally by the openmls backend (StoreCrypto). The generate_key_package() method on GroupMember triggers this.
  • Storage: The DiskKeyStore is either ephemeral (in-memory HashMap) or persistent (bincode-serialized to disk). The init private key is stored as a JSON-serialized MlsEntity keyed by the init key reference bytes.
  • Single use: Per RFC 9420, each HPKE init key is used exactly once. This prevents replay attacks and ensures forward secrecy of the initial key exchange.
  • Critical constraint: The same GroupMember instance (and therefore the same StoreCrypto backend) that called generate_key_package() must later call join_group(). If a different backend is used, the init private key will not be found and new_from_welcome() will fail.
  • Persistence mode: DiskKeyStore::persistent(path) writes the entire key-value map to disk on every store/delete operation, ensuring HPKE init keys survive process restarts.

MLS Epoch Keys

Managed by: openmls (internal to the MlsGroup state machine)

MLS epoch keys are derived internally by the openmls ratchet tree. They are not directly accessible in quicproquo code but are critical to understanding the system's security properties.

Lifecycle

              +----------------------------+
              | create_group(group_id)     |
              | or join_group(welcome)     |
              +-------------+--------------+
                            |
              Epoch 0: initial key material
              derived from ratchet tree
                            |
              +-------------v--------------+
              | send_message(plaintext)    |
              | encrypts with current      |
              | epoch's application key    |
              +-------------+--------------+
                            |
              +-------------v--------------+
              | add_member() / Commit      |
              | merge_pending_commit()     |
              | or merge_staged_commit()   |
              +-------------+--------------+
                            |
              Epoch advances: new key
              material derived from
              updated ratchet tree
                            |
              +-------------v--------------+
              | Old epoch keys deleted     |
              | by openmls internally      |
              | (forward secrecy)          |
              +----------------------------+

Key Properties

  • Derivation: Each epoch's key material is derived from the ratchet tree, a binary tree where each leaf represents a group member. Internal nodes hold derived key material. See Post-Compromise Security for details.
  • Advancement: Epochs advance when a Commit is processed -- merge_pending_commit() (for the sender) or merge_staged_commit() (for receivers).
  • Deletion: Old epoch keys are deleted after the Commit is processed. This deletion is what provides forward secrecy at the MLS layer.
  • No direct access: quicproquo code interacts with epoch keys only indirectly through send_message() and receive_message().

Hybrid KEM Keys (Future -- M5+)

Source: crates/quicproquo-core/src/hybrid_kem.rs

The hybrid KEM keypair combines X25519 (classical) with ML-KEM-768 (post-quantum) for content encryption that resists both classical and quantum attacks.

Lifecycle

              +----------------------------+
              | HybridKeypair::generate()  |
              | OsRng for both components  |
              +-------------+--------------+
                            |
              +-------------v--------------+
              | HybridKeypair              |
              | x25519_sk: StaticSecret    |  32B (ZeroizeOnDrop)
              | x25519_pk: PublicKey       |  32B
              | mlkem_dk: DecapsulationKey |  2400B
              | mlkem_ek: EncapsulationKey |  1184B
              +-------------+--------------+
                            |
              +--------------+--------------+
              |                             |
         public_key()                  to_bytes()
         -> HybridPublicKey            -> HybridKeypairBytes
         (32B + 1184B)                 (for persistence)
              |                             |
         Distributed to peers          Stored securely
         for encryption                (serde Serialize)
              |
         +----v----+
         | Sender  |
         | hybrid_encrypt(pk, pt)      |
         | 1. Ephemeral X25519 DH      |
         | 2. ML-KEM-768 encapsulate   |
         | 3. HKDF(x25519_ss||mlkem_ss)|
         | 4. ChaCha20-Poly1305 AEAD   |
         +----+----+
              |
         Envelope: ver(1) | eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct(var)
              |
         +----v----+
         | Recipient                   |
         | hybrid_decrypt(kp, env)     |
         | 1. X25519 DH with eph_pk    |
         | 2. ML-KEM-768 decapsulate   |
         | 3. HKDF derive same key     |
         | 4. ChaCha20-Poly1305 decrypt|
         +---------+

Key Properties

  • Generation: Both X25519 and ML-KEM-768 components use OsRng.
  • Key sizes: X25519 secret: 32B, ML-KEM-768 decapsulation key: 2400B, encapsulation key: 1184B.
  • Serialization: HybridKeypairBytes (serde) stores all components for persistence. HybridPublicKey stores only the public portions.
  • Zeroization of IKM: The combined shared secret (x25519_ss || mlkem_ss) is wrapped in Zeroizing<Vec<u8>> and cleared after HKDF derivation.
  • Ephemeral per encryption: Each call to hybrid_encrypt generates a fresh EphemeralSecret for X25519, ensuring that even if the static keypair is compromised, past encryptions remain protected.
  • Integration timeline: M5 will integrate this into the MLS crypto provider. See Post-Quantum Readiness.

Zeroization Summary

Key Type Zeroization Mechanism When
Ed25519 seed Zeroizing<[u8; 32]> IdentityKeypair drop
Ed25519 seed (accessor) Plain [u8; 32] copy Caller responsibility
HPKE init private Managed by openmls/DiskKeyStore After Welcome processing
MLS epoch keys Managed by openmls internally After Commit processing
Hybrid IKM Zeroizing<Vec<u8>> After HKDF derivation
Hybrid X25519 static ZeroizeOnDrop (x25519-dalek) HybridKeypair drop
Hybrid ephemeral EphemeralSecret (x25519-dalek) After DH computation

Security Considerations

  1. Memory residue: Zeroization prevents key material from lingering in freed memory, but it does not protect against an attacker with live memory access (e.g., a debugger or cold-boot attack). Full protection would require hardware-backed key storage (e.g., HSM, TPM, or OS keychain), which is not yet implemented.

  2. Swap and core dumps: If the process's memory is swapped to disk or written to a core dump, key material may persist on non-volatile storage. Mitigations include mlock() (not yet implemented) and disabling core dumps.

  3. Compiler optimizations: The zeroize crate uses compiler barriers (core::sync::atomic::compiler_fence) to prevent the optimizer from eliding the zeroing write as a dead store.

  4. Copies via seed_bytes(): The IdentityKeypair::seed_bytes() method returns a plain [u8; 32]. The caller (typically the persistence layer) is responsible for zeroizing this copy after writing it to disk.