# Key Lifecycle and Zeroization quicprochat 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 ```text 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/quicprochat-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 ```text +-----------------+ | 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 | | 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/quicprochat-core/src/keystore.rs` and `crates/quicprochat-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 ```text +----------------------------+ | 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 quicprochat code but are critical to understanding the system's security properties. ### Lifecycle ```text +----------------------------+ | 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](post-compromise-security.md) 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](forward-secrecy.md) at the MLS layer. - **No direct access:** quicprochat code interacts with epoch keys only indirectly through `send_message()` and `receive_message()`. ## Hybrid KEM Keys (Future -- M5+) **Source:** `crates/quicprochat-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 ```text +----------------------------+ | 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>` 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](post-quantum-readiness.md). ## 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>` | 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. ## Related Pages - [Cryptography Overview](overview.md) -- algorithm inventory - [Ed25519 Identity Keys](identity-keys.md) -- identity key details - [Forward Secrecy](forward-secrecy.md) -- how key deletion enables FS - [Post-Compromise Security](post-compromise-security.md) -- epoch advancement - [Post-Quantum Readiness](post-quantum-readiness.md) -- hybrid KEM integration