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:
2026-02-22 08:07:48 +01:00
parent d1ddef4cea
commit f334ed3d43
81 changed files with 14502 additions and 2289 deletions

View File

@@ -0,0 +1,269 @@
# Coding Standards
This page defines the engineering standards for quicnprotochat. These are
non-negotiable -- all code merged into the repository must conform to these
rules. The standards exist to ensure that every milestone produces
production-ready, auditable, and secure code.
---
## Production-Ready Only
Every deliverable must be complete and functional. The following are prohibited
in any merged code:
- `todo!()` or `unimplemented!()` macros
- Stub implementations or placeholder logic
- Mock objects in production code paths (mocks are acceptable only in test code)
- Commented-out code blocks
- `#[allow(unused)]` on production code (acceptable on generated code from
Cap'n Proto codegen)
If a feature is out of scope for the current milestone, it is **explicitly
omitted** with a documented reason (in an ADR or code comment explaining why it
is deferred), not silently stubbed.
---
## YAGNI / KISS / DRY
- **YAGNI (You Aren't Gonna Need It):** Do not add features, abstractions, or
generic type parameters that are not required by the current milestone.
- **KISS (Keep It Simple):** Favour clarity over cleverness. A straightforward
`match` statement is preferred over a complex trait hierarchy.
- **DRY (Don't Repeat Yourself):** Extract shared logic into functions or
modules, but do not create abstractions prematurely. Two occurrences is a
coincidence; three is a pattern worth extracting.
---
## Spec-First Development
Document the design before implementing it:
1. **ADR (Architecture Decision Record)** for significant design decisions. ADRs
live in `docs/src/design-rationale/` and are referenced from the
[Design Rationale](../design-rationale/overview.md) section.
2. **Doc comments** on every public API (`///` for items, `//!` for modules).
Doc comments must explain:
- What the function/type does.
- Invariants and preconditions.
- Error conditions and what each error variant means.
- Examples where the API is non-obvious.
```rust
/// Creates a new MLS group with the given group ID and returns
/// the initial `GroupMember` state.
///
/// # Errors
///
/// Returns `GroupError::CryptoBackend` if the MLS crypto provider
/// fails to generate the initial key schedule.
///
/// Returns `GroupError::InvalidGroupId` if `group_id` is empty.
pub fn create_group(
group_id: &[u8],
identity: &IdentityKeypair,
) -> Result<GroupMember, GroupError> {
// ...
}
```
---
## Security-by-Design
### Secrets and Key Material
- All private key material must be wrapped in `Zeroizing<T>` (from the `zeroize`
crate) or implement `Zeroize + ZeroizeOnDrop`.
- No secret material in log output at any level (`TRACE`, `DEBUG`, `INFO`,
`WARN`, `ERROR`).
- When logging key-related operations, log only fingerprints (SHA-256 of the
public key), never the key bytes themselves.
### Error Handling
- No `unwrap()` or `expect()` on cryptographic operations. All crypto errors
must be typed and propagated.
- Use `thiserror` for library error types (`quicnprotochat-core`,
`quicnprotochat-proto`) and `anyhow` for application-level error handling
(`quicnprotochat-server`, `quicnprotochat-client`).
- `unwrap()` is acceptable only in:
- Test code.
- Cases where the invariant is provably guaranteed by the type system
(e.g., indexing a fixed-size array with a constant).
### Constant-Time Comparisons
- Use constant-time comparison (`subtle::ConstantTimeEq` or equivalent) when
comparing authentication tokens, key fingerprints, or any value that could
be used in a timing side-channel attack.
- Never use `==` to compare secrets or tokens in authentication paths.
### Input Validation
- Validate all incoming data at the boundary (RPC handler entry point) before
passing it to internal logic.
- Length checks: group ID (32 bytes), identity key (32 bytes), channel ID
(16 bytes), payload (max 5 MB).
- Reject unexpected enum variants or unknown wire versions.
---
## Containerisation
- The server runs in Docker. The `Dockerfile` and `docker-compose.yml` must
always be kept up to date with the current build.
- Multi-stage build: `rust:bookworm` builder stage, `debian:bookworm-slim`
runtime stage.
- The Docker image must build and run correctly after every merge to the main
branch.
---
## Dependency Hygiene
### Pinned Major Versions
All dependencies are pinned to a major version in `Cargo.toml`. Minor and patch
updates are allowed; major version bumps require justification and review.
### Preferred Ecosystem
| Domain | Preferred Crate(s) |
|--------|-------------------|
| Classical crypto (signing) | `ed25519-dalek` |
| Classical crypto (key exchange) | `x25519-dalek` |
| Noise protocol | `snow` |
| MLS | `openmls`, `openmls_rust_crypto` |
| Post-quantum KEM | `ml-kem` |
| Serialisation / RPC | `capnp`, `capnp-rpc` |
| Async runtime | `tokio` |
| Zeroisation | `zeroize` |
Do not introduce new dependencies without justification. In particular:
- No alternative async runtimes (async-std, smol).
- No alternative serialisation formats (protobuf, MessagePack, JSON) for wire
protocol use.
- No alternative crypto libraries unless the preferred crate lacks required
functionality.
### Dependency Auditing
- `cargo audit` must pass in CI with no known vulnerabilities.
- `cargo deny check` for license compatibility and duplicate detection.
---
## Git Standards
### Signed Commits
All commits must be GPG-signed. Unsigned commits will be rejected by CI.
Configure signing:
```bash
git config --global commit.gpgsign true
git config --global user.signingkey <YOUR_GPG_KEY_ID>
```
### Conventional Commits
Commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/)
specification:
| Prefix | Use |
|--------|-----|
| `feat:` | A new feature or capability |
| `fix:` | A bug fix |
| `chore:` | Maintenance (deps, CI, config) |
| `docs:` | Documentation changes |
| `test:` | Adding or updating tests |
| `refactor:` | Code restructuring without behaviour change |
### Commit Message Format
```
<type>: <short description>
<body: explain WHY, not just WHAT>
```
The body describes the motivation and context for the change, not just a
restatement of the diff. Why was this change necessary? What problem does it
solve? What alternatives were considered?
Example:
```
feat: add KeyPackage TTL eviction on fetch
KeyPackages older than 24 hours are now filtered out at fetch time,
preventing stale key material from being used in MLS group additions.
Background sweep is deferred to M6 (requires persistent storage).
```
### Branch Strategy
- Feature branches per milestone: `feat/m1-noise-transport`,
`feat/m2-keypackage-as`, etc.
- Branch names use lowercase with hyphens.
- All work happens on feature branches; direct commits to the main branch are
prohibited.
---
## Code Style
### Formatting
- `cargo fmt` with default settings. No custom `rustfmt.toml` overrides.
- CI rejects unformatted code.
### Linting
- `cargo clippy` with default lints. No `#[allow(clippy::...)]` without a
comment explaining why the lint is suppressed.
- CI treats clippy warnings as errors.
### Naming
- Types: `PascalCase` (Rust convention).
- Functions and variables: `snake_case`.
- Constants: `SCREAMING_SNAKE_CASE`.
- Module names: `snake_case`, matching the file name.
### Module Organisation
- One concept per file. A file should not exceed ~500 lines (guideline, not
hard rule).
- Public API at the top of the file; private helpers at the bottom.
- `mod.rs` files are avoided; use `foo.rs` + `foo/` directory when a module
has submodules.
---
## Review Checklist
Before presenting any code for review, verify:
- [ ] No missing error handling (all `Result` values handled).
- [ ] No security gaps (secrets zeroized, no secret logging, typed crypto errors).
- [ ] No incomplete implementations (no `todo!()`, `unimplemented!()`, stubs).
- [ ] No deviation from these standards.
- [ ] Doc comments on all public items.
- [ ] Tests for all new functionality (see [Testing Strategy](testing.md)).
- [ ] `cargo fmt`, `cargo clippy`, and `cargo test --workspace` all pass.
---
## Cross-references
- [Testing Strategy](testing.md) -- test structure and conventions
- [Design Rationale](../design-rationale/overview.md) -- ADR index
- [Milestones](../roadmap/milestones.md) -- what each milestone delivers
- [Production Readiness WBS](../roadmap/production-readiness.md) -- governance and CI requirements

View File

@@ -0,0 +1,239 @@
# Testing Strategy
This page describes the testing structure, conventions, and current coverage for
quicnprotochat. All tests run with `cargo test --workspace` and must pass before
any code is merged.
For the coding standards that tests must follow, see
[Coding Standards](coding-standards.md).
---
## Test Organisation
### Unit Tests
Unit tests live alongside the code they test, in `#[cfg(test)] mod tests` blocks
at the bottom of each source file. They test individual functions and types in
isolation.
**quicnprotochat-core:**
| Module | Tests | What they cover |
|--------|-------|----------------|
| `codec` | 7 tests | Length-prefixed frame encoding/decoding, edge cases (empty payload, max size, partial frame, exact boundary) |
| `keypair` | 3 tests | Ed25519 keypair generation, public key extraction, deterministic re-derivation |
| `group` | 2 tests | Group round-trip (create + add + join + send + recv), group\_id lifecycle |
| `hybrid_kem` | 11 tests | Encapsulate/decapsulate round-trip, key generation, combiner correctness, wrong-key rejection, serialisation |
**quicnprotochat-proto:**
| Module | Tests | What they cover |
|--------|-------|----------------|
| `lib` | 3 tests | Cap'n Proto builder/reader round-trip, canonical serialisation, schema validation |
### Integration Tests
Integration tests live in `crates/quicnprotochat-client/tests/` and test the
full client-server interaction. Each test spawns a server using `tokio::spawn`
within the same test binary, then runs client operations against it.
| File | Milestone | What it covers |
|------|-----------|---------------|
| `noise_transport.rs` | M1 | Noise\_XX handshake over TCP, Ping/Pong frame exchange, connection lifecycle |
| `auth_service.rs` | M2 | KeyPackage upload via AS, KeyPackage fetch (single-use consume semantics), identity key validation |
| `mls_group.rs` | M3 | Full MLS round-trip: register state, create group, add member via Welcome, send encrypted message, receive and decrypt |
### Test Pattern
All integration tests follow the same pattern:
```rust
#[tokio::test]
async fn test_something() {
// 1. Start server in background
let server_handle = tokio::spawn(async move {
server::run(config).await.unwrap();
});
// 2. Wait for server to be ready
tokio::time::sleep(Duration::from_millis(100)).await;
// 3. Run client operations
let result = client::do_something(server_addr).await;
// 4. Assert
assert!(result.is_ok());
// ...
// 5. Cleanup
server_handle.abort();
}
```
This pattern ensures tests are self-contained and do not require an external
server process.
---
## Running Tests
### Full Workspace
```bash
cargo test --workspace
```
This runs all unit tests and integration tests across all four crates.
### Single Crate
```bash
cargo test -p quicnprotochat-core
cargo test -p quicnprotochat-proto
cargo test -p quicnprotochat-server
cargo test -p quicnprotochat-client
```
### Single Test
```bash
cargo test -p quicnprotochat-core -- codec::tests::test_round_trip
cargo test -p quicnprotochat-client --test mls_group
```
### With Output
```bash
cargo test --workspace -- --nocapture
```
---
## Current Results
All tests pass as of the M3 milestone on branch `feat/m1-noise-transport`.
Summary:
| Crate | Unit Tests | Integration Tests | Total |
|-------|-----------|-------------------|-------|
| `quicnprotochat-core` | 23 | -- | 23 |
| `quicnprotochat-proto` | 3 | -- | 3 |
| `quicnprotochat-server` | 0 | -- | 0 |
| `quicnprotochat-client` | 0 | 6 | 6 |
| **Total** | **26** | **6** | **32** |
---
## Test Conventions
### Naming
Test functions use descriptive names that state what is being tested and the
expected outcome:
```rust
#[test]
fn encode_decode_round_trip_preserves_payload() { ... }
#[test]
fn empty_payload_produces_length_zero_frame() { ... }
#[test]
fn fetch_consumes_keypackage_single_use() { ... }
```
### Assertions
- Use `assert_eq!` with both expected and actual values.
- Use `assert!(result.is_ok(), "descriptive message: {result:?}")` for
`Result` checks.
- For crypto operations, assert on specific error variants, not just
`is_err()`.
### No External Dependencies
Tests must not depend on external services, network access, or filesystem state
outside the test's temporary directory. The `tokio::spawn` pattern for
client-server tests ensures everything runs in-process.
### Determinism
Tests must be deterministic. If randomness is needed (e.g., key generation),
the test must not depend on specific random values -- only on the properties of
the output (correct length, successful round-trip, etc.).
---
## Planned Testing Enhancements
The following testing improvements are planned for future milestones:
### Fuzzing Targets (M5+)
Fuzz testing for parser and deserialisation code:
- **Cap'n Proto message parser:** Feed arbitrary bytes to the Cap'n Proto reader
and verify it either parses correctly or returns a typed error (no panics,
no undefined behaviour).
- **MLS message handler:** Feed arbitrary `MLSMessage` bytes to the
`GroupMember::receive_message` path.
- **Length-prefixed codec:** Fuzz the frame decoder with arbitrary byte streams.
Tool: `cargo-fuzz` with `libfuzzer`.
### Golden-Wire Fixtures (M5+)
Serialised test vectors for regression testing across versions:
- Capture the wire bytes of known-good Cap'n Proto messages (Envelope, Auth,
Delivery structs) at the current version.
- Store as `.bin` files in `tests/fixtures/`.
- Each test deserialises the fixture and verifies the expected field values.
- When the wire format changes, fixtures are updated with a version bump.
This catches accidental wire-format regressions that would break client-server
compatibility.
### N-1 Compatibility Tests (M5+)
Test that a client built at version N can communicate with a server built at
version N-1 (and vice versa):
- Build two versions of the binary (current and previous release).
- Run the older server with the newer client and verify all RPCs succeed.
- Run the newer server with the older client and verify graceful degradation
(legacy mode works, new features return clean errors).
### Criterion Benchmarks (M5)
Performance benchmarks using [Criterion.rs](https://docs.rs/criterion/):
- Key generation latency (Ed25519, X25519, ML-KEM-768).
- MLS encap/decap (KeyPackage generation, Welcome processing).
- Group-add latency scaling: 2, 10, 100, 1000 members.
- Cap'n Proto serialise/deserialise throughput.
- Noise handshake latency.
Benchmarks run separately from tests (`cargo bench`) and are not part of the
CI gate, but are tracked for regression detection.
### Docker-based E2E Tests (Phase 5)
End-to-end tests using `testcontainers-rs` (see
[Future Research: Testcontainers-rs](../roadmap/future-research.md#testcontainers-rs)):
- Spin up server container from the Docker image.
- Run client operations from the test process against the containerised server.
- Verify real network boundaries, container startup, and multi-process
interactions.
---
## Cross-references
- [Coding Standards](coding-standards.md) -- quality requirements for test code
- [Milestones](../roadmap/milestones.md) -- which tests were added at each milestone
- [Production Readiness WBS](../roadmap/production-readiness.md) -- Phase 5 (E2E Harness and Security Tests)
- [Future Research: Testcontainers-rs](../roadmap/future-research.md#testcontainers-rs) -- Docker-based testing