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
|
||||
Reference in New Issue
Block a user