docs: add threat model, crypto boundaries, and audit scope documents
Security audit preparation: - Threat model with STRIDE analysis and 5 threat actors - Crypto boundaries documenting all 11 primitives and key lifecycle - Audit scope document for external security firms
This commit is contained in:
@@ -124,6 +124,14 @@
|
||||
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
- [Threat Model](security/threat-model.md)
|
||||
- [Cryptographic Boundaries](security/crypto-boundaries.md)
|
||||
- [Audit Scope](security/audit-scope.md)
|
||||
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
- [Coding Standards](contributing/coding-standards.md)
|
||||
|
||||
148
docs/src/security/audit-scope.md
Normal file
148
docs/src/security/audit-scope.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Security Audit Scope
|
||||
|
||||
## Project Summary
|
||||
|
||||
quicprochat is a production-grade end-to-end encrypted group messenger implemented in Rust. It uses MLS (RFC 9420) for group key agreement with a hybrid post-quantum KEM (X25519 + ML-KEM-768), OPAQUE for password-authenticated key exchange, and QUIC + TLS 1.3 as the transport layer. The project comprises approximately 38,000 lines of Rust across 9 workspace crates, with 300+ tests passing.
|
||||
|
||||
## Scope
|
||||
|
||||
### Primary Scope (Critical)
|
||||
|
||||
These components handle all cryptographic operations and should receive the deepest scrutiny.
|
||||
|
||||
#### `quicprochat-core` -- All Cryptographic Primitives
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| `src/hybrid_kem.rs` | ~630 | Hybrid X25519 + ML-KEM-768 KEM: key generation, encrypt/decrypt, encapsulate/decapsulate, HKDF key derivation |
|
||||
| `src/hybrid_crypto.rs` | ~540 | OpenMLS `OpenMlsCrypto` + `OpenMlsProvider` implementation with hybrid HPKE routing |
|
||||
| `src/identity.rs` | ~250 | Ed25519 identity keypair: generation, signing, verification, zeroization |
|
||||
| `src/group.rs` | ~1080 | MLS group state machine: create, join, add/remove members, send/receive, epoch management |
|
||||
| `src/keypackage.rs` | ~100 | MLS KeyPackage generation with hybrid init keys |
|
||||
| `src/keystore.rs` | ~710 | Disk-backed OpenMLS key store with file permission restrictions |
|
||||
| `src/sealed_sender.rs` | ~160 | Sender identity + Ed25519 signature envelope inside MLS payload |
|
||||
| `src/pq_noise.rs` | ~690 | Post-quantum Noise_XX handshake with ML-KEM-768 mixing |
|
||||
| `src/opaque_auth.rs` | ~20 | OPAQUE cipher suite definition (Ristretto255, Triple-DH, Argon2id) |
|
||||
| `src/recovery.rs` | ~340 | Recovery code generation, Argon2id key derivation, encrypted backup bundles |
|
||||
| `src/padding.rs` | ~270 | Message padding to fixed buckets + uniform boundary padding |
|
||||
| `src/safety_numbers.rs` | ~80 | Signal-style safety number computation |
|
||||
| `src/transcript.rs` | ~400 | Encrypted hash-chained transcript archive |
|
||||
|
||||
#### `quicprochat-server/src/domain/auth.rs` -- OPAQUE Server Logic
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| `src/domain/auth.rs` | ~170 | OPAQUE registration (start/finish), session token validation, expiry cleanup |
|
||||
|
||||
### Secondary Scope (Important)
|
||||
|
||||
These components handle transport security and trust anchors. They are lower risk than the primary scope but still security-relevant.
|
||||
|
||||
#### `quicprochat-rpc` -- RPC Transport Security
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| `src/framing.rs` | ~200 | Wire format encoding/decoding, payload size limits (4 MiB max) |
|
||||
| `src/auth_handshake.rs` | ~80 | Session token exchange over QUIC bi-stream |
|
||||
| `src/server.rs` | ~300 | QUIC server setup, TLS configuration, connection handling |
|
||||
| `src/middleware.rs` | ~200 | Tower middleware: rate limiting, timeouts |
|
||||
|
||||
#### `quicprochat-kt` -- Key Transparency
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| `src/lib.rs` | ~65 | Merkle log hash functions (leaf_hash, node_hash) with RFC 6962 domain separation |
|
||||
| `src/tree.rs` | ~150 | Append-only Merkle tree (insert, root computation) |
|
||||
| `src/proof.rs` | ~100 | Inclusion proof generation and verification |
|
||||
| `src/revocation.rs` | ~100 | Key revocation log with reason codes |
|
||||
|
||||
#### `quicprochat-server` -- Security-Relevant Server Components
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| `src/domain/rate_limit.rs` | ~150 | Per-client rate limiting |
|
||||
| `src/domain/traffic_resistance.rs` | ~100 | Decoy traffic generation, timing jitter, payload padding |
|
||||
| `src/domain/moderation.rs` | ~100 | Abuse prevention (report handling) |
|
||||
| `src/tls.rs` | ~100 | TLS certificate loading and configuration |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following components are explicitly excluded from the audit:
|
||||
|
||||
- **Client SDKs**: `quicprochat-sdk`, Go/Python/TypeScript/WASM/FFI bindings (thin wrappers)
|
||||
- **CLI/TUI client**: `quicprochat-client` (user interface, no crypto logic)
|
||||
- **Plugin API**: `quicprochat-plugin-api` (`#![no_std]` C-ABI interface, no crypto)
|
||||
- **P2P networking**: `quicprochat-p2p` (iroh integration, feature-gated)
|
||||
- **Proto definitions**: `quicprochat-proto` (generated code, no security logic)
|
||||
- **Documentation**: mdBook sources, README, ROADMAP
|
||||
- **CI/CD**: GitHub Actions, Dockerfiles, justfile
|
||||
- **Non-crypto server domain**: user management, group metadata, blob storage, delivery routing
|
||||
|
||||
## Specific Questions for Auditors
|
||||
|
||||
### Hybrid KEM Construction
|
||||
|
||||
1. Is the hybrid combiner `HKDF-SHA256(salt, X25519_ss || ML-KEM_ss, info)` a sound dual-PRF construction? Does it provide IND-CCA2 security assuming either component is secure?
|
||||
2. Is the nonce handling correct? The hybrid KEM uses random 12-byte nonces (not derived from HKDF). Is there a nonce collision risk at the expected message volume?
|
||||
3. Is the `derive_from_ikm()` construction (HKDF -> seeded StdRng -> key generation) suitable for deterministic key derivation in the MLS HPKE key schedule?
|
||||
|
||||
### OpenMLS Integration
|
||||
|
||||
4. Does the `HybridCryptoProvider` correctly satisfy the `OpenMlsProvider` trait contract? Are there edge cases where hybrid key detection by length could fail or be spoofed?
|
||||
5. Is the `DiskKeyStore` implementation (wrapping `MemoryStorage` with disk flush) safe for concurrent access? Could a crash between `MemoryStorage` update and disk flush cause key loss?
|
||||
6. Is the MLS group lifecycle (create, add_member with merge_pending_commit, join_group via StagedWelcome) correctly implemented? Are there state consistency issues after failed operations?
|
||||
|
||||
### Timing Side-Channels
|
||||
|
||||
7. Are there timing side-channels in the OPAQUE registration/login flow? The `opaque-ke` crate uses Ristretto255 (constant-time), but is the surrounding code (deserialization, error handling) timing-safe?
|
||||
8. Is `constant_time_eq()` in `recovery.rs` correctly implemented? The early return on length mismatch is intentional (lengths are not secret), but verify the XOR-accumulate loop.
|
||||
|
||||
### Noise_XX + ML-KEM Layering
|
||||
|
||||
9. Is the PQ Noise handshake (`pq_noise.rs`) sound? Specifically:
|
||||
- Is ML-KEM ciphertext placement in message 2 correct (after `ee` DH, before `se` DH)?
|
||||
- Is `mix_key(mlkem_ss)` the right integration point for the post-quantum shared secret?
|
||||
- Does ML-KEM implicit rejection (pseudorandom wrong shared secret) cause any subtle failures beyond AEAD decryption error?
|
||||
|
||||
### Zeroization
|
||||
|
||||
10. Is zeroization complete? Specifically:
|
||||
- `x25519_dalek::StaticSecret` does not publicly implement `Zeroize`. Is the `HybridKeypair` struct's `x25519_sk` field securely erased on drop?
|
||||
- Are there intermediate buffers or stack copies of secret material that escape `Zeroizing` wrappers?
|
||||
- Does the `DiskKeyStore` flush-on-write pattern leave secret material in OS page cache or filesystem buffers?
|
||||
|
||||
### Key Transparency
|
||||
|
||||
11. Is the Merkle log construction (RFC 6962-style domain separation with `0x00`/`0x01` prefixes) resistant to second-preimage attacks?
|
||||
12. Could an adversarial server forge inclusion proofs for non-existent entries?
|
||||
|
||||
## Access
|
||||
|
||||
- **Repository**: `git clone https://github.com/quicprochat/quicprochat`
|
||||
- **Build**: `cargo build --workspace` (Rust 1.75+, no system dependencies -- `protobuf-src` vendors protoc)
|
||||
- **Test**: `cargo test --workspace` (300+ tests, runs in ~60s)
|
||||
- **Lint**: `cargo clippy --workspace -- -D warnings`
|
||||
- **Documentation**: `cd docs && mdbook build`
|
||||
|
||||
### Key Entry Points
|
||||
|
||||
- Crypto primitives: `crates/quicprochat-core/src/`
|
||||
- Server auth: `crates/quicprochat-server/src/domain/auth.rs`
|
||||
- RPC framing: `crates/quicprochat-rpc/src/framing.rs`
|
||||
- Key transparency: `crates/quicprochat-kt/src/`
|
||||
|
||||
## Timeline and Budget
|
||||
|
||||
Based on the scope (approximately 5,000 lines of security-critical Rust code in primary scope, plus ~1,000 lines in secondary scope), we recommend:
|
||||
|
||||
- **Duration**: 4-6 weeks (2 auditors)
|
||||
- **Budget range**: $80,000 - $150,000
|
||||
- **Firm type**: Specialized cryptography audit firm (e.g., NCC Group Cryptography Services, Trail of Bits, Cure53, Quarkslab)
|
||||
|
||||
### Suggested Audit Phases
|
||||
|
||||
1. **Week 1-2**: Hybrid KEM construction review, HKDF key derivation, zeroization audit
|
||||
2. **Week 2-3**: OpenMLS integration, MLS group lifecycle, KeyPackage handling
|
||||
3. **Week 3-4**: OPAQUE integration, PQ Noise handshake, timing analysis
|
||||
4. **Week 4-5**: Key Transparency Merkle log, transport security (RPC framing)
|
||||
5. **Week 5-6**: Report writing, finding triage, remediation discussion
|
||||
223
docs/src/security/crypto-boundaries.md
Normal file
223
docs/src/security/crypto-boundaries.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Cryptographic Boundaries
|
||||
|
||||
This document catalogs every cryptographic primitive used in quicprochat, its purpose, the implementing crate, key lifecycle, and relevant security properties.
|
||||
|
||||
## Cryptographic Primitives
|
||||
|
||||
### 1. X25519 (Elliptic Curve Diffie-Hellman)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Key exchange in Noise_XX handshake, classical component of hybrid KEM, MLS HPKE baseline |
|
||||
| **Crate** | `x25519-dalek 2.x` (features: `static_secrets`) |
|
||||
| **Security level** | ~128-bit classical |
|
||||
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |
|
||||
|
||||
Uses `StaticSecret` for long-term keys and `EphemeralSecret` for one-time key exchange. In the hybrid KEM, the X25519 shared secret is combined with the ML-KEM-768 shared secret via HKDF.
|
||||
|
||||
### 2. Ed25519 (Digital Signatures)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Identity keys, MLS credential signing, sealed sender signatures, delivery proofs, Key Transparency leaf binding |
|
||||
| **Crate** | `ed25519-dalek 2.x` (features: `rand_core`) |
|
||||
| **Security level** | ~128-bit classical |
|
||||
| **Source files** | `quicprochat-core/src/identity.rs`, `quicprochat-core/src/sealed_sender.rs` |
|
||||
|
||||
The `IdentityKeypair` stores the 32-byte private seed in `Zeroizing<[u8; 32]>`. Implements the `openmls_traits::signatures::Signer` trait for MLS integration.
|
||||
|
||||
### 3. ML-KEM-768 (Post-Quantum Key Encapsulation)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Post-quantum component of hybrid KEM (HPKE init keys in MLS), PQ Noise handshake |
|
||||
| **Crate** | `ml-kem 0.2` (FIPS 203) |
|
||||
| **Security level** | NIST Level 3 (~192-bit post-quantum) |
|
||||
| **Key sizes** | Encapsulation key: 1184 bytes, Decapsulation key: 2400 bytes, Ciphertext: 1088 bytes |
|
||||
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |
|
||||
|
||||
ML-KEM-768 uses implicit rejection: decapsulation with a wrong ciphertext returns a pseudorandom shared secret rather than an error, which is detected downstream by AEAD failure.
|
||||
|
||||
### 4. ChaCha20-Poly1305 (AEAD Encryption)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Hybrid KEM envelope encryption, PQ Noise transport encryption, transcript archive encryption, recovery bundle encryption |
|
||||
| **Crate** | `chacha20poly1305 0.10` |
|
||||
| **Security level** | 256-bit key, 128-bit authentication tag |
|
||||
| **Nonce** | 96-bit (12 bytes). Random in hybrid KEM; counter-based in PQ Noise transport |
|
||||
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs`, `quicprochat-core/src/transcript.rs`, `quicprochat-core/src/recovery.rs` |
|
||||
|
||||
In the hybrid KEM, the nonce is generated randomly per encryption (not derived from HKDF), preventing nonce reuse when the same shared secret is accidentally used more than once. In PQ Noise, nonces are monotonic counters (`u64` in the low 8 bytes of a 12-byte nonce).
|
||||
|
||||
### 5. HKDF-SHA256 (Key Derivation)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Deriving AEAD keys from combined X25519 + ML-KEM shared secrets, PQ Noise chaining key evolution, hybrid HPKE key schedule |
|
||||
| **Crate** | `hkdf 0.12` with `sha2 0.10` |
|
||||
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |
|
||||
|
||||
Domain separation labels:
|
||||
- Hybrid KEM: `info = "quicnprotochat-hybrid-v1"`, `salt = "quicnprotochat-hybrid-v1-salt"`
|
||||
- Hybrid HPKE keypair derivation: `info = "quicnprotochat-hybrid-hpke-keypair-v1"`
|
||||
- PQ Noise: `info = "quicprochat-pq-noise-v1"` (protocol name used as initial hash)
|
||||
- PQ Noise split: `info = "initiator"` / `info = "responder"` for transport key derivation
|
||||
|
||||
### 6. Argon2id (Password Hashing / Key Stretching)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | OPAQUE key stretching function (KSF), recovery code key derivation, SQLCipher key derivation (client-side) |
|
||||
| **Crate** | `argon2 0.5` |
|
||||
| **Parameters** | Memory: 19 MiB (`19 * 1024`), Iterations: 2, Parallelism: 1, Output: 32 bytes |
|
||||
| **Source files** | `quicprochat-core/src/recovery.rs`, `quicprochat-core/src/opaque_auth.rs` |
|
||||
|
||||
Used as the KSF in the OPAQUE cipher suite (`OpaqueSuite`). For recovery codes, derives a 32-byte encryption key from a 6-character alphanumeric code with a random 16-byte salt.
|
||||
|
||||
### 7. OPAQUE (Password-Authenticated Key Exchange)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | User registration and login without exposing passwords to the server |
|
||||
| **Crate** | `opaque-ke 4.x` (features: `ristretto255`, `argon2`) |
|
||||
| **OPRF group** | Ristretto255 (~128-bit security) |
|
||||
| **Key exchange** | Triple-DH (3DH) over Ristretto255 with SHA-512 |
|
||||
| **KSF** | Argon2id (see above) |
|
||||
| **Source files** | `quicprochat-core/src/opaque_auth.rs`, `quicprochat-server/src/domain/auth.rs` |
|
||||
|
||||
The `OpaqueSuite` struct defines the cipher suite used by both client and server, ensuring identical parameters.
|
||||
|
||||
### 8. MLS (Messaging Layer Security, RFC 9420)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Group key agreement, epoch rotation, forward secrecy, post-compromise security |
|
||||
| **Crate** | `openmls 0.8`, `openmls_rust_crypto 0.5` |
|
||||
| **Ciphersuite** | `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` |
|
||||
| **HPKE backend** | `HybridCryptoProvider` (custom `OpenMlsProvider` with hybrid KEM for HPKE operations) |
|
||||
| **Source files** | `quicprochat-core/src/group.rs`, `quicprochat-core/src/hybrid_crypto.rs`, `quicprochat-core/src/keypackage.rs` |
|
||||
|
||||
The `HybridCryptoProvider` detects hybrid keys by length (1216-byte public = hybrid, 32-byte = classical) and routes HPKE operations accordingly. All non-HPKE operations (AEAD, hash, signatures, KDF) delegate to `openmls_rust_crypto::RustCrypto`.
|
||||
|
||||
### 9. Noise_XX (Transport Handshake)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Mutual authentication and transport encryption (v1 transport layer) |
|
||||
| **Crate** | `snow 0.9` (v1), custom implementation in `pq_noise` module (v2 hybrid) |
|
||||
| **Pattern** | XX (mutual authentication with identity hiding for initiator) |
|
||||
| **Source files** | `quicprochat-core/src/pq_noise.rs` |
|
||||
|
||||
The PQ Noise handshake extends Noise_XX with an ML-KEM-768 encapsulation in message 2. The ML-KEM shared secret is mixed into the chaining key via `mix_key()`, so the final transport keys incorporate both X25519 DH and ML-KEM shared secrets.
|
||||
|
||||
### 10. SHA-256 (Hashing)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Identity key fingerprints, Key Transparency Merkle log, HKDF extract, recovery token derivation, transcript hash chains |
|
||||
| **Crate** | `sha2 0.10` |
|
||||
| **Source files** | `quicprochat-core/src/identity.rs`, `quicprochat-kt/src/lib.rs`, `quicprochat-core/src/recovery.rs`, `quicprochat-core/src/transcript.rs` |
|
||||
|
||||
### 11. HMAC-SHA256
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Purpose** | Safety number computation (Signal-style, 5200-iteration key stretching) |
|
||||
| **Crate** | `hmac 0.12` with `sha2 0.10` |
|
||||
| **Source files** | `quicprochat-core/src/safety_numbers.rs` |
|
||||
|
||||
## Key Lifecycle
|
||||
|
||||
### Identity Key (Ed25519)
|
||||
|
||||
| Phase | Mechanism |
|
||||
|-------|-----------|
|
||||
| **Generation** | `SigningKey::generate(&mut OsRng)` — 32-byte seed from OS CSPRNG |
|
||||
| **Storage** | Seed stored in `Zeroizing<[u8; 32]>` in memory; persisted via SQLCipher on disk with Argon2id-derived key |
|
||||
| **Distribution** | Public key (32 bytes) uploaded to server, stored in Key Transparency log |
|
||||
| **Rotation** | MLS self-update proposal (`propose_self_update()`) |
|
||||
| **Zeroization** | `Zeroizing<[u8; 32]>` zeroes seed bytes on `Drop` |
|
||||
|
||||
### Hybrid KEM Key (X25519 + ML-KEM-768)
|
||||
|
||||
| Phase | Mechanism |
|
||||
|-------|-----------|
|
||||
| **Generation** | `HybridKeypair::generate()` — X25519 via `StaticSecret::random_from_rng(OsRng)`, ML-KEM-768 via `MlKem768::generate(&mut OsRng)` |
|
||||
| **Derivation** | `HybridKeypair::derive_from_ikm(ikm)` — deterministic from IKM via HKDF-SHA256 + seeded StdRng (for MLS HPKE key schedule) |
|
||||
| **Storage** | `HybridKeypairBytes` with `Zeroizing<[u8; 32]>` for X25519 SK and `Zeroizing<Vec<u8>>` for ML-KEM DK; stored in `DiskKeyStore` |
|
||||
| **Distribution** | `HybridPublicKey` (x25519_pk + mlkem_ek = 1216 bytes) embedded in MLS KeyPackages |
|
||||
| **Consumption** | Single-use: each KeyPackage is consumed on fetch (ADR-005) |
|
||||
| **Zeroization** | `Zeroizing` wrappers on all private key material |
|
||||
|
||||
### MLS Group Epoch Keys
|
||||
|
||||
| Phase | Mechanism |
|
||||
|-------|-----------|
|
||||
| **Derivation** | Derived by OpenMLS key schedule from group secrets + commit transcript |
|
||||
| **Storage** | Held in `DiskKeyStore` (implements `StorageProvider`); flushed to disk on every write |
|
||||
| **Rotation** | Advanced on every MLS Commit (epoch increment) |
|
||||
| **Deletion** | Previous epoch keys are deleted by OpenMLS after processing; `DiskKeyStore` flushes deletions to disk |
|
||||
| **Persistence** | `DiskKeyStore::persistent(path)` stores to file with 0o600 permissions (Unix) |
|
||||
|
||||
### OPAQUE Server Record
|
||||
|
||||
| Phase | Mechanism |
|
||||
|-------|-----------|
|
||||
| **Generation** | `ServerSetup::new(&mut OsRng)` on server startup |
|
||||
| **Registration** | `ServerRegistration::start()` + `ServerRegistration::finish()` — server never sees plaintext password |
|
||||
| **Storage** | Serialized OPAQUE record stored via `Store::store_user_record()` |
|
||||
| **Login** | `ServerLogin::start()` + `ServerLogin::finish()` — session token issued on success |
|
||||
|
||||
### Session Tokens
|
||||
|
||||
| Phase | Mechanism |
|
||||
|-------|-----------|
|
||||
| **Generation** | Random bytes from OsRng on successful OPAQUE login |
|
||||
| **Storage** | Server-side `DashMap<Vec<u8>, SessionInfo>` (in memory) |
|
||||
| **Validation** | O(1) lookup; expired tokens are removed on access |
|
||||
| **Expiry** | Time-bounded (`expires_at` field in `SessionInfo`) |
|
||||
|
||||
## Zeroization Audit
|
||||
|
||||
The following types implement zeroization of secret material:
|
||||
|
||||
| Type | Secret Field | Zeroization Mechanism |
|
||||
|------|-------------|----------------------|
|
||||
| `IdentityKeypair` | `seed: Zeroizing<[u8; 32]>` | `Zeroizing` zeroes on `Drop` |
|
||||
| `HybridKeypairBytes` | `x25519_sk: Zeroizing<[u8; 32]>`, `mlkem_dk: Zeroizing<Vec<u8>>` | `Zeroizing` zeroes on `Drop` |
|
||||
| `HybridKeypair::private_to_bytes()` | Return value | Returns `Zeroizing<Vec<u8>>` |
|
||||
| `GroupMember::identity_seed()` | Return value | Returns `Zeroizing<[u8; 32]>` |
|
||||
| `derive_aead_key()` (hybrid_kem) | `ikm`, `key_bytes` | Both wrapped in `Zeroizing` |
|
||||
| `HandshakeState` (pq_noise) | `ck`, `k` | `Zeroizing<[u8; 32]>` for chaining key and encryption key |
|
||||
| `TransportKey` (pq_noise) | `key` | `Zeroizing<[u8; 32]>` |
|
||||
| `derive_recovery_key()` | Return value | Returns `Zeroizing<[u8; 32]>` |
|
||||
|
||||
**Known gap**: `x25519_dalek::StaticSecret` does not implement `Zeroize` in the public API. The `HybridKeypair` struct holds `StaticSecret` directly; its internal representation may not be zeroed on drop. This is mitigated by the `HybridKeypairBytes` serialization layer using `Zeroizing` for persistence, and by the short lifetime of `HybridKeypair` instances.
|
||||
|
||||
## Constant-Time Operations
|
||||
|
||||
| Location | Operation | Mechanism |
|
||||
|----------|-----------|-----------|
|
||||
| `quicprochat-core/src/recovery.rs` | Recovery code token comparison | `constant_time_eq()` — XOR-accumulate, branch-free |
|
||||
| `ed25519-dalek` internals | Signature verification | Constant-time via `curve25519-dalek` |
|
||||
| `x25519-dalek` internals | Diffie-Hellman computation | Constant-time via `curve25519-dalek` |
|
||||
| `opaque-ke` internals | OPRF evaluation, 3DH | Constant-time via Ristretto255 |
|
||||
|
||||
## RNG Sources
|
||||
|
||||
| Usage | RNG | Crate |
|
||||
|-------|-----|-------|
|
||||
| Identity key generation | `OsRng` | `rand::rngs::OsRng` |
|
||||
| Hybrid KEM key generation | `OsRng` | `rand::rngs::OsRng` |
|
||||
| Hybrid KEM ephemeral DH | `OsRng` via `EphemeralSecret::random_from_rng(OsRng)` | `x25519-dalek` |
|
||||
| ML-KEM encapsulation | `OsRng` | `ml-kem` |
|
||||
| AEAD nonce generation (hybrid KEM) | `OsRng.fill_bytes()` | `rand` |
|
||||
| PQ Noise ephemeral keys | `OsRng` via `StaticSecret::random_from_rng(OsRng)` | `x25519-dalek` |
|
||||
| Message padding | `OsRng.fill_bytes()` | `rand` |
|
||||
| Decoy traffic padding | `OsRng.fill_bytes()` | `rand` |
|
||||
| Recovery code generation | `OsRng.next_u32()` | `rand` |
|
||||
| OPAQUE server setup | `OsRng` | `rand` |
|
||||
| Deterministic key derivation (MLS HPKE) | `StdRng::from_seed(hkdf_output)` | `rand` |
|
||||
|
||||
All cryptographic randomness is sourced from `OsRng` (backed by `getrandom`), which reads from the OS CSPRNG (`/dev/urandom` on Linux, `BCryptGenRandom` on Windows). The only exception is deterministic derivation in `HybridKeypair::derive_from_ikm()`, which uses a seeded `StdRng` for reproducible key generation as required by the MLS HPKE key schedule.
|
||||
242
docs/src/security/threat-model.md
Normal file
242
docs/src/security/threat-model.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Threat Model
|
||||
|
||||
## System Overview
|
||||
|
||||
quicprochat is a production-grade end-to-end encrypted group messenger built in Rust. It provides confidential group messaging using the MLS protocol (RFC 9420) for group key agreement, with a hybrid post-quantum key encapsulation mechanism (X25519 + ML-KEM-768) to protect against future quantum adversaries.
|
||||
|
||||
### Architecture
|
||||
|
||||
```text
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
| Client A | QUIC | quicprochat | QUIC | Client B |
|
||||
| |<--------->| Server |<--------->| |
|
||||
| - MLS group | TLS 1.3 | | TLS 1.3 | - MLS group |
|
||||
| state machine | | - Auth Service | | state machine |
|
||||
| - Hybrid KEM | | - Delivery Svc | | - Hybrid KEM |
|
||||
| - Ed25519 ID | | - Key Transparency| | - Ed25519 ID |
|
||||
| - OPAQUE client | | - OPAQUE server | | - OPAQUE client |
|
||||
| - Sealed sender | | - Rate limiting | | - Sealed sender |
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
|
|
||||
+-------+-------+
|
||||
| |
|
||||
+----+----+ +-----+-----+
|
||||
| SQLite/ | | Federation |
|
||||
|SQLCipher| | Peers |
|
||||
+---------+ +-----------+
|
||||
```
|
||||
|
||||
### Protocol Stack
|
||||
|
||||
```text
|
||||
QUIC (quinn) + TLS 1.3 (rustls)
|
||||
+-- Session auth (OPAQUE token handshake)
|
||||
+-- RPC framing [method_id:u16][req_id:u32][len:u32][protobuf]
|
||||
+-- MLS (openmls 0.8) — group key agreement, epoch rotation
|
||||
+-- Hybrid KEM (X25519 + ML-KEM-768) — HPKE init keys
|
||||
+-- Sealed sender — Ed25519-signed inner envelope
|
||||
+-- Padded application messages
|
||||
```
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
### B1: Client to Server
|
||||
|
||||
The client communicates with the server over QUIC + TLS 1.3. The server is authenticated via its TLS certificate. The client authenticates via OPAQUE (password-authenticated key exchange), receiving a session token.
|
||||
|
||||
**Trust assumption**: The server correctly routes messages but cannot read their content. The server is a semi-trusted relay; it sees encrypted MLS blobs, metadata (group IDs, sender/recipient identity key fingerprints, timestamps, message sizes), and connection information (IP addresses).
|
||||
|
||||
### B2: Server to Federation Peer
|
||||
|
||||
Federation peers communicate over mutual-TLS QUIC connections. Each server authenticates the other via certificate pinning or a CA trust chain.
|
||||
|
||||
**Trust assumption**: A federated peer is trusted to relay messages to its local users but is not trusted with message content (MLS blobs are opaque). A compromised federation peer can drop, delay, or reorder messages but cannot forge or decrypt them.
|
||||
|
||||
### B3: Server to Storage
|
||||
|
||||
The server stores OPAQUE registration records, user identity keys, KeyPackages, group metadata, and message queues in SQLite. Client-side local storage uses SQLCipher (AES-256-CBC) with Argon2id-derived keys.
|
||||
|
||||
**Trust assumption**: Server-side storage is trusted for availability but not for confidentiality of message content (the server never holds plaintext). Client-side storage is encrypted at rest; the encryption key is derived from the user's password via Argon2id.
|
||||
|
||||
### B4: Client to Client (via MLS)
|
||||
|
||||
Clients share MLS group state through the server. Each client maintains its own MLS group state machine and validates all incoming MLS messages (signature verification, epoch checks). The MLS ratchet tree is embedded in Welcome messages (`use_ratchet_tree_extension = true`).
|
||||
|
||||
**Trust assumption**: Each client trusts only its own MLS state and verifies all protocol messages. A malicious client cannot decrypt messages from groups it has not been invited to.
|
||||
|
||||
## Assets
|
||||
|
||||
| Asset | Sensitivity | Storage Location |
|
||||
|-------|-------------|------------------|
|
||||
| Message plaintext | Critical | Client memory only (never on server) |
|
||||
| Ed25519 identity keypair (seed) | Critical | Client disk (SQLCipher), zeroized in memory |
|
||||
| MLS group epoch secrets | Critical | Client key store (DiskKeyStore), zeroized on drop |
|
||||
| Hybrid KEM private keys | Critical | Client key store, zeroized via `Zeroizing<Vec<u8>>` |
|
||||
| OPAQUE password file (server record) | High | Server SQLite (no plaintext password) |
|
||||
| Session tokens | High | Server memory (DashMap), time-bounded expiry |
|
||||
| Recovery codes | High | Shown once to user; encrypted bundles on server |
|
||||
| Key Transparency Merkle log | Medium | Server (append-only, publicly auditable) |
|
||||
| Group membership metadata | Medium | Server (group IDs, member fingerprints) |
|
||||
| Message timing/size metadata | Medium | Server (observable during relay) |
|
||||
| User identity public keys | Low | Server, Key Transparency log |
|
||||
|
||||
## Threat Actors
|
||||
|
||||
### TA1: Passive Network Observer
|
||||
|
||||
**Capability**: Can observe all network traffic between clients and server (or between federation peers). Cannot modify traffic (TLS prevents tampering).
|
||||
|
||||
**Goal**: Learn message content, communication patterns, or group membership.
|
||||
|
||||
### TA2: Compromised Server
|
||||
|
||||
**Capability**: Full control of the server process, including storage, OPAQUE state, and message routing. Can read all server-side data, modify routing behavior, and forge server-signed artifacts.
|
||||
|
||||
**Goal**: Read message content, impersonate users, or disrupt service.
|
||||
|
||||
### TA3: Compromised Client
|
||||
|
||||
**Capability**: Full access to one client's local state, including identity keys, MLS group state, and decrypted messages. May be an insider who was legitimately added to a group.
|
||||
|
||||
**Goal**: Access messages from groups they were removed from, impersonate the compromised user, or exfiltrate keys for future decryption.
|
||||
|
||||
### TA4: Malicious Group Member
|
||||
|
||||
**Capability**: A legitimate group member who acts adversarially within the MLS protocol. Can send messages, propose updates, and attempt protocol deviations.
|
||||
|
||||
**Goal**: Impersonate other members, forge message history, prevent key rotation, or silently add unauthorized members.
|
||||
|
||||
### TA5: Quantum Adversary (Harvest Now, Decrypt Later)
|
||||
|
||||
**Capability**: Can record all current network traffic and will have access to a cryptographically-relevant quantum computer in the future.
|
||||
|
||||
**Goal**: Decrypt recorded traffic to recover message content or long-term keys.
|
||||
|
||||
## Threats and Mitigations
|
||||
|
||||
### T1: Message Confidentiality
|
||||
|
||||
**Threat**: An attacker intercepts encrypted messages and attempts to recover plaintext.
|
||||
|
||||
**Mitigations**:
|
||||
- MLS (RFC 9420) provides end-to-end encryption; the server only sees opaque `MLSMessage` blobs.
|
||||
- The delivery service is MLS-unaware (ADR-004): it routes blobs by `group_id` without inspecting content.
|
||||
- Application messages are padded to fixed bucket sizes (256, 1024, 4096, 16384 bytes) before MLS encryption, hiding actual message length from the server.
|
||||
- Sealed sender envelopes embed the sender identity inside the encrypted payload, so the server cannot determine which group member sent a message.
|
||||
|
||||
### T2: Message Integrity and Authenticity
|
||||
|
||||
**Threat**: An attacker modifies messages in transit or forges messages from another user.
|
||||
|
||||
**Mitigations**:
|
||||
- MLS Commit messages are signed with Ed25519 by the sender's identity key.
|
||||
- Sealed sender envelopes include an Ed25519 signature over `magic || sender_key || payload`, verified by recipients.
|
||||
- QUIC + TLS 1.3 provides transport-level integrity (AEAD).
|
||||
- The Key Transparency Merkle log binds (username, identity_key) pairs; clients can audit the log to detect key substitution attacks.
|
||||
|
||||
### T3: Authentication Bypass
|
||||
|
||||
**Threat**: An attacker registers with a stolen identity or logs in without valid credentials.
|
||||
|
||||
**Mitigations**:
|
||||
- Authentication uses OPAQUE (RFC 9497) with Ristretto255 and Argon2id KSF. The server never sees the plaintext password.
|
||||
- Session tokens have time-bounded expiry; expired sessions are cleaned up on validation.
|
||||
- OPAQUE's triple-DH key exchange provides mutual authentication and resistance to pre-computation attacks.
|
||||
|
||||
### T4: Key Compromise and Forward Secrecy
|
||||
|
||||
**Threat**: Compromise of a long-term key allows decryption of past messages.
|
||||
|
||||
**Mitigations**:
|
||||
- MLS provides forward secrecy via epoch rotation. Each Commit advances the group epoch and derives fresh encryption keys. Compromise of current keys does not reveal past epoch keys.
|
||||
- MLS provides post-compromise security: a self-update (key rotation) proposal, once committed, re-derives all group secrets, healing from a key compromise.
|
||||
- KeyPackages are single-use (ADR-005): each KeyPackage is consumed on fetch, preserving forward secrecy of the initial key exchange.
|
||||
|
||||
### T5: Metadata Privacy
|
||||
|
||||
**Threat**: The server learns communication patterns (who talks to whom, when, and how much) even without reading message content.
|
||||
|
||||
**Mitigations**:
|
||||
- Sealed sender hides the sender identity from the server within group messages.
|
||||
- Message padding (uniform boundary, default 256 bytes) reduces size-based traffic analysis.
|
||||
- Traffic resistance mode: the server injects decoy messages at a configurable rate, adds random timing jitter before responses, and pads all payloads uniformly. Decoy messages are indistinguishable from real messages on the wire; recipients discard them by unpadding to an empty payload.
|
||||
- Decoy generation uses `generate_decoy()` which produces cryptographically random padding.
|
||||
|
||||
**Residual risk**: Group IDs and recipient identity key fingerprints are visible to the server for routing. IP addresses are visible at the transport layer.
|
||||
|
||||
### T6: Post-Quantum Threats
|
||||
|
||||
**Threat**: A quantum adversary records current traffic and later uses a quantum computer to break classical key exchange.
|
||||
|
||||
**Mitigations**:
|
||||
- MLS HPKE operations use a hybrid KEM: X25519 + ML-KEM-768 (FIPS 203). The shared secret is derived as `HKDF-SHA256(X25519_ss || ML-KEM_ss, info="quicnprotochat-hybrid-v1")`.
|
||||
- The hybrid construction follows the combiner approach from draft-ietf-tls-hybrid-design. Security degrades gracefully: if either component is broken, the other still provides protection.
|
||||
- The `HybridCryptoProvider` integrates with OpenMLS, producing hybrid (1216-byte public, 2432-byte private) init keys in KeyPackages.
|
||||
- A post-quantum Noise handshake (`pq_noise` module) mixes ML-KEM-768 shared secrets into the Noise_XX chaining key, providing PQ-protected transport when enabled.
|
||||
|
||||
**Residual risk**: The classical Noise_XX handshake (when `pq_noise` is not enabled) uses X25519 only. This is an accepted residual risk (ADR-006) — no long-lived content secrets transit the Noise handshake.
|
||||
|
||||
### T7: Replay Attacks
|
||||
|
||||
**Threat**: An attacker replays a previously observed message to cause duplicate delivery or state confusion.
|
||||
|
||||
**Mitigations**:
|
||||
- MLS epochs provide implicit replay protection: messages from a previous epoch are rejected by the receiver (`receive_stale_epoch_message_returns_error` test).
|
||||
- MLS sequence numbers within an epoch detect duplicate or out-of-order application messages.
|
||||
- The PQ Noise transport uses monotonic nonce counters; replaying a ciphertext at the wrong nonce causes decryption failure.
|
||||
- QUIC itself provides transport-level replay protection.
|
||||
|
||||
### T8: Key Transparency Attacks
|
||||
|
||||
**Threat**: The server silently replaces a user's identity key to perform a man-in-the-middle attack.
|
||||
|
||||
**Mitigations**:
|
||||
- The Key Transparency Merkle log uses SHA-256 hash chains with RFC 6962 domain-separation prefixes (`0x00` for leaves, `0x01` for internal nodes) to prevent second-preimage attacks.
|
||||
- Leaf hashes commit to both username and identity key: `SHA-256(0x00 || SHA-256(username || 0x00 || identity_key))`.
|
||||
- Clients can request inclusion proofs and verify them against a known root hash.
|
||||
- A key revocation log records revocation entries with reason codes, preventing revoked keys from being reused.
|
||||
- Safety numbers (Signal-style, 60-digit) allow users to verify identity keys out-of-band.
|
||||
|
||||
### T9: Account Recovery Attacks
|
||||
|
||||
**Threat**: An attacker obtains recovery codes and recovers a victim's identity.
|
||||
|
||||
**Mitigations**:
|
||||
- Recovery codes are 8 alphanumeric strings of 6 characters each, generated from OS CSPRNG.
|
||||
- Each code derives an encryption key via Argon2id (19 MiB memory, 2 iterations) before decrypting the recovery bundle (ChaCha20-Poly1305).
|
||||
- The server stores only encrypted bundles keyed by `SHA-256(SHA-256("qpc-recovery-token:" || code))` -- it never sees plaintext codes.
|
||||
- Recovery code validation uses constant-time comparison (`constant_time_eq`).
|
||||
- Bundle size is capped at 64 KiB to prevent resource exhaustion.
|
||||
|
||||
### T10: Denial of Service
|
||||
|
||||
**Threat**: An attacker floods the server with requests to prevent legitimate users from communicating.
|
||||
|
||||
**Mitigations**:
|
||||
- Tower middleware provides rate limiting per client.
|
||||
- RPC framing enforces a maximum payload size of 4 MiB.
|
||||
- Session token validation is O(1) via DashMap.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly out of scope for a security audit of quicprochat:
|
||||
|
||||
- **User interface code** (TUI, CLI argument parsing) -- not security-sensitive
|
||||
- **Build system and CI** (Dockerfiles, justfile, GitHub Actions)
|
||||
- **Documentation** (mdBook, README)
|
||||
- **SDK bindings** (Go, Python, TypeScript, WASM, FFI) -- thin wrappers over the Rust SDK
|
||||
- **Performance optimization** -- not a security concern unless it introduces side channels
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Client reconnection**: There is currently no automatic reconnection logic. A disconnected client must re-authenticate and re-join groups manually. This is a reliability concern, not a security concern.
|
||||
|
||||
2. **In-flight RPC tracking**: The client does not track in-flight RPCs for timeout/retry. Lost messages during network interruption are not automatically retransmitted.
|
||||
|
||||
3. **Classical Noise transport**: When the `pq_noise` feature is not enabled, the QUIC + TLS 1.3 transport uses classical cryptography only (X25519 for key exchange). This is an accepted residual risk (ADR-006) as the content layer is PQ-protected via MLS hybrid KEM.
|
||||
|
||||
4. **Server-side metadata**: The server necessarily observes group IDs, recipient fingerprints, message sizes (before MLS encryption, after padding), and timing of messages. Traffic resistance mitigates but does not eliminate this.
|
||||
|
||||
5. **No server-side message expiration**: The offline message queue does not currently enforce a maximum retention period. Messages queue indefinitely until consumed.
|
||||
|
||||
6. **OPAQUE registration not rate-limited**: Registration endpoints should be rate-limited to prevent mass account creation. The rate limiter covers authenticated endpoints but registration-time rate limiting is not yet enforced.
|
||||
Reference in New Issue
Block a user