feat: add post-quantum hybrid KEM + SQLCipher persistence
Feature 1 — Post-Quantum Hybrid KEM (X25519 + ML-KEM-768): - Create hybrid_kem.rs with keygen, encrypt, decrypt + 11 unit tests - Wire format: version(1) | x25519_eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct - Add uploadHybridKey/fetchHybridKey RPCs to node.capnp schema - Server: hybrid key storage in FileBackedStore + RPC handlers - Client: hybrid keypair in StoredState, auto-wrap/unwrap in send/recv/invite/join - demo-group runs full hybrid PQ envelope round-trip Feature 2 — SQLCipher Persistence: - Extract Store trait from FileBackedStore API - Create SqlStore (rusqlite + bundled-sqlcipher) with encrypted-at-rest SQLite - Schema: key_packages, deliveries, hybrid_keys tables with indexes - Server CLI: --store-backend=sql, --db-path, --db-key flags - 5 unit tests for SqlStore (FIFO, round-trip, upsert, channel isolation) Also includes: client lib.rs refactor, auth config, TOML config file support, mdBook documentation, and various cleanups by user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
205
docs/src/cryptography/forward-secrecy.md
Normal file
205
docs/src/cryptography/forward-secrecy.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Forward Secrecy
|
||||
|
||||
Forward secrecy (FS), also called perfect forward secrecy (PFS), is a property
|
||||
of a cryptographic protocol that guarantees: **if a long-term secret key is
|
||||
compromised, past session keys cannot be recovered.** In other words, an
|
||||
attacker who obtains today's long-term key cannot use it to decrypt messages
|
||||
recorded yesterday.
|
||||
|
||||
quicnprotochat provides forward secrecy at two independent layers: the transport
|
||||
layer and the application layer. Even if one layer's FS mechanism is defeated,
|
||||
the other continues to protect message confidentiality.
|
||||
|
||||
## Transport Layer Forward Secrecy
|
||||
|
||||
### TLS 1.3 (QUIC)
|
||||
|
||||
The QUIC transport (via `quinn 0.11` + `rustls 0.23`) uses TLS 1.3, which
|
||||
mandates ephemeral key exchange in every handshake. Unlike TLS 1.2, which
|
||||
allowed static RSA key exchange (no FS), TLS 1.3 exclusively uses ephemeral
|
||||
ECDHE (Elliptic Curve Diffie-Hellman Ephemeral).
|
||||
|
||||
In each TLS 1.3 handshake:
|
||||
|
||||
1. Both client and server generate ephemeral ECDHE key pairs.
|
||||
2. They exchange public keys and compute a shared secret via Diffie-Hellman.
|
||||
3. Session keys are derived from the shared secret using HKDF.
|
||||
4. The ephemeral private keys are discarded after key derivation.
|
||||
|
||||
Because the ephemeral keys exist only for the duration of the handshake,
|
||||
compromising the server's long-term TLS certificate key (currently self-signed
|
||||
in quicnprotochat) does not reveal past session keys.
|
||||
|
||||
### Noise\_XX
|
||||
|
||||
Inside the QUIC stream, the Noise\_XX handshake
|
||||
(`Noise_XX_25519_ChaChaPoly_BLAKE2s`) provides an additional layer of forward
|
||||
secrecy. The Noise\_XX pattern uses both ephemeral and static X25519 keys:
|
||||
|
||||
```text
|
||||
→ e Initiator sends ephemeral public key
|
||||
← e, ee, s, es Responder: ephemeral, DH(e,e), static, DH(e,s)
|
||||
→ s, se Initiator: static, DH(s,e)
|
||||
```
|
||||
|
||||
The `ee` DH (ephemeral-ephemeral) provides forward secrecy: even if both
|
||||
parties' static keys (`s`) are later compromised, the ephemeral keys that
|
||||
contributed to `ee` have already been discarded.
|
||||
|
||||
The `es` and `se` DH operations mix in the static keys for authentication, but
|
||||
the session key depends on the ephemeral contribution. An attacker who
|
||||
compromises only the static key learns the identity of the parties but cannot
|
||||
recover the session key without the ephemeral key.
|
||||
|
||||
See [X25519 Transport Keys](transport-keys.md) for details on the static
|
||||
keypair.
|
||||
|
||||
## Application Layer Forward Secrecy
|
||||
|
||||
### MLS Epoch Ratchet
|
||||
|
||||
The MLS protocol (RFC 9420) provides forward secrecy at the application layer
|
||||
through its epoch ratchet mechanism. This is independent of the transport
|
||||
layer's FS and protects message content even if transport session keys are
|
||||
leaked.
|
||||
|
||||
Each MLS group maintains a **ratchet tree** -- a binary tree where each leaf
|
||||
represents a group member and internal nodes hold derived key material. The
|
||||
tree defines a current **epoch**, which determines the encryption keys for all
|
||||
messages in that epoch.
|
||||
|
||||
When the epoch advances (via a Commit message):
|
||||
|
||||
1. The ratchet tree is updated with new key material from the committing member.
|
||||
2. New epoch keys are derived from the updated tree.
|
||||
3. **Old epoch keys are deleted.**
|
||||
|
||||
This deletion is the mechanism that provides forward secrecy: once old epoch
|
||||
keys are erased, messages encrypted under those keys cannot be decrypted, even
|
||||
if the current group state is compromised.
|
||||
|
||||
In quicnprotochat, epoch advancement occurs when:
|
||||
|
||||
- `add_member()` is called, which creates a Commit and calls
|
||||
`merge_pending_commit()`.
|
||||
- A received Commit is processed via `receive_message()`, which calls
|
||||
`merge_staged_commit()`.
|
||||
|
||||
```rust
|
||||
// Epoch advances here -- old keys deleted internally by openmls
|
||||
group.merge_pending_commit(&self.backend)?; // sender side
|
||||
group.merge_staged_commit(&self.backend, *staged)?; // receiver side
|
||||
```
|
||||
|
||||
### Single-Use KeyPackages
|
||||
|
||||
MLS KeyPackages contain a single-use HPKE init public key. Each init key is
|
||||
used exactly once -- to encrypt the Welcome message that bootstraps a new
|
||||
member's group state. After the Welcome is processed, the init private key is
|
||||
consumed and deleted from the `DiskKeyStore`.
|
||||
|
||||
This single-use design provides forward secrecy for the initial key exchange:
|
||||
|
||||
- Even if a member's long-term Ed25519 identity key is later compromised, the
|
||||
attacker cannot reconstruct the HPKE init private key that was used to decrypt
|
||||
the Welcome.
|
||||
- The init key was ephemeral to the join operation and no longer exists.
|
||||
|
||||
This property is critical because the Welcome message contains the full ratchet
|
||||
tree state, including the secrets needed to decrypt messages in the initial
|
||||
epoch. If the init key could be reused or recovered, an attacker could
|
||||
reconstruct the entire initial group state.
|
||||
|
||||
See [Key Lifecycle and Zeroization](key-lifecycle.md) for the full lifecycle of
|
||||
HPKE init keys.
|
||||
|
||||
## Layered Forward Secrecy
|
||||
|
||||
A distinctive property of quicnprotochat's design is that forward secrecy
|
||||
operates at two independent layers:
|
||||
|
||||
```text
|
||||
+------------------------------------------------------+
|
||||
| Network Adversary (records ciphertext) |
|
||||
+------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------+
|
||||
| TLS 1.3 / Noise_XX |
|
||||
| Forward secrecy via ephemeral ECDHE / X25519 DH |
|
||||
| Even if TLS cert or Noise static key is compromised,|
|
||||
| past transport sessions are protected. |
|
||||
+------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------+
|
||||
| MLS (RFC 9420) |
|
||||
| Forward secrecy via epoch ratchet |
|
||||
| Even if current MLS state is compromised, |
|
||||
| past epochs are protected (keys deleted). |
|
||||
+------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------------------------------------+
|
||||
| Plaintext message content |
|
||||
+------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Why this matters:** If the transport layer's forward secrecy is broken (e.g.,
|
||||
an attacker obtains a TLS session key through a side channel), the MLS layer
|
||||
still protects message content independently. The attacker would see the MLS
|
||||
ciphertext but could not decrypt it without the MLS epoch keys.
|
||||
|
||||
Conversely, if MLS epoch keys are somehow leaked, the transport layer prevents
|
||||
a network-level attacker from correlating them with specific network flows
|
||||
unless they also break the transport encryption.
|
||||
|
||||
## Comparison with Signal
|
||||
|
||||
Signal's Double Ratchet protocol also provides forward secrecy, but the
|
||||
mechanisms differ:
|
||||
|
||||
| Property | Signal Double Ratchet | MLS (quicnprotochat) |
|
||||
|----------|----------------------|---------------------|
|
||||
| Scope | Pairwise (1:1 sessions) | Group (n-party) |
|
||||
| Ratchet granularity | Per message (symmetric ratchet) + per DH round (DH ratchet) | Per epoch (Commit) |
|
||||
| FS granularity | Individual messages | All messages in an epoch |
|
||||
| Group support | Sender Keys (no per-message FS in groups) | Native group FS via ratchet tree |
|
||||
| Efficiency | O(1) per message | O(log n) per Commit, O(1) per message |
|
||||
|
||||
Signal achieves finer-grained forward secrecy in 1:1 conversations (per message
|
||||
via the symmetric ratchet), but in group settings, Signal uses Sender Keys,
|
||||
which do **not** provide per-message forward secrecy. A compromised Sender Key
|
||||
reveals all past messages from that sender.
|
||||
|
||||
MLS provides forward secrecy at the epoch level for the entire group. Within an
|
||||
epoch, all messages share the same key material. The trade-off is that FS
|
||||
granularity is coarser (per epoch rather than per message), but it applies
|
||||
uniformly to all group members.
|
||||
|
||||
## Practical Implications
|
||||
|
||||
1. **Epoch advancement frequency:** More frequent Commits provide more
|
||||
fine-grained forward secrecy. In the current implementation, epochs advance
|
||||
when members are added. Future milestones will add periodic Update proposals
|
||||
to advance epochs even without membership changes.
|
||||
|
||||
2. **Key deletion timing:** Forward secrecy depends on old keys being actually
|
||||
deleted from memory and disk. The `DiskKeyStore`'s flush-on-write behavior
|
||||
ensures that consumed HPKE init keys are removed from the persistent store.
|
||||
MLS epoch key deletion is handled internally by openmls.
|
||||
|
||||
3. **State file security:** The client state file contains the Ed25519 identity
|
||||
seed and potentially the DiskKeyStore contents. If this file is compromised,
|
||||
the attacker obtains the current identity key and any stored HPKE init keys
|
||||
(for pending Welcome messages). Past epoch keys are not in the state file
|
||||
(they have been deleted), so forward secrecy is preserved for past epochs.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- when keys are created and destroyed
|
||||
- [Post-Compromise Security](post-compromise-security.md) -- the complementary property (protecting the future)
|
||||
- [Threat Model](threat-model.md) -- attacker models and what FS protects against
|
||||
- [X25519 Transport Keys](transport-keys.md) -- Noise ephemeral DH details
|
||||
- [Ed25519 Identity Keys](identity-keys.md) -- long-term key that FS protects against compromising
|
||||
199
docs/src/cryptography/identity-keys.md
Normal file
199
docs/src/cryptography/identity-keys.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Ed25519 Identity Keys
|
||||
|
||||
The Ed25519 identity keypair is the long-term cryptographic identity of a
|
||||
quicnprotochat client. It is generated once, persisted across sessions, and used
|
||||
for MLS credential signing, Authentication Service registration, and delivery
|
||||
queue addressing.
|
||||
|
||||
**Source:** `crates/quicnprotochat-core/src/identity.rs`
|
||||
|
||||
## Structure
|
||||
|
||||
The `IdentityKeypair` struct holds two fields:
|
||||
|
||||
```rust
|
||||
pub struct IdentityKeypair {
|
||||
/// Raw 32-byte private seed -- zeroized on drop.
|
||||
seed: Zeroizing<[u8; 32]>,
|
||||
/// Corresponding 32-byte public verifying key.
|
||||
verifying: VerifyingKey,
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Size | Secret? |
|
||||
|-------|------|------|---------|
|
||||
| `seed` | `Zeroizing<[u8; 32]>` | 32 bytes | Yes -- zeroized on drop |
|
||||
| `verifying` | `ed25519_dalek::VerifyingKey` | 32 bytes | No -- public |
|
||||
|
||||
The private seed is stored as raw bytes wrapped in `Zeroizing<[u8; 32]>` rather
|
||||
than directly as a `SigningKey`. This design choice avoids a conflict with
|
||||
`ed25519-dalek`'s own `Zeroize` implementation: the `Zeroizing<T>` wrapper
|
||||
requires `T: DefaultIsZeroes`, which `[u8; 32]` satisfies (being `Copy +
|
||||
Default`) but `SigningKey` does not.
|
||||
|
||||
## Key Generation
|
||||
|
||||
A fresh identity keypair is generated from the OS CSPRNG (`OsRng`) via
|
||||
`ed25519-dalek`:
|
||||
|
||||
```rust
|
||||
use quicnprotochat_core::identity::IdentityKeypair;
|
||||
|
||||
let identity = IdentityKeypair::generate();
|
||||
// The signing key seed is generated from OsRng (getrandom on Linux).
|
||||
// The verifying key is derived from the seed automatically.
|
||||
```
|
||||
|
||||
Internally, `generate()` calls `SigningKey::generate(&mut OsRng)`, extracts the
|
||||
32-byte seed with `to_bytes()`, wraps it in `Zeroizing`, and derives the
|
||||
`VerifyingKey`:
|
||||
|
||||
```rust
|
||||
pub fn generate() -> Self {
|
||||
use rand::rngs::OsRng;
|
||||
let signing = SigningKey::generate(&mut OsRng);
|
||||
let verifying = signing.verifying_key();
|
||||
let seed = Zeroizing::new(signing.to_bytes());
|
||||
Self { seed, verifying }
|
||||
}
|
||||
```
|
||||
|
||||
## Fingerprint Computation
|
||||
|
||||
The fingerprint is a SHA-256 digest of the raw 32-byte Ed25519 public key. It
|
||||
serves as a compact, collision-resistant identifier for logging and protocol
|
||||
indexing:
|
||||
|
||||
```rust
|
||||
pub fn fingerprint(&self) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.verifying.to_bytes());
|
||||
hasher.finalize().into()
|
||||
}
|
||||
```
|
||||
|
||||
The `Debug` implementation uses the first 4 bytes of the fingerprint as a
|
||||
human-readable prefix:
|
||||
|
||||
```rust
|
||||
// Output example:
|
||||
// IdentityKeypair { fingerprint: "a1b2c3d4...", .. }
|
||||
```
|
||||
|
||||
This ensures the private seed is never accidentally printed to logs.
|
||||
|
||||
## Zeroization
|
||||
|
||||
The 32-byte private seed is wrapped in `Zeroizing<[u8; 32]>` from the `zeroize`
|
||||
crate. When the `IdentityKeypair` struct is dropped, the `Zeroizing` wrapper
|
||||
overwrites the seed bytes with zeros before deallocation. This mitigates the
|
||||
risk of key material lingering in memory after the struct is no longer needed.
|
||||
|
||||
Key points about the zeroization strategy:
|
||||
|
||||
- **On drop:** The seed is overwritten with zeros automatically.
|
||||
- **Serialization:** `seed_bytes()` returns a plain `[u8; 32]` copy for
|
||||
persistence. The caller is responsible for securely handling this copy.
|
||||
- **Reconstruction:** `from_seed(seed)` wraps the provided bytes in a fresh
|
||||
`Zeroizing` immediately.
|
||||
- **No `Clone`/`Copy`:** `IdentityKeypair` does not implement `Clone` or
|
||||
`Copy`, preventing accidental duplication of secret material.
|
||||
|
||||
See [Key Lifecycle and Zeroization](key-lifecycle.md) for the full lifecycle of
|
||||
this key type.
|
||||
|
||||
## Role in MLS
|
||||
|
||||
The `IdentityKeypair` implements the `openmls_traits::signatures::Signer` trait,
|
||||
allowing it to be passed directly to `KeyPackage::builder().build(...)`:
|
||||
|
||||
```rust
|
||||
impl Signer for IdentityKeypair {
|
||||
fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, MlsError> {
|
||||
let sk = self.signing_key();
|
||||
let sig: ed25519_dalek::Signature = sk.sign(payload);
|
||||
Ok(sig.to_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn signature_scheme(&self) -> SignatureScheme {
|
||||
SignatureScheme::ED25519
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This integration means `IdentityKeypair`:
|
||||
|
||||
1. Signs MLS Commits, Proposals, and KeyPackages with Ed25519.
|
||||
2. Is embedded in `BasicCredential` as the raw 32-byte public key bytes.
|
||||
3. Provides the `signature_key` field in `CredentialWithKey` used throughout
|
||||
the `GroupMember` lifecycle.
|
||||
|
||||
The MLS ciphersuite (`MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`) mandates
|
||||
Ed25519 for signing, making the `IdentityKeypair` the natural fit.
|
||||
|
||||
## Role in the Authentication Service
|
||||
|
||||
The Ed25519 public key bytes (`public_key_bytes()`) are used as the
|
||||
`identityKey` in `auth.capnp` RPC calls. The Authentication Service stores
|
||||
KeyPackages indexed by this key, and the Delivery Service routes messages to
|
||||
queues indexed by the same key.
|
||||
|
||||
## Distinction from the X25519 Noise Keypair
|
||||
|
||||
It is critical to understand that the Ed25519 identity key and the X25519
|
||||
transport key are **separate keys on different curves serving different
|
||||
purposes**:
|
||||
|
||||
| Property | Ed25519 Identity Key | X25519 Noise Key |
|
||||
|----------|---------------------|-----------------|
|
||||
| Curve | Twisted Edwards (Ed25519) | Montgomery (Curve25519) |
|
||||
| Operation | Digital signatures | Diffie-Hellman key exchange |
|
||||
| Purpose | MLS credentials, AS registration | Noise\_XX mutual authentication |
|
||||
| Lifetime | Permanent (per client) | Per server process or per connection |
|
||||
| Persistence | Serialized to state file | Not serialized (M6 deferred) |
|
||||
| Source | `identity.rs` | `keypair.rs` |
|
||||
|
||||
Although both curves are related (Curve25519 is birationally equivalent to
|
||||
Ed25519's curve), the keys are **not interchangeable**. Converting between them
|
||||
requires explicit birational mapping, which quicnprotochat intentionally avoids
|
||||
to maintain clean separation of concerns.
|
||||
|
||||
## Serialization
|
||||
|
||||
`IdentityKeypair` implements `Serialize` and `Deserialize` (serde) by
|
||||
serializing only the 32-byte seed. On deserialization, `from_seed()` is called
|
||||
to reconstruct the verifying key:
|
||||
|
||||
```rust
|
||||
impl Serialize for IdentityKeypair {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.seed[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for IdentityKeypair {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: serde::Deserializer<'de>,
|
||||
{
|
||||
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
|
||||
let seed: [u8; 32] = bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
|
||||
Ok(IdentityKeypair::from_seed(seed))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This means the state file contains only the 32-byte seed, and the verifying key
|
||||
is deterministically re-derived on load.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory
|
||||
- [X25519 Transport Keys](transport-keys.md) -- the other keypair
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- full lifecycle diagram
|
||||
- [Post-Compromise Security](post-compromise-security.md) -- how MLS credentials interact with PCS
|
||||
- [Threat Model](threat-model.md) -- what identity keys protect and do not protect
|
||||
402
docs/src/cryptography/key-lifecycle.md
Normal file
402
docs/src/cryptography/key-lifecycle.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Key Lifecycle and Zeroization
|
||||
|
||||
quicnprotochat 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
|
||||
|
||||
X25519 Noise Per server process Noise_XX handshake DH key exchange ZeroizeOnDrop
|
||||
or per client conn (in-band) (transport session) 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/quicnprotochat-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<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.
|
||||
|
||||
## X25519 Noise Key
|
||||
|
||||
**Source:** `crates/quicnprotochat-core/src/keypair.rs`
|
||||
|
||||
The X25519 Noise key provides mutual authentication during the Noise\_XX
|
||||
handshake. It is shorter-lived than the identity key and is not currently
|
||||
persisted.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```text
|
||||
+-----------------+
|
||||
| OsRng |
|
||||
| (getrandom) |
|
||||
+--------+--------+
|
||||
|
|
||||
generate()
|
||||
|
|
||||
+--------v--------+
|
||||
| NoiseKeypair |
|
||||
| private: Secret | <-- StaticSecret (ZeroizeOnDrop)
|
||||
| public: PubKey | <-- 32-byte public key
|
||||
+--------+--------+
|
||||
|
|
||||
+--------------+--------------+
|
||||
| |
|
||||
private_bytes() public_bytes()
|
||||
-> Zeroizing<[u8;32]> -> [u8; 32]
|
||||
| |
|
||||
Passed to snow::Builder Exchanged during
|
||||
local_private_key() Noise_XX handshake
|
||||
| |
|
||||
Zeroizing copy drops Stored by peer
|
||||
immediately after use (not secret)
|
||||
| |
|
||||
+-------------+---------------+
|
||||
|
|
||||
Noise handshake completes
|
||||
|
|
||||
+-------------v--------------+
|
||||
| Transport session holds |
|
||||
| derived symmetric keys |
|
||||
| (managed by snow) |
|
||||
+-------------+--------------+
|
||||
|
|
||||
Connection closes
|
||||
|
|
||||
+-------------v--------------+
|
||||
| NoiseKeypair dropped |
|
||||
| StaticSecret::drop() |
|
||||
| overwrites scalar with 0 |
|
||||
+----------------------------+
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **Generation:** `StaticSecret::random_from_rng(OsRng)` generates a 32-byte
|
||||
Curve25519 scalar.
|
||||
- **Dual zeroization:** The `StaticSecret` itself implements `ZeroizeOnDrop`,
|
||||
and `private_bytes()` returns a `Zeroizing<[u8; 32]>` wrapper.
|
||||
- **Debug redaction:** The `Debug` impl shows only the first 4 bytes of the
|
||||
public key and prints `[redacted]` for the private key.
|
||||
- **No serialization:** `NoiseKeypair` does not implement `Serialize`. Persistence
|
||||
is deferred to M6.
|
||||
- **Current lifetime:** Per server process start (server) or per connection
|
||||
attempt (client). After M6, keys may be persisted with passphrase encryption.
|
||||
|
||||
## HPKE Init Keys
|
||||
|
||||
**Source:** `crates/quicnprotochat-core/src/keystore.rs` and
|
||||
`crates/quicnprotochat-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 quicnprotochat 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:** quicnprotochat code interacts with epoch keys only
|
||||
indirectly through `send_message()` and `receive_message()`.
|
||||
|
||||
## Hybrid KEM Keys (Future -- M5+)
|
||||
|
||||
**Source:** `crates/quicnprotochat-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<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](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 |
|
||||
| X25519 private | `ZeroizeOnDrop` (x25519-dalek) | `NoiseKeypair` drop |
|
||||
| X25519 private (accessor) | `Zeroizing<[u8; 32]>` | Accessor drop |
|
||||
| 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.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory
|
||||
- [Ed25519 Identity Keys](identity-keys.md) -- identity key details
|
||||
- [X25519 Transport Keys](transport-keys.md) -- transport 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
|
||||
102
docs/src/cryptography/overview.md
Normal file
102
docs/src/cryptography/overview.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Cryptography Overview
|
||||
|
||||
quicnprotochat layers multiple cryptographic protocols to provide confidentiality,
|
||||
integrity, authentication, forward secrecy, and post-compromise security. This
|
||||
page catalogues every algorithm in the system, the crate that supplies it, and
|
||||
the security margin it provides.
|
||||
|
||||
## Algorithm Inventory
|
||||
|
||||
| Algorithm | Purpose | Crate | Security Level |
|
||||
|-----------|---------|-------|----------------|
|
||||
| Ed25519 | Identity signing, MLS credentials | `ed25519-dalek 2` | 128-bit classical |
|
||||
| X25519 | Noise DH, MLS HPKE key exchange | `x25519-dalek 2` | 128-bit classical |
|
||||
| ChaCha20-Poly1305 | Noise AEAD | `chacha20poly1305 0.10` | 256-bit key |
|
||||
| AES-128-GCM | MLS AEAD | `openmls` (via RustCrypto) | 128-bit |
|
||||
| BLAKE2s | Noise hash function | `snow 0.9` (built-in) | 128-bit |
|
||||
| SHA-256 | Key fingerprints, HKDF | `sha2 0.10` | 128-bit collision resistance |
|
||||
| ML-KEM-768 | Post-quantum KEM | `ml-kem 0.2` | NIST Level 3 (~192-bit PQ) |
|
||||
| HKDF-SHA256 | Key derivation | `hkdf 0.12` | Depends on input entropy |
|
||||
|
||||
> **Note:** The system provides 128-bit classical security throughout. When the
|
||||
> hybrid KEM is active (M5 onward), content encryption gains 192-bit
|
||||
> post-quantum security via ML-KEM-768.
|
||||
|
||||
## Where Each Algorithm Appears
|
||||
|
||||
### Transport Layer
|
||||
|
||||
The transport layer uses two independent encryption substrates:
|
||||
|
||||
1. **QUIC/TLS 1.3** (via `quinn 0.11` + `rustls 0.23`): Provides the
|
||||
outermost encrypted tunnel. The TLS 1.3 handshake negotiates an ephemeral
|
||||
ECDHE key exchange (X25519 or P-256, depending on the peer) and an AEAD
|
||||
cipher (AES-128-GCM or ChaCha20-Poly1305). This layer protects connection
|
||||
metadata from passive network observers.
|
||||
|
||||
2. **Noise\_XX** (via `snow 0.9`): Runs inside the QUIC stream. The Noise
|
||||
pattern `Noise_XX_25519_ChaChaPoly_BLAKE2s` provides mutual authentication
|
||||
using static X25519 keys, with ChaCha20-Poly1305 as the AEAD and BLAKE2s
|
||||
as the hash function. See [X25519 Transport Keys](transport-keys.md) for
|
||||
details on the keypair.
|
||||
|
||||
### Application Layer
|
||||
|
||||
1. **MLS (RFC 9420)** (via `openmls 0.5`): Provides end-to-end encrypted
|
||||
group messaging. The ciphersuite is
|
||||
`MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`, which uses:
|
||||
- X25519 for DHKEM (HPKE key exchange)
|
||||
- AES-128-GCM for content encryption
|
||||
- SHA-256 for the KDF and transcript hashing
|
||||
- Ed25519 for signing Commits, Proposals, and credentials
|
||||
|
||||
2. **Hybrid KEM** (via `ml-kem 0.2` + `x25519-dalek 2` + `hkdf 0.12`):
|
||||
An outer encryption layer combining X25519 and ML-KEM-768. The combined
|
||||
shared secret is derived through HKDF-SHA256 and used with
|
||||
ChaCha20-Poly1305 for AEAD. See
|
||||
[Post-Quantum Readiness](post-quantum-readiness.md) for integration plans.
|
||||
|
||||
### Identity Layer
|
||||
|
||||
- **Ed25519** provides long-term identity signing. Each client generates a
|
||||
single Ed25519 keypair that serves as the MLS `BasicCredential`, the
|
||||
Authentication Service registration key, and the delivery queue index. See
|
||||
[Ed25519 Identity Keys](identity-keys.md).
|
||||
|
||||
- **SHA-256** computes key fingerprints -- a 32-byte digest of the Ed25519
|
||||
public key bytes used for compact, collision-resistant identification in logs
|
||||
and protocol messages.
|
||||
|
||||
## Security Level Summary
|
||||
|
||||
All classical algorithms in the system target at least 128-bit security. The
|
||||
post-quantum component (ML-KEM-768) targets NIST Level 3, which corresponds to
|
||||
roughly 192-bit security against quantum adversaries.
|
||||
|
||||
The weakest classical link is the 128-bit security level of AES-128-GCM in the
|
||||
MLS ciphersuite. This is consistent with the IETF's recommended MLS ciphersuite
|
||||
and is considered adequate for the foreseeable future.
|
||||
|
||||
```text
|
||||
Layer Classical Security Post-Quantum Security
|
||||
--------------------------------------------------------------------
|
||||
QUIC/TLS 1.3 128-bit (ECDHE) None (classical only)
|
||||
Noise_XX 128-bit (X25519) None (classical only)
|
||||
MLS (content) 128-bit (AES-128-GCM) None (classical only)
|
||||
Hybrid KEM (M5+) 128-bit (X25519) ~192-bit (ML-KEM-768)
|
||||
```
|
||||
|
||||
See the [Threat Model](threat-model.md) for a discussion of what is and is not
|
||||
protected, and [Forward Secrecy](forward-secrecy.md) and
|
||||
[Post-Compromise Security](post-compromise-security.md) for the advanced
|
||||
security properties these algorithms enable.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Ed25519 Identity Keys](identity-keys.md) -- long-term signing keypair
|
||||
- [X25519 Transport Keys](transport-keys.md) -- Noise handshake keypair
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- creation through destruction
|
||||
- [Forward Secrecy](forward-secrecy.md) -- past message protection
|
||||
- [Post-Compromise Security](post-compromise-security.md) -- future message recovery
|
||||
- [Post-Quantum Readiness](post-quantum-readiness.md) -- ML-KEM-768 hybrid KEM
|
||||
- [Threat Model](threat-model.md) -- attacker models and known gaps
|
||||
239
docs/src/cryptography/post-compromise-security.md
Normal file
239
docs/src/cryptography/post-compromise-security.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Post-Compromise Security
|
||||
|
||||
Post-compromise security (PCS) is a property of a cryptographic protocol that
|
||||
guarantees: **after an attacker compromises a participant's state, the protocol
|
||||
automatically heals so that future messages are protected.** The attacker's
|
||||
window of access is limited to the current epoch; once the compromised member (or
|
||||
any other member) issues an Update or Commit, the group state is re-randomized
|
||||
and the attacker is locked out.
|
||||
|
||||
PCS is the complement of [forward secrecy](forward-secrecy.md):
|
||||
|
||||
- **Forward secrecy** protects the **past** from a future compromise.
|
||||
- **Post-compromise security** protects the **future** from a past compromise.
|
||||
|
||||
MLS (RFC 9420) is specifically designed to provide both properties simultaneously
|
||||
for group messaging. This is a key differentiator of quicnprotochat's design.
|
||||
|
||||
## How MLS Provides PCS
|
||||
|
||||
### The Ratchet Tree
|
||||
|
||||
At the heart of MLS's PCS mechanism is the **ratchet tree**, a binary tree where:
|
||||
|
||||
- Each **leaf** represents a group member and contains their public key material.
|
||||
- Each **internal node** holds derived key material computed from its children.
|
||||
- The **root** of the tree determines the epoch's group key material.
|
||||
|
||||
```text
|
||||
[Root]
|
||||
/ \
|
||||
[A,B] [C,D]
|
||||
/ \ / \
|
||||
[A] [B] [C] [D]
|
||||
```
|
||||
|
||||
When a member updates their leaf (by generating fresh key material and issuing an
|
||||
Update proposal or Commit), the change propagates up the tree path from the leaf
|
||||
to the root:
|
||||
|
||||
```text
|
||||
[Root]* <- re-derived
|
||||
/ \
|
||||
[A,B]* [C,D] <- re-derived
|
||||
/ \ / \
|
||||
[A]* [B] [C] [D] <- A updated leaf
|
||||
```
|
||||
|
||||
Every node marked with `*` receives new key material. This means the new epoch's
|
||||
group secrets depend on A's freshly generated randomness. An attacker who
|
||||
previously compromised A's state must now also compromise the new key material --
|
||||
which they cannot do because it was generated after the compromise was detected
|
||||
(or automatically healed by the protocol).
|
||||
|
||||
### Cost
|
||||
|
||||
The path from a leaf to the root has length O(log n) for a group of n members.
|
||||
This means:
|
||||
|
||||
- An Update/Commit produces O(log n) encrypted path secrets.
|
||||
- Each group member processes the Update in O(log n) time.
|
||||
- This is dramatically more efficient than pairwise rekeying (O(n)) or
|
||||
broadcast encryption.
|
||||
|
||||
For a group of 1,000 members, the path length is approximately 10 nodes --
|
||||
making PCS practical even for large groups.
|
||||
|
||||
## Epoch Advancement in quicnprotochat
|
||||
|
||||
In the current implementation, epoch advancement occurs through the `GroupMember`
|
||||
methods in `group.rs`:
|
||||
|
||||
### Adding a Member (Commit)
|
||||
|
||||
When `add_member()` is called, openmls creates a Commit that adds the new member
|
||||
and updates the ratchet tree:
|
||||
|
||||
```rust
|
||||
// Alice adds Bob -- this creates a Commit + Welcome
|
||||
let (commit_bytes, welcome_bytes) = alice.add_member(&bob_kp)?;
|
||||
|
||||
// Alice merges the pending Commit, advancing her epoch
|
||||
// Internally, openmls re-derives the ratchet tree with Bob's leaf
|
||||
group.merge_pending_commit(&self.backend)?;
|
||||
```
|
||||
|
||||
After `merge_pending_commit()`, Alice's group is in a new epoch with fresh key
|
||||
material. Any attacker who had compromised Alice's state before this Commit
|
||||
must now also compromise the new epoch's keys.
|
||||
|
||||
### Receiving a Commit
|
||||
|
||||
When a member receives and processes a Commit from another member:
|
||||
|
||||
```rust
|
||||
ProcessedMessageContent::StagedCommitMessage(staged) => {
|
||||
// Merge the Commit into local state -- epoch advances
|
||||
group.merge_staged_commit(&self.backend, *staged)?;
|
||||
Ok(None)
|
||||
}
|
||||
```
|
||||
|
||||
This advances the receiver's epoch, incorporating the committer's fresh key
|
||||
material into the ratchet tree.
|
||||
|
||||
### Future: Periodic Updates
|
||||
|
||||
The current implementation only advances epochs when members are added. A more
|
||||
robust PCS strategy involves periodic Update proposals, where members
|
||||
re-randomize their leaf key material on a regular schedule (e.g., every hour, or
|
||||
every N messages). This is planned for future milestones and will look like:
|
||||
|
||||
```text
|
||||
1. Member generates fresh leaf key material
|
||||
2. Member creates an Update proposal
|
||||
3. Any member (or the updater) creates a Commit including the Update
|
||||
4. All members process the Commit and advance to the new epoch
|
||||
5. The attacker's compromised state is now stale
|
||||
```
|
||||
|
||||
## PCS vs Forward Secrecy
|
||||
|
||||
These two properties are often confused but protect against different attack
|
||||
scenarios:
|
||||
|
||||
| Property | Protects | Mechanism | Threat |
|
||||
|----------|----------|-----------|--------|
|
||||
| Forward Secrecy | Past messages | Delete old epoch keys | Attacker compromises state **now**, tries to read **past** |
|
||||
| Post-Compromise Security | Future messages | Re-randomize ratchet tree | Attacker compromised state **before**, tries to read **future** |
|
||||
|
||||
Together, they provide a strong guarantee: the attacker's window of access is
|
||||
limited to the **current epoch only**. Past epochs are protected by FS (old keys
|
||||
deleted), and future epochs are protected by PCS (new key material generated).
|
||||
|
||||
```text
|
||||
Past epochs Current epoch Future epochs
|
||||
+-----------------+ +-----------------+ +-----------------+
|
||||
| Protected by | | Attacker has | | Protected by |
|
||||
| Forward | | access if they | | Post-Compromise|
|
||||
| Secrecy | | hold current | | Security |
|
||||
| (keys deleted) | | epoch keys | | (tree updated) |
|
||||
+-----------------+ +-----------------+ +-----------------+
|
||||
```
|
||||
|
||||
## Comparison with Signal Groups
|
||||
|
||||
Signal's group messaging uses **Sender Keys**, a fundamentally different
|
||||
mechanism from MLS's ratchet tree. The comparison is instructive because it
|
||||
highlights why MLS was chosen for quicnprotochat:
|
||||
|
||||
### Signal Sender Keys
|
||||
|
||||
In Signal's group protocol:
|
||||
|
||||
1. Each member generates a Sender Key -- a symmetric key used to encrypt all
|
||||
messages they send to the group.
|
||||
2. The Sender Key is distributed to all group members via pairwise Signal
|
||||
sessions.
|
||||
3. Each message from a sender is encrypted with their Sender Key.
|
||||
4. The Sender Key includes a symmetric ratchet (hash ratchet) that advances per
|
||||
message, providing forward secrecy within a sender's chain.
|
||||
|
||||
**The critical limitation:** Sender Keys do **not** provide post-compromise
|
||||
security. If an attacker compromises a member's Sender Key:
|
||||
|
||||
- The attacker can derive all future message keys from that sender (the hash
|
||||
ratchet is one-way, but the attacker has the current state).
|
||||
- The key is only rotated when the member manually refreshes it or when group
|
||||
membership changes.
|
||||
- There is no automatic healing mechanism analogous to MLS's ratchet tree.
|
||||
|
||||
### MLS Ratchet Tree (quicnprotochat)
|
||||
|
||||
In contrast, MLS's ratchet tree provides PCS because:
|
||||
|
||||
1. Any member can issue an Update that re-randomizes their leaf.
|
||||
2. The Commit propagates new key material up the tree, affecting the group
|
||||
secret.
|
||||
3. The attacker, who holds old state, cannot predict the new randomness.
|
||||
4. The group automatically heals after at most one epoch advance.
|
||||
|
||||
| Property | Signal Sender Keys | MLS Ratchet Tree |
|
||||
|----------|-------------------|-----------------|
|
||||
| Forward secrecy (group) | Per-sender hash ratchet | Per-epoch (Commit) |
|
||||
| Post-compromise security | **No** -- compromised Sender Key reveals all future messages from that sender | **Yes** -- any Commit/Update heals the tree |
|
||||
| Key rotation | Manual or on membership change | Any Commit (add/remove/update) |
|
||||
| Healing time | Until manual rotation | Next epoch (automatic) |
|
||||
| Cost per update | O(n) pairwise re-encryption | O(log n) tree path |
|
||||
|
||||
## Practical Implications
|
||||
|
||||
### What happens during a compromise?
|
||||
|
||||
Suppose an attacker compromises Member A's MLS state (including the current epoch
|
||||
keys):
|
||||
|
||||
1. **Current epoch:** The attacker can decrypt all messages in the current epoch
|
||||
from all members (because epoch keys are shared group secrets).
|
||||
|
||||
2. **Past epochs:** Protected by forward secrecy. Old epoch keys have been
|
||||
deleted by openmls.
|
||||
|
||||
3. **After the next Commit:** Any member (including A, after recovering) can
|
||||
issue a Commit. The ratchet tree is updated with fresh key material. The
|
||||
attacker's stale state cannot derive the new epoch keys. The attacker is
|
||||
locked out.
|
||||
|
||||
### How quickly does healing occur?
|
||||
|
||||
In the current implementation, healing occurs whenever:
|
||||
|
||||
- A new member is added (`add_member()` issues a Commit).
|
||||
- A member is removed (not yet implemented, but will issue a Commit).
|
||||
- In the future: periodic Update proposals are issued on a timer.
|
||||
|
||||
The practical healing window is the time between Commits. For active groups
|
||||
with frequent membership changes, this window is small. For static groups,
|
||||
periodic Updates (planned) will bound the healing window.
|
||||
|
||||
### Server compromise does not prevent PCS
|
||||
|
||||
The quicnprotochat server is MLS-unaware -- it stores and forwards encrypted
|
||||
MLS messages without access to the group state. A compromised server cannot:
|
||||
|
||||
- Prevent PCS by blocking Commits (it could perform denial-of-service, but
|
||||
cannot selectively suppress Update proposals without being detected, because
|
||||
MLS epoch numbers must be sequential).
|
||||
- Inject fraudulent Commits (it lacks the signing key of any group member).
|
||||
|
||||
See [Threat Model](threat-model.md) for the full analysis of a compromised
|
||||
server.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory
|
||||
- [Forward Secrecy](forward-secrecy.md) -- the complementary property
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- how key deletion enables FS and PCS
|
||||
- [Threat Model](threat-model.md) -- attacker models including compromised clients
|
||||
- [Post-Quantum Readiness](post-quantum-readiness.md) -- PQ protection for PCS mechanisms
|
||||
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- protocol deep dive
|
||||
256
docs/src/cryptography/post-quantum-readiness.md
Normal file
256
docs/src/cryptography/post-quantum-readiness.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Post-Quantum Readiness
|
||||
|
||||
quicnprotochat includes a fully implemented and tested hybrid key encapsulation
|
||||
mechanism (KEM) combining X25519 (classical) with ML-KEM-768 (post-quantum).
|
||||
This page describes the current implementation, the integration plan, the
|
||||
security rationale, and the known gaps.
|
||||
|
||||
**Source:** `crates/quicnprotochat-core/src/hybrid_kem.rs`
|
||||
|
||||
## Current State
|
||||
|
||||
The hybrid KEM is **fully implemented and tested** in `quicnprotochat-core`. The
|
||||
implementation provides:
|
||||
|
||||
- `HybridKeypair::generate()` -- generate a combined X25519 + ML-KEM-768 keypair
|
||||
- `hybrid_encrypt(recipient_pk, plaintext)` -- encrypt to a hybrid public key
|
||||
- `hybrid_decrypt(keypair, envelope)` -- decrypt with the hybrid private key
|
||||
- Serialization/deserialization for both keypairs and public keys
|
||||
- Comprehensive test coverage: round-trip, wrong-key rejection, tampering
|
||||
detection, version validation, large payload handling
|
||||
|
||||
The test suite in `hybrid_kem.rs` includes 10 tests covering:
|
||||
|
||||
- Basic encrypt/decrypt round-trip
|
||||
- Wrong key decryption failure
|
||||
- Tampered AEAD ciphertext detection
|
||||
- Tampered ML-KEM ciphertext detection
|
||||
- Tampered X25519 ephemeral public key detection
|
||||
- Unsupported version rejection
|
||||
- Envelope-too-short rejection
|
||||
- Keypair serialization round-trip
|
||||
- Public key serialization round-trip
|
||||
- Large payload (50 KB) round-trip
|
||||
|
||||
## ML-KEM-768 (FIPS 203)
|
||||
|
||||
ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) is the NIST-standardized
|
||||
post-quantum KEM, published as FIPS 203. quicnprotochat uses ML-KEM-768, the
|
||||
middle parameter set:
|
||||
|
||||
| Parameter Set | NIST Level | Security (PQ) | EK Size | CT Size | SS Size |
|
||||
|---------------|-----------|---------------|---------|---------|---------|
|
||||
| ML-KEM-512 | 1 | ~128-bit | 800 B | 768 B | 32 B |
|
||||
| **ML-KEM-768** | **3** | **~192-bit** | **1184 B** | **1088 B** | **32 B** |
|
||||
| ML-KEM-1024 | 5 | ~256-bit | 1568 B | 1568 B | 32 B |
|
||||
|
||||
### Why ML-KEM-768 (not 512 or 1024)?
|
||||
|
||||
- **Level 3 provides a strong security margin.** The ~192-bit post-quantum
|
||||
security level exceeds the 128-bit classical security of the rest of the
|
||||
system, providing headroom against future cryptanalytic advances.
|
||||
- **Moderate key and ciphertext sizes.** The encapsulation key (1184 bytes) and
|
||||
ciphertext (1088 bytes) are large compared to X25519 (32 bytes each) but
|
||||
manageable for a messaging protocol. ML-KEM-1024 would add ~400 bytes to each
|
||||
with diminishing returns.
|
||||
- **Consistent with industry practice.** Signal, Google Chrome, and Cloudflare
|
||||
have all deployed ML-KEM-768 (or its predecessor Kyber-768) in production.
|
||||
|
||||
The `ml-kem 0.2` crate provides a pure-Rust implementation of FIPS 203 with all
|
||||
three parameter sets compiled in by default.
|
||||
|
||||
## Hybrid Construction
|
||||
|
||||
The hybrid KEM follows the combiner approach described in
|
||||
`draft-ietf-tls-hybrid-design`. Both a classical and a post-quantum KEM are
|
||||
executed, and their shared secrets are combined through a KDF:
|
||||
|
||||
### Key Derivation
|
||||
|
||||
```text
|
||||
ikm = X25519_shared_secret(32 bytes) || ML-KEM_shared_secret(32 bytes)
|
||||
key = HKDF-SHA256(salt=[], ikm, info="quicnprotochat-hybrid-v1", L=32)
|
||||
nonce = HKDF-SHA256(salt=[], ikm, info="quicnprotochat-hybrid-nonce-v1", L=12)
|
||||
```
|
||||
|
||||
The combined IKM (input key material) is wrapped in `Zeroizing<Vec<u8>>` and
|
||||
cleared after HKDF expansion.
|
||||
|
||||
### Why a Hybrid?
|
||||
|
||||
A pure ML-KEM deployment would be vulnerable if lattice-based cryptography is
|
||||
broken (which, while considered unlikely, cannot be ruled out for a newly
|
||||
standardized algorithm). A pure X25519 deployment provides no post-quantum
|
||||
protection. The hybrid approach provides a "belt and suspenders" guarantee:
|
||||
|
||||
- If ML-KEM-768 is broken, X25519 still provides 128-bit classical security.
|
||||
- If X25519 is broken (by a quantum computer), ML-KEM-768 still provides
|
||||
~192-bit post-quantum security.
|
||||
- Both must be broken simultaneously to compromise the shared secret.
|
||||
|
||||
## Wire Format
|
||||
|
||||
The hybrid envelope uses a versioned binary format:
|
||||
|
||||
```text
|
||||
Offset Length Field
|
||||
------ ------ -----
|
||||
0 1 Version byte (0x01)
|
||||
1 32 X25519 ephemeral public key
|
||||
33 1088 ML-KEM-768 ciphertext
|
||||
1121 12 ChaCha20-Poly1305 nonce
|
||||
1133 var ChaCha20-Poly1305 AEAD ciphertext (plaintext + 16-byte tag)
|
||||
```
|
||||
|
||||
Total overhead: **1133 bytes** of header + 16 bytes of AEAD tag = **1149 bytes**
|
||||
per message (in addition to the plaintext length).
|
||||
|
||||
The version byte enables future algorithm agility. Version `0x01` denotes
|
||||
X25519 + ML-KEM-768 + HKDF-SHA256 + ChaCha20-Poly1305.
|
||||
|
||||
## Encryption Flow
|
||||
|
||||
```text
|
||||
Sender Recipient
|
||||
------ ---------
|
||||
HybridKeypair::generate()
|
||||
├── x25519_sk, x25519_pk
|
||||
└── mlkem_dk, mlkem_ek
|
||||
│
|
||||
public_key()│
|
||||
│
|
||||
┌─── HybridPublicKey ◄──────────────────────────────┘
|
||||
│ (x25519_pk: 32B, mlkem_ek: 1184B)
|
||||
│
|
||||
│ hybrid_encrypt(pk, plaintext):
|
||||
│ 1. eph_sk = EphemeralSecret::random()
|
||||
│ 2. eph_pk = PublicKey::from(&eph_sk)
|
||||
│ 3. x25519_ss = eph_sk.diffie_hellman(pk.x25519_pk)
|
||||
│ 4. (mlkem_ct, mlkem_ss) = pk.mlkem_ek.encapsulate()
|
||||
│ 5. ikm = x25519_ss || mlkem_ss
|
||||
│ 6. (key, nonce) = HKDF(ikm, info)
|
||||
│ 7. ct = ChaCha20Poly1305::encrypt(key, nonce, plaintext)
|
||||
│ 8. envelope = ver || eph_pk || mlkem_ct || nonce || ct
|
||||
│
|
||||
└──────────── envelope ──────────────────────────────►
|
||||
│
|
||||
hybrid_decrypt(kp, envelope):
|
||||
1. Parse ver, eph_pk, mlkem_ct, nonce, ct
|
||||
2. x25519_ss = kp.x25519_sk.dh(eph_pk)
|
||||
3. mlkem_ss = kp.mlkem_dk.decapsulate(mlkem_ct)
|
||||
4. ikm = x25519_ss || mlkem_ss
|
||||
5. (key, _) = HKDF(ikm, info)
|
||||
6. plaintext = ChaCha20Poly1305::decrypt(key, nonce, ct)
|
||||
```
|
||||
|
||||
## Integration Plan (M5)
|
||||
|
||||
The hybrid KEM is currently a standalone module. Milestone M5 will integrate it
|
||||
into the MLS pipeline by creating a custom `OpenMlsCryptoProvider` that uses the
|
||||
hybrid KEM for HPKE init key exchange:
|
||||
|
||||
1. **Custom crypto provider:** Wrap the existing `StoreCrypto` with a hybrid KEM
|
||||
layer that intercepts HPKE operations and replaces the classical X25519 DHKEM
|
||||
with the hybrid X25519 + ML-KEM-768 KEM.
|
||||
|
||||
2. **KeyPackage extension:** Store the hybrid public key (1216 bytes:
|
||||
32B X25519 + 1184B ML-KEM) in a custom MLS extension within the KeyPackage.
|
||||
|
||||
3. **Welcome encryption:** When creating a Welcome message, the hybrid KEM
|
||||
encrypts the group secrets instead of (or in addition to) the standard HPKE.
|
||||
|
||||
4. **Backward compatibility:** Groups can negotiate whether to use hybrid KEM
|
||||
via the MLS group context extensions. Classical-only clients can still
|
||||
participate in groups that do not require PQ protection.
|
||||
|
||||
## The PQ Gap (ADR-006)
|
||||
|
||||
There is an important asymmetry in quicnprotochat's post-quantum protection:
|
||||
|
||||
```text
|
||||
Layer Classical Protection Post-Quantum Protection
|
||||
---------------------------------------------------------------------
|
||||
QUIC/TLS 1.3 Yes (ECDHE) No
|
||||
Noise_XX Yes (X25519) No
|
||||
MLS content (M5+) Yes (X25519 DHKEM) Yes (hybrid KEM)
|
||||
```
|
||||
|
||||
**What this means:**
|
||||
|
||||
- **Message content** (the MLS application data) is protected against quantum
|
||||
adversaries from M5 onward. An attacker with a quantum computer cannot decrypt
|
||||
the message payload.
|
||||
|
||||
- **Transport metadata** (who connects to the server, when, message sizes) is
|
||||
protected only by classical cryptography. A quantum attacker who recorded the
|
||||
TLS/Noise handshake transcripts could, in theory, recover the transport session
|
||||
keys and observe the metadata.
|
||||
|
||||
This is the **PQ gap**: content is safe, but metadata is not.
|
||||
|
||||
### Why not PQ transport?
|
||||
|
||||
Post-quantum TLS (via ML-KEM in the TLS 1.3 handshake) is being standardized by
|
||||
the IETF and is supported by some TLS libraries, but `rustls` does not yet
|
||||
support it in a stable release. When `rustls` adds ML-KEM support, quicnprotochat
|
||||
will adopt it to close the PQ gap at the transport layer.
|
||||
|
||||
Similarly, post-quantum Noise patterns are an active research area but are not
|
||||
yet standardized. The `snow` crate does not currently support post-quantum DH
|
||||
primitives.
|
||||
|
||||
## Harvest-Now, Decrypt-Later Risk
|
||||
|
||||
The "harvest-now, decrypt-later" (HNDL) threat model assumes an adversary who:
|
||||
|
||||
1. Records all encrypted traffic today (inexpensive storage).
|
||||
2. Waits for a sufficiently powerful quantum computer (years or decades).
|
||||
3. Decrypts the recorded traffic retroactively.
|
||||
|
||||
In quicnprotochat's case:
|
||||
|
||||
- **Content is safe from M5 onward.** The hybrid KEM wrapping MLS content uses
|
||||
ML-KEM-768, which resists quantum attacks. Even if the recorded traffic is
|
||||
decrypted at the transport layer, the MLS ciphertext inside is still protected.
|
||||
|
||||
- **Transport metadata is at risk.** An HNDL attacker who records TLS/Noise
|
||||
handshakes today could, with a future quantum computer, recover the transport
|
||||
session keys and observe:
|
||||
- Which clients connected to the server and when.
|
||||
- Message sizes and timing patterns.
|
||||
- The encrypted MLS blobs (which they still cannot decrypt if hybrid KEM is
|
||||
active).
|
||||
|
||||
- **Content before M5 is at risk.** Messages sent before the hybrid KEM
|
||||
integration (M5) use classical-only MLS encryption. If the HPKE init key
|
||||
exchange used X25519-only DHKEM, a quantum attacker could recover the HPKE
|
||||
shared secret and decrypt the Welcome message, gaining access to the group
|
||||
state.
|
||||
|
||||
This risk is the primary motivation for deploying the hybrid KEM as early as
|
||||
possible.
|
||||
|
||||
## Key Sizes and Performance
|
||||
|
||||
| Component | Key/Ciphertext | Size |
|
||||
|-----------|---------------|------|
|
||||
| X25519 public key | `x25519_pk` | 32 bytes |
|
||||
| X25519 shared secret | DH result | 32 bytes |
|
||||
| ML-KEM-768 encapsulation key | `mlkem_ek` | 1,184 bytes |
|
||||
| ML-KEM-768 decapsulation key | `mlkem_dk` | 2,400 bytes |
|
||||
| ML-KEM-768 ciphertext | `mlkem_ct` | 1,088 bytes |
|
||||
| ML-KEM-768 shared secret | KEM result | 32 bytes |
|
||||
| Hybrid public key | `x25519_pk + mlkem_ek` | 1,216 bytes |
|
||||
| Hybrid envelope overhead | Header + AEAD tag | 1,149 bytes |
|
||||
|
||||
The ML-KEM-768 operations (keygen, encapsulate, decapsulate) are fast in
|
||||
software -- typically sub-millisecond on modern hardware. The primary cost is
|
||||
bandwidth, not computation.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory including ML-KEM-768
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- hybrid KEM key lifecycle
|
||||
- [Forward Secrecy](forward-secrecy.md) -- how FS interacts with PQ protection
|
||||
- [Threat Model](threat-model.md) -- harvest-now-decrypt-later in context
|
||||
- [Hybrid KEM: X25519 + ML-KEM-768](../protocol-layers/hybrid-kem.md) -- protocol layer details
|
||||
332
docs/src/cryptography/threat-model.md
Normal file
332
docs/src/cryptography/threat-model.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Threat Model
|
||||
|
||||
This page defines the attacker models quicnprotochat is designed to resist,
|
||||
catalogues what is and is not protected, identifies known gaps in the current
|
||||
implementation, and outlines future mitigations.
|
||||
|
||||
## Attacker Models
|
||||
|
||||
### 1. Passive Eavesdropper
|
||||
|
||||
**Capabilities:** Records all network traffic between clients and the server.
|
||||
Can observe IP addresses, connection timing, message sizes, and encrypted
|
||||
payloads. Cannot modify traffic.
|
||||
|
||||
**What they learn:**
|
||||
|
||||
- Connection metadata: which IP addresses connect to the server and when.
|
||||
- Message timing and sizes: observable patterns (e.g., message frequency,
|
||||
payload lengths) that could reveal communication patterns.
|
||||
- Encrypted payloads: TLS 1.3 ciphertext containing Noise ciphertext containing
|
||||
MLS ciphertext. Three layers of encryption must be broken to access content.
|
||||
|
||||
**What they cannot learn:**
|
||||
|
||||
- Message content: protected by MLS encryption inside Noise inside TLS.
|
||||
- Group membership details: MLS Commits are encrypted.
|
||||
- Which specific recipient a message is destined for (from the network
|
||||
perspective, all messages go to the server).
|
||||
|
||||
**Residual risk:** Traffic analysis. Even without decryption, the timing and
|
||||
size of messages can reveal communication patterns. For example, a message sent
|
||||
by Alice followed shortly by a message to Bob may indicate they are in the same
|
||||
group. See [Future Mitigations](#future-mitigations) for countermeasures.
|
||||
|
||||
### 2. Active Network Attacker (MITM)
|
||||
|
||||
**Capabilities:** Can intercept, modify, drop, and inject network traffic.
|
||||
Positioned between the client and server (e.g., compromised router, ISP, or
|
||||
state-level adversary).
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- Attempt TLS 1.3 MITM: TLS 1.3 prevents this if the client validates the
|
||||
server's certificate. However, quicnprotochat currently uses **self-signed
|
||||
certificates**, which means the client has no CA chain to verify. On the first
|
||||
connection, a MITM could present their own certificate and intercept the
|
||||
session (trust-on-first-use vulnerability).
|
||||
- Denial of service: drop or delay packets.
|
||||
- Traffic analysis: same as passive eavesdropper, with the added ability to
|
||||
inject timing perturbations.
|
||||
|
||||
**What they cannot do (assuming no cert MITM):**
|
||||
|
||||
- Decrypt TLS/Noise traffic: both use authenticated ephemeral key exchange.
|
||||
- Forge MLS messages: MLS Commits and application messages are signed by the
|
||||
sender's Ed25519 identity key. The attacker does not possess any member's
|
||||
signing key.
|
||||
- Inject members into MLS groups: adding a member requires a valid KeyPackage
|
||||
signed by the new member's identity key.
|
||||
|
||||
**Current weakness:** Self-signed TLS certificates. See
|
||||
[Known Gaps](#known-gaps).
|
||||
|
||||
### 3. Compromised Server
|
||||
|
||||
**Capabilities:** Full access to the server's memory, disk, and network
|
||||
interfaces. Can read all data stored on the server, modify server behavior,
|
||||
and observe all client connections.
|
||||
|
||||
**What the server sees:**
|
||||
|
||||
- Connection metadata: which clients connect, when, how often, from which IPs.
|
||||
- Ed25519 public keys: used as delivery queue indices. The server knows the
|
||||
public identity key of every registered client.
|
||||
- Message sizes and timing: the server forwards MLS messages and can observe
|
||||
their sizes and the timing of enqueue/fetch operations.
|
||||
- Encrypted MLS blobs: the server stores and forwards MLS ciphertext but cannot
|
||||
decrypt it (the server is MLS-unaware by design, per ADR-004).
|
||||
|
||||
**What the server cannot do:**
|
||||
|
||||
- **Decrypt message content:** The server does not hold any MLS group keys. MLS
|
||||
application messages are encrypted end-to-end between group members. The
|
||||
server sees only opaque ciphertext.
|
||||
- **Forge MLS messages:** MLS messages are signed by the sender's Ed25519 key.
|
||||
The server does not possess any member's signing key and cannot produce valid
|
||||
MLS signatures.
|
||||
- **Read past messages:** Even if the server stored old MLS ciphertext, it
|
||||
cannot decrypt it. Forward secrecy at the MLS layer (epoch key deletion)
|
||||
ensures that even compromising a member's state in the future does not reveal
|
||||
past epoch keys.
|
||||
|
||||
**What the server can do maliciously:**
|
||||
|
||||
- **Traffic analysis:** Correlate senders and recipients based on timing,
|
||||
message sizes, and queue access patterns.
|
||||
- **Selective denial of service:** Drop or delay specific messages or refuse
|
||||
service to specific clients.
|
||||
- **Metadata correlation:** Link Ed25519 public keys to IP addresses and
|
||||
connection patterns.
|
||||
- **Replay (limited):** Re-deliver an MLS message. MLS has replay protection
|
||||
via epoch numbers and message counters, so the recipient will reject the
|
||||
duplicate.
|
||||
- **KeyPackage manipulation:** Withhold or substitute KeyPackages during the
|
||||
join flow. If the server substitutes a KeyPackage, the resulting MLS group
|
||||
would include the attacker's key, but the legitimate member would not be able
|
||||
to join (they would not receive a matching Welcome). This is detectable.
|
||||
|
||||
### 4. Compromised Client
|
||||
|
||||
**Capabilities:** Full access to a group member's device, including the MLS
|
||||
group state, Ed25519 identity key, and any stored messages.
|
||||
|
||||
**What the attacker learns:**
|
||||
|
||||
- **Current epoch messages:** The attacker can decrypt all messages in the
|
||||
current MLS epoch from all group members (epoch keys are shared group secrets).
|
||||
- **Identity key:** The attacker obtains the member's Ed25519 signing key and
|
||||
can impersonate the member (sign messages, create KeyPackages).
|
||||
|
||||
**What the attacker cannot learn:**
|
||||
|
||||
- **Past epoch messages:** Protected by [forward secrecy](forward-secrecy.md).
|
||||
Old epoch keys have been deleted by openmls.
|
||||
- **Future epoch messages (after healing):** Protected by
|
||||
[post-compromise security](post-compromise-security.md). After the next
|
||||
Commit or Update, the ratchet tree is re-randomized and the attacker is
|
||||
locked out.
|
||||
|
||||
**Healing mechanism:**
|
||||
|
||||
1. The compromised member (or any other member) issues a Commit.
|
||||
2. The ratchet tree is updated with fresh key material.
|
||||
3. The attacker's stale state cannot derive the new epoch keys.
|
||||
4. The attacker is locked out of future epochs.
|
||||
|
||||
The healing window is the time between the compromise and the next Commit. See
|
||||
[Post-Compromise Security](post-compromise-security.md) for details.
|
||||
|
||||
## What Is Protected
|
||||
|
||||
| Asset | Protection Mechanism | Against |
|
||||
|-------|---------------------|---------|
|
||||
| Message content | MLS end-to-end encryption (AES-128-GCM) | All attacker models |
|
||||
| Message integrity | MLS signing (Ed25519) | Forgery by server or network |
|
||||
| Group membership changes | MLS Commits (signed, authenticated) | Unauthorized modification |
|
||||
| Key exchange material | Single-use HPKE init keys | Replay, forward compromise |
|
||||
| Transport confidentiality | TLS 1.3 + Noise\_XX (double encryption) | Passive eavesdropper |
|
||||
| Transport integrity | TLS 1.3 AEAD + Noise AEAD | Active network attacker |
|
||||
| Past messages | Forward secrecy (epoch key deletion) | Future client compromise |
|
||||
| Future messages | Post-compromise security (ratchet tree update) | Past client compromise |
|
||||
|
||||
## What Is NOT Protected (Current State)
|
||||
|
||||
| Asset | Visibility | Observer |
|
||||
|-------|-----------|----------|
|
||||
| Transport metadata (who connects, when) | IP addresses, connection timing | Network adversary, server |
|
||||
| Message timing and sizes | Observable in TLS records | Network adversary, server |
|
||||
| Recipient identity | Server routes by Ed25519 public key | Server |
|
||||
| Sender identity (partial) | Server can correlate connections to senders | Server |
|
||||
| Number of groups a client belongs to | Observable via message patterns | Server (with analysis) |
|
||||
| Client IP address | Visible in TCP/QUIC connection | Server, network adversary |
|
||||
|
||||
## Known Gaps
|
||||
|
||||
### Self-Signed TLS Certificates
|
||||
|
||||
The server uses self-signed TLS certificates generated at startup via `rcgen`.
|
||||
Clients currently accept any server certificate without CA chain validation.
|
||||
This makes the system vulnerable to a man-in-the-middle attack on the first
|
||||
connection: an attacker positioned between the client and server can present
|
||||
their own certificate and intercept all traffic.
|
||||
|
||||
**Impact:** Complete loss of transport confidentiality and integrity for affected
|
||||
connections. MLS content remains protected (the MITM cannot decrypt MLS
|
||||
ciphertext or forge MLS signatures), but the attacker can observe encrypted MLS
|
||||
blobs, perform traffic analysis, and potentially block or delay messages.
|
||||
|
||||
**Mitigation path:** Implement certificate pinning (trust-on-first-use) or
|
||||
integrate with a public CA (e.g., Let's Encrypt). Certificate transparency logs
|
||||
could provide an additional detection mechanism.
|
||||
|
||||
### No Client Authentication on the Delivery Service
|
||||
|
||||
The Delivery Service does not currently authenticate clients. Anyone who knows
|
||||
a recipient's Ed25519 public key can enqueue messages for that recipient. This
|
||||
enables spam and potential denial-of-service by flooding a recipient's queue.
|
||||
|
||||
**Impact:** Queue flooding, spam delivery. MLS provides its own authentication
|
||||
(the recipient will reject messages not signed by a group member), so forged
|
||||
content will not be accepted, but the recipient must still download and attempt
|
||||
to process the spam.
|
||||
|
||||
**Mitigation path:** The AUTHZ\_PLAN introduces token-based authentication,
|
||||
binding identityKey to accounts and requiring valid access tokens for all
|
||||
DS operations.
|
||||
|
||||
### No Rate Limiting
|
||||
|
||||
The server does not currently enforce per-client or per-IP rate limits. A
|
||||
malicious client could flood the server with requests, consuming resources and
|
||||
degrading service for other users.
|
||||
|
||||
**Impact:** Denial of service.
|
||||
|
||||
**Mitigation path:** The AUTHZ\_PLAN specifies per-IP and per-account/device
|
||||
rate limits (e.g., 50 requests/second, 5 MB payload cap).
|
||||
|
||||
### BasicCredential Only
|
||||
|
||||
MLS `BasicCredential` contains only the raw Ed25519 public key bytes. There is
|
||||
no certificate authority chain, no credential revocation mechanism, and no
|
||||
binding to a human-readable identity (e.g., phone number, email).
|
||||
|
||||
**Impact:** No way to verify that a public key belongs to a specific person
|
||||
without out-of-band verification (e.g., comparing fingerprints in person). An
|
||||
attacker who compromises the Authentication Service could substitute public keys.
|
||||
|
||||
**Mitigation path:** Implement X.509-based MLS credentials with a certificate
|
||||
chain, or integrate with a Key Transparency system that provides a verifiable
|
||||
log of public key bindings.
|
||||
|
||||
### Classical-Only Transport
|
||||
|
||||
As discussed in [Post-Quantum Readiness](post-quantum-readiness.md), the
|
||||
transport layer (TLS 1.3, Noise\_XX) uses classical-only cryptography. An
|
||||
adversary performing harvest-now-decrypt-later (HNDL) could record transport
|
||||
traffic today and decrypt it with a future quantum computer, revealing transport
|
||||
metadata.
|
||||
|
||||
**Impact:** Future exposure of transport metadata (not content, assuming
|
||||
hybrid KEM is active for MLS).
|
||||
|
||||
**Mitigation path:** Adopt post-quantum TLS (ML-KEM in TLS 1.3 handshake) when
|
||||
`rustls` supports it. Investigate post-quantum Noise patterns.
|
||||
|
||||
## Future Mitigations
|
||||
|
||||
### Sealed Sender
|
||||
|
||||
**Goal:** Hide the sender's identity from the server.
|
||||
|
||||
**Approach:** Encrypt the sender's identity inside the MLS ciphertext. The
|
||||
server cannot determine who sent a message -- it only knows the recipient
|
||||
(delivery queue index). Signal implements a version of this as "Sealed Sender."
|
||||
|
||||
**Benefit:** Reduces the server's metadata visibility from "who sent to whom"
|
||||
to "someone sent to this recipient."
|
||||
|
||||
### Private Information Retrieval (PIR)
|
||||
|
||||
**Goal:** Fetch messages without revealing the recipient's identity to the
|
||||
server.
|
||||
|
||||
**Approach:** Use PIR protocols (e.g., SealPIR, SimplePIR) to query the
|
||||
delivery queue without the server learning which queue was accessed.
|
||||
|
||||
**Benefit:** Combined with Sealed Sender, this would make the server metadata-blind:
|
||||
it would know only that "someone sent something to someone."
|
||||
|
||||
**Trade-off:** PIR is computationally expensive and may increase latency
|
||||
significantly, especially for large mailboxes.
|
||||
|
||||
### Key Transparency
|
||||
|
||||
**Goal:** Detect public key substitution attacks.
|
||||
|
||||
**Approach:** Publish all Ed25519 public keys in a verifiable, append-only log
|
||||
(similar to Certificate Transparency for TLS). Clients can audit the log to
|
||||
verify that their public key has not been replaced by an attacker.
|
||||
|
||||
**Benefit:** Detects attacks where the server (or an attacker who compromised
|
||||
the server) substitutes a victim's public key with the attacker's key.
|
||||
|
||||
### OPAQUE Authentication
|
||||
|
||||
**Goal:** Zero-knowledge password authentication.
|
||||
|
||||
**Approach:** Use the OPAQUE protocol (RFC 9497) for client-server
|
||||
authentication. OPAQUE allows the client to prove knowledge of a password
|
||||
without revealing it to the server, even during registration.
|
||||
|
||||
**Benefit:** The server never learns the client's password, preventing
|
||||
credential theft in a server compromise.
|
||||
|
||||
### Tor/I2P Integration
|
||||
|
||||
**Goal:** Hide client IP addresses from the server and network adversaries.
|
||||
|
||||
**Approach:** Route QUIC connections through the Tor network or I2P. The server
|
||||
sees only the Tor exit node's IP, not the client's real IP.
|
||||
|
||||
**Benefit:** Prevents the server and network adversaries from linking
|
||||
connections to physical locations or ISP accounts.
|
||||
|
||||
**Trade-off:** Significant latency increase. QUIC over Tor requires careful
|
||||
configuration to avoid leaking the real IP through WebRTC-style mechanisms.
|
||||
|
||||
### Padding and Traffic Shaping
|
||||
|
||||
**Goal:** Defeat traffic analysis based on message sizes and timing.
|
||||
|
||||
**Approach:** Pad all messages to fixed sizes (or random sizes from a
|
||||
distribution) and send dummy messages at regular intervals to mask real
|
||||
communication patterns.
|
||||
|
||||
**Benefit:** Makes it harder for network adversaries and the server to infer
|
||||
communication patterns from traffic analysis.
|
||||
|
||||
**Trade-off:** Increased bandwidth usage.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Threat | Current Protection | Gap | Planned Fix |
|
||||
|--------|-------------------|-----|-------------|
|
||||
| Passive eavesdropper | TLS + Noise + MLS (3 layers) | Traffic analysis | Padding, Tor |
|
||||
| Active MITM | TLS 1.3 + Noise\_XX | Self-signed certs | Cert pinning, CA |
|
||||
| Compromised server | MLS E2E encryption | Metadata visible | Sealed Sender, PIR |
|
||||
| Compromised client | FS + PCS | Current epoch exposed | Periodic Updates |
|
||||
| Spam/flooding | None | No auth on DS | AUTHZ\_PLAN |
|
||||
| Key substitution | None | BasicCredential only | Key Transparency |
|
||||
| Quantum adversary (content) | Hybrid KEM (M5+) | Pre-M5 messages | Deploy hybrid ASAP |
|
||||
| Quantum adversary (transport) | None | Classical TLS/Noise | PQ TLS, PQ Noise |
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory and security levels
|
||||
- [Forward Secrecy](forward-secrecy.md) -- protecting past messages
|
||||
- [Post-Compromise Security](post-compromise-security.md) -- protecting future messages
|
||||
- [Post-Quantum Readiness](post-quantum-readiness.md) -- ML-KEM-768 and the PQ gap
|
||||
- [Ed25519 Identity Keys](identity-keys.md) -- identity key used for MLS credentials
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- key destruction guarantees
|
||||
191
docs/src/cryptography/transport-keys.md
Normal file
191
docs/src/cryptography/transport-keys.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# X25519 Transport Keys
|
||||
|
||||
The X25519 transport keypair is used for mutual authentication in the Noise\_XX
|
||||
handshake. Unlike the [Ed25519 identity key](identity-keys.md), which is a
|
||||
signing key, the X25519 key performs Diffie-Hellman key exchange to establish
|
||||
encrypted transport sessions.
|
||||
|
||||
**Source:** `crates/quicnprotochat-core/src/keypair.rs`
|
||||
|
||||
## Structure
|
||||
|
||||
The `NoiseKeypair` struct holds two fields:
|
||||
|
||||
```rust
|
||||
pub struct NoiseKeypair {
|
||||
/// Private scalar -- zeroized on drop via x25519_dalek's ZeroizeOnDrop impl.
|
||||
private: StaticSecret,
|
||||
/// Corresponding public key -- derived from private at construction time.
|
||||
public: PublicKey,
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Size | Secret? |
|
||||
|-------|------|------|---------|
|
||||
| `private` | `x25519_dalek::StaticSecret` | 32 bytes | Yes -- `ZeroizeOnDrop` |
|
||||
| `public` | `x25519_dalek::PublicKey` | 32 bytes | No -- safe to log/transmit |
|
||||
|
||||
## Key Generation
|
||||
|
||||
A fresh keypair is generated from the OS CSPRNG:
|
||||
|
||||
```rust
|
||||
use quicnprotochat_core::keypair::NoiseKeypair;
|
||||
|
||||
let keypair = NoiseKeypair::generate();
|
||||
// private: random 32-byte scalar from OsRng
|
||||
// public: derived via Curve25519 scalar multiplication
|
||||
```
|
||||
|
||||
Internally:
|
||||
|
||||
```rust
|
||||
pub fn generate() -> Self {
|
||||
let private = StaticSecret::random_from_rng(OsRng);
|
||||
let public = PublicKey::from(&private);
|
||||
Self { private, public }
|
||||
}
|
||||
```
|
||||
|
||||
The `StaticSecret::random_from_rng` call uses the operating system's CSPRNG
|
||||
(`getrandom` on Linux, `SecRandomCopyBytes` on macOS) and is suitable for
|
||||
generating long-lived static identity keys.
|
||||
|
||||
## Accessing Key Material
|
||||
|
||||
### Private Key Bytes
|
||||
|
||||
The `private_bytes()` method returns the raw 32-byte private scalar wrapped in
|
||||
`Zeroizing<[u8; 32]>`:
|
||||
|
||||
```rust
|
||||
pub fn private_bytes(&self) -> Zeroizing<[u8; 32]> {
|
||||
Zeroizing::new(self.private.to_bytes())
|
||||
}
|
||||
```
|
||||
|
||||
The `Zeroizing` wrapper ensures the caller's copy of the key material is
|
||||
overwritten with zeros when it goes out of scope. The intended usage pattern is
|
||||
to pass the bytes directly to `snow::Builder` and let the wrapper drop
|
||||
immediately:
|
||||
|
||||
```rust
|
||||
let private = keypair.private_bytes();
|
||||
let session = snow::Builder::new(params)
|
||||
.local_private_key(&private[..])
|
||||
.build_initiator()?;
|
||||
// private is zeroized here when it falls out of scope.
|
||||
```
|
||||
|
||||
### Public Key Bytes
|
||||
|
||||
The `public_bytes()` method returns a plain `[u8; 32]`:
|
||||
|
||||
```rust
|
||||
pub fn public_bytes(&self) -> [u8; 32] {
|
||||
self.public.to_bytes()
|
||||
}
|
||||
```
|
||||
|
||||
The public key is not secret and may be freely cloned, logged, or transmitted
|
||||
over the wire.
|
||||
|
||||
## Zeroization Strategy
|
||||
|
||||
The `NoiseKeypair` has two layers of zeroization protection:
|
||||
|
||||
1. **`StaticSecret` (inner):** The `x25519_dalek` crate implements
|
||||
`ZeroizeOnDrop` on `StaticSecret`. When the `NoiseKeypair` struct is dropped,
|
||||
the private scalar is automatically overwritten with zeros.
|
||||
|
||||
2. **`Zeroizing<[u8; 32]>` (accessor):** When callers use `private_bytes()`, the
|
||||
returned copy is also wrapped in `Zeroizing`, so the caller's copy is zeroed
|
||||
on drop too.
|
||||
|
||||
This dual-layer approach ensures that key material does not linger in memory
|
||||
whether the key is accessed by value or held in the struct.
|
||||
|
||||
## Debug Redaction
|
||||
|
||||
The `Debug` implementation intentionally redacts the private key and shows only
|
||||
the first 4 bytes of the public key as a sanity identifier:
|
||||
|
||||
```rust
|
||||
impl std::fmt::Debug for NoiseKeypair {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let pub_bytes = self.public_bytes();
|
||||
write!(
|
||||
f,
|
||||
"NoiseKeypair {{ public: {:02x}{:02x}{:02x}{:02x}..., private: [redacted] }}",
|
||||
pub_bytes[0], pub_bytes[1], pub_bytes[2], pub_bytes[3],
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This prevents accidental leakage of secret material through logging or
|
||||
`println!("{:?}", keypair)`.
|
||||
|
||||
## Role in Noise\_XX
|
||||
|
||||
The Noise\_XX handshake pattern performs mutual authentication: both initiator
|
||||
and responder prove possession of their static X25519 keys. The handshake
|
||||
proceeds in three messages:
|
||||
|
||||
```text
|
||||
→ e (initiator sends ephemeral public key)
|
||||
← e, ee, s, es (responder sends ephemeral + static, DH results)
|
||||
→ s, se (initiator sends static, final DH result)
|
||||
```
|
||||
|
||||
After the handshake completes, both parties have:
|
||||
|
||||
- Authenticated each other's static X25519 public keys.
|
||||
- Derived symmetric transport keys from the DH shared secrets.
|
||||
- Established forward secrecy via the ephemeral keys (which are discarded).
|
||||
|
||||
The `NoiseKeypair` provides the `s` (static) key in this pattern. Ephemeral keys
|
||||
(`e`) are generated internally by `snow` during each handshake.
|
||||
|
||||
## Ephemeral vs Static
|
||||
|
||||
In the context of Noise\_XX:
|
||||
|
||||
- **Ephemeral keys** are generated per-handshake by `snow` and discarded after
|
||||
key derivation. They provide forward secrecy.
|
||||
- **Static keys** (the `NoiseKeypair`) are longer-lived and provide identity.
|
||||
In the current implementation, the server generates a new `NoiseKeypair` per
|
||||
process start, and the client generates one per connection.
|
||||
|
||||
## Persistence
|
||||
|
||||
`NoiseKeypair` **intentionally does not implement `Serialize`**. Key persistence
|
||||
to disk is deferred to milestone M6, which will add:
|
||||
|
||||
- Appropriate file permission checks (e.g., `0600` on Unix).
|
||||
- Optional passphrase-based encryption of the key file.
|
||||
- A key rotation mechanism.
|
||||
|
||||
Until M6, the transport key is ephemeral to the process lifetime. This is
|
||||
acceptable because the Noise key is not used for MLS group membership -- that
|
||||
role belongs to the [Ed25519 identity key](identity-keys.md).
|
||||
|
||||
## Comparison with Ed25519 Identity Key
|
||||
|
||||
| Property | X25519 Noise Key | Ed25519 Identity Key |
|
||||
|----------|-----------------|---------------------|
|
||||
| Curve | Montgomery (Curve25519) | Twisted Edwards (Ed25519) |
|
||||
| Operation | Diffie-Hellman key exchange | Digital signatures |
|
||||
| Purpose | Noise\_XX mutual authentication | MLS credentials, AS registration |
|
||||
| Lifetime | Per process / per connection | Permanent (per client) |
|
||||
| Serialization | Not implemented | Serde (seed bytes) |
|
||||
| Zeroize | `ZeroizeOnDrop` (x25519-dalek) | `Zeroizing<[u8; 32]>` (manual) |
|
||||
| Source file | `keypair.rs` | `identity.rs` |
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [Cryptography Overview](overview.md) -- algorithm inventory
|
||||
- [Ed25519 Identity Keys](identity-keys.md) -- the other keypair
|
||||
- [Key Lifecycle and Zeroization](key-lifecycle.md) -- full lifecycle diagram
|
||||
- [Forward Secrecy](forward-secrecy.md) -- how ephemeral DH provides FS at the transport layer
|
||||
- [Noise\_XX Handshake](../protocol-layers/noise-xx.md) -- protocol details
|
||||
Reference in New Issue
Block a user