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
14 KiB
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
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
+-----------------+
| 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 inZeroizing. - 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 theVerifyingKeyfrom the seed deterministically. The seed is wrapped inZeroizingupon construction. - Destruction: When the
IdentityKeypairstruct is dropped, theZeroizing<[u8; 32]>wrapper overwrites the 32 seed bytes with zeros. - No Clone/Copy: The struct does not implement
CloneorCopy, 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
+----------------------------+
| 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). Thegenerate_key_package()method onGroupMembertriggers this. - Storage: The
DiskKeyStoreis either ephemeral (in-memoryHashMap) or persistent (bincode-serialized to disk). The init private key is stored as a JSON-serializedMlsEntitykeyed 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
GroupMemberinstance (and therefore the sameStoreCryptobackend) that calledgenerate_key_package()must later calljoin_group(). If a different backend is used, the init private key will not be found andnew_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
+----------------------------+
| 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) ormerge_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: quicprochat code interacts with epoch keys only
indirectly through
send_message()andreceive_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
+----------------------------+
| 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.HybridPublicKeystores only the public portions. - Zeroization of IKM: The combined shared secret (
x25519_ss || mlkem_ss) is wrapped inZeroizing<Vec<u8>>and cleared after HKDF derivation. - Ephemeral per encryption: Each call to
hybrid_encryptgenerates a freshEphemeralSecretfor 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
-
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.
-
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. -
Compiler optimizations: The
zeroizecrate uses compiler barriers (core::sync::atomic::compiler_fence) to prevent the optimizer from eliding the zeroing write as a dead store. -
Copies via
seed_bytes(): TheIdentityKeypair::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 -- algorithm inventory
- Ed25519 Identity Keys -- identity key details
- Forward Secrecy -- how key deletion enables FS
- Post-Compromise Security -- epoch advancement
- Post-Quantum Readiness -- hybrid KEM integration