Files
quicproquo/docs/SECURITY-AUDIT.md
Chris Nennemann 853ca4fec0 chore: rename project quicnprotochat -> quicproquo (binaries: qpq)
Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
  quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
  *.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated

HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:11:51 +01:00

227 lines
13 KiB
Markdown

# Security Audit
This document is a security audit of the quicproquo codebase as of the audit date. It aligns with the [Threat Model](src/cryptography/threat-model.md) and [Production Readiness Audit](PRODUCTION-READINESS-AUDIT.md). The project has **not** undergone a formal third-party audit; this is an internal review.
---
## Executive Summary
| Area | Finding | Severity |
|------|---------|----------|
| Authentication & sessions | Token comparison constant-time; session tokens CSPRNG; OPAQUE used correctly; no secrets in logs | ✅ Strong |
| Cryptography | MLS, Ed25519, hybrid KEM, zeroization where appropriate; Argon2/ChaCha20 for state | ✅ Strong |
| Transport (TLS) | TLS 1.3 only; client verifies server cert; self-signed default is documented weakness | ⚠️ Known gap |
| Authorization (DS/AS) | Enqueue/fetch/fetchWait/key ops require auth + identity binding (or sealed_sender); health unauthenticated by design | ✅ Appropriate |
| Input validation & limits | Key/recipient length, payload/KeyPackage size, queue depth, rate limit, UTF-8 username | ✅ Good |
| Secrets handling | No tokens/keys/passwords in logs; DB key optional (documented); state encryption optional | ✅ Good |
| Dependency hygiene | No `cargo audit` in tree; recommend adding and running in CI | ⚠️ Recommendation |
**Overall:** The design and implementation are security-conscious and match the documented threat model. Remaining risks are largely documented (self-signed TLS, metadata visibility, BasicCredential) or operational (deps, production config).
---
## 1. Authentication and Session Management
### 1.1 Token comparison
- **Location:** `crates/quicproquo-server/src/auth.rs`
- **Finding:** Bearer token and identity key comparisons use `subtle::ConstantTimeEq` (`ct_eq`). Length is checked before comparison where applicable.
- **Status:** ✅ No timing leakage from token or identity comparison.
### 1.2 Session token generation
- **Location:** `crates/quicproquo-server/src/node_service/auth_ops.rs` (login finish)
- **Finding:** Session tokens are 32 bytes from `rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token)`. Stored in `sessions` with TTL (24h). Expired sessions are removed on next use.
- **Status:** ✅ Cryptographically strong, single-use style (opaque 32-byte token).
### 1.3 OPAQUE (RFC 9497)
- **Location:** `crates/quicproquo-core/src/opaque_auth.rs`, server `auth_ops.rs`
- **Finding:** Shared `OpaqueSuite` (Ristretto255, Triple-DH, Argon2id). Server never sees password. Registration and login flows use `ServerRegistration`/`ServerLogin` correctly. Pending login state is stored server-side and removed on consume. Identity key is bound at login finish; mismatch returns E016 and is not logged with secrets.
- **DoS:** Pending-login check runs **before** expensive OPAQUE work (login start); repeated attempts for the same username within 60s are rejected early.
- **Status:** ✅ Correct usage; DoS mitigation in place.
### 1.4 Audit logging (no secrets)
- **Finding:** Comments in `auth_ops.rs` and `delivery.rs` explicitly forbid logging tokens, passwords, or raw keys. Logged fields: username, recipient/key prefix (`fmt_hex`), payload length, seq, counts. Login success/failure and rate-limit hit are logged without session token or identity.
- **Status:** ✅ No sensitive material in logs.
---
## 2. Cryptography
### 2.1 MLS and identity
- **Location:** `quicproquo-core` (group, identity, keypackage)
- **Finding:** MLS ciphersuite `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` (RFC 9420). Ed25519 identity seed stored in `Zeroizing<[u8; 32]>`; zeroize-on-drop. KeyPackages validated for ciphersuite before server stores. Single-use KeyPackage semantics enforced (consume-on-fetch).
- **Status:** ✅ Aligns with key lifecycle and zeroization goals.
### 2.2 Hybrid KEM (X25519 + ML-KEM-768)
- **Location:** `crates/quicproquo-core/src/hybrid_kem.rs`
- **Finding:** Hybrid keypair and shared secrets use `Zeroizing` where appropriate. HKDF domain separation (`quicproquo-hybrid-v1`). ChaCha20-Poly1305 for AEAD. Versioned envelope.
- **Status:** ✅ PQ-ready envelope layer; secret handling is careful.
### 2.3 Client state encryption (QPCE)
- **Location:** `crates/quicproquo-client/src/client/state.rs`
- **Finding:** Optional password protection: Argon2id (default params) for key derivation, ChaCha20-Poly1305, random salt and nonce. Derived key held in `Zeroizing` during use. Unencrypted state is a documented option (e.g. dev).
- **Recommendation:** Document Argon2 params (memory, iterations) for auditability; consider explicit `Argon2::new()` with named params in a future revision.
- **Status:** ✅ Appropriate for optional at-rest protection.
---
## 3. Transport (TLS / QUIC)
### 3.1 Server TLS
- **Location:** `crates/quicproquo-server/src/tls.rs`
- **Finding:** TLS 1.3 only. No client cert. ALPN `capnp`. When not in production, missing cert/key triggers self-signed generation; key file permissions set to `0o600` on Unix. Production mode requires existing cert/key (no auto-generation).
- **Status:** ✅ Matches documented design; self-signed limitation is documented in threat model.
### 3.2 Client TLS
- **Location:** `crates/quicproquo-client/src/client/rpc.rs`
- **Finding:** Client loads CA cert from file, builds `RootCertStore` with that single cert, uses it for server verification. Server name from CLI/env is used for connection (SNI and cert verification). No custom bypass.
- **Status:** ✅ Proper verification against provided CA; trust-on-first-use / self-signed caveat is documented.
---
## 4. Authorization and Access Control
### 4.1 RPC auth matrix
| RPC | Auth required | Identity binding |
|-----|----------------|------------------|
| health | No | N/A (liveness) |
| opaqueLoginStart/Finish, opaqueRegisterStart/Finish | No (password/session flow) | After login |
| uploadKeyPackage, fetchKeyPackage | Yes | Must match identity_key (or allow_insecure) |
| enqueue | Yes | Must match recipient_key unless sealed_sender |
| fetch, fetchWait | Yes | Must match recipient_key (or allow_insecure) |
| uploadHybridKey, fetchHybridKey | Yes | Must match identity_key (or allow_insecure) |
| publishEndpoint, resolveEndpoint | Yes | Publish: match identity_key; Resolve: any valid token |
- **Finding:** Sensitive operations require `validate_auth_context` and, where relevant, `require_identity_or_request`. Fetch/fetchWait ensure the authenticated identity matches the requested recipient_key, so only the recipient (or someone with their session) can pull messages. With `sealed_sender`, enqueue only requires a valid token (no identity binding to sender).
- **Status:** ✅ Authorization consistent with design; sealed_sender trade-off is documented.
### 4.2 Rate limiting
- **Location:** `crates/quicproquo-server/src/auth.rs`, `delivery.rs`
- **Finding:** Per-token rate limit (e.g. 100 enqueues per 60s). Enqueue path checks before storage. Queue depth and payload size caps (1000 messages, 5 MB) enforced.
- **Status:** ✅ Limits in place to curb abuse and DoS.
---
## 5. Input Validation and Limits
- **Identity/recipient keys:** Rejected unless length exactly 32 bytes (E004).
- **Payload:** Non-empty; max 5 MB (E005, E006).
- **KeyPackage:** Non-empty; max 1 MB (E007, E008); ciphersuite validated before store (E021).
- **Username:** Non-empty; must be valid UTF-8 (E011, E020).
- **Wire version:** Rejected if &gt; CURRENT_WIRE_VERSION (E012).
- **Cap'n Proto:** Server and client set `traversal_limit_in_words(Some(4 * 1024 * 1024))` (4M words = 32 MiB) to bound parsing DoS.
- **Status:** ✅ Validation and limits are consistently applied.
---
## 6. Storage and Persistence
### 6.1 Server
- **File-backed store:** Mutex-protected; lock errors mapped to `StorageError` (no unwrap in hot path). OPAQUE server setup file permissions `0o600` on Unix.
- **SQL store:** Optional SQLCipher with `db_key`; empty key = plaintext (documented). Production validation requires non-empty `db_key` when backend is SQL. User records use INSERT (no OR REPLACE) and unique constraint; duplicate user returns `StorageError::DuplicateUser` (E018).
- **Status:** ✅ Matches production-readiness and auth design; DB encryption caveat documented.
### 6.2 Client
- **State file:** Optional QPCE encryption (Argon2id + ChaCha20-Poly1305). Unencrypted state contains identity seed; documented.
- **Keystore:** Persisted for HPKE init keys so Welcome can be processed after restart; path and format documented.
- **Status:** ✅ Acceptable for threat model; optional encryption and handling of secrets are clear.
---
## 7. Known Gaps (from Threat Model and Docs)
These remain as documented, not new findings:
1. **Self-signed TLS:** MITM possible on first connection if client does not pin or verify out-of-band. Mitigation: certificate pinning or CA-signed certs.
2. **No client auth on DS (by design):** Anyone with a valid token can enqueue to any recipient_key when identity is not required (e.g. sealed_sender). Rate limit and queue/payload caps mitigate abuse.
3. **BasicCredential only:** No CA or revocation; key substitution possible if AS is compromised. Mitigation: Key Transparency or X.509 credentials.
4. **Metadata:** Server sees recipient_key, timing, sizes; Sealed Sender and PIR are future mitigations.
---
## 8. Recommendations
### 8.1 High value
- **Dependency audit:** Run `cargo install cargo-audit` then `cargo audit` locally (and in CI if available) to check for known-vulnerable dependencies. Fix or document any findings. See [Checking dependencies](#checking-dependencies) below.
- **Argon2 params:** Implemented: client state KDF now uses explicit Argon2id parameters (19 MiB memory, 2 iterations, 1 lane) in `quicproquo-client` so they are auditable.
### 8.2 Medium value
- **Certificate pinning:** To pin the server, use the server's certificate as the client's `ca_cert` (e.g. copy `server-cert.der` from the server and pass it via `--ca-cert` or `QPQ_CA_CERT`). Do not use a general CA unless you intend to trust that CA for all servers. See [Certificate pinning](#certificate-pinning) below.
- **Health endpoint:** The `health` RPC is unauthenticated by design for liveness probes and load balancers; this is documented in code and in this audit.
### 8.3 Lower priority
- **Cap'n Proto traversal limit:** Implemented: reduced to 4M words (32 MiB) with a named constant; trade-off documented in code.
- **Username enumeration:** OPAQUE login start uses `get_user_record`; timing or response shape might still reveal user existence. Mitigation in scope: consider constant-time or uniform response for unknown users in a future revision (e.g. fixed dummy work when user not found) without weakening OPAQUE. Current code does not implement this.
---
## 9. Summary Table
| Category | Item | Status |
|----------|------|--------|
| Auth | Constant-time token/identity comparison | ✅ |
| Auth | Session token from CSPRNG, not logged | ✅ |
| Auth | OPAQUE used correctly; no password on server | ✅ |
| Auth | Pending-login DoS check before OPAQUE work | ✅ |
| Auth | No secrets in audit logs | ✅ |
| Crypto | MLS ciphersuite, KeyPackage validation | ✅ |
| Crypto | Identity seed zeroized on drop | ✅ |
| Crypto | Hybrid KEM and state encryption use Zeroizing | ✅ |
| Transport | TLS 1.3 only; client verifies server cert | ✅ |
| Transport | Self-signed default (documented weakness) | ⚠️ Known |
| Authz | Enqueue/fetch/key/p2p require auth + identity where required | ✅ |
| Authz | Rate limit and size/depth limits | ✅ |
| Input | Lengths, sizes, UTF-8, wire version, traversal limit | ✅ |
| Storage | Server: lock Result, DB key in prod, duplicate user | ✅ |
| Storage | Client: optional QPCE; explicit Argon2 params; unencrypted state documented | ✅ |
| Deps | Run `cargo audit` locally/CI (see [Checking dependencies](#checking-dependencies)) | ⚠️ Recommend |
---
## Checking dependencies
To check for known vulnerabilities in dependencies:
```bash
cargo install cargo-audit
cargo audit
```
Fix or document any reported issues. Running `cargo audit` in CI (e.g. GitHub Actions) is recommended.
---
## Certificate pinning
The client trusts the server certificate(s) in the file given by `--ca-cert` (or `QPQ_CA_CERT`). To **pin** a specific server:
1. Obtain the server's certificate (e.g. copy `data/server-cert.der` from the server, or export from your deployment).
2. Use that file as the client's `ca_cert`. The client will only connect to a server that presents that exact certificate (or chain).
3. Do not use a broad CA bundle as `ca_cert` unless you intend to trust any server certified by that CA.
This gives trust-on-first-use behaviour when you deploy the server and then distribute its cert to clients.
---
## Related Documents
- [Threat Model](src/cryptography/threat-model.md)
- [Production Readiness Audit](PRODUCTION-READINESS-AUDIT.md)
- [Cryptography Overview](src/cryptography/overview.md)
- [Key Lifecycle and Zeroization](src/cryptography/key-lifecycle.md)