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>
13 KiB
Security Audit
This document is a security audit of the quicproquo codebase as of the audit date. It aligns with the Threat Model and Production Readiness Audit. 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 insessionswith 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, serverauth_ops.rs - Finding: Shared
OpaqueSuite(Ristretto255, Triple-DH, Argon2id). Server never sees password. Registration and login flows useServerRegistration/ServerLogincorrectly. 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.rsanddelivery.rsexplicitly 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 inZeroizing<[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
Zeroizingwhere 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
Zeroizingduring 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 to0o600on 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
RootCertStorewith 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_contextand, 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. Withsealed_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 > 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 permissions0o600on Unix. - SQL store: Optional SQLCipher with
db_key; empty key = plaintext (documented). Production validation requires non-emptydb_keywhen backend is SQL. User records use INSERT (no OR REPLACE) and unique constraint; duplicate user returnsStorageError::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:
- 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.
- 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.
- BasicCredential only: No CA or revocation; key substitution possible if AS is compromised. Mitigation: Key Transparency or X.509 credentials.
- 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-auditthencargo auditlocally (and in CI if available) to check for known-vulnerable dependencies. Fix or document any findings. See Checking dependencies below. - Argon2 params: Implemented: client state KDF now uses explicit Argon2id parameters (19 MiB memory, 2 iterations, 1 lane) in
quicproquo-clientso 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. copyserver-cert.derfrom the server and pass it via--ca-certorQPQ_CA_CERT). Do not use a general CA unless you intend to trust that CA for all servers. See Certificate pinning below. - Health endpoint: The
healthRPC 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) |
⚠️ Recommend |
Checking dependencies
To check for known vulnerabilities in dependencies:
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:
- Obtain the server's certificate (e.g. copy
data/server-cert.derfrom the server, or export from your deployment). - Use that file as the client's
ca_cert. The client will only connect to a server that presents that exact certificate (or chain). - Do not use a broad CA bundle as
ca_certunless 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.