DM channels (createChannel), channel authz, security/docs, future improvements

- Add createChannel RPC (node.capnp @18): create 1:1 channel, returns 16-byte channelId
- Store: create_channel(member_a, member_b), get_channel_members(channel_id)
- FileBackedStore: channels.bin; SqlStore: migration 003_channels, schema v4
- channel_ops: handle_create_channel (auth + identity, peerKey 32 bytes)
- Delivery authz: when channel_id.len() == 16, require caller and recipient are channel members (E022/E023)
- Error codes E022 CHANNEL_ACCESS_DENIED, E023 CHANNEL_NOT_FOUND
- SUMMARY: link Certificate lifecycle; security audit, future improvements, multi-agent plan docs
- Certificate lifecycle doc, SECURITY-AUDIT, FUTURE-IMPROVEMENTS, MULTI-AGENT-WORK-PLAN
- Client/core/tls/auth/server main: assorted fixes and updates from review and audit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-23 22:54:28 +01:00
parent 6b8b61c6ae
commit 750b794342
40 changed files with 4715 additions and 152 deletions

View File

@@ -17,6 +17,7 @@
- [Building from Source](getting-started/building.md)
- [Running the Server](getting-started/running-the-server.md)
- [Running the Client](getting-started/running-the-client.md)
- [Certificate Lifecycle and CA-Signed TLS](getting-started/certificate-lifecycle.md)
- [Docker Deployment](getting-started/docker.md)
- [Demo Walkthrough: Alice and Bob](getting-started/demo-walkthrough.md)

View File

@@ -1,9 +1,11 @@
# Crate Responsibilities
The quicnprotochat workspace is split into four crates with strict layering
rules. Each crate owns one concern and depends only on the crates below it.
This page documents what each crate provides, what it explicitly avoids, and
how the crates relate to one another.
The quicnprotochat workspace contains six crates. The main four (proto, core,
server, client) follow strict layering rules; each owns one concern and depends
only on the crates below it. The workspace also includes **quicnprotochat-gui**
(Tauri desktop app) and **quicnprotochat-p2p** (P2P endpoint resolution). This
page documents what each crate provides, what it explicitly avoids, and how the
crates relate to one another.
---
@@ -198,6 +200,17 @@ group state to disk.
---
## Other workspace crates
| Crate | Role |
|-------------------------|------|
| **quicnprotochat-gui** | Tauri 2 desktop application; provides a GUI on top of the client/core stack. |
| **quicnprotochat-p2p** | P2P endpoint publish/resolve; used by the server and clients for direct peer discovery. |
These crates are optional for building and running the server and CLI client.
---
## Layering Rules
1. **proto** depends on nothing in-workspace. It is pure data definition.
@@ -207,6 +220,8 @@ group state to disk.
4. **client** depends on **core** and **proto**. It does not depend on server.
5. **server** and **client** never depend on each other. They communicate
exclusively via the Cap'n Proto RPC wire protocol.
6. **quicnprotochat-gui** and **quicnprotochat-p2p** are optional; they depend
on client/core/proto as needed and do not change the core layering.
This layering ensures that:

View File

@@ -0,0 +1,75 @@
# Certificate lifecycle and CA-signed TLS
This page describes how to use CA-issued certificates with quicnprotochat and how to think about certificate pinning, rotation, and lifecycle.
For basic server TLS setup (self-signed certs, generation), see [Running the Server](running-the-server.md#tls-certificate-handling).
---
## Current behaviour
- **Server:** Uses a single TLS certificate and private key (DER format). If the files are missing and the server is not in production mode, it generates a self-signed certificate. Production mode (`QUICNPROTOCHAT_PRODUCTION=1`) requires existing cert and key files.
- **Client:** Trusts exactly the roots in the file given by `--ca-cert` (or `QUICNPROTOCHAT_CA_CERT`). Typically this is the server's own certificate (pinning) or a CA that signed the server cert.
---
## Certificate pinning (recommended for single-server)
To pin the server so the client only connects to that server:
1. Copy the server's certificate file (e.g. `data/server-cert.der`) from the server (or your deployment).
2. Use that file as the client's CA cert:
```bash
quicnprotochat --ca-cert /path/to/server-cert.der ...
```
3. The client will only accept a connection if the server presents that exact certificate (or a chain ending in it). No separate CA bundle is required.
This is **trust-on-first-use**: whoever deploys the server and distributes the cert to clients is the trust anchor. Suitable for single-server or small deployments.
---
## CA-issued certificates (e.g. Let's Encrypt)
To use a certificate issued by a public CA (e.g. Let's Encrypt):
1. **Obtain the certificate and key** using your preferred method (e.g. certbot, acme-client). The server expects:
- Certificate in **DER** format (not PEM). Convert if needed:
```bash
openssl x509 -in fullchain.pem -outform DER -out server-cert.der
```
- Private key in **DER** format (PKCS#8). Convert if needed:
```bash
openssl pkcs8 -topk8 -inform PEM -outform DER -in privkey.pem -out server-key.der -nocrypt
```
2. **Configure the server** to use those paths:
```bash
export QUICNPROTOCHAT_TLS_CERT=/etc/quicnprotochat/server-cert.der
export QUICNPROTOCHAT_TLS_KEY=/etc/quicnprotochat/server-key.der
```
3. **Configure the client** to trust the CA that signed the server cert. Use the CAs certificate (or the CA bundle) as `--ca-cert`:
```bash
quicnprotochat --ca-cert /etc/ssl/certs/your-ca.der --server-name your.server.example ...
```
The `--server-name` must match the certificates SAN (e.g. DNS name).
**Note:** The server does not currently reload the certificate on SIGHUP or on a timer. Certificate rotation is done by replacing the cert/key files and restarting the server (or by adding a future “reload” mechanism).
---
## Certificate rotation
- **Manual rotation:** Replace `server-cert.der` and `server-key.der` on disk, then restart the server. Clients that pin the new cert must be updated with the new cert file.
- **Lets Encrypt renewal:** After renewing (e.g. via certbot), convert the new cert and key to DER, replace the files, and restart the server. If clients use the CA cert (e.g. ISRG Root X1) as `--ca-cert`, they do not need updates when the server cert is renewed.
- **OCSP / CRL:** The quicnprotochat server does not currently perform OCSP stapling or CRL checks. Revocation is handled by the client or by operational procedures (e.g. short-lived certs, rotation on compromise).
---
## Summary
| Deployment style | Server cert | Client `--ca-cert` |
|------------------|-------------|--------------------|
| Pinned (single server) | Self-signed or any | Servers cert file |
| CA-issued | Lets Encrypt (or other CA) | CA cert (or bundle) |
| Production | Always use existing cert/key; set `QUICNPROTOCHAT_PRODUCTION=1` | CA or pinned server cert |
For production, prefer either (a) certificate pinning with the servers cert or (b) a CA-issued server cert with clients trusting the CA, and plan for rotation and restart (or future reload support).

View File

@@ -55,6 +55,14 @@ RUST_LOG=debug \
cargo run -p quicnprotochat-server
```
### Production deployment
Set `QUICNPROTOCHAT_PRODUCTION=1` (or `true` / `yes`) so the server enforces production checks:
- **Auth:** A non-empty `QUICNPROTOCHAT_AUTH_TOKEN` is required; the value `devtoken` is rejected.
- **TLS:** Existing cert and key files are required (auto-generation is disabled).
- **SQL store:** When `--store-backend=sql`, a non-empty `QUICNPROTOCHAT_DB_KEY` is required. An empty key leaves the database unencrypted on disk and is not acceptable for production.
---
## TLS certificate handling

View File

@@ -90,7 +90,8 @@ Known limitations:
- MLS credentials use `CredentialType::Basic` (raw public key). A production system would bind credentials to a certificate authority or use X.509 certificates.
- The Delivery Service performs **no authentication** of the `recipientKey` field -- anyone who knows a recipient's public key can enqueue messages for them. Access control is a future milestone.
- The HPKE init private key generated during `register-state` is held in-process memory (or on-disk via the key store). If the process exits before the corresponding Welcome is consumed, `join` will fail because the private key is lost.
- Group membership is currently limited to two-party groups in practice. Multi-party Commit fan-out is planned for milestone M5.
Multi-party groups (N > 2) are supported (milestone M5): Commit fan-out, `send --all`, and epoch sync work for all members.
For the full milestone tracker, see [Milestones](roadmap/milestones.md).

View File

@@ -175,10 +175,10 @@ pub fn from_bytes(bytes: &[u8]) -> Result<Reader<OwnedSegments>, capnp::Error>
```
`from_bytes` uses `ReaderOptions::new()` with default limits:
- **Traversal limit**: 64 MiB (8 * 1024 * 1024 words)
- **Traversal limit**: 32 MiB (4 * 1024 * 1024 words)
- **Nesting limit**: 512 levels
These defaults are reasonable for trusted data. For untrusted data from the network, callers should consider tightening `traversal_limit_in_words` to prevent denial-of-service via deeply nested or excessively large messages. The server enforces its own size limits: 5 MB per payload (`MAX_PAYLOAD_BYTES`) and 1 MB per KeyPackage (`MAX_KEYPACKAGE_BYTES`).
The traversal limit bounds DoS from deeply nested or excessively large Cap'n Proto messages. The server also enforces size limits: 5 MB per payload (`MAX_PAYLOAD_BYTES`) and 1 MB per KeyPackage (`MAX_KEYPACKAGE_BYTES`).
## The NodeService RPC interface