Files
quicproquo/docs/SECURITY-AUDIT.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc
across documentation, Docker configuration, CI workflows, packaging
scripts, operational configs, and build tooling.

- Docker: crate paths, binary names, user/group, data dirs, env vars
- CI: workflow crate references, binary names, artifact names
- Docs: all markdown files under docs/, SDK READMEs, book.toml
- Packaging: OpenWrt Makefile, init script, UCI config (file renames)
- Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team
- Operations: Prometheus config, alert rules, Grafana dashboard
- Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths
- Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
2026-03-21 19:14:06 +01:00

13 KiB

Security Audit

This document is a security audit of the quicprochat 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/quicprochat-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/quicprochat-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/quicprochat-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: quicprochat-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/quicprochat-core/src/hybrid_kem.rs
  • Finding: Hybrid keypair and shared secrets use Zeroizing where appropriate. HKDF domain separation (quicprochat-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/quicprochat-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/quicprochat-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/quicprochat-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/quicprochat-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 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 below.
  • Argon2 params: Implemented: client state KDF now uses explicit Argon2id parameters (19 MiB memory, 2 iterations, 1 lane) in quicprochat-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 QPC_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 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) ⚠️ 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 QPC_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.