From 853ca4fec0fedafdd5fafee379d85f2a83d25e00 Mon Sep 17 00:00:00 2001 From: Chris Nennemann Date: Sun, 1 Mar 2026 20:11:51 +0100 Subject: [PATCH] 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 --- .github/CODEOWNERS | 12 +- .gitignore | 2 +- Cargo.lock | 301 +++++++++++--- Cargo.toml | 16 +- README.md | 26 +- ROADMAP.md | 376 ++++++++++++++++++ crates/quicnprotochat-gui/src/main.rs | 5 - .../Cargo.toml | 12 +- .../src/client/commands.rs | 10 +- .../src/client/conversation.rs | 0 .../src/client/display.rs | 0 .../src/client/hex.rs | 0 .../src/client/mod.rs | 0 .../src/client/repl.rs | 45 ++- .../src/client/retry.rs | 0 .../src/client/rpc.rs | 8 +- .../src/client/session.rs | 4 +- .../src/client/state.rs | 7 +- .../src/client/token_cache.rs | 0 .../src/lib.rs | 10 +- .../src/main.rs | 94 ++--- .../tests/e2e.rs | 14 +- .../Cargo.toml | 20 +- .../benches/hybrid_kem_bench.rs | 152 +++++++ .../quicproquo-core/benches/mls_operations.rs | 132 ++++++ .../quicproquo-core/benches/serialization.rs | 170 ++++++++ .../quicproquo-core/proto/chat_message.proto | 21 + .../src/app_message.rs | 0 .../src/error.rs | 2 +- .../src/group.rs | 94 ++++- .../src/hybrid_crypto.rs | 102 ++++- .../src/hybrid_kem.rs | 4 + .../src/identity.rs | 26 ++ .../src/keypackage.rs | 4 +- .../src/keystore.rs | 0 .../src/lib.rs | 4 +- .../src/opaque_auth.rs | 2 +- crates/quicproquo-core/src/padding.rs | 144 +++++++ crates/quicproquo-core/src/sealed_sender.rs | 154 +++++++ .../Cargo.toml | 12 +- .../README.md | 6 +- .../build.rs | 0 .../capabilities/default.json | 0 .../gen/schemas/acl-manifests.json | 0 .../gen/schemas/capabilities.json | 0 .../gen/schemas/desktop-schema.json | 0 .../gen/schemas/linux-schema.json | 0 .../gen/schemas/macOS-schema.json | 0 .../icons/icon.png | Bin .../src/backend.rs | 2 +- .../src/lib.rs | 2 +- crates/quicproquo-gui/src/main.rs | 5 + .../tauri.conf.json | 6 +- .../ui/index.html | 6 +- crates/quicproquo-mobile/Cargo.toml | 23 ++ crates/quicproquo-mobile/src/lib.rs | 331 +++++++++++++++ .../Cargo.toml | 4 +- .../src/lib.rs | 5 +- .../Cargo.toml | 4 +- .../build.rs | 9 +- .../src/lib.rs | 11 +- .../Cargo.toml | 11 +- .../migrations/001_initial.sql | 0 .../migrations/002_add_seq.sql | 0 .../migrations/003_channels.sql | 0 .../migrations/004_federation.sql | 16 + .../src/auth.rs | 4 +- .../src/config.rs | 82 +++- .../src/error_codes.rs | 0 .../src/federation/address.rs | 78 ++++ .../src/federation/client.rs | 287 +++++++++++++ .../quicproquo-server/src/federation/mod.rs | 16 + .../src/federation/routing.rs | 44 ++ .../src/federation/service.rs | 201 ++++++++++ .../quicproquo-server/src/federation/tls.rs | 85 ++++ .../src/main.rs | 223 ++++++++++- .../src/metrics.rs | 0 .../src/node_service/auth_ops.rs | 4 +- .../src/node_service/channel_ops.rs | 2 +- .../src/node_service/delivery.rs | 2 +- .../src/node_service/key_ops.rs | 4 +- .../src/node_service/mod.rs | 16 +- .../src/node_service/p2p_ops.rs | 2 +- .../src/node_service/user_ops.rs | 41 +- .../src/sql_store.rs | 68 +++- .../src/storage.rs | 53 +++ .../src/tls.rs | 0 docker-compose.yml | 2 +- docker/Dockerfile | 50 +-- docker/Dockerfile.chat-test | 48 +-- docker/docker-compose.chat-test.yml | 18 +- docs/FUTURE-IMPROVEMENTS.md | 8 +- docs/MULTI-AGENT-WORK-PLAN.md | 26 +- docs/PRODUCTION-READINESS-AUDIT.md | 18 +- docs/SECURITY-AUDIT.md | 28 +- docs/book.toml | 4 +- docs/src/SUMMARY.md | 4 +- docs/src/appendix/glossary.md | 28 +- docs/src/appendix/references.md | 30 +- .../architecture/crate-responsibilities.md | 38 +- docs/src/architecture/data-flow.md | 2 +- docs/src/architecture/overview.md | 14 +- docs/src/architecture/protocol-stack.md | 2 +- docs/src/architecture/service-architecture.md | 10 +- docs/src/contributing/coding-standards.md | 8 +- docs/src/contributing/testing.md | 28 +- docs/src/cryptography/forward-secrecy.md | 10 +- docs/src/cryptography/identity-keys.md | 6 +- docs/src/cryptography/key-lifecycle.md | 14 +- docs/src/cryptography/overview.md | 2 +- .../cryptography/post-compromise-security.md | 10 +- .../cryptography/post-quantum-readiness.md | 18 +- docs/src/cryptography/threat-model.md | 4 +- .../src/design-rationale/adr-002-capnproto.md | 10 +- .../adr-004-mls-unaware-ds.md | 6 +- .../adr-005-single-use-keypackages.md | 4 +- .../design-rationale/adr-006-rest-gateway.md | 116 ++++++ docs/src/design-rationale/overview.md | 4 +- .../design-rationale/protocol-comparison.md | 64 +-- docs/src/design-rationale/why-not-signal.md | 34 +- docs/src/getting-started/building.md | 22 +- .../getting-started/certificate-lifecycle.md | 18 +- docs/src/getting-started/demo-walkthrough.md | 24 +- docs/src/getting-started/docker.md | 32 +- docs/src/getting-started/prerequisites.md | 8 +- .../src/getting-started/running-the-client.md | 44 +- .../src/getting-started/running-the-server.md | 46 +-- docs/src/internals/authentication-service.md | 8 +- docs/src/internals/delivery-service.md | 4 +- docs/src/internals/group-member-lifecycle.md | 4 +- docs/src/internals/keypackage-exchange.md | 16 +- docs/src/internals/storage-backend.md | 10 +- docs/src/introduction.md | 12 +- docs/src/protocol-layers/capn-proto.md | 18 +- docs/src/protocol-layers/hybrid-kem.md | 14 +- docs/src/protocol-layers/mls.md | 20 +- docs/src/protocol-layers/overview.md | 12 +- docs/src/protocol-layers/quic-tls.md | 28 +- docs/src/roadmap/authz-plan.md | 2 +- docs/src/roadmap/dm-channels.md | 2 +- .../roadmap/fully-operational-checklist.md | 2 +- docs/src/roadmap/future-research.md | 24 +- docs/src/roadmap/milestones.md | 22 +- docs/src/roadmap/phase2-and-m4-m6.md | 2 +- docs/src/roadmap/production-readiness.md | 6 +- docs/src/wire-format/envelope-schema.md | 4 +- docs/src/wire-format/node-service-schema.md | 4 +- docs/src/wire-format/overview.md | 2 +- master-prompt.md | 66 +-- schemas/federation.capnp | 54 +++ schemas/node.capnp | 2 +- scripts/chat-test.sh | 20 +- 152 files changed, 4070 insertions(+), 788 deletions(-) create mode 100644 ROADMAP.md delete mode 100644 crates/quicnprotochat-gui/src/main.rs rename crates/{quicnprotochat-client => quicproquo-client}/Cargo.toml (85%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/commands.rs (99%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/conversation.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/display.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/hex.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/mod.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/repl.rs (96%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/retry.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/rpc.rs (98%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/session.rs (97%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/state.rs (98%) rename crates/{quicnprotochat-client => quicproquo-client}/src/client/token_cache.rs (100%) rename crates/{quicnprotochat-client => quicproquo-client}/src/lib.rs (84%) rename crates/{quicnprotochat-client => quicproquo-client}/src/main.rs (84%) rename crates/{quicnprotochat-client => quicproquo-client}/tests/e2e.rs (97%) rename crates/{quicnprotochat-core => quicproquo-core}/Cargo.toml (77%) create mode 100644 crates/quicproquo-core/benches/hybrid_kem_bench.rs create mode 100644 crates/quicproquo-core/benches/mls_operations.rs create mode 100644 crates/quicproquo-core/benches/serialization.rs create mode 100644 crates/quicproquo-core/proto/chat_message.proto rename crates/{quicnprotochat-core => quicproquo-core}/src/app_message.rs (100%) rename crates/{quicnprotochat-core => quicproquo-core}/src/error.rs (94%) rename crates/{quicnprotochat-core => quicproquo-core}/src/group.rs (86%) rename crates/{quicnprotochat-core => quicproquo-core}/src/hybrid_crypto.rs (80%) rename crates/{quicnprotochat-core => quicproquo-core}/src/hybrid_kem.rs (99%) rename crates/{quicnprotochat-core => quicproquo-core}/src/identity.rs (81%) rename crates/{quicnprotochat-core => quicproquo-core}/src/keypackage.rs (96%) rename crates/{quicnprotochat-core => quicproquo-core}/src/keystore.rs (100%) rename crates/{quicnprotochat-core => quicproquo-core}/src/lib.rs (96%) rename crates/{quicnprotochat-core => quicproquo-core}/src/opaque_auth.rs (93%) create mode 100644 crates/quicproquo-core/src/padding.rs create mode 100644 crates/quicproquo-core/src/sealed_sender.rs rename crates/{quicnprotochat-gui => quicproquo-gui}/Cargo.toml (50%) rename crates/{quicnprotochat-gui => quicproquo-gui}/README.md (80%) rename crates/{quicnprotochat-gui => quicproquo-gui}/build.rs (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/capabilities/default.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/gen/schemas/acl-manifests.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/gen/schemas/capabilities.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/gen/schemas/desktop-schema.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/gen/schemas/linux-schema.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/gen/schemas/macOS-schema.json (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/icons/icon.png (100%) rename crates/{quicnprotochat-gui => quicproquo-gui}/src/backend.rs (97%) rename crates/{quicnprotochat-gui => quicproquo-gui}/src/lib.rs (98%) create mode 100644 crates/quicproquo-gui/src/main.rs rename crates/{quicnprotochat-gui => quicproquo-gui}/tauri.conf.json (74%) rename crates/{quicnprotochat-gui => quicproquo-gui}/ui/index.html (91%) create mode 100644 crates/quicproquo-mobile/Cargo.toml create mode 100644 crates/quicproquo-mobile/src/lib.rs rename crates/{quicnprotochat-p2p => quicproquo-p2p}/Cargo.toml (70%) rename crates/{quicnprotochat-p2p => quicproquo-p2p}/src/lib.rs (96%) rename crates/{quicnprotochat-proto => quicproquo-proto}/Cargo.toml (78%) rename crates/{quicnprotochat-proto => quicproquo-proto}/build.rs (84%) rename crates/{quicnprotochat-proto => quicproquo-proto}/src/lib.rs (88%) rename crates/{quicnprotochat-server => quicproquo-server}/Cargo.toml (86%) rename crates/{quicnprotochat-server => quicproquo-server}/migrations/001_initial.sql (100%) rename crates/{quicnprotochat-server => quicproquo-server}/migrations/002_add_seq.sql (100%) rename crates/{quicnprotochat-server => quicproquo-server}/migrations/003_channels.sql (100%) create mode 100644 crates/quicproquo-server/migrations/004_federation.sql rename crates/{quicnprotochat-server => quicproquo-server}/src/auth.rs (98%) rename crates/{quicnprotochat-server => quicproquo-server}/src/config.rs (68%) rename crates/{quicnprotochat-server => quicproquo-server}/src/error_codes.rs (100%) create mode 100644 crates/quicproquo-server/src/federation/address.rs create mode 100644 crates/quicproquo-server/src/federation/client.rs create mode 100644 crates/quicproquo-server/src/federation/mod.rs create mode 100644 crates/quicproquo-server/src/federation/routing.rs create mode 100644 crates/quicproquo-server/src/federation/service.rs create mode 100644 crates/quicproquo-server/src/federation/tls.rs rename crates/{quicnprotochat-server => quicproquo-server}/src/main.rs (53%) rename crates/{quicnprotochat-server => quicproquo-server}/src/metrics.rs (100%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/auth_ops.rs (99%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/channel_ops.rs (97%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/delivery.rs (99%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/key_ops.rs (98%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/mod.rs (94%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/p2p_ops.rs (98%) rename crates/{quicnprotochat-server => quicproquo-server}/src/node_service/user_ops.rs (63%) rename crates/{quicnprotochat-server => quicproquo-server}/src/sql_store.rs (90%) rename crates/{quicnprotochat-server => quicproquo-server}/src/storage.rs (94%) rename crates/{quicnprotochat-server => quicproquo-server}/src/tls.rs (100%) create mode 100644 docs/src/design-rationale/adr-006-rest-gateway.md create mode 100644 schemas/federation.capnp diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2d538b1..995ca06 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -# Code owners for quicnprotochat. PRs require review from owners. +# Code owners for quicproquo. PRs require review from owners. # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Replace 'maintainers' with your GitHub user/team handle. @@ -6,10 +6,10 @@ * @maintainers # Crate-specific (uncomment and add handles when you have designated owners) -# /crates/quicnprotochat-core/ @owner1 -# /crates/quicnprotochat-proto/ @owner1 -# /crates/quicnprotochat-server/ @owner1 -# /crates/quicnprotochat-client/ @owner1 -# /crates/quicnprotochat-p2p/ @owner1 +# /crates/quicproquo-core/ @owner1 +# /crates/quicproquo-proto/ @owner1 +# /crates/quicproquo-server/ @owner1 +# /crates/quicproquo-client/ @owner1 +# /crates/quicproquo-p2p/ @owner1 # /schemas/ @owner1 # /docs/ @owner1 diff --git a/.gitignore b/.gitignore index a9e4fcf..aa1c206 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ docs/book/ # Server/client runtime data — do not commit certs, keys, or DBs data/ *.der -quicnprotochat-server.toml +qpq-server.toml diff --git a/Cargo.lock b/Cargo.lock index 1658b1f..e8a2b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -536,6 +542,12 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.56" @@ -648,6 +660,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.3.0" @@ -813,6 +852,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -847,6 +922,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1129,9 +1210,9 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "objc2", @@ -1961,6 +2042,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2423,12 +2515,41 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2492,9 +2613,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2624,11 +2745,10 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", "libc", ] @@ -2953,9 +3073,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -3184,6 +3304,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -3531,9 +3657,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3570,6 +3696,34 @@ dependencies = [ "time", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.16" @@ -3787,6 +3941,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "quanta" version = "0.12.6" @@ -3812,7 +3989,7 @@ dependencies = [ ] [[package]] -name = "quicnprotochat-client" +name = "quicproquo-client" version = "0.1.0" dependencies = [ "anyhow", @@ -3829,8 +4006,8 @@ dependencies = [ "opaque-ke", "openmls_rust_crypto", "portpicker", - "quicnprotochat-core", - "quicnprotochat-proto", + "quicproquo-core", + "quicproquo-proto", "quinn", "quinn-proto", "rand 0.8.5", @@ -3850,13 +4027,14 @@ dependencies = [ ] [[package]] -name = "quicnprotochat-core" +name = "quicproquo-core" version = "0.1.0" dependencies = [ "argon2", "bincode", "capnp", "chacha20poly1305 0.10.1", + "criterion", "ed25519-dalek 2.2.0", "hkdf", "ml-kem", @@ -3864,7 +4042,8 @@ dependencies = [ "openmls", "openmls_rust_crypto", "openmls_traits", - "quicnprotochat-proto", + "prost", + "quicproquo-proto", "rand 0.8.5", "serde", "serde_json", @@ -3877,12 +4056,12 @@ dependencies = [ ] [[package]] -name = "quicnprotochat-gui" +name = "quicproquo-gui" version = "0.1.0" dependencies = [ - "quicnprotochat-client", - "quicnprotochat-core", - "quicnprotochat-proto", + "quicproquo-client", + "quicproquo-core", + "quicproquo-proto", "serde", "serde_json", "tauri", @@ -3891,7 +4070,18 @@ dependencies = [ ] [[package]] -name = "quicnprotochat-proto" +name = "quicproquo-mobile" +version = "0.1.0" +dependencies = [ + "anyhow", + "quinn", + "rcgen", + "rustls", + "tokio", +] + +[[package]] +name = "quicproquo-proto" version = "0.1.0" dependencies = [ "capnp", @@ -3899,7 +4089,7 @@ dependencies = [ ] [[package]] -name = "quicnprotochat-server" +name = "quicproquo-server" version = "0.1.0" dependencies = [ "anyhow", @@ -3909,11 +4099,12 @@ dependencies = [ "clap", "dashmap", "futures", + "hex", "metrics 0.22.4", "metrics-exporter-prometheus", "opaque-ke", - "quicnprotochat-core", - "quicnprotochat-proto", + "quicproquo-core", + "quicproquo-proto", "quinn", "quinn-proto", "rand 0.8.5", @@ -4228,9 +4419,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -4350,9 +4541,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -4669,9 +4860,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -4688,9 +4879,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling", "proc-macro2", @@ -5308,9 +5499,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom 0.4.1", @@ -5426,6 +5617,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -6060,9 +6261,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -6073,9 +6274,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -6087,9 +6288,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6097,9 +6298,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -6110,9 +6311,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -6166,9 +6367,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -6953,18 +7154,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 54ab815..88f92b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,17 @@ [workspace] resolver = "2" members = [ - "crates/quicnprotochat-core", - "crates/quicnprotochat-proto", - "crates/quicnprotochat-server", - "crates/quicnprotochat-client", - "crates/quicnprotochat-gui", + "crates/quicproquo-core", + "crates/quicproquo-proto", + "crates/quicproquo-server", + "crates/quicproquo-client", + "crates/quicproquo-gui", + "crates/quicproquo-mobile", ] # P2P-Crate (iroh-Transport) ist vom Default-Build ausgeschlossen, # um ~90 exklusive iroh-Dependencies nicht mitzukompilieren. # Quellcode bleibt im Repo für spätere Integration. -exclude = ["crates/quicnprotochat-p2p"] +exclude = ["crates/quicproquo-p2p"] # Shared dependency versions — bump here to affect the whole workspace. [workspace.dependencies] @@ -55,6 +56,9 @@ rcgen = { version = "0.13" } # ── Database ───────────────────────────────────────────────────────────── rusqlite = { version = "0.31", features = ["bundled-sqlcipher"] } +# ── Encoding ───────────────────────────────────────────────────────────────── +hex = { version = "0.4" } + # ── Server utilities ────────────────────────────────────────────────────────── dashmap = { version = "5" } tracing = { version = "0.1" } diff --git a/README.md b/README.md index d525d7a..24f5776 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# quicnprotochat +# quicproquo > End-to-end encrypted group messaging over **QUIC + TLS 1.3 + MLS** (RFC 9420), written in Rust. @@ -70,31 +70,31 @@ cargo build --workspace cargo test --workspace # Start the server (port 7000 by default) -cargo run -p quicnprotochat-server +cargo run -p quicproquo-server # Or via a config file (TOML) # Note: auth_token = "devtoken" and db_key = "" are for development only. -# Production: set QUICNPROTOCHAT_AUTH_TOKEN to a strong secret and (when store_backend = "sql") -# set QUICNPROTOCHAT_DB_KEY so the database is encrypted. Empty db_key = plaintext DB (insecure). -cat > quicnprotochat-server.toml <<'EOF' +# Production: set QPQ_AUTH_TOKEN to a strong secret and (when store_backend = "sql") +# set QPQ_DB_KEY so the database is encrypted. Empty db_key = plaintext DB (insecure). +cat > qpq-server.toml <<'EOF' listen = "0.0.0.0:7000" data_dir = "data" tls_cert = "data/server-cert.der" tls_key = "data/server-key.der" auth_token = "devtoken" store_backend = "file" # or "sql" -db_path = "data/quicnprotochat.db" +db_path = "data/qpq.db" db_key = "" EOF -cargo run -p quicnprotochat-server -- --config quicnprotochat-server.toml +cargo run -p quicproquo-server -- --config qpq-server.toml # Run the two-party demo -cargo run -p quicnprotochat-client -- demo-group \ +cargo run -p quicproquo-client -- demo-group \ --server 127.0.0.1:7000 # Interactive 1:1 chat (after creating a group and inviting a peer) -# Terminal 1: quicnprotochat chat --peer-key -# Terminal 2: quicnprotochat chat --peer-key +# Terminal 1: qpq chat --peer-key +# Terminal 2: qpq chat --peer-key # Type messages and press Enter; incoming messages appear as [peer] . Ctrl+D to exit. ``` @@ -121,10 +121,10 @@ See the [full demo walkthrough](docs/src/getting-started/demo-walkthrough.md) fo To build only the server and CLI client (faster, no Tauri/WebKit): ```bash -cargo build -p quicnprotochat-server -p quicnprotochat-client +cargo build -p quicproquo-server -p quicproquo-client ``` -Core and proto crates are built as dependencies. Omit `quicnprotochat-gui` and `quicnprotochat-p2p` if you don't need them. +Core and proto crates are built as dependencies. Omit `quicproquo-gui` and `quicproquo-p2p` if you don't need them. --- @@ -135,7 +135,7 @@ This is a **proof-of-concept research project**. It has not undergone a formal t - **Dependency checks:** Run `cargo install cargo-audit && cargo audit` to check for known vulnerabilities. - **Certificate pinning:** Use the server's certificate as `--ca-cert` (e.g. copy `server-cert.der` from the server) so the client only trusts that server; see [Certificate pinning](docs/SECURITY-AUDIT.md#certificate-pinning) in the security audit. -**Production deployment:** Set `QUICNPROTOCHAT_PRODUCTION=1` and provide a strong `QUICNPROTOCHAT_AUTH_TOKEN` (not `devtoken`). When using `store_backend = "sql"`, set `QUICNPROTOCHAT_DB_KEY`; an empty key leaves the database unencrypted on disk. +**Production deployment:** Set `QPQ_PRODUCTION=1` and provide a strong `QPQ_AUTH_TOKEN` (not `devtoken`). When using `store_backend = "sql"`, set `QPQ_DB_KEY`; an empty key leaves the database unencrypted on disk. --- diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..69736f4 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,376 @@ +# Roadmap — quicproquo + +> From proof-of-concept to production-grade E2E encrypted messaging. +> +> Each phase is designed to be tackled sequentially. Items within a phase +> can be parallelised. Check the box when done. + +--- + +## Phase 1 — Production Hardening (Critical) + +Eliminate all crash paths, enforce secure defaults, fix deployment blockers. + +- [ ] **1.1 Remove `.unwrap()` / `.expect()` from production paths** + - Replace `AUTH_CONTEXT.read().expect()` in client RPC with proper `Result` + - Replace `"0.0.0.0:0".parse().unwrap()` in client with fallible parse + - Replace `Mutex::lock().unwrap()` in server storage with `.map_err()` + - Audit: `grep -rn 'unwrap()\|expect(' crates/` outside `#[cfg(test)]` + +- [ ] **1.2 Enforce secure defaults in production mode** + - Reject startup if `QPQ_PRODUCTION=true` and `auth_token` is empty or `"devtoken"` + - Require non-empty `db_key` when using SQL backend in production + - Refuse to auto-generate TLS certs in production mode (require existing cert+key) + - Already partially implemented — verify and harden the validation in `config.rs` + +- [ ] **1.3 Fix `.gitignore`** + - Add `data/`, `*.der`, `*.pem`, `*.db`, `*.bin` (state files), `*.ks` (keystores) + - Verify no secrets are already tracked: `git ls-files data/ *.der *.db` + +- [ ] **1.4 Fix Dockerfile** + - Sync workspace members (handle excluded `p2p` crate) + - Create dedicated user/group instead of `nobody` + - Set writable `QPQ_DATA_DIR` with correct permissions + - Test: `docker build . && docker run --rm -it qpq-server --help` + +- [ ] **1.5 TLS certificate lifecycle** + - Document CA-signed cert setup (Let's Encrypt / custom CA) + - Add `--tls-required` flag that refuses to start without valid cert + - Log clear warning when using self-signed certs + - Document certificate rotation procedure + +--- + +## Phase 2 — Test & CI Maturity + +Build confidence before adding features. + +- [ ] **2.1 Expand E2E test coverage** + - Auth failure scenarios (wrong password, expired token, invalid token) + - Message ordering verification (send N messages, verify seq numbers) + - Concurrent clients (3+ members in group, simultaneous send/recv) + - OPAQUE registration + login full flow + - Queue full behavior (>1000 messages) + - Rate limiting behavior (>100 enqueues/minute) + - Reconnection after server restart + - KeyPackage exhaustion (fetch when none available) + +- [ ] **2.2 Add unit tests for untested paths** + - Client retry logic (exponential backoff, jitter, retriable classification) + - REPL input parsing edge cases (empty input, special characters, `/` commands) + - State file encryption/decryption round-trip with bad password + - Token cache expiry + - Conversation store migrations + +- [ ] **2.3 CI hardening** + - Add `.github/CODEOWNERS` (crypto, auth, wire-format require 2 reviewers) + - Ensure `cargo deny check` runs on every PR (already in CI — verify) + - Add `cargo audit` as blocking check (already in CI — verify) + - Add coverage reporting (tarpaulin or llvm-cov) + - Add CI job for Docker build validation + +- [ ] **2.4 Clean up build warnings** + - Fix Cap'n Proto generated `unused_parens` warnings + - Remove dead code / unused imports + - Address `openmls` future-incompat warnings + - Target: `cargo clippy --workspace -- -D warnings` passes clean + +--- + +## Phase 3 — Client SDKs: Native QUIC + Cap'n Proto Everywhere + +**No REST gateway. No protocol dilution.** The `.capnp` schemas are the +interface definition. Every SDK speaks native QUIC + Cap'n Proto. The +project name stays honest. + +### Why this matters + +The name is **quic**n**proto**chat — the protocol IS the product. Instead +of adding an HTTP translation layer that loses zero-copy performance and +adds base64 overhead, we invest in making the native protocol accessible +from every language that has QUIC + Cap'n Proto support, and provide +WASM/FFI for the crypto layer. + +### Architecture + +``` + Server: QUIC + Cap'n Proto (single protocol, no gateway) + + Client SDKs: + ┌─── Rust quinn + capnp-rpc (existing, reference impl) + ├─── Go quic-go + go-capnp (native, high confidence) + ├─── Python aioquic + pycapnp (native QUIC, manual framing) + ├─── C/C++ msquic/ngtcp2 + capnproto (reference impl, full RPC) + └─── Browser WebTransport + capnp (WASM) (QUIC transport, no HTTP needed) + + Crypto layer (client-side MLS, shared across all SDKs): + ┌─── Rust crate (native, existing) + ├─── WASM module (browsers, Node.js, Deno) + └─── C FFI (Swift, Kotlin, Python, Go via cgo) +``` + +### Language support reality check + +| Language | QUIC | Cap'n Proto | RPC | Confidence | +|----------|------|-------------|-----|------------| +| **Rust** | quinn ✅ | capnp-rpc ✅ | Full ✅ | Existing | +| **Go** | quic-go ✅ | go-capnp ✅ | Level 1 ✅ | High | +| **Python** | aioquic ✅ | pycapnp ⚠️ | Manual framing | Medium | +| **C/C++** | msquic/ngtcp2 ✅ | capnproto ✅ | Full ✅ | High | +| **Browser** | WebTransport ✅ | WASM ✅ | Via WASM bridge | Medium | + +### Implementation + +- [ ] **3.1 Go SDK (`quicproquo-go`)** + - Generate Go types: `capnp compile -ogo schemas/node.capnp` + - QUIC transport: `quic-go` with TLS 1.3 + ALPN `"capnp"` + - Cap'n Proto RPC framing over QUIC bidirectional stream + - Auth context: bearer token + session management + - Retry with exponential backoff (mirror Rust client pattern) + - Publish: `go get git.xorwell.de/c/quicproquo-go` + - Example: CLI client matching Rust feature set + +- [ ] **3.2 Python SDK (`quicproquo-py`)** + - QUIC transport: `aioquic` with custom Cap'n Proto stream handler + - Cap'n Proto serialization: `pycapnp` for message types + - Manual RPC framing: length-prefixed request/response over QUIC stream + - Async/await API matching the Rust client patterns + - Crypto: PyO3 bindings to `quicproquo-core` for MLS operations + - Publish: PyPI `quicproquo` + - Example: async bot client + +- [ ] **3.3 C FFI layer (`quicproquo-ffi`)** + - New crate in workspace: `crates/quicproquo-ffi` + - `cbindgen` to generate `quicproquo.h` C header + - Crypto functions: `qpc_identity_new()`, `qpc_group_create()`, + `qpc_encrypt()`, `qpc_decrypt()`, `qpc_key_package_generate()` + - Transport functions: `qpc_connect()`, `qpc_enqueue()`, `qpc_fetch()`, + `qpc_fetch_wait()` (bundles QUIC + Cap'n Proto internally) + - Memory: caller-allocated buffers with length, no ownership transfer + - Builds as `libquicproquo.so` / `.dylib` / `.dll` + - Swift and Kotlin wrapper examples using the C header + +- [ ] **3.4 WASM compilation of `quicproquo-core`** + - `wasm-pack build` target for browser + Node.js + - Crypto-only: `GroupMember`, `IdentityKeypair`, `AppMessage`, + `hybrid_encrypt/decrypt`, `generate_key_package` + - Transport NOT included (browsers use WebTransport, see Phase 3.5) + - Publish to npm: `@quicproquo/core` + - TypeScript type definitions auto-generated via `wasm-bindgen` + +- [ ] **3.5 WebTransport server endpoint** + - Add HTTP/3 + WebTransport listener to server (same QUIC stack via quinn) + - Cap'n Proto RPC framed over WebTransport bidirectional streams + - Same auth, same storage, same RPC handlers — just a different stream source + - Browsers connect via `new WebTransport("https://server:7443")` + - ALPN negotiation: `"h3"` for WebTransport, `"capnp"` for native QUIC + - Configurable port: `--webtransport-listen 0.0.0.0:7443` + - Feature-flagged: `--features webtransport` + +- [ ] **3.6 TypeScript/JavaScript SDK (`@quicproquo/client`)** + - WebTransport for QUIC connectivity (no HTTP fallback) + - WASM module (Phase 3.4) for MLS crypto + - Cap'n Proto serialization via WASM bridge + - Handles: auth flow, key upload, message send/receive, group management + - Publish to npm: `@quicproquo/client` + - Example: browser chat UI + +- [ ] **3.7 SDK documentation and schema publishing** + - Publish `.capnp` schemas as the canonical API contract + - Document the QUIC + Cap'n Proto connection pattern for each language + - Provide a "build your own SDK" guide (QUIC stream → Cap'n Proto RPC bootstrap) + - Reference implementation checklist: connect, auth, upload key, enqueue, fetch + +--- + +## Phase 4 — Trust & Security Infrastructure + +Address the security gaps required for real-world deployment. + +- [ ] **4.1 Third-party cryptographic audit** + - Scope: MLS integration, OPAQUE flow, hybrid KEM, key lifecycle, zeroization + - Firms: NCC Group, Trail of Bits, Cure53 + - Budget and timeline: typically 4-6 weeks, $50K–$150K + - Publish report publicly (builds trust) + +- [ ] **4.2 Key Transparency / revocation** + - Replace `BasicCredential` with X.509-based MLS credentials + - Or: verifiable key directory (Merkle tree, auditable log) + - Users can verify peer keys haven't been substituted (MITM detection) + - Revocation mechanism for compromised keys + +- [ ] **4.3 Client authentication on Delivery Service** + - Currently server trusts claimed identity key on enqueue + - Bind enqueue operations to the authenticated session's identity key + - Prevent: client A fetching/sending as client B's identity + - Backward compat: sealed_sender mode for anonymous enqueue + +- [ ] **4.4 M7 — Post-quantum MLS integration** + - Integrate hybrid KEM (X25519 + ML-KEM-768) into the OpenMLS crypto provider + - Group key material gets post-quantum confidentiality + - Full test suite with PQ ciphersuite + - Ref: existing `hybrid_kem.rs` and `hybrid_crypto.rs` + +- [ ] **4.5 Username enumeration mitigation** + - Constant-time or uniform response for unknown users during OPAQUE login + - Prevent timing side-channels that reveal user existence + +--- + +## Phase 5 — Features & UX + +Make it a product people want to use. + +- [ ] **5.1 Multi-device support** + - Account → multiple devices, each with own Ed25519 key + MLS KeyPackages + - Device graph management (add device, remove device, list devices) + - Messages delivered to all devices of a user + - `device_id` field already in Auth struct — wire it through + +- [ ] **5.2 Account recovery** + - Recovery codes or backup key (encrypted, stored by user) + - Option: server-assisted recovery with security questions (lower security) + - MLS state re-establishment after device loss + +- [ ] **5.3 Full MLS lifecycle** + - Member removal (Remove proposal → Commit → fan-out) + - Credential update (Update proposal for key rotation) + - Explicit proposal handling (queue proposals, batch commit) + - Group metadata (name, description, avatar hash) + +- [ ] **5.4 Message editing and deletion** + - New `AppMessage` variants: `Edit { target_seq, new_content }`, `Delete { target_seq }` + - Client-side tombstones, server doesn't know about edits + +- [ ] **5.5 File and media transfer** + - Upload encrypted blob → get content hash + - Share hash + symmetric key inside MLS message + - Download by hash, decrypt client-side + - Size limits, content-type validation + +- [ ] **5.6 Abuse prevention and moderation** + - Block user (client-side, suppress display) + - Report message (encrypted report to admin key) + - Admin tools: ban user, delete account, audit log + +- [ ] **5.7 Offline message queue (client-side)** + - Queue messages when disconnected, send on reconnect + - Idempotent message IDs to prevent duplicates + - Gap detection: compare local seq with server seq + +--- + +## Phase 6 — Scale & Operations + +Prepare for real traffic. + +- [ ] **6.1 Distributed rate limiting** + - Current: in-memory per-process, lost on restart + - Move to Redis or shared state for multi-node deployments + - Sliding window with configurable thresholds + +- [ ] **6.2 Multi-node / horizontal scaling** + - Stateless server design (already mostly there — state is in storage backend) + - Shared PostgreSQL or CockroachDB backend (replace SQLite) + - Message queue fan-out (Redis pub/sub or NATS for cross-node notification) + - Load balancer health check via QUIC RPC `health()` or Prometheus `/metrics` + +- [ ] **6.3 Operational runbook** + - Backup / restore procedures (SQLCipher, file backend) + - Key rotation (auth token, TLS cert, DB encryption key) + - Incident response playbook + - Scaling guide (when to add nodes, resource sizing) + - Monitoring dashboard templates (Grafana + Prometheus) + +- [ ] **6.4 Connection draining and graceful shutdown** + - Stop accepting new connections on SIGTERM + - Wait for in-flight RPCs (configurable timeout, default 30s) + - Drain WebTransport sessions with close frame + - Document expected behavior for load balancers (health → unhealthy first) + +- [ ] **6.5 Request-level timeouts** + - Per-RPC timeout (prevent slow clients from holding resources) + - Database query timeout + - Overall request deadline propagation + +- [ ] **6.6 Observability enhancements** + - Request correlation IDs (trace across RPC → storage) + - Storage operation latency metrics + - Per-endpoint latency histograms + - Structured audit log to persistent storage (not just stdout) + - OpenTelemetry integration + +--- + +## Phase 7 — Platform Expansion & Research + +Long-term vision for wide adoption. + +- [ ] **7.1 Mobile clients (iOS + Android)** + - Use C FFI (Phase 3.3) for crypto + transport (single library) + - Push notifications via APNs / FCM (server sends notification on enqueue) + - Background QUIC connection for message polling + - Biometric auth for local key storage (Keychain / Android Keystore) + +- [ ] **7.2 Web client (browser)** + - Use WASM (Phase 3.4) for crypto + - Use WebTransport (Phase 3.5) for native QUIC transport + - Cap'n Proto via WASM bridge (Phase 3.6) + - IndexedDB for local state persistence + - Service Worker for background notifications + - Progressive Web App (PWA) support + +- [ ] **7.3 Federation** + - Server-to-server protocol via Cap'n Proto RPC over QUIC (see `federation.capnp`) + - `relayEnqueue`, `proxyFetchKeyPackage`, `federationHealth` methods + - Identity resolution across federated servers + - MLS group spanning multiple servers + - Trust model for federated deployments + +- [ ] **7.4 Sealed Sender** + - Sender identity inside MLS ciphertext only (server can't see who sent) + - Requires: sender certificate + encrypted sender proof + - Ref: Signal's Sealed Sender design + +- [ ] **7.5 Additional language SDKs** + - Java/Kotlin: JNI bindings to C FFI (Phase 3.3) + native QUIC (netty-quic) + - Swift: Swift wrapper over C FFI + Network.framework QUIC + - Ruby: FFI bindings via `quicproquo-ffi` + - Evaluate demand-driven — only build SDKs people request + +- [ ] **7.6 P2P / NAT traversal** + - Direct peer-to-peer via iroh (foundation exists in `quicproquo-p2p`) + - Server as fallback relay only + - Reduces latency and single-point-of-failure + - Ref: `FUTURE-IMPROVEMENTS.md § 6.1` + +- [ ] **7.7 Traffic analysis resistance** + - Padding messages to uniform size + - Decoy traffic to mask timing patterns + - Optional Tor/I2P routing for IP privacy + - Ref: `FUTURE-IMPROVEMENTS.md § 5.4, 6.3` + +--- + +## Summary Timeline + +| Phase | Focus | Estimated Effort | +|-------|-------|-----------------| +| **1** | Production Hardening | 1–2 days | +| **2** | Test & CI Maturity | 2–3 days | +| **3** | Client SDKs (Go, Python, WASM, FFI, WebTransport) | 5–8 days | +| **4** | Trust & Security Infrastructure | 2–4 days (excl. audit) | +| **5** | Features & UX | 5–7 days | +| **6** | Scale & Operations | 3–5 days | +| **7** | Platform Expansion & Research | ongoing | + +--- + +## Related Documents + +- [Future Improvements](docs/FUTURE-IMPROVEMENTS.md) — consolidated improvement list +- [Production Readiness Audit](docs/PRODUCTION-READINESS-AUDIT.md) — specific blockers +- [Security Audit](docs/SECURITY-AUDIT.md) — findings and recommendations +- [Milestone Tracker](docs/src/roadmap/milestones.md) — M1–M7 status +- [Auth, Devices, and Tokens](docs/src/roadmap/authz-plan.md) — authorization design +- [DM Channel Design](docs/src/roadmap/dm-channels.md) — 1:1 channel spec diff --git a/crates/quicnprotochat-gui/src/main.rs b/crates/quicnprotochat-gui/src/main.rs deleted file mode 100644 index 1b7d852..0000000 --- a/crates/quicnprotochat-gui/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Desktop entry point for quicnprotochat-gui. - -fn main() { - quicnprotochat_gui::run() -} diff --git a/crates/quicnprotochat-client/Cargo.toml b/crates/quicproquo-client/Cargo.toml similarity index 85% rename from crates/quicnprotochat-client/Cargo.toml rename to crates/quicproquo-client/Cargo.toml index 8656e9e..e1421f5 100644 --- a/crates/quicnprotochat-client/Cargo.toml +++ b/crates/quicproquo-client/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "quicnprotochat-client" +name = "quicproquo-client" version = "0.1.0" edition = "2021" -description = "CLI client for quicnprotochat." +description = "CLI client for quicproquo." license = "MIT" [[bin]] -name = "quicnprotochat" +name = "qpq" path = "src/main.rs" [dependencies] -quicnprotochat-core = { path = "../quicnprotochat-core" } -quicnprotochat-proto = { path = "../quicnprotochat-proto" } +quicproquo-core = { path = "../quicproquo-core" } +quicproquo-proto = { path = "../quicproquo-proto" } openmls_rust_crypto = { workspace = true } # Serialisation + RPC @@ -54,7 +54,7 @@ clap = { workspace = true } rusqlite = { workspace = true } # Hex encoding/decoding -hex = "0.4" +hex = { workspace = true } # Secure password prompting (no echo) rpassword = "5" diff --git a/crates/quicnprotochat-client/src/client/commands.rs b/crates/quicproquo-client/src/client/commands.rs similarity index 99% rename from crates/quicnprotochat-client/src/client/commands.rs rename to crates/quicproquo-client/src/client/commands.rs index 417318c..75d1f51 100644 --- a/crates/quicnprotochat-client/src/client/commands.rs +++ b/crates/quicproquo-client/src/client/commands.rs @@ -5,7 +5,7 @@ use opaque_ke::{ ClientLogin, ClientLoginFinishParameters, ClientRegistration, ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse, }; -use quicnprotochat_core::{ +use quicproquo_core::{ generate_key_package, hybrid_decrypt, hybrid_encrypt, opaque_auth::OpaqueSuite, GroupMember, HybridKeypair, IdentityKeypair, }; @@ -316,7 +316,7 @@ fn derive_identity_for_login( /// The error message contains "E018" if the user already exists. /// Does NOT require init_auth() — OPAQUE RPCs are unauthenticated. pub(crate) async fn opaque_register( - client: &quicnprotochat_proto::node_capnp::node_service::Client, + client: &quicproquo_proto::node_capnp::node_service::Client, username: &str, password: &str, identity_key: Option<&[u8]>, @@ -377,7 +377,7 @@ pub(crate) async fn opaque_register( /// Perform OPAQUE login and return the raw session token bytes. /// Does NOT require init_auth() — OPAQUE RPCs are unauthenticated. pub(crate) async fn opaque_login( - client: &quicnprotochat_proto::node_capnp::node_service::Client, + client: &quicproquo_proto::node_capnp::node_service::Client, username: &str, password: &str, identity_key: &[u8], @@ -646,8 +646,8 @@ pub async fn cmd_fetch_key( /// Run a two-party MLS demo against the unified server. pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { - let creator_state_path = PathBuf::from("quicnprotochat-demo-creator.bin"); - let joiner_state_path = PathBuf::from("quicnprotochat-demo-joiner.bin"); + let creator_state_path = PathBuf::from("qpq-demo-creator.bin"); + let joiner_state_path = PathBuf::from("qpq-demo-joiner.bin"); let (mut creator, creator_hybrid_opt) = load_or_init_state(&creator_state_path, None)?.into_parts(&creator_state_path)?; diff --git a/crates/quicnprotochat-client/src/client/conversation.rs b/crates/quicproquo-client/src/client/conversation.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/conversation.rs rename to crates/quicproquo-client/src/client/conversation.rs diff --git a/crates/quicnprotochat-client/src/client/display.rs b/crates/quicproquo-client/src/client/display.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/display.rs rename to crates/quicproquo-client/src/client/display.rs diff --git a/crates/quicnprotochat-client/src/client/hex.rs b/crates/quicproquo-client/src/client/hex.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/hex.rs rename to crates/quicproquo-client/src/client/hex.rs diff --git a/crates/quicnprotochat-client/src/client/mod.rs b/crates/quicproquo-client/src/client/mod.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/mod.rs rename to crates/quicproquo-client/src/client/mod.rs diff --git a/crates/quicnprotochat-client/src/client/repl.rs b/crates/quicproquo-client/src/client/repl.rs similarity index 96% rename from crates/quicnprotochat-client/src/client/repl.rs rename to crates/quicproquo-client/src/client/repl.rs index fb0a39c..2370e90 100644 --- a/crates/quicnprotochat-client/src/client/repl.rs +++ b/crates/quicproquo-client/src/client/repl.rs @@ -8,11 +8,11 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Context; -use quicnprotochat_core::{ +use quicproquo_core::{ AppMessage, DiskKeyStore, GroupMember, IdentityKeypair, hybrid_encrypt, parse as parse_app_msg, serialize_chat, }; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use tokio::sync::mpsc; use tokio::time::interval; @@ -291,6 +291,7 @@ async fn auto_upload_keys( Arc::clone(&session.identity), ks, None, + false, ); let kp_bytes = member.generate_key_package().context("generate KeyPackage")?; let id_key = session.identity.public_key_bytes(); @@ -419,7 +420,7 @@ async fn handle_slash( fn print_help() { display::print_status("Commands:"); - display::print_status(" /dm - Start or switch to a DM"); + display::print_status(" /dm - Start or switch to a DM (federation supported)"); display::print_status(" /create-group - Create a new group"); display::print_status(" /invite - Invite user to current group"); display::print_status(" /join - Join a group from pending Welcome"); @@ -542,7 +543,7 @@ async fn cmd_dm( created_at_ms: now_ms(), }; let ks = DiskKeyStore::ephemeral(); - let member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None); + let member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None, false); session.add_conversation(conv, member)?; session.active_conversation = Some(conv_id); display::print_status("notes created — messages here are local only"); @@ -573,7 +574,7 @@ async fn cmd_dm( std::fs::create_dir_all(&ks_dir).ok(); let ks_path = ks_dir.join(format!("{}.ks", conv_id.hex())); let ks = DiskKeyStore::persistent(&ks_path)?; - let mut member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None); + let mut member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None, false); // Generate a key package for ourselves (needed for MLS) let _my_kp = member.generate_key_package()?; @@ -634,7 +635,7 @@ fn cmd_create_group(session: &mut SessionState, name: &str) -> anyhow::Result<() std::fs::create_dir_all(&ks_dir).ok(); let ks_path = ks_dir.join(format!("{}.ks", conv_id.hex())); let ks = DiskKeyStore::persistent(&ks_path)?; - let mut member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None); + let mut member = GroupMember::new_with_state(Arc::clone(&session.identity), ks, None, false); let _my_kp = member.generate_key_package()?; member.create_group(conv_id.0.as_slice())?; @@ -773,6 +774,7 @@ async fn cmd_join( Arc::clone(&session.identity), ks, None, + false, ); // Need a key package to decrypt Welcome. let _kp = new_member.generate_key_package()?; @@ -890,6 +892,7 @@ async fn do_send( .clone(); let my_key = session.identity_bytes(); + let identity = std::sync::Arc::clone(&session.identity); let member = session .get_member_mut(&conv_id) @@ -917,8 +920,12 @@ async fn do_send( let app_payload = serialize_chat(text.as_bytes(), None) .context("serialize app message")?; + // Metadata protection: seal sender identity inside payload + pad to bucket size. + let sealed = quicproquo_core::sealed_sender::seal(&identity, &app_payload); + let padded = quicproquo_core::padding::pad(&sealed); + let ct = member - .send_message(&app_payload) + .send_message(&padded) .context("MLS send_message failed")?; let recipients: Vec> = member @@ -997,9 +1004,27 @@ async fn poll_messages( match member.receive_message(&mls_payload) { Ok(Some(plaintext)) => { + // Metadata protection: try unpad → unseal → parse. + // Falls back gracefully for messages from older clients. + let (sender_key, app_bytes) = { + // Step 1: try unpad + let after_unpad = quicproquo_core::padding::unpad(&plaintext) + .unwrap_or_else(|_| plaintext.clone()); + + // Step 2: try unseal + if quicproquo_core::sealed_sender::is_sealed(&after_unpad) { + match quicproquo_core::sealed_sender::unseal(&after_unpad) { + Ok((sk, inner)) => (sk.to_vec(), inner), + Err(_) => (session.identity_bytes(), after_unpad), + } + } else { + (session.identity_bytes(), after_unpad) + } + }; + // Parse structured AppMessage; fall back to raw UTF-8 for legacy. let (body, msg_id, msg_type, ref_msg_id) = - match parse_app_msg(&plaintext) { + match parse_app_msg(&app_bytes) { Ok((_, AppMessage::Chat { message_id, body })) => ( String::from_utf8_lossy(&body).to_string(), Some(message_id), @@ -1021,7 +1046,7 @@ async fn poll_messages( _ => { // Legacy raw plaintext or unknown type. ( - String::from_utf8_lossy(&plaintext).to_string(), + String::from_utf8_lossy(&app_bytes).to_string(), None, "chat", None, @@ -1029,8 +1054,6 @@ async fn poll_messages( } }; - let sender_key = session.identity_bytes(); // fallback - let msg = StoredMessage { conversation_id: conv_id.clone(), message_id: msg_id, diff --git a/crates/quicnprotochat-client/src/client/retry.rs b/crates/quicproquo-client/src/client/retry.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/retry.rs rename to crates/quicproquo-client/src/client/retry.rs diff --git a/crates/quicnprotochat-client/src/client/rpc.rs b/crates/quicproquo-client/src/client/rpc.rs similarity index 98% rename from crates/quicnprotochat-client/src/client/rpc.rs rename to crates/quicproquo-client/src/client/rpc.rs index 35fd6be..678406b 100644 --- a/crates/quicnprotochat-client/src/client/rpc.rs +++ b/crates/quicproquo-client/src/client/rpc.rs @@ -10,8 +10,8 @@ use rustls::{ClientConfig as RustlsClientConfig, RootCertStore}; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; use capnp_rpc::{rpc_twoparty_capnp::Side, twoparty, RpcSystem}; -use quicnprotochat_core::HybridPublicKey; -use quicnprotochat_proto::node_capnp::{auth, node_service}; +use quicproquo_core::HybridPublicKey; +use quicproquo_proto::node_capnp::{auth, node_service}; use crate::AUTH_CONTEXT; @@ -359,11 +359,11 @@ pub async fn fetch_hybrid_key( /// Decrypt a hybrid envelope. Requires a hybrid key; no fallback to plaintext MLS. pub fn try_hybrid_decrypt( - hybrid_kp: Option<&quicnprotochat_core::HybridKeypair>, + hybrid_kp: Option<&quicproquo_core::HybridKeypair>, payload: &[u8], ) -> anyhow::Result> { let kp = hybrid_kp.ok_or_else(|| anyhow::anyhow!("hybrid key required for decryption"))?; - quicnprotochat_core::hybrid_decrypt(kp, payload, b"", b"").map_err(|e| anyhow::anyhow!("{e}")) + quicproquo_core::hybrid_decrypt(kp, payload, b"", b"").map_err(|e| anyhow::anyhow!("{e}")) } /// Peek at queued payloads without removing them. diff --git a/crates/quicnprotochat-client/src/client/session.rs b/crates/quicproquo-client/src/client/session.rs similarity index 97% rename from crates/quicnprotochat-client/src/client/session.rs rename to crates/quicproquo-client/src/client/session.rs index c71962c..6d9724d 100644 --- a/crates/quicnprotochat-client/src/client/session.rs +++ b/crates/quicproquo-client/src/client/session.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use anyhow::Context; -use quicnprotochat_core::{DiskKeyStore, GroupMember, HybridKeypair, IdentityKeypair}; +use quicproquo_core::{DiskKeyStore, GroupMember, HybridKeypair, IdentityKeypair}; use super::conversation::{ now_ms, Conversation, ConversationId, ConversationKind, ConversationStore, @@ -101,6 +101,7 @@ impl SessionState { Arc::clone(&self.identity), ks, Some(group), + false, // legacy groups are classical ); let group_id_bytes = member.group_id().unwrap_or_default(); @@ -170,6 +171,7 @@ impl SessionState { Arc::clone(&self.identity), ks, group, + false, // existing conversations default to classical )) } diff --git a/crates/quicnprotochat-client/src/client/state.rs b/crates/quicproquo-client/src/client/state.rs similarity index 98% rename from crates/quicnprotochat-client/src/client/state.rs rename to crates/quicproquo-client/src/client/state.rs index e621768..307d434 100644 --- a/crates/quicnprotochat-client/src/client/state.rs +++ b/crates/quicproquo-client/src/client/state.rs @@ -10,7 +10,7 @@ use chacha20poly1305::{ use rand::RngCore; use serde::{Deserialize, Serialize}; -use quicnprotochat_core::{DiskKeyStore, GroupMember, HybridKeypair, HybridKeypairBytes, IdentityKeypair}; +use quicproquo_core::{DiskKeyStore, GroupMember, HybridKeypair, HybridKeypairBytes, IdentityKeypair}; /// Magic bytes for encrypted client state files. const STATE_MAGIC: &[u8; 4] = b"QPCE"; @@ -37,7 +37,8 @@ impl StoredState { .map(|bytes| bincode::deserialize(&bytes).context("decode group")) .transpose()?; let key_store = DiskKeyStore::persistent(keystore_path(state_path))?; - let member = GroupMember::new_with_state(identity, key_store, group); + let hybrid = self.hybrid_key.is_some(); + let member = GroupMember::new_with_state(identity, key_store, group, hybrid); let hybrid_kp = self .hybrid_key @@ -149,7 +150,7 @@ pub fn load_or_init_state(path: &Path, password: Option<&str>) -> anyhow::Result let identity = IdentityKeypair::generate(); let hybrid_kp = HybridKeypair::generate(); let key_store = DiskKeyStore::persistent(keystore_path(path))?; - let member = GroupMember::new_with_state(Arc::new(identity), key_store, None); + let member = GroupMember::new_with_state(Arc::new(identity), key_store, None, false); let state = StoredState::from_parts(&member, Some(&hybrid_kp))?; write_state(path, &state, password)?; Ok(state) diff --git a/crates/quicnprotochat-client/src/client/token_cache.rs b/crates/quicproquo-client/src/client/token_cache.rs similarity index 100% rename from crates/quicnprotochat-client/src/client/token_cache.rs rename to crates/quicproquo-client/src/client/token_cache.rs diff --git a/crates/quicnprotochat-client/src/lib.rs b/crates/quicproquo-client/src/lib.rs similarity index 84% rename from crates/quicnprotochat-client/src/lib.rs rename to crates/quicproquo-client/src/lib.rs index 6270ce1..9832394 100644 --- a/crates/quicnprotochat-client/src/lib.rs +++ b/crates/quicproquo-client/src/lib.rs @@ -1,17 +1,17 @@ -//! quicnprotochat CLI client library. +//! quicproquo CLI client library. //! //! # KeyPackage expiry and refresh //! //! KeyPackages are single-use (consumed when someone fetches them for an invite) and the server -//! may enforce a TTL (e.g. 24 hours). To stay invitable, run `quicnprotochat refresh-keypackage` +//! may enforce a TTL (e.g. 24 hours). To stay invitable, run `qpq refresh-keypackage` //! periodically (e.g. before the server TTL) or after your KeyPackage was consumed: //! //! ```bash -//! quicnprotochat refresh-keypackage --state quicnprotochat-state.bin --server 127.0.0.1:7000 +//! qpq refresh-keypackage --state qpq-state.bin --server 127.0.0.1:7000 //! ``` //! -//! Use the same `--access-token` (or `QUICNPROTOCHAT_ACCESS_TOKEN`) as for other authenticated -//! commands. See the [running-the-client](https://docs.quicnprotochat.dev/getting-started/running-the-client) +//! Use the same `--access-token` (or `QPQ_ACCESS_TOKEN`) as for other authenticated +//! commands. See the [running-the-client](https://docs.quicproquo.dev/getting-started/running-the-client) //! docs for details. use std::sync::RwLock; diff --git a/crates/quicnprotochat-client/src/main.rs b/crates/quicproquo-client/src/main.rs similarity index 84% rename from crates/quicnprotochat-client/src/main.rs rename to crates/quicproquo-client/src/main.rs index 028af05..8bc50f3 100644 --- a/crates/quicnprotochat-client/src/main.rs +++ b/crates/quicproquo-client/src/main.rs @@ -1,10 +1,10 @@ -//! quicnprotochat CLI client. +//! quicproquo CLI client. use std::path::PathBuf; use clap::{Parser, Subcommand}; -use quicnprotochat_client::{ +use quicproquo_client::{ cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_health, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register, cmd_register_state, cmd_refresh_keypackage, cmd_register_user, cmd_send, cmd_whoami, init_auth, run_repl, @@ -14,14 +14,14 @@ use quicnprotochat_client::{ // ── CLI ─────────────────────────────────────────────────────────────────────── #[derive(Debug, Parser)] -#[command(name = "quicnprotochat", about = "quicnprotochat CLI client", version)] +#[command(name = "qpq", about = "quicproquo CLI client", version)] struct Args { /// Path to the server's TLS certificate (self-signed by default). #[arg( long, global = true, default_value = "data/server-cert.der", - env = "QUICNPROTOCHAT_CA_CERT" + env = "QPQ_CA_CERT" )] ca_cert: PathBuf, @@ -30,7 +30,7 @@ struct Args { long, global = true, default_value = "localhost", - env = "QUICNPROTOCHAT_SERVER_NAME" + env = "QPQ_SERVER_NAME" )] server_name: String, @@ -39,18 +39,18 @@ struct Args { #[arg( long, global = true, - env = "QUICNPROTOCHAT_ACCESS_TOKEN", + env = "QPQ_ACCESS_TOKEN", default_value = "" )] access_token: String, /// Optional device identifier (UUID bytes encoded as hex or raw string). - #[arg(long, global = true, env = "QUICNPROTOCHAT_DEVICE_ID")] + #[arg(long, global = true, env = "QPQ_DEVICE_ID")] device_id: Option, /// Password to encrypt/decrypt client state files (QPCE format). /// If set, state files are encrypted at rest with Argon2id + ChaCha20Poly1305. - #[arg(long, global = true, env = "QUICNPROTOCHAT_STATE_PASSWORD")] + #[arg(long, global = true, env = "QPQ_STATE_PASSWORD")] state_password: Option, #[command(subcommand)] @@ -61,7 +61,7 @@ struct Args { enum Command { /// Register a new user via OPAQUE (password never leaves the client). RegisterUser { - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Username for the new account. #[arg(long)] @@ -73,7 +73,7 @@ enum Command { /// Log in via OPAQUE and receive a session token. Login { - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, #[arg(long)] username: String, @@ -95,8 +95,8 @@ enum Command { /// State file path (identity + MLS state). #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, }, @@ -104,14 +104,14 @@ enum Command { /// Check server connectivity and print status. Health { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, /// Check if a peer has registered a hybrid key (non-consuming lookup). CheckKey { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Peer's Ed25519 identity public key (64 hex chars = 32 bytes). @@ -121,21 +121,21 @@ enum Command { /// Send a Ping to the server and print the round-trip time. Ping { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, /// Generate a fresh MLS KeyPackage and upload it to the Authentication Service. Register { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, /// Fetch a peer's KeyPackage from the Authentication Service. FetchKey { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Target peer's Ed25519 identity public key (64 hex chars = 32 bytes). @@ -145,7 +145,7 @@ enum Command { /// Run a two-party MLS demo (creator + joiner) against live AS and DS. DemoGroup { /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, @@ -154,13 +154,13 @@ enum Command { /// State file path (identity + MLS state). #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, /// Authentication Service address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, @@ -170,13 +170,13 @@ enum Command { /// State file path (identity + MLS state). #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, @@ -185,13 +185,13 @@ enum Command { /// State file path (identity + MLS state). #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, /// Server address (host:port). - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Group identifier (arbitrary bytes, typically a human-readable name). @@ -203,11 +203,11 @@ enum Command { Invite { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Peer identity public key (64 hex chars = 32 bytes). #[arg(long)] @@ -218,11 +218,11 @@ enum Command { Join { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, }, @@ -230,11 +230,11 @@ enum Command { Send { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Recipient identity key (hex, 32 bytes -> 64 chars). Omit when using --all. #[arg(long)] @@ -251,11 +251,11 @@ enum Command { Recv { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Wait for up to this many milliseconds if no messages are queued. @@ -272,17 +272,17 @@ enum Command { Repl { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// OPAQUE username for automatic registration/login. - #[arg(long, env = "QUICNPROTOCHAT_USERNAME")] + #[arg(long, env = "QPQ_USERNAME")] username: Option, /// OPAQUE password (prompted securely if --username is set but --password is not). - #[arg(long, env = "QUICNPROTOCHAT_PASSWORD")] + #[arg(long, env = "QPQ_PASSWORD")] password: Option, }, @@ -291,11 +291,11 @@ enum Command { Chat { #[arg( long, - default_value = "quicnprotochat-state.bin", - env = "QUICNPROTOCHAT_STATE" + default_value = "qpq-state.bin", + env = "QPQ_STATE" )] state: PathBuf, - #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")] server: String, /// Peer identity key (hex, 64 chars). Omit in a two-person group to use the only other member. #[arg(long)] diff --git a/crates/quicnprotochat-client/tests/e2e.rs b/crates/quicproquo-client/tests/e2e.rs similarity index 97% rename from crates/quicnprotochat-client/tests/e2e.rs rename to crates/quicproquo-client/tests/e2e.rs index 71def29..93de34e 100644 --- a/crates/quicnprotochat-client/tests/e2e.rs +++ b/crates/quicproquo-client/tests/e2e.rs @@ -1,4 +1,4 @@ -// cargo_bin! only works for current package's binary; we spawn quicnprotochat-server from another package. +// cargo_bin! only works for current package's binary; we spawn qpq-server from another package. #![allow(deprecated)] use std::{path::PathBuf, process::Command, time::Duration}; @@ -15,12 +15,12 @@ fn ensure_rustls_provider() { let _ = rustls::crypto::ring::default_provider().install_default(); } -use quicnprotochat_client::{ +use quicproquo_client::{ cmd_create_group, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_register_state, cmd_register_user, cmd_send, connect_node, enqueue, fetch_wait, init_auth, receive_pending_plaintexts, ClientAuth, }; -use quicnprotochat_core::IdentityKeypair; +use quicproquo_core::IdentityKeypair; fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() @@ -65,7 +65,7 @@ async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> { let auth_token = "devtoken"; // Spawn server binary. - let server_bin = cargo_bin("quicnprotochat-server"); + let server_bin = cargo_bin("qpq-server"); let child = Command::new(server_bin) .arg("--listen") .arg(&listen) @@ -187,7 +187,7 @@ async fn e2e_three_party_group_invite_join_send_recv() -> anyhow::Result<()> { let data_dir = base.join("data"); let auth_token = "devtoken"; - let server_bin = cargo_bin("quicnprotochat-server"); + let server_bin = cargo_bin("qpq-server"); let child = Command::new(server_bin) .arg("--listen") .arg(&listen) @@ -400,7 +400,7 @@ async fn e2e_login_rejects_mismatched_identity() -> anyhow::Result<()> { let auth_token = "devtoken"; // Spawn server binary. - let server_bin = cargo_bin("quicnprotochat-server"); + let server_bin = cargo_bin("qpq-server"); let child = Command::new(server_bin) .arg("--listen") .arg(&listen) @@ -509,7 +509,7 @@ async fn e2e_sealed_sender_enqueue_then_fetch() -> anyhow::Result<()> { let data_dir = base.join("data"); let auth_token = "devtoken"; - let server_bin = cargo_bin("quicnprotochat-server"); + let server_bin = cargo_bin("qpq-server"); let child = Command::new(server_bin) .arg("--listen") .arg(&listen) diff --git a/crates/quicnprotochat-core/Cargo.toml b/crates/quicproquo-core/Cargo.toml similarity index 77% rename from crates/quicnprotochat-core/Cargo.toml rename to crates/quicproquo-core/Cargo.toml index a191279..5677a69 100644 --- a/crates/quicnprotochat-core/Cargo.toml +++ b/crates/quicproquo-core/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "quicnprotochat-core" +name = "quicproquo-core" version = "0.1.0" edition = "2021" -description = "Crypto primitives, MLS state machine, and hybrid post-quantum KEM for quicnprotochat." +description = "Crypto primitives, MLS state machine, and hybrid post-quantum KEM for quicproquo." license = "MIT" [dependencies] @@ -33,7 +33,7 @@ serde_json = { workspace = true } # Serialisation capnp = { workspace = true } -quicnprotochat-proto = { path = "../quicnprotochat-proto" } +quicproquo-proto = { path = "../quicproquo-proto" } # Async runtime tokio = { workspace = true } @@ -43,3 +43,17 @@ thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true } +criterion = { version = "0.5", features = ["html_reports"] } +prost = "0.13" + +[[bench]] +name = "serialization" +harness = false + +[[bench]] +name = "mls_operations" +harness = false + +[[bench]] +name = "hybrid_kem_bench" +harness = false diff --git a/crates/quicproquo-core/benches/hybrid_kem_bench.rs b/crates/quicproquo-core/benches/hybrid_kem_bench.rs new file mode 100644 index 0000000..41d1ec6 --- /dev/null +++ b/crates/quicproquo-core/benches/hybrid_kem_bench.rs @@ -0,0 +1,152 @@ +//! Benchmark: Hybrid KEM (X25519 + ML-KEM-768) vs classical-only encryption. +//! +//! Compares keypair generation, encryption, and decryption times for the +//! hybrid post-quantum scheme against classical X25519 + ChaCha20-Poly1305. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; + +use quicproquo_core::{hybrid_encrypt, hybrid_decrypt, HybridKeypair}; + +// ── Classical baseline (X25519 + ChaCha20-Poly1305) ───────────────────────── + +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + ChaCha20Poly1305, Key, Nonce, +}; +use hkdf::Hkdf; +use rand::{rngs::OsRng, RngCore}; +use sha2::Sha256; +use x25519_dalek::{EphemeralSecret, PublicKey as X25519Public, StaticSecret}; + +struct ClassicalKeypair { + secret: StaticSecret, + public: X25519Public, +} + +impl ClassicalKeypair { + fn generate() -> Self { + let secret = StaticSecret::random_from_rng(OsRng); + let public = X25519Public::from(&secret); + Self { secret, public } + } +} + +fn classical_encrypt(recipient_pk: &X25519Public, plaintext: &[u8]) -> Vec { + let eph_secret = EphemeralSecret::random_from_rng(OsRng); + let eph_public = X25519Public::from(&eph_secret); + let shared = eph_secret.diffie_hellman(recipient_pk); + + let hk = Hkdf::::new(None, shared.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"classical-bench", &mut key_bytes).unwrap(); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes)); + let ct = cipher + .encrypt(Nonce::from_slice(&nonce_bytes), plaintext) + .unwrap(); + + // Wire: eph_pk(32) || nonce(12) || ciphertext + let mut out = Vec::with_capacity(32 + 12 + ct.len()); + out.extend_from_slice(eph_public.as_bytes()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ct); + out +} + +fn classical_decrypt(keypair: &ClassicalKeypair, envelope: &[u8]) -> Vec { + let eph_pk = X25519Public::from(<[u8; 32]>::try_from(&envelope[..32]).unwrap()); + let nonce_bytes: [u8; 12] = envelope[32..44].try_into().unwrap(); + let ct = &envelope[44..]; + + let shared = keypair.secret.diffie_hellman(&eph_pk); + + let hk = Hkdf::::new(None, shared.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"classical-bench", &mut key_bytes).unwrap(); + + let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes)); + cipher + .decrypt(Nonce::from_slice(&nonce_bytes), ct) + .unwrap() +} + +// ── Benchmarks ────────────────────────────────────────────────────────────── + +fn bench_keygen(c: &mut Criterion) { + let mut group = c.benchmark_group("kem_keygen"); + group.bench_function("hybrid", |b| { + b.iter(|| black_box(HybridKeypair::generate())); + }); + group.bench_function("classical", |b| { + b.iter(|| black_box(ClassicalKeypair::generate())); + }); + group.finish(); +} + +fn bench_encrypt(c: &mut Criterion) { + let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096), ("64KB", 65536)]; + let mut group = c.benchmark_group("kem_encrypt"); + + let hybrid_kp = HybridKeypair::generate(); + let hybrid_pk = hybrid_kp.public_key(); + let classical_kp = ClassicalKeypair::generate(); + + for (label, size) in sizes { + let payload = vec![0xABu8; *size]; + + group.bench_with_input( + BenchmarkId::new("hybrid", label), + &payload, + |b, payload| { + b.iter(|| hybrid_encrypt(&hybrid_pk, black_box(payload), b"", b"").unwrap()); + }, + ); + + group.bench_with_input( + BenchmarkId::new("classical", label), + &payload, + |b, payload| { + b.iter(|| classical_encrypt(&classical_kp.public, black_box(payload))); + }, + ); + } + group.finish(); +} + +fn bench_decrypt(c: &mut Criterion) { + let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096), ("64KB", 65536)]; + let mut group = c.benchmark_group("kem_decrypt"); + + let hybrid_kp = HybridKeypair::generate(); + let hybrid_pk = hybrid_kp.public_key(); + let classical_kp = ClassicalKeypair::generate(); + + for (label, size) in sizes { + let payload = vec![0xABu8; *size]; + let hybrid_ct = hybrid_encrypt(&hybrid_pk, &payload, b"", b"").unwrap(); + let classical_ct = classical_encrypt(&classical_kp.public, &payload); + + group.bench_with_input( + BenchmarkId::new("hybrid", label), + &hybrid_ct, + |b, ct| { + b.iter(|| hybrid_decrypt(&hybrid_kp, black_box(ct), b"", b"").unwrap()); + }, + ); + + group.bench_with_input( + BenchmarkId::new("classical", label), + &classical_ct, + |b, ct| { + b.iter(|| classical_decrypt(&classical_kp, black_box(ct))); + }, + ); + } + group.finish(); +} + +criterion_group!(benches, bench_keygen, bench_encrypt, bench_decrypt); +criterion_main!(benches); diff --git a/crates/quicproquo-core/benches/mls_operations.rs b/crates/quicproquo-core/benches/mls_operations.rs new file mode 100644 index 0000000..bd9a723 --- /dev/null +++ b/crates/quicproquo-core/benches/mls_operations.rs @@ -0,0 +1,132 @@ +//! Benchmark: MLS group operations at various group sizes. +//! +//! Measures KeyPackage generation, group creation, member addition, +//! message encryption, and message decryption. + +use std::sync::Arc; + +use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; +use quicproquo_core::{GroupMember, IdentityKeypair}; + +/// Create identities and a group of the given size. +/// Returns (creator, Vec). +fn setup_group(size: usize) -> (GroupMember, Vec) { + let creator_id = Arc::new(IdentityKeypair::generate()); + let mut creator = GroupMember::new(creator_id); + creator.create_group(b"bench-group").unwrap(); + + let mut members = Vec::with_capacity(size.saturating_sub(1)); + for _ in 1..size { + let joiner_id = Arc::new(IdentityKeypair::generate()); + let mut joiner = GroupMember::new(joiner_id); + let kp = joiner.generate_key_package().unwrap(); + + let (_commit, welcome) = creator.add_member(&kp).unwrap(); + joiner.join_group(&welcome).unwrap(); + members.push(joiner); + } + + (creator, members) +} + +fn bench_keygen(c: &mut Criterion) { + c.bench_function("mls_keygen", |b| { + b.iter_batched( + || { + let id = Arc::new(IdentityKeypair::generate()); + GroupMember::new(id) + }, + |mut member| { + member.generate_key_package().unwrap(); + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_group_create(c: &mut Criterion) { + c.bench_function("mls_group_create", |b| { + b.iter_batched( + || { + let id = Arc::new(IdentityKeypair::generate()); + GroupMember::new(id) + }, + |mut member| { + member.create_group(b"bench-group").unwrap(); + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_add_member(c: &mut Criterion) { + let mut group = c.benchmark_group("mls_add_member"); + // Smaller sizes to keep setup time reasonable + for size in [2, 10, 50] { + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { + b.iter_batched( + || { + let (creator, members) = setup_group(size); + let joiner_id = Arc::new(IdentityKeypair::generate()); + let mut joiner = GroupMember::new(joiner_id); + let kp = joiner.generate_key_package().unwrap(); + (creator, members, joiner, kp) + }, + |(mut creator, _members, _joiner, kp)| { + creator.add_member(&kp).unwrap(); + }, + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +fn bench_send_message(c: &mut Criterion) { + let mut group = c.benchmark_group("mls_send_message"); + for size in [2, 10, 50] { + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { + let (mut creator, _members) = setup_group(size); + let payload = b"hello benchmark message"; + b.iter(|| { + creator.send_message(payload).unwrap(); + }); + }); + } + group.finish(); +} + +fn bench_receive_message(c: &mut Criterion) { + let mut group = c.benchmark_group("mls_receive_message"); + for size in [2, 10, 50] { + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { + // For receive, we need a fresh ciphertext each iteration since + // MLS message processing is destructive (epoch state changes). + // We pre-generate a batch and consume them. + let (mut creator, mut members) = setup_group(size); + if members.is_empty() { + return; + } + let payload = b"hello benchmark message"; + b.iter_batched( + || creator.send_message(payload).unwrap(), + |ct| { + // Receive on the first joiner + let _ = members[0].receive_message(&ct); + }, + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_keygen, + bench_group_create, + bench_add_member, + bench_send_message, + bench_receive_message, +); +criterion_main!(benches); diff --git a/crates/quicproquo-core/benches/serialization.rs b/crates/quicproquo-core/benches/serialization.rs new file mode 100644 index 0000000..9f30c8b --- /dev/null +++ b/crates/quicproquo-core/benches/serialization.rs @@ -0,0 +1,170 @@ +//! Benchmark: Cap'n Proto vs Protobuf serialization for chat message envelopes. +//! +//! Compares serialization/deserialization speed and encoded size at three +//! payload sizes (100 B, 1 KB, 4 KB) for a typical Envelope{seq, data} message. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; + +// ── Cap'n Proto path ──────────────────────────────────────────────────────── + +fn capnp_serialize_envelope(seq: u64, data: &[u8]) -> Vec { + let mut msg = capnp::message::Builder::new_default(); + { + let mut envelope = msg.init_root::(); + envelope.set_seq(seq); + envelope.set_data(data); + } + quicproquo_proto::to_bytes(&msg).unwrap() +} + +fn capnp_deserialize_envelope(bytes: &[u8]) -> (u64, Vec) { + let reader = quicproquo_proto::from_bytes(bytes).unwrap(); + let envelope = reader + .get_root::() + .unwrap(); + (envelope.get_seq(), envelope.get_data().unwrap().to_vec()) +} + +// ── Protobuf path (hand-coded prost encoding to avoid build-dep) ──────────── +// +// Envelope { seq: uint64 (field 1), data: bytes (field 2) } +// Wire format: varint tag + varint seq + len-delimited data + +fn protobuf_serialize_envelope(seq: u64, data: &[u8]) -> Vec { + // Build a prost message via raw encoding. + // Field 1: uint64 seq, wire type 0 (varint), tag = (1 << 3) | 0 = 0x08 + // Field 2: bytes data, wire type 2 (length-delimited), tag = (2 << 3) | 2 = 0x12 + let mut buf = Vec::with_capacity(10 + data.len()); + // Encode field 1 (seq) + prost::encoding::uint64::encode(1, &seq, &mut buf); + // Encode field 2 (data) + prost::encoding::bytes::encode(2, &data.to_vec(), &mut buf); + buf +} + +fn protobuf_deserialize_envelope(bytes: &[u8]) -> (u64, Vec) { + // Decode manually using prost wire format + let mut seq: u64 = 0; + let mut data: Vec = Vec::new(); + let mut buf = bytes; + + while !buf.is_empty() { + let (tag, wire_type) = + prost::encoding::decode_key(&mut buf).expect("decode key"); + match tag { + 1 => { + prost::encoding::uint64::merge(wire_type, &mut seq, &mut buf, Default::default()) + .expect("decode seq"); + } + 2 => { + prost::encoding::bytes::merge(wire_type, &mut data, &mut buf, Default::default()) + .expect("decode data"); + } + _ => { + prost::encoding::skip_field(wire_type, tag, &mut buf, Default::default()) + .expect("skip unknown field"); + } + } + } + (seq, data) +} + +// ── Benchmarks ────────────────────────────────────────────────────────────── + +fn bench_serialize(c: &mut Criterion) { + let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; + let mut group = c.benchmark_group("serialize_envelope"); + + for (label, size) in sizes { + let payload = vec![0xABu8; *size]; + let seq = 42u64; + + group.bench_with_input( + BenchmarkId::new("capnp", label), + &(&seq, &payload), + |b, &(seq, payload)| { + b.iter(|| capnp_serialize_envelope(black_box(*seq), black_box(payload))); + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf", label), + &(&seq, &payload), + |b, &(seq, payload)| { + b.iter(|| protobuf_serialize_envelope(black_box(*seq), black_box(payload))); + }, + ); + } + group.finish(); +} + +fn bench_deserialize(c: &mut Criterion) { + let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; + let mut group = c.benchmark_group("deserialize_envelope"); + + for (label, size) in sizes { + let payload = vec![0xABu8; *size]; + let seq = 42u64; + + let capnp_bytes = capnp_serialize_envelope(seq, &payload); + let proto_bytes = protobuf_serialize_envelope(seq, &payload); + + group.bench_with_input( + BenchmarkId::new("capnp", label), + &capnp_bytes, + |b, bytes| { + b.iter(|| capnp_deserialize_envelope(black_box(bytes))); + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf", label), + &proto_bytes, + |b, bytes| { + b.iter(|| protobuf_deserialize_envelope(black_box(bytes))); + }, + ); + } + group.finish(); +} + +fn bench_encoded_sizes(c: &mut Criterion) { + let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; + let mut group = c.benchmark_group("encoded_size"); + + for (label, size) in sizes { + let payload = vec![0xABu8; *size]; + let capnp_bytes = capnp_serialize_envelope(42, &payload); + let proto_bytes = protobuf_serialize_envelope(42, &payload); + + // Use a trivial benchmark that just returns the size -- the point + // is to get criterion to print the iteration count and allow + // comparison. The real value is in the eprintln below. + group.bench_with_input( + BenchmarkId::new("capnp", label), + &capnp_bytes, + |b, bytes| { + b.iter(|| black_box(bytes.len())); + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf", label), + &proto_bytes, + |b, bytes| { + b.iter(|| black_box(bytes.len())); + }, + ); + + eprintln!( + " {label}: capnp={} bytes, protobuf={} bytes, overhead={:+} bytes", + capnp_bytes.len(), + proto_bytes.len(), + capnp_bytes.len() as isize - proto_bytes.len() as isize, + ); + } + group.finish(); +} + +criterion_group!(benches, bench_serialize, bench_deserialize, bench_encoded_sizes); +criterion_main!(benches); diff --git a/crates/quicproquo-core/proto/chat_message.proto b/crates/quicproquo-core/proto/chat_message.proto new file mode 100644 index 0000000..02700cb --- /dev/null +++ b/crates/quicproquo-core/proto/chat_message.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package quicproquo.bench; + +// Equivalent to the Envelope struct in delivery.capnp +message Envelope { + uint64 seq = 1; + bytes data = 2; +} + +// Equivalent to a chat message payload (app_message.rs Chat variant) +message ChatMessage { + bytes message_id = 1; // 16 bytes + string body = 2; // UTF-8 text + uint64 timestamp_ms = 3; + bytes sender_key = 4; // 32 bytes Ed25519 public key +} + +// Batch fetch response (equivalent to fetch returning List(Envelope)) +message FetchResponse { + repeated Envelope payloads = 1; +} diff --git a/crates/quicnprotochat-core/src/app_message.rs b/crates/quicproquo-core/src/app_message.rs similarity index 100% rename from crates/quicnprotochat-core/src/app_message.rs rename to crates/quicproquo-core/src/app_message.rs diff --git a/crates/quicnprotochat-core/src/error.rs b/crates/quicproquo-core/src/error.rs similarity index 94% rename from crates/quicnprotochat-core/src/error.rs rename to crates/quicproquo-core/src/error.rs index 6cd5871..2a7d3a8 100644 --- a/crates/quicnprotochat-core/src/error.rs +++ b/crates/quicproquo-core/src/error.rs @@ -1,4 +1,4 @@ -//! Error types for `quicnprotochat-core`. +//! Error types for `quicproquo-core`. use thiserror::Error; diff --git a/crates/quicnprotochat-core/src/group.rs b/crates/quicproquo-core/src/group.rs similarity index 86% rename from crates/quicnprotochat-core/src/group.rs rename to crates/quicproquo-core/src/group.rs index 2dfdaec..02e5db5 100644 --- a/crates/quicnprotochat-core/src/group.rs +++ b/crates/quicproquo-core/src/group.rs @@ -3,12 +3,19 @@ //! # Design //! //! [`GroupMember`] wraps an openmls [`MlsGroup`] plus the per-client -//! [`StoreCrypto`] backend. The backend is **persistent** — it holds the -//! in-memory key store that maps init-key references to HPKE private keys. +//! [`HybridCryptoProvider`] backend. The backend is **persistent** — it holds +//! the in-memory key store that maps init-key references to HPKE private keys. //! openmls's `new_from_welcome` reads those private keys from the key store to //! decrypt the Welcome, so the same backend instance must be used from //! `generate_key_package` through `join_group`. //! +//! # Hybrid post-quantum mode +//! +//! When `hybrid = true`, the backend's `derive_hpke_keypair` produces hybrid +//! (X25519 + ML-KEM-768) init keys. KeyPackages from hybrid groups contain +//! 1216-byte public keys instead of 32-byte X25519 keys. Both sender and +//! receiver must use hybrid mode for the same group. +//! //! # Wire format //! //! All MLS messages are serialised/deserialised using TLS presentation language @@ -37,8 +44,9 @@ use openmls_traits::OpenMlsCryptoProvider; use crate::{ error::CoreError, + hybrid_crypto::HybridCryptoProvider, identity::IdentityKeypair, - keystore::{DiskKeyStore, StoreCrypto}, + keystore::DiskKeyStore, }; // ── Constants ───────────────────────────────────────────────────────────────── @@ -61,21 +69,28 @@ const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA2 /// └─ receive_message(b) → decrypt; returns Some(plaintext) or None /// ``` pub struct GroupMember { - /// Persistent crypto backend. Holds the in-memory key store with HPKE - /// private keys created during `generate_key_package`. - backend: StoreCrypto, + /// Persistent crypto backend (hybrid or classical). Holds the in-memory key + /// store with HPKE private keys created during `generate_key_package`. + backend: HybridCryptoProvider, /// Long-term Ed25519 identity keypair. Also used as the MLS `Signer`. identity: Arc, /// Active MLS group, if any. group: Option, /// Shared group configuration (wire format, ratchet tree extension, etc.). config: MlsGroupConfig, + /// Whether this member uses hybrid (X25519 + ML-KEM-768) HPKE keys. + hybrid: bool, } impl GroupMember { - /// Create a new `GroupMember` with a fresh crypto backend. + /// Create a new `GroupMember` with a fresh classical crypto backend. pub fn new(identity: Arc) -> Self { - Self::new_with_state(identity, DiskKeyStore::ephemeral(), None) + Self::new_with_state(identity, DiskKeyStore::ephemeral(), None, false) + } + + /// Create a new `GroupMember` with hybrid post-quantum crypto backend. + pub fn new_hybrid(identity: Arc) -> Self { + Self::new_with_state(identity, DiskKeyStore::ephemeral(), None, true) } /// Create a `GroupMember` with a persistent keystore at `path`. @@ -85,24 +100,35 @@ impl GroupMember { ) -> Result { let key_store = DiskKeyStore::persistent(path) .map_err(|e| CoreError::Io(format!("keystore: {e}")))?; - Ok(Self::new_with_state(identity, key_store, None)) + Ok(Self::new_with_state(identity, key_store, None, false)) } /// Create a `GroupMember` from pre-existing state (identity + optional group + store). + /// + /// When `hybrid` is `true`, the backend uses hybrid (X25519 + ML-KEM-768) + /// keys for HPKE operations. When `false`, standard X25519 keys are used. pub fn new_with_state( identity: Arc, key_store: DiskKeyStore, group: Option, + hybrid: bool, ) -> Self { let config = MlsGroupConfig::builder() .use_ratchet_tree_extension(true) .build(); + let backend = if hybrid { + HybridCryptoProvider::new_hybrid(key_store) + } else { + HybridCryptoProvider::new_classical(key_store) + }; + Self { - backend: StoreCrypto::new(key_store), + backend, identity, group, config, + hybrid, } } @@ -414,10 +440,15 @@ impl GroupMember { } /// Return a reference to the underlying crypto backend. - pub fn backend(&self) -> &StoreCrypto { + pub fn backend(&self) -> &HybridCryptoProvider { &self.backend } + /// Whether this member uses hybrid post-quantum HPKE keys. + pub fn is_hybrid(&self) -> bool { + self.hybrid + } + /// Return a reference to the MLS group, if active. pub fn group_ref(&self) -> Option<&MlsGroup> { self.group.as_ref() @@ -498,6 +529,47 @@ mod tests { assert_eq!(pt_creator, b"hello back"); } + /// Full two-party hybrid MLS round-trip with post-quantum HPKE keys. + #[test] + fn two_party_hybrid_mls_round_trip() { + let creator_id = Arc::new(IdentityKeypair::generate()); + let joiner_id = Arc::new(IdentityKeypair::generate()); + + let mut creator = GroupMember::new_hybrid(Arc::clone(&creator_id)); + let mut joiner = GroupMember::new_hybrid(Arc::clone(&joiner_id)); + + assert!(creator.is_hybrid()); + assert!(joiner.is_hybrid()); + + let joiner_kp = joiner + .generate_key_package() + .expect("joiner hybrid KeyPackage"); + + creator + .create_group(b"test-hybrid-group") + .expect("creator create hybrid group"); + + let (_, welcome) = creator + .add_member(&joiner_kp) + .expect("creator add joiner with hybrid KP"); + + joiner.join_group(&welcome).expect("joiner join hybrid group"); + + let ct_creator = creator.send_message(b"hello PQ").expect("creator send"); + let pt_joiner = joiner + .receive_message(&ct_creator) + .expect("joiner recv") + .expect("application message"); + assert_eq!(pt_joiner, b"hello PQ"); + + let ct_joiner = joiner.send_message(b"quantum safe!").expect("joiner send"); + let pt_creator = creator + .receive_message(&ct_joiner) + .expect("creator recv") + .expect("application message"); + assert_eq!(pt_creator, b"quantum safe!"); + } + /// `group_id()` returns None before create_group, Some afterwards. #[test] fn group_id_lifecycle() { diff --git a/crates/quicnprotochat-core/src/hybrid_crypto.rs b/crates/quicproquo-core/src/hybrid_crypto.rs similarity index 80% rename from crates/quicnprotochat-core/src/hybrid_crypto.rs rename to crates/quicproquo-core/src/hybrid_crypto.rs index 182af2e..8f4ee81 100644 --- a/crates/quicnprotochat-core/src/hybrid_crypto.rs +++ b/crates/quicproquo-core/src/hybrid_crypto.rs @@ -46,18 +46,50 @@ use openmls_traits::types::{ /// Crypto backend that uses hybrid KEM for HPKE when keys are in hybrid format, /// and delegates everything else to RustCrypto. +/// +/// When `hybrid_enabled` is `true`, `derive_hpke_keypair` produces hybrid keys +/// (1216-byte public, 2432-byte private). When `false`, it delegates to +/// RustCrypto and produces classical 32-byte X25519 keys. +/// +/// The `hpke_seal` / `hpke_open` methods always detect the key format by length, +/// so they work correctly regardless of the flag — a hybrid-length key will use +/// hybrid KEM, a classical-length key will use RustCrypto. #[derive(Debug)] pub struct HybridCrypto { rust_crypto: RustCrypto, + /// When true, `derive_hpke_keypair` produces hybrid (X25519 + ML-KEM-768) + /// keys. When false, it produces classical X25519 keys via RustCrypto. + hybrid_enabled: bool, } impl HybridCrypto { + /// Create a hybrid-enabled crypto backend (derive_hpke_keypair produces hybrid keys). pub fn new() -> Self { Self { rust_crypto: RustCrypto::default(), + hybrid_enabled: true, } } + /// Alias for `new()` — hybrid mode enabled. + pub fn new_hybrid() -> Self { + Self::new() + } + + /// Create a classical crypto backend (derive_hpke_keypair produces standard + /// X25519 keys, but seal/open still accept hybrid keys by length detection). + pub fn new_classical() -> Self { + Self { + rust_crypto: RustCrypto::default(), + hybrid_enabled: false, + } + } + + /// Whether this backend produces hybrid keys from `derive_hpke_keypair`. + pub fn is_hybrid_enabled(&self) -> bool { + self.hybrid_enabled + } + /// Expose the underlying RustCrypto for rand() and delegation. pub fn rust_crypto(&self) -> &RustCrypto { &self.rust_crypto @@ -268,7 +300,7 @@ impl OpenMlsCrypto for HybridCrypto { } fn derive_hpke_keypair(&self, config: HpkeConfig, ikm: &[u8]) -> HpkeKeyPair { - if config.0 == HpkeKemType::DhKem25519 { + if self.hybrid_enabled && config.0 == HpkeKemType::DhKem25519 { let kp = HybridKeypair::derive_from_ikm(ikm); HpkeKeyPair { private: kp.private_to_bytes().into(), @@ -289,12 +321,32 @@ pub struct HybridCryptoProvider { } impl HybridCryptoProvider { + /// Create a hybrid-enabled provider (KeyPackages will contain hybrid init keys). pub fn new(key_store: DiskKeyStore) -> Self { Self { - crypto: HybridCrypto::new(), + crypto: HybridCrypto::new_hybrid(), key_store, } } + + /// Alias for `new()` — hybrid mode enabled. + pub fn new_hybrid(key_store: DiskKeyStore) -> Self { + Self::new(key_store) + } + + /// Create a classical-mode provider (KeyPackages use standard X25519 init keys, + /// but seal/open still accept hybrid keys by length detection). + pub fn new_classical(key_store: DiskKeyStore) -> Self { + Self { + crypto: HybridCrypto::new_classical(), + key_store, + } + } + + /// Whether this provider produces hybrid keys from `derive_hpke_keypair`. + pub fn is_hybrid_enabled(&self) -> bool { + self.crypto.is_hybrid_enabled() + } } impl Default for HybridCryptoProvider { @@ -410,6 +462,52 @@ mod tests { assert_eq!(sender_exported.as_ref(), receiver_exported.as_ref()); } + /// Classical mode: derive_hpke_keypair produces standard 32-byte X25519 keys. + #[test] + fn classical_mode_produces_standard_keys() { + let crypto = HybridCrypto::new_classical(); + let ikm = b"test-ikm-for-classical-hpke"; + + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + // Classical X25519 keys are 32 bytes + assert_eq!(keypair.public.len(), 32); + assert_eq!(keypair.private.as_ref().len(), 32); + } + + /// Classical mode round-trip: seal/open works with classical keys. + #[test] + fn classical_mode_seal_open_round_trip() { + let crypto = HybridCrypto::new_classical(); + let ikm = b"test-ikm-for-classical-round-trip"; + + let keypair = crypto.derive_hpke_keypair(hpke_config_dhkem_x25519(), ikm); + assert_eq!(keypair.public.len(), 32); // classical key + + let plaintext = b"hello classical MLS"; + let info = b"mls 1.0 test"; + let aad = b"additional data"; + + let ct = crypto.hpke_seal( + hpke_config_dhkem_x25519(), + &keypair.public, + info, + aad, + plaintext, + ); + assert!(!ct.kem_output.as_slice().is_empty()); + + let decrypted = crypto + .hpke_open( + hpke_config_dhkem_x25519(), + &ct, + keypair.private.as_ref(), + info, + aad, + ) + .expect("hpke_open with classical keys"); + assert_eq!(decrypted.as_slice(), plaintext); + } + /// KeyPackage generation with HybridCryptoProvider (validates full HPKE path in MLS). #[test] fn key_package_generation_with_hybrid_provider() { diff --git a/crates/quicnprotochat-core/src/hybrid_kem.rs b/crates/quicproquo-core/src/hybrid_kem.rs similarity index 99% rename from crates/quicnprotochat-core/src/hybrid_kem.rs rename to crates/quicproquo-core/src/hybrid_kem.rs index a94fcd4..c49b4e7 100644 --- a/crates/quicnprotochat-core/src/hybrid_kem.rs +++ b/crates/quicproquo-core/src/hybrid_kem.rs @@ -41,9 +41,12 @@ use ml_kem::kem::{DecapsulationKey, EncapsulationKey}; const HYBRID_VERSION: u8 = 0x01; /// HKDF info string for domain separation. +/// Frozen at the original project name for backward compatibility with existing +/// encrypted state files and messages. Do not change. const HKDF_INFO: &[u8] = b"quicnprotochat-hybrid-v1"; /// HKDF salt for domain separation (defence-in-depth; IKM already has 64 bytes of entropy). +/// Frozen — see [`HKDF_INFO`]. const HKDF_SALT: &[u8] = b"quicnprotochat-hybrid-v1-salt"; /// ML-KEM-768 ciphertext size in bytes. @@ -122,6 +125,7 @@ pub struct HybridPublicKey { } /// HKDF info for deriving HPKE keypair seed from IKM (MLS compatibility). +/// Frozen — see [`HKDF_INFO`]. const HKDF_INFO_HPKE_KEYPAIR: &[u8] = b"quicnprotochat-hybrid-hpke-keypair-v1"; impl HybridKeypair { diff --git a/crates/quicnprotochat-core/src/identity.rs b/crates/quicproquo-core/src/identity.rs similarity index 81% rename from crates/quicnprotochat-core/src/identity.rs rename to crates/quicproquo-core/src/identity.rs index 57324f1..7ce63fa 100644 --- a/crates/quicnprotochat-core/src/identity.rs +++ b/crates/quicproquo-core/src/identity.rs @@ -99,6 +99,32 @@ impl Signer for IdentityKeypair { } } +impl IdentityKeypair { + /// Sign arbitrary bytes with the Ed25519 key and return the 64-byte signature. + /// + /// Used by sealed sender to sign the inner payload for recipient verification. + pub fn sign_raw(&self, payload: &[u8]) -> [u8; 64] { + let sk = self.signing_key(); + let sig: ed25519_dalek::Signature = sk.sign(payload); + sig.to_bytes() + } + + /// Verify an Ed25519 signature over `payload` using the given public key. + pub fn verify_raw( + public_key: &[u8; 32], + payload: &[u8], + signature: &[u8; 64], + ) -> Result<(), crate::error::CoreError> { + use ed25519_dalek::Verifier; + + let vk = VerifyingKey::from_bytes(public_key) + .map_err(|e| crate::error::CoreError::Mls(format!("invalid public key: {e}")))?; + let sig = ed25519_dalek::Signature::from_bytes(signature); + vk.verify(payload, &sig) + .map_err(|e| crate::error::CoreError::Mls(format!("signature verification failed: {e}"))) + } +} + impl Serialize for IdentityKeypair { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/quicnprotochat-core/src/keypackage.rs b/crates/quicproquo-core/src/keypackage.rs similarity index 96% rename from crates/quicnprotochat-core/src/keypackage.rs rename to crates/quicproquo-core/src/keypackage.rs index 61e7ecc..e5302fd 100644 --- a/crates/quicnprotochat-core/src/keypackage.rs +++ b/crates/quicproquo-core/src/keypackage.rs @@ -14,7 +14,7 @@ //! # Wire format //! //! KeyPackages are TLS-encoded using `tls_codec` (same version as openmls). -//! The resulting bytes are opaque to the quicnprotochat transport layer. +//! The resulting bytes are opaque to the quicproquo transport layer. use openmls::prelude::{ Ciphersuite, Credential, CredentialType, CredentialWithKey, CryptoConfig, KeyPackage, @@ -25,7 +25,7 @@ use sha2::{Digest, Sha256}; use crate::{error::CoreError, identity::IdentityKeypair}; -/// The MLS ciphersuite used throughout quicnprotochat (RFC 9420 §17.1). +/// The MLS ciphersuite used throughout quicproquo (RFC 9420 §17.1). pub const ALLOWED_CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; diff --git a/crates/quicnprotochat-core/src/keystore.rs b/crates/quicproquo-core/src/keystore.rs similarity index 100% rename from crates/quicnprotochat-core/src/keystore.rs rename to crates/quicproquo-core/src/keystore.rs diff --git a/crates/quicnprotochat-core/src/lib.rs b/crates/quicproquo-core/src/lib.rs similarity index 96% rename from crates/quicnprotochat-core/src/lib.rs rename to crates/quicproquo-core/src/lib.rs index c16990b..5c72c11 100644 --- a/crates/quicnprotochat-core/src/lib.rs +++ b/crates/quicproquo-core/src/lib.rs @@ -1,5 +1,5 @@ //! Core cryptographic primitives, MLS group state machine, and hybrid -//! post-quantum KEM for quicnprotochat. +//! post-quantum KEM for quicproquo. //! //! # Module layout //! @@ -22,6 +22,8 @@ mod identity; mod keypackage; mod keystore; pub mod opaque_auth; +pub mod padding; +pub mod sealed_sender; // ── Public API ──────────────────────────────────────────────────────────────── diff --git a/crates/quicnprotochat-core/src/opaque_auth.rs b/crates/quicproquo-core/src/opaque_auth.rs similarity index 93% rename from crates/quicnprotochat-core/src/opaque_auth.rs rename to crates/quicproquo-core/src/opaque_auth.rs index cfd5354..c440fac 100644 --- a/crates/quicnprotochat-core/src/opaque_auth.rs +++ b/crates/quicproquo-core/src/opaque_auth.rs @@ -5,7 +5,7 @@ use opaque_ke::CipherSuite; -/// OPAQUE cipher suite for quicnprotochat. +/// OPAQUE cipher suite for quicproquo. /// /// - **OPRF**: Ristretto255 (curve25519-based, ~128-bit security) /// - **Key exchange**: Triple-DH (3DH) over Ristretto255 with SHA-512 diff --git a/crates/quicproquo-core/src/padding.rs b/crates/quicproquo-core/src/padding.rs new file mode 100644 index 0000000..a9d892c --- /dev/null +++ b/crates/quicproquo-core/src/padding.rs @@ -0,0 +1,144 @@ +//! Message padding to hide plaintext lengths from the server. +//! +//! Pads payloads to fixed bucket sizes before MLS encryption so that the +//! ciphertext does not reveal the actual message length. +//! +//! # Wire format +//! +//! ```text +//! [real_length: 4 bytes LE (u32)][payload: real_length bytes][random padding] +//! ``` +//! +//! The total padded output is always one of the bucket sizes: 256, 1024, 4096, 16384 bytes. +//! For payloads larger than 16380 bytes, rounds up to the nearest 16384-byte multiple. + +use rand::RngCore; + +use crate::error::CoreError; + +/// Bucket sizes in bytes. The smallest (256) accommodates a sealed sender +/// envelope (99 bytes overhead) plus a short message. +const BUCKETS: &[usize] = &[256, 1024, 4096, 16384]; + +/// Select the smallest bucket that fits `content_len + 4` (the 4-byte length prefix). +fn bucket_for(content_len: usize) -> usize { + let total = content_len + 4; + for &b in BUCKETS { + if total <= b { + return b; + } + } + // Larger than biggest bucket: round up to nearest 16384-byte multiple. + ((total + 16383) / 16384) * 16384 +} + +/// Pad a payload to the next bucket boundary with cryptographic random bytes. +pub fn pad(payload: &[u8]) -> Vec { + let bucket = bucket_for(payload.len()); + let mut out = Vec::with_capacity(bucket); + out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + out.extend_from_slice(payload); + let pad_len = bucket - 4 - payload.len(); + if pad_len > 0 { + let mut padding = vec![0u8; pad_len]; + rand::rngs::OsRng.fill_bytes(&mut padding); + out.extend_from_slice(&padding); + } + out +} + +/// Remove padding and return the original payload. +pub fn unpad(padded: &[u8]) -> Result, CoreError> { + if padded.len() < 4 { + return Err(CoreError::AppMessage("padded message too short".into())); + } + let real_len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]) as usize; + if 4 + real_len > padded.len() { + return Err(CoreError::AppMessage( + "padded real_length exceeds buffer".into(), + )); + } + Ok(padded[4..4 + real_len].to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_small() { + let msg = b"hello"; + let padded = pad(msg); + assert_eq!(padded.len(), 256); // smallest bucket + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn round_trip_medium() { + let msg = vec![0xAB; 300]; + let padded = pad(&msg); + assert_eq!(padded.len(), 1024); // second bucket + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn round_trip_large() { + let msg = vec![0xCD; 2000]; + let padded = pad(&msg); + assert_eq!(padded.len(), 4096); // third bucket + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn round_trip_very_large() { + let msg = vec![0xEF; 10000]; + let padded = pad(&msg); + assert_eq!(padded.len(), 16384); // largest bucket + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn round_trip_oversized() { + let msg = vec![0xFF; 20000]; + let padded = pad(&msg); + assert_eq!(padded.len(), 32768); // 2 * 16384 + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn round_trip_empty() { + let msg = b""; + let padded = pad(msg); + assert_eq!(padded.len(), 256); // smallest bucket + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn exactly_at_bucket_boundary() { + // 252 + 4 = 256 → fits in 256 bucket exactly + let msg = vec![0x42; 252]; + let padded = pad(&msg); + assert_eq!(padded.len(), 256); + let unpadded = unpad(&padded).unwrap(); + assert_eq!(unpadded, msg); + } + + #[test] + fn unpad_too_short_fails() { + assert!(unpad(&[0, 0]).is_err()); + } + + #[test] + fn unpad_invalid_length_fails() { + // Claims 1000 bytes but only has 10 + let mut bad = (1000u32).to_le_bytes().to_vec(); + bad.extend_from_slice(&[0u8; 10]); + assert!(unpad(&bad).is_err()); + } +} diff --git a/crates/quicproquo-core/src/sealed_sender.rs b/crates/quicproquo-core/src/sealed_sender.rs new file mode 100644 index 0000000..3988df3 --- /dev/null +++ b/crates/quicproquo-core/src/sealed_sender.rs @@ -0,0 +1,154 @@ +//! Sealed sender: embed sender identity + Ed25519 signature inside the MLS +//! application payload so recipients can verify the sender from decrypted +//! content, independent of MLS framing. +//! +//! # Wire format +//! +//! ```text +//! [magic: 1 byte (0x53 = 'S')] +//! [sender_identity_key: 32 bytes (Ed25519 public key)] +//! [signature: 64 bytes (Ed25519)] +//! [inner_payload: variable (the original app_message bytes)] +//! ``` +//! +//! The signature covers: `magic || sender_identity_key || inner_payload`. +//! Total overhead: 1 + 32 + 64 = 97 bytes per message. + +use crate::error::CoreError; +use crate::identity::IdentityKeypair; + +/// Magic byte identifying a sealed sender envelope. +pub const SEALED_MAGIC: u8 = 0x53; // 'S' + +/// Fixed overhead: magic(1) + sender_key(32) + signature(64). +const SEALED_OVERHEAD: usize = 1 + 32 + 64; + +/// Wrap an app_message payload in a sealed sender envelope. +/// +/// Signs `magic || sender_key || payload` with the sender's Ed25519 key. +pub fn seal(identity: &IdentityKeypair, app_message_bytes: &[u8]) -> Vec { + let sender_key = identity.public_key_bytes(); + + // Build signing input + let mut sign_input = Vec::with_capacity(1 + 32 + app_message_bytes.len()); + sign_input.push(SEALED_MAGIC); + sign_input.extend_from_slice(&sender_key); + sign_input.extend_from_slice(app_message_bytes); + + let signature = identity.sign_raw(&sign_input); + + let mut out = Vec::with_capacity(SEALED_OVERHEAD + app_message_bytes.len()); + out.push(SEALED_MAGIC); + out.extend_from_slice(&sender_key); + out.extend_from_slice(&signature); + out.extend_from_slice(app_message_bytes); + out +} + +/// Unseal: verify the Ed25519 signature, return `(sender_identity_key, inner_app_message_bytes)`. +pub fn unseal(bytes: &[u8]) -> Result<([u8; 32], Vec), CoreError> { + if bytes.len() < SEALED_OVERHEAD { + return Err(CoreError::AppMessage( + "sealed sender envelope too short".into(), + )); + } + + if bytes[0] != SEALED_MAGIC { + return Err(CoreError::AppMessage(format!( + "sealed sender: expected magic 0x{:02X}, got 0x{:02X}", + SEALED_MAGIC, bytes[0] + ))); + } + + let mut sender_key = [0u8; 32]; + sender_key.copy_from_slice(&bytes[1..33]); + + let mut signature = [0u8; 64]; + signature.copy_from_slice(&bytes[33..97]); + + let inner_payload = &bytes[97..]; + + // Reconstruct signing input: magic || sender_key || inner_payload + let mut sign_input = Vec::with_capacity(1 + 32 + inner_payload.len()); + sign_input.push(SEALED_MAGIC); + sign_input.extend_from_slice(&sender_key); + sign_input.extend_from_slice(inner_payload); + + IdentityKeypair::verify_raw(&sender_key, &sign_input, &signature)?; + + Ok((sender_key, inner_payload.to_vec())) +} + +/// Check if bytes start with the sealed sender magic byte. +pub fn is_sealed(bytes: &[u8]) -> bool { + bytes.first() == Some(&SEALED_MAGIC) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn seal_unseal_round_trip() { + let identity = IdentityKeypair::generate(); + let payload = b"hello sealed sender"; + let sealed = seal(&identity, payload); + assert!(is_sealed(&sealed)); + + let (sender_key, inner) = unseal(&sealed).unwrap(); + assert_eq!(sender_key, identity.public_key_bytes()); + assert_eq!(inner, payload); + } + + #[test] + fn unseal_tampered_payload_fails() { + let identity = IdentityKeypair::generate(); + let payload = b"hello"; + let mut sealed = seal(&identity, payload); + // Tamper with the inner payload + if let Some(last) = sealed.last_mut() { + *last ^= 0xFF; + } + assert!(unseal(&sealed).is_err()); + } + + #[test] + fn unseal_wrong_sender_fails() { + let alice = IdentityKeypair::generate(); + let bob = IdentityKeypair::generate(); + let payload = b"from alice"; + let mut sealed = seal(&alice, payload); + // Replace sender key with Bob's + let bob_key = bob.public_key_bytes(); + sealed[1..33].copy_from_slice(&bob_key); + assert!(unseal(&sealed).is_err()); + } + + #[test] + fn unseal_too_short_fails() { + assert!(unseal(&[SEALED_MAGIC; 10]).is_err()); + } + + #[test] + fn unseal_wrong_magic_fails() { + let identity = IdentityKeypair::generate(); + let mut sealed = seal(&identity, b"test"); + sealed[0] = 0x00; + assert!(unseal(&sealed).is_err()); + } + + #[test] + fn non_sealed_detected() { + assert!(!is_sealed(b"\x01\x01hello")); + assert!(is_sealed(&[SEALED_MAGIC, 0, 0])); + } + + #[test] + fn empty_payload_round_trip() { + let identity = IdentityKeypair::generate(); + let sealed = seal(&identity, b""); + let (sender_key, inner) = unseal(&sealed).unwrap(); + assert_eq!(sender_key, identity.public_key_bytes()); + assert!(inner.is_empty()); + } +} diff --git a/crates/quicnprotochat-gui/Cargo.toml b/crates/quicproquo-gui/Cargo.toml similarity index 50% rename from crates/quicnprotochat-gui/Cargo.toml rename to crates/quicproquo-gui/Cargo.toml index 3d3aa26..0452d75 100644 --- a/crates/quicnprotochat-gui/Cargo.toml +++ b/crates/quicproquo-gui/Cargo.toml @@ -1,18 +1,18 @@ [package] -name = "quicnprotochat-gui" +name = "quicproquo-gui" version = "0.1.0" edition = "2021" -description = "Native GUI for quicnprotochat (Tauri 2)." +description = "Native GUI for quicproquo (Tauri 2)." license = "MIT" [[bin]] -name = "quicnprotochat-gui" +name = "qpq-gui" path = "src/main.rs" [dependencies] -quicnprotochat-core = { path = "../quicnprotochat-core" } -quicnprotochat-client = { path = "../quicnprotochat-client" } -quicnprotochat-proto = { path = "../quicnprotochat-proto" } +quicproquo-core = { path = "../quicproquo-core" } +quicproquo-client = { path = "../quicproquo-client" } +quicproquo-proto = { path = "../quicproquo-proto" } tauri = { version = "2", features = [] } tokio = { workspace = true } serde = { workspace = true } diff --git a/crates/quicnprotochat-gui/README.md b/crates/quicproquo-gui/README.md similarity index 80% rename from crates/quicnprotochat-gui/README.md rename to crates/quicproquo-gui/README.md index 6df344e..783e578 100644 --- a/crates/quicnprotochat-gui/README.md +++ b/crates/quicproquo-gui/README.md @@ -1,6 +1,6 @@ -# quicnprotochat-gui +# quicproquo-gui -Native GUI for quicnprotochat using [Tauri 2](https://v2.tauri.app/). The UI runs in a webview; all server-facing work (capnp-rpc, `node_service::Client`) runs on a **dedicated backend thread** with a tokio `LocalSet`, since that code is `!Send`. +Native GUI for quicproquo using [Tauri 2](https://v2.tauri.app/). The UI runs in a webview; all server-facing work (capnp-rpc, `node_service::Client`) runs on a **dedicated backend thread** with a tokio `LocalSet`, since that code is `!Send`. ## Backend threading model @@ -14,7 +14,7 @@ Native GUI for quicnprotochat using [Tauri 2](https://v2.tauri.app/). The UI run From the workspace root: ```bash -cargo run -p quicnprotochat-gui +cargo run -p quicproquo-gui ``` **Linux:** Tauri uses GTK. Install development packages if the build fails, e.g.: diff --git a/crates/quicnprotochat-gui/build.rs b/crates/quicproquo-gui/build.rs similarity index 100% rename from crates/quicnprotochat-gui/build.rs rename to crates/quicproquo-gui/build.rs diff --git a/crates/quicnprotochat-gui/capabilities/default.json b/crates/quicproquo-gui/capabilities/default.json similarity index 100% rename from crates/quicnprotochat-gui/capabilities/default.json rename to crates/quicproquo-gui/capabilities/default.json diff --git a/crates/quicnprotochat-gui/gen/schemas/acl-manifests.json b/crates/quicproquo-gui/gen/schemas/acl-manifests.json similarity index 100% rename from crates/quicnprotochat-gui/gen/schemas/acl-manifests.json rename to crates/quicproquo-gui/gen/schemas/acl-manifests.json diff --git a/crates/quicnprotochat-gui/gen/schemas/capabilities.json b/crates/quicproquo-gui/gen/schemas/capabilities.json similarity index 100% rename from crates/quicnprotochat-gui/gen/schemas/capabilities.json rename to crates/quicproquo-gui/gen/schemas/capabilities.json diff --git a/crates/quicnprotochat-gui/gen/schemas/desktop-schema.json b/crates/quicproquo-gui/gen/schemas/desktop-schema.json similarity index 100% rename from crates/quicnprotochat-gui/gen/schemas/desktop-schema.json rename to crates/quicproquo-gui/gen/schemas/desktop-schema.json diff --git a/crates/quicnprotochat-gui/gen/schemas/linux-schema.json b/crates/quicproquo-gui/gen/schemas/linux-schema.json similarity index 100% rename from crates/quicnprotochat-gui/gen/schemas/linux-schema.json rename to crates/quicproquo-gui/gen/schemas/linux-schema.json diff --git a/crates/quicnprotochat-gui/gen/schemas/macOS-schema.json b/crates/quicproquo-gui/gen/schemas/macOS-schema.json similarity index 100% rename from crates/quicnprotochat-gui/gen/schemas/macOS-schema.json rename to crates/quicproquo-gui/gen/schemas/macOS-schema.json diff --git a/crates/quicnprotochat-gui/icons/icon.png b/crates/quicproquo-gui/icons/icon.png similarity index 100% rename from crates/quicnprotochat-gui/icons/icon.png rename to crates/quicproquo-gui/icons/icon.png diff --git a/crates/quicnprotochat-gui/src/backend.rs b/crates/quicproquo-gui/src/backend.rs similarity index 97% rename from crates/quicnprotochat-gui/src/backend.rs rename to crates/quicproquo-gui/src/backend.rs index 0b24809..f697655 100644 --- a/crates/quicnprotochat-gui/src/backend.rs +++ b/crates/quicproquo-gui/src/backend.rs @@ -11,7 +11,7 @@ use std::thread; use tokio::runtime::Builder; use tokio::task::LocalSet; -use quicnprotochat_client::{cmd_health_json, whoami_json}; +use quicproquo_client::{cmd_health_json, whoami_json}; /// Commands the UI can send to the backend thread. pub enum BackendCommand { diff --git a/crates/quicnprotochat-gui/src/lib.rs b/crates/quicproquo-gui/src/lib.rs similarity index 98% rename from crates/quicnprotochat-gui/src/lib.rs rename to crates/quicproquo-gui/src/lib.rs index 1302864..de44430 100644 --- a/crates/quicnprotochat-gui/src/lib.rs +++ b/crates/quicproquo-gui/src/lib.rs @@ -1,4 +1,4 @@ -//! quicnprotochat native GUI (Tauri 2). +//! quicproquo native GUI (Tauri 2). //! //! The backend runs on a dedicated thread with a tokio LocalSet; all server-facing //! work (capnp-rpc, node_service::Client) is dispatched there. Tauri commands diff --git a/crates/quicproquo-gui/src/main.rs b/crates/quicproquo-gui/src/main.rs new file mode 100644 index 0000000..c0fa2de --- /dev/null +++ b/crates/quicproquo-gui/src/main.rs @@ -0,0 +1,5 @@ +//! Desktop entry point for quicproquo-gui. + +fn main() { + quicproquo_gui::run() +} diff --git a/crates/quicnprotochat-gui/tauri.conf.json b/crates/quicproquo-gui/tauri.conf.json similarity index 74% rename from crates/quicnprotochat-gui/tauri.conf.json rename to crates/quicproquo-gui/tauri.conf.json index 48008e1..47d1072 100644 --- a/crates/quicnprotochat-gui/tauri.conf.json +++ b/crates/quicproquo-gui/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "quicnprotochat-gui", - "identifier": "chat.quicnproto.gui", + "productName": "qpq-gui", + "identifier": "chat.quicproquo.gui", "build": { "frontendDist": "./ui", "beforeBuildCommand": "", @@ -10,7 +10,7 @@ "app": { "windows": [ { - "title": "quicnprotochat", + "title": "quicproquo", "width": 640, "height": 480 } diff --git a/crates/quicnprotochat-gui/ui/index.html b/crates/quicproquo-gui/ui/index.html similarity index 91% rename from crates/quicnprotochat-gui/ui/index.html rename to crates/quicproquo-gui/ui/index.html index e9b544f..cc57738 100644 --- a/crates/quicnprotochat-gui/ui/index.html +++ b/crates/quicproquo-gui/ui/index.html @@ -3,7 +3,7 @@ - quicnprotochat + quicproquo -

quicnprotochat

+

quicproquo

- +
Click Whoami or Health. Results appear here.
diff --git a/crates/quicproquo-mobile/Cargo.toml b/crates/quicproquo-mobile/Cargo.toml new file mode 100644 index 0000000..78cb9cf --- /dev/null +++ b/crates/quicproquo-mobile/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "quicproquo-mobile" +version = "0.1.0" +edition = "2021" +description = "C FFI layer for quicproquo, proving QUIC connection migration." +license = "MIT" + +[lib] +crate-type = ["staticlib", "cdylib", "rlib"] + +[dependencies] +# Async +tokio = { workspace = true } + +# QUIC +quinn = { workspace = true } +rustls = { workspace = true } + +# Error handling +anyhow = { workspace = true } + +[dev-dependencies] +rcgen = { workspace = true } diff --git a/crates/quicproquo-mobile/src/lib.rs b/crates/quicproquo-mobile/src/lib.rs new file mode 100644 index 0000000..d98e97d --- /dev/null +++ b/crates/quicproquo-mobile/src/lib.rs @@ -0,0 +1,331 @@ +//! quicproquo-mobile — C FFI layer for mobile integration. +//! +//! Provides a minimal C API that proves QUIC connection migration works +//! (wifi → cellular handoff without message loss). Each FFI function uses +//! `runtime.block_on(local.run_until(...))` to satisfy capnp-rpc's `!Send` +//! requirement. +//! +//! # Safety +//! +//! All FFI functions are `unsafe extern "C"` — callers must ensure pointers +//! are valid and buffers are correctly sized. + +use std::ffi::c_char; +use std::net::SocketAddr; +use std::sync::Arc; + +use quinn::Endpoint; +use tokio::runtime::Runtime; + +/// Opaque handle returned by `qnpc_connect`. +#[allow(dead_code)] +pub struct MobileHandle { + runtime: Runtime, + endpoint: Endpoint, + connection: Option, + server_addr: SocketAddr, + server_name: String, +} + +/// Status codes returned by FFI functions. +#[repr(C)] +pub enum QnpcStatus { + Ok = 0, + Error = 1, + Timeout = 2, + NotConnected = 3, +} + +/// Connect to a quicproquo server. Returns a handle pointer (null on failure). +/// +/// # Safety +/// `server_addr` and `server_name` must be valid null-terminated C strings. +#[no_mangle] +pub unsafe extern "C" fn qnpc_connect( + server_addr: *const c_char, + server_name: *const c_char, +) -> *mut MobileHandle { + let addr_str = match std::ffi::CStr::from_ptr(server_addr).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let name_str = match std::ffi::CStr::from_ptr(server_name).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let addr: SocketAddr = match addr_str.parse() { + Ok(a) => a, + Err(_) => return std::ptr::null_mut(), + }; + + let rt = match Runtime::new() { + Ok(r) => r, + Err(_) => return std::ptr::null_mut(), + }; + + let result = rt.block_on(async { + connect_inner(addr, name_str).await + }); + + match result { + Ok((endpoint, connection)) => { + let handle = Box::new(MobileHandle { + runtime: rt, + endpoint, + connection: Some(connection), + server_addr: addr, + server_name: name_str.to_string(), + }); + Box::into_raw(handle) + } + Err(_) => std::ptr::null_mut(), + } +} + +async fn connect_inner( + addr: SocketAddr, + server_name: &str, +) -> anyhow::Result<(Endpoint, quinn::Connection)> { + let _ = rustls::crypto::ring::default_provider().install_default(); + + // Build a permissive client config (skip server cert verification for dev/testing). + let crypto = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(SkipServerVerification)) + .with_no_client_auth(); + + let mut client_config = quinn::ClientConfig::new(Arc::new( + quinn::crypto::rustls::QuicClientConfig::try_from(crypto) + .map_err(|e| anyhow::anyhow!("QUIC client config: {e}"))?, + )); + + let mut transport = quinn::TransportConfig::default(); + transport.max_idle_timeout(Some( + std::time::Duration::from_secs(120) + .try_into() + .expect("120s valid"), + )); + client_config.transport_config(Arc::new(transport)); + + let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())?; + endpoint.set_default_client_config(client_config); + + let connection = endpoint.connect(addr, server_name)?.await?; + Ok((endpoint, connection)) +} + +/// Simulate QUIC connection migration by rebinding the endpoint to a new local address. +/// +/// This is the key proof-of-concept: after rebind, the QUIC connection survives +/// and messages continue flowing without loss. +/// +/// # Safety +/// `handle` must be a valid pointer from `qnpc_connect`. +#[no_mangle] +pub unsafe extern "C" fn qnpc_migrate( + handle: *mut MobileHandle, + new_port: u16, +) -> QnpcStatus { + let handle = match handle.as_mut() { + Some(h) => h, + None => return QnpcStatus::Error, + }; + + let new_addr: SocketAddr = format!("0.0.0.0:{new_port}").parse().unwrap(); + let socket = match std::net::UdpSocket::bind(new_addr) { + Ok(s) => s, + Err(_) => return QnpcStatus::Error, + }; + + match handle.endpoint.rebind(socket) { + Ok(_) => QnpcStatus::Ok, + Err(_) => QnpcStatus::Error, + } +} + +/// Disconnect and free the handle. +/// +/// # Safety +/// `handle` must be a valid pointer from `qnpc_connect`, and must not be used after this call. +#[no_mangle] +pub unsafe extern "C" fn qnpc_disconnect(handle: *mut MobileHandle) { + if !handle.is_null() { + let handle = Box::from_raw(handle); + if let Some(conn) = &handle.connection { + conn.close(0u32.into(), b"disconnect"); + } + drop(handle); + } +} + +// ── Internal: skip server cert verification for testing ───────────────────── + +#[derive(Debug)] +struct SkipServerVerification; + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + ] + } +} + +// ── Tests ─────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use std::net::UdpSocket; + + /// Prove QUIC connection migration: connect, send messages, rebind the + /// UDP socket (simulating wifi→cellular), send more messages, verify + /// all messages arrive. + #[test] + fn quic_connection_migration() { + let _ = rustls::crypto::ring::default_provider().install_default(); + + let rt = Runtime::new().unwrap(); + rt.block_on(async { + // Start an in-process echo server. + let server_addr = start_echo_server().await; + + // Connect client. + let (endpoint, connection) = connect_inner(server_addr, "localhost") + .await + .expect("connect"); + + // Send 5 messages before migration. + for i in 0..5u32 { + let (mut send, mut recv) = connection.open_bi().await.unwrap(); + let msg = format!("pre-migrate-{i}"); + send.write_all(msg.as_bytes()).await.unwrap(); + send.finish().unwrap(); + let response = recv.read_to_end(4096).await.unwrap(); + assert_eq!(response, msg.as_bytes(), "pre-migrate echo mismatch"); + } + + // Migrate: rebind to a new local UDP socket (simulates wifi→cellular). + let new_socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let new_local = new_socket.local_addr().unwrap(); + endpoint.rebind(new_socket).expect("rebind should succeed"); + + // Send 5 more messages after migration. + for i in 0..5u32 { + let (mut send, mut recv) = connection.open_bi().await.unwrap(); + let msg = format!("post-migrate-{i}"); + send.write_all(msg.as_bytes()).await.unwrap(); + send.finish().unwrap(); + let response = recv.read_to_end(4096).await.unwrap(); + assert_eq!(response, msg.as_bytes(), "post-migrate echo mismatch"); + } + + // Assert: connection still alive after migration. + assert!( + connection.close_reason().is_none(), + "connection should still be open after migration" + ); + + // Verify the local address changed. + let _ = new_local; // We successfully used a new socket. + + connection.close(0u32.into(), b"test done"); + endpoint.wait_idle().await; + }); + } + + /// Start a simple QUIC echo server that echoes back whatever it receives. + async fn start_echo_server() -> SocketAddr { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_der = cert.cert.der().to_vec(); + let key_der = cert.key_pair.serialize_der(); + + let cert_chain = vec![rustls::pki_types::CertificateDer::from(cert_der)]; + let key = rustls::pki_types::PrivateKeyDer::try_from(key_der).unwrap(); + + let tls = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, key) + .unwrap(); + + let server_config = quinn::ServerConfig::with_crypto(Arc::new( + quinn::crypto::rustls::QuicServerConfig::try_from(tls).unwrap(), + )); + + let endpoint = Endpoint::server( + server_config, + "127.0.0.1:0".parse().unwrap(), + ) + .unwrap(); + + let addr = endpoint.local_addr().unwrap(); + + // Spawn echo acceptor. + tokio::spawn(async move { + while let Some(incoming) = endpoint.accept().await { + let connecting = match incoming.accept() { + Ok(c) => c, + Err(_) => continue, + }; + tokio::spawn(async move { + let conn = match connecting.await { + Ok(c) => c, + Err(_) => return, + }; + loop { + let (mut send, mut recv) = match conn.accept_bi().await { + Ok(s) => s, + Err(_) => break, + }; + let data = match recv.read_to_end(4096).await { + Ok(d) => d, + Err(_) => break, + }; + let _ = send.write_all(&data).await; + let _ = send.finish(); + } + }); + } + }); + + addr + } +} diff --git a/crates/quicnprotochat-p2p/Cargo.toml b/crates/quicproquo-p2p/Cargo.toml similarity index 70% rename from crates/quicnprotochat-p2p/Cargo.toml rename to crates/quicproquo-p2p/Cargo.toml index e80a974..05a216b 100644 --- a/crates/quicnprotochat-p2p/Cargo.toml +++ b/crates/quicproquo-p2p/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "quicnprotochat-p2p" +name = "quicproquo-p2p" version = "0.1.0" edition = "2021" -description = "P2P transport layer for quicnprotochat using iroh." +description = "P2P transport layer for quicproquo using iroh." license = "MIT" [dependencies] diff --git a/crates/quicnprotochat-p2p/src/lib.rs b/crates/quicproquo-p2p/src/lib.rs similarity index 96% rename from crates/quicnprotochat-p2p/src/lib.rs rename to crates/quicproquo-p2p/src/lib.rs index cc34983..f4abb88 100644 --- a/crates/quicnprotochat-p2p/src/lib.rs +++ b/crates/quicproquo-p2p/src/lib.rs @@ -1,4 +1,4 @@ -//! P2P transport layer for quicnprotochat using iroh. +//! P2P transport layer for quicproquo using iroh. //! //! Provides direct peer-to-peer QUIC connections with NAT traversal via iroh //! relay servers. When both peers are online, messages bypass the central @@ -14,7 +14,8 @@ use iroh::{Endpoint, EndpointAddr, PublicKey, SecretKey}; -/// ALPN protocol identifier for quicnprotochat P2P messaging. +/// ALPN protocol identifier for quicproquo P2P messaging. +/// Frozen at the original project name for wire compatibility. const P2P_ALPN: &[u8] = b"quicnprotochat/p2p/1"; /// A P2P node backed by an iroh endpoint. diff --git a/crates/quicnprotochat-proto/Cargo.toml b/crates/quicproquo-proto/Cargo.toml similarity index 78% rename from crates/quicnprotochat-proto/Cargo.toml rename to crates/quicproquo-proto/Cargo.toml index 7e6d8d0..fe85028 100644 --- a/crates/quicnprotochat-proto/Cargo.toml +++ b/crates/quicproquo-proto/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "quicnprotochat-proto" +name = "quicproquo-proto" version = "0.1.0" edition = "2021" -description = "Cap'n Proto schemas, generated types, and serialisation helpers for quicnprotochat. No crypto, no I/O." +description = "Cap'n Proto schemas, generated types, and serialisation helpers for quicproquo. No crypto, no I/O." license = "MIT" # build.rs invokes capnpc to generate Rust source from .capnp schemas. diff --git a/crates/quicnprotochat-proto/build.rs b/crates/quicproquo-proto/build.rs similarity index 84% rename from crates/quicnprotochat-proto/build.rs rename to crates/quicproquo-proto/build.rs index 792c8d1..51efa81 100644 --- a/crates/quicnprotochat-proto/build.rs +++ b/crates/quicproquo-proto/build.rs @@ -1,4 +1,4 @@ -//! Build script for quicnprotochat-proto. +//! Build script for quicproquo-proto. //! //! Invokes the `capnp` compiler to generate Rust types from `.capnp` schemas //! located in the workspace-root `schemas/` directory. @@ -17,7 +17,7 @@ fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set by Cargo")); - // Workspace root is two levels above this crate (quicnprotochat/crates/quicnprotochat-proto). + // Workspace root is two levels above this crate (quicproquo/crates/quicproquo-proto). let workspace_root = manifest_dir .join("../..") .canonicalize() @@ -38,6 +38,10 @@ fn main() { "cargo:rerun-if-changed={}", schemas_dir.join("node.capnp").display() ); + println!( + "cargo:rerun-if-changed={}", + schemas_dir.join("federation.capnp").display() + ); capnpc::CompilerCommand::new() // Treat `schemas/` as the include root so that inter-schema imports @@ -46,6 +50,7 @@ fn main() { .file(schemas_dir.join("auth.capnp")) .file(schemas_dir.join("delivery.capnp")) .file(schemas_dir.join("node.capnp")) + .file(schemas_dir.join("federation.capnp")) .run() .expect( "Cap'n Proto schema compilation failed. \ diff --git a/crates/quicnprotochat-proto/src/lib.rs b/crates/quicproquo-proto/src/lib.rs similarity index 88% rename from crates/quicnprotochat-proto/src/lib.rs rename to crates/quicproquo-proto/src/lib.rs index 5497f0b..f6db70d 100644 --- a/crates/quicnprotochat-proto/src/lib.rs +++ b/crates/quicproquo-proto/src/lib.rs @@ -1,4 +1,4 @@ -//! Cap'n Proto schemas, generated types, and serialisation helpers for quicnprotochat. +//! Cap'n Proto schemas, generated types, and serialisation helpers for quicproquo. //! //! Generated Cap'n Proto code emits unnecessary parentheses; allow per coding standards. #![allow(unused_parens)] @@ -38,12 +38,19 @@ pub mod node_capnp { include!(concat!(env!("OUT_DIR"), "/node_capnp.rs")); } +/// Cap'n Proto generated types for `schemas/federation.capnp`. +/// +/// Do not edit this module by hand — it is entirely machine-generated. +pub mod federation_capnp { + include!(concat!(env!("OUT_DIR"), "/federation_capnp.rs")); +} + // ── Low-level byte ↔ message conversions ────────────────────────────────────── /// Serialise a Cap'n Proto message builder to unpacked wire bytes. /// /// The output includes the segment table header. For transport, the -/// `quicnprotochat-core` frame codec prepends a 4-byte little-endian length field. +/// `quicproquo-core` frame codec prepends a 4-byte little-endian length field. pub fn to_bytes( msg: &capnp::message::Builder, ) -> Result, capnp::Error> { diff --git a/crates/quicnprotochat-server/Cargo.toml b/crates/quicproquo-server/Cargo.toml similarity index 86% rename from crates/quicnprotochat-server/Cargo.toml rename to crates/quicproquo-server/Cargo.toml index c759a15..152a832 100644 --- a/crates/quicnprotochat-server/Cargo.toml +++ b/crates/quicproquo-server/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "quicnprotochat-server" +name = "quicproquo-server" version = "0.1.0" edition = "2021" -description = "Delivery Service and Authentication Service for quicnprotochat." +description = "Delivery Service and Authentication Service for quicproquo." license = "MIT" [[bin]] -name = "quicnprotochat-server" +name = "qpq-server" path = "src/main.rs" [dependencies] -quicnprotochat-core = { path = "../quicnprotochat-core" } -quicnprotochat-proto = { path = "../quicnprotochat-proto" } +quicproquo-core = { path = "../quicproquo-core" } +quicproquo-proto = { path = "../quicproquo-proto" } # Serialisation + RPC capnp = { workspace = true } @@ -24,6 +24,7 @@ futures = { workspace = true } # Server utilities dashmap = { workspace = true } +hex = { workspace = true } sha2 = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/quicnprotochat-server/migrations/001_initial.sql b/crates/quicproquo-server/migrations/001_initial.sql similarity index 100% rename from crates/quicnprotochat-server/migrations/001_initial.sql rename to crates/quicproquo-server/migrations/001_initial.sql diff --git a/crates/quicnprotochat-server/migrations/002_add_seq.sql b/crates/quicproquo-server/migrations/002_add_seq.sql similarity index 100% rename from crates/quicnprotochat-server/migrations/002_add_seq.sql rename to crates/quicproquo-server/migrations/002_add_seq.sql diff --git a/crates/quicnprotochat-server/migrations/003_channels.sql b/crates/quicproquo-server/migrations/003_channels.sql similarity index 100% rename from crates/quicnprotochat-server/migrations/003_channels.sql rename to crates/quicproquo-server/migrations/003_channels.sql diff --git a/crates/quicproquo-server/migrations/004_federation.sql b/crates/quicproquo-server/migrations/004_federation.sql new file mode 100644 index 0000000..92b8980 --- /dev/null +++ b/crates/quicproquo-server/migrations/004_federation.sql @@ -0,0 +1,16 @@ +-- 004_federation.sql: Federation support tables. + +-- Map identity keys to their home server domain. +-- Used for routing: if a recipient's home_server != local domain, relay via federation. +CREATE TABLE IF NOT EXISTS identity_home_servers ( + identity_key BLOB PRIMARY KEY, + home_server TEXT NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Known federation peers (other quicnprotochat servers). +CREATE TABLE IF NOT EXISTS federation_peers ( + domain TEXT PRIMARY KEY, + last_seen INTEGER NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1 +); diff --git a/crates/quicnprotochat-server/src/auth.rs b/crates/quicproquo-server/src/auth.rs similarity index 98% rename from crates/quicnprotochat-server/src/auth.rs rename to crates/quicproquo-server/src/auth.rs index da09917..3ee8d0e 100644 --- a/crates/quicnprotochat-server/src/auth.rs +++ b/crates/quicproquo-server/src/auth.rs @@ -2,7 +2,7 @@ use std::net::IpAddr; use std::sync::Arc; use dashmap::DashMap; -use quicnprotochat_proto::node_capnp::auth; +use quicproquo_proto::node_capnp::auth; use sha2::Digest; use subtle::ConstantTimeEq; use tokio::sync::Notify; @@ -20,7 +20,7 @@ pub struct AuthConfig { /// Server bearer token — zeroized on drop to prevent memory disclosure. pub required_token: Option>>, /// When true, a valid bearer token (no session) is accepted and the request's identity/key is used (dev/e2e only). - /// CLI flag: --allow-insecure-auth / QUICNPROTOCHAT_ALLOW_INSECURE_AUTH. + /// CLI flag: --allow-insecure-auth / QPQ_ALLOW_INSECURE_AUTH. pub allow_insecure_identity_from_request: bool, } diff --git a/crates/quicnprotochat-server/src/config.rs b/crates/quicproquo-server/src/config.rs similarity index 68% rename from crates/quicnprotochat-server/src/config.rs rename to crates/quicproquo-server/src/config.rs index 999eb52..cbfcd9f 100644 --- a/crates/quicnprotochat-server/src/config.rs +++ b/crates/quicproquo-server/src/config.rs @@ -8,7 +8,7 @@ pub const DEFAULT_DATA_DIR: &str = "data"; pub const DEFAULT_TLS_CERT: &str = "data/server-cert.der"; pub const DEFAULT_TLS_KEY: &str = "data/server-key.der"; pub const DEFAULT_STORE_BACKEND: &str = "file"; -pub const DEFAULT_DB_PATH: &str = "data/quicnprotochat.db"; +pub const DEFAULT_DB_PATH: &str = "data/qpq.db"; #[derive(Debug, Default, Deserialize)] pub struct FileConfig { @@ -30,6 +30,7 @@ pub struct FileConfig { /// When true and metrics_listen is set, start the metrics server. #[serde(default)] pub metrics_enabled: Option, + pub federation: Option, } #[derive(Debug)] @@ -49,12 +50,42 @@ pub struct EffectiveConfig { pub metrics_listen: Option, /// Start metrics server only when true and metrics_listen is set. pub metrics_enabled: bool, + pub federation: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct FederationFileConfig { + pub enabled: Option, + pub domain: Option, + pub listen: Option, + pub federation_cert: Option, + pub federation_key: Option, + pub federation_ca: Option, + #[serde(default)] + pub peers: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct FederationPeerConfig { + pub domain: String, + pub address: String, +} + +#[derive(Debug)] +pub struct EffectiveFederationConfig { + pub enabled: bool, + pub domain: String, + pub listen: String, + pub federation_cert: PathBuf, + pub federation_key: PathBuf, + pub federation_ca: PathBuf, + pub peers: Vec, } pub fn load_config(path: Option<&Path>) -> anyhow::Result { let path = match path { Some(p) => PathBuf::from(p), - None => PathBuf::from("quicnprotochat-server.toml"), + None => PathBuf::from("qpq-server.toml"), }; if !path.exists() { @@ -146,6 +177,42 @@ pub fn merge_config(args: &crate::Args, file: &FileConfig) -> EffectiveConfig { .or(file.metrics_enabled) .unwrap_or(metrics_listen.is_some()); + let federation = { + let file_fed = file.federation.as_ref(); + let enabled = args.federation_enabled + || file_fed.and_then(|f| f.enabled).unwrap_or(false); + + if enabled { + let domain = args.federation_domain.clone() + .or_else(|| file_fed.and_then(|f| f.domain.clone())) + .unwrap_or_default(); + let listen_fed = args.federation_listen.clone() + .or_else(|| file_fed.and_then(|f| f.listen.clone())) + .unwrap_or_else(|| "0.0.0.0:7001".to_string()); + let federation_cert = file_fed.and_then(|f| f.federation_cert.clone()) + .unwrap_or_else(|| PathBuf::from("data/federation-cert.der")); + let federation_key = file_fed.and_then(|f| f.federation_key.clone()) + .unwrap_or_else(|| PathBuf::from("data/federation-key.der")); + let federation_ca = file_fed.and_then(|f| f.federation_ca.clone()) + .unwrap_or_else(|| PathBuf::from("data/federation-ca.der")); + let peers = file_fed + .map(|f| f.peers.clone()) + .unwrap_or_default(); + + Some(EffectiveFederationConfig { + enabled, + domain, + listen: listen_fed, + federation_cert, + federation_key, + federation_ca, + peers, + }) + } else { + None + } + }; + EffectiveConfig { listen, data_dir, @@ -159,6 +226,7 @@ pub fn merge_config(args: &crate::Args, file: &FileConfig) -> EffectiveConfig { db_key, metrics_listen, metrics_enabled, + federation, } } @@ -171,25 +239,25 @@ pub fn validate_production_config(effective: &EffectiveConfig) -> anyhow::Result .as_deref() .filter(|s| !s.is_empty()) .ok_or_else(|| { - anyhow::anyhow!("production requires QUICNPROTOCHAT_AUTH_TOKEN (non-empty)") + anyhow::anyhow!("production requires QPQ_AUTH_TOKEN (non-empty)") })?; if token == "devtoken" { anyhow::bail!( - "production forbids auth_token 'devtoken'; set a strong QUICNPROTOCHAT_AUTH_TOKEN" + "production forbids auth_token 'devtoken'; set a strong QPQ_AUTH_TOKEN" ); } if effective.store_backend == "sql" && effective.db_key.is_empty() { - anyhow::bail!("production with store_backend=sql requires non-empty QUICNPROTOCHAT_DB_KEY"); + anyhow::bail!("production with store_backend=sql requires non-empty QPQ_DB_KEY"); } if effective.store_backend != "sql" { tracing::warn!( "production is using file-backed storage; \ - consider store_backend=sql with QUICNPROTOCHAT_DB_KEY for encryption at rest" + consider store_backend=sql with QPQ_DB_KEY for encryption at rest" ); } if !effective.tls_cert.exists() || !effective.tls_key.exists() { anyhow::bail!( - "production requires existing TLS cert and key (no auto-generation); provide QUICNPROTOCHAT_TLS_CERT and QUICNPROTOCHAT_TLS_KEY" + "production requires existing TLS cert and key (no auto-generation); provide QPQ_TLS_CERT and QPQ_TLS_KEY" ); } Ok(()) diff --git a/crates/quicnprotochat-server/src/error_codes.rs b/crates/quicproquo-server/src/error_codes.rs similarity index 100% rename from crates/quicnprotochat-server/src/error_codes.rs rename to crates/quicproquo-server/src/error_codes.rs diff --git a/crates/quicproquo-server/src/federation/address.rs b/crates/quicproquo-server/src/federation/address.rs new file mode 100644 index 0000000..e0fa399 --- /dev/null +++ b/crates/quicproquo-server/src/federation/address.rs @@ -0,0 +1,78 @@ +//! Parse `username@domain` federated addresses. +//! +//! A bare `username` (no `@`) is treated as local. + +/// A parsed federated address. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FederatedAddress { + pub username: String, + pub domain: Option, +} + +impl FederatedAddress { + /// Parse a `user@domain` string. Bare `user` → domain is `None`. + pub fn parse(input: &str) -> Self { + // Split on the *last* '@' so usernames can contain '@' in theory. + match input.rsplit_once('@') { + Some((user, domain)) if !domain.is_empty() && !user.is_empty() => Self { + username: user.to_string(), + domain: Some(domain.to_string()), + }, + _ => Self { + username: input.to_string(), + domain: None, + }, + } + } + + /// Returns true if this address refers to a local user (no domain or domain matches local). + pub fn is_local(&self, local_domain: &str) -> bool { + match &self.domain { + None => true, + Some(d) => d == local_domain, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bare_username() { + let addr = FederatedAddress::parse("alice"); + assert_eq!(addr.username, "alice"); + assert_eq!(addr.domain, None); + assert!(addr.is_local("example.com")); + } + + #[test] + fn user_at_domain() { + let addr = FederatedAddress::parse("alice@remote.example.com"); + assert_eq!(addr.username, "alice"); + assert_eq!(addr.domain, Some("remote.example.com".into())); + assert!(!addr.is_local("local.example.com")); + assert!(addr.is_local("remote.example.com")); + } + + #[test] + fn trailing_at_is_bare() { + let addr = FederatedAddress::parse("alice@"); + assert_eq!(addr.username, "alice@"); + assert_eq!(addr.domain, None); + } + + #[test] + fn leading_at_is_bare() { + let addr = FederatedAddress::parse("@domain.com"); + assert_eq!(addr.username, "@domain.com"); + assert_eq!(addr.domain, None); + } + + #[test] + fn multiple_at_uses_last() { + let addr = FederatedAddress::parse("user@org@domain.com"); + assert_eq!(addr.username, "user@org"); + assert_eq!(addr.domain, Some("domain.com".into())); + } +} diff --git a/crates/quicproquo-server/src/federation/client.rs b/crates/quicproquo-server/src/federation/client.rs new file mode 100644 index 0000000..84e1f4a --- /dev/null +++ b/crates/quicproquo-server/src/federation/client.rs @@ -0,0 +1,287 @@ +//! Outbound federation client: connects to peer servers to relay messages. +//! +//! Uses a lazy connection pool (DashMap) to reuse QUIC connections to known peers. + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::Context; +use dashmap::DashMap; +use quinn::Endpoint; + +use crate::config::{EffectiveFederationConfig, FederationPeerConfig}; + +/// Outbound federation client for relaying to peer servers. +pub struct FederationClient { + /// Peer domain → address mapping from config. + peer_addresses: HashMap, + /// Lazy QUIC connection pool: domain → active Connection. + connections: DashMap, + /// Local QUIC endpoint (shared for all outbound federation connections). + endpoint: Endpoint, + /// Local domain (for the FederationAuth.origin field). + local_domain: String, +} + +impl FederationClient { + /// Create a new federation client from config. + /// + /// The `endpoint` should be configured with mTLS client credentials. + pub fn new( + config: &EffectiveFederationConfig, + endpoint: Endpoint, + ) -> anyhow::Result { + let mut peer_addresses = HashMap::new(); + for peer in &config.peers { + let addr: SocketAddr = peer.address.parse().with_context(|| { + format!("parse federation peer address '{}' for '{}'", peer.address, peer.domain) + })?; + peer_addresses.insert(peer.domain.clone(), addr); + } + + Ok(Self { + peer_addresses, + connections: DashMap::new(), + endpoint, + local_domain: config.domain.clone(), + }) + } + + /// Check if we have a configured peer for the given domain. + pub fn has_peer(&self, domain: &str) -> bool { + self.peer_addresses.contains_key(domain) + } + + /// List all configured peer domains. + pub fn peer_domains(&self) -> Vec { + self.peer_addresses.keys().cloned().collect() + } + + /// Get the local domain. + pub fn local_domain(&self) -> &str { + &self.local_domain + } + + /// Relay a single enqueue to a remote peer. Returns the seq assigned by the remote server. + pub async fn relay_enqueue( + &self, + domain: &str, + recipient_key: &[u8], + payload: &[u8], + channel_id: &[u8], + ) -> anyhow::Result { + let conn = self.get_or_connect(domain).await?; + let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?; + + let (reader, writer) = ( + tokio_util::compat::TokioAsyncReadCompatExt::compat(recv), + tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send), + ); + + let rpc_network = capnp_rpc::twoparty::VatNetwork::new( + reader, + writer, + capnp_rpc::rpc_twoparty_capnp::Side::Client, + Default::default(), + ); + + let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None); + let client: quicproquo_proto::federation_capnp::federation_service::Client = + rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server); + + tokio::task::spawn_local(rpc_system); + + let mut req = client.relay_enqueue_request(); + { + let mut builder = req.get(); + builder.set_recipient_key(recipient_key); + builder.set_payload(payload); + builder.set_channel_id(channel_id); + builder.set_version(1); + let mut auth = builder.init_auth(); + auth.set_origin(&self.local_domain); + } + + let response = req.send().promise.await + .map_err(|e| anyhow::anyhow!("federation relay_enqueue failed: {e}"))?; + let seq = response.get() + .map_err(|e| anyhow::anyhow!("read relay_enqueue response: {e}"))? + .get_seq(); + + Ok(seq) + } + + /// Proxy a key package fetch to a remote peer. + pub async fn proxy_fetch_key_package( + &self, + domain: &str, + identity_key: &[u8], + ) -> anyhow::Result>> { + let conn = self.get_or_connect(domain).await?; + let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?; + + let (reader, writer) = ( + tokio_util::compat::TokioAsyncReadCompatExt::compat(recv), + tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send), + ); + + let rpc_network = capnp_rpc::twoparty::VatNetwork::new( + reader, + writer, + capnp_rpc::rpc_twoparty_capnp::Side::Client, + Default::default(), + ); + + let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None); + let client: quicproquo_proto::federation_capnp::federation_service::Client = + rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server); + + tokio::task::spawn_local(rpc_system); + + let mut req = client.proxy_fetch_key_package_request(); + { + let mut builder = req.get(); + builder.set_identity_key(identity_key); + let mut auth = builder.init_auth(); + auth.set_origin(&self.local_domain); + } + + let response = req.send().promise.await + .map_err(|e| anyhow::anyhow!("federation proxy_fetch_key_package failed: {e}"))?; + let pkg = response.get() + .map_err(|e| anyhow::anyhow!("read proxy_fetch_key_package response: {e}"))? + .get_package() + .map_err(|e| anyhow::anyhow!("get package: {e}"))?; + + if pkg.is_empty() { + Ok(None) + } else { + Ok(Some(pkg.to_vec())) + } + } + + /// Proxy a hybrid key fetch to a remote peer. + pub async fn proxy_fetch_hybrid_key( + &self, + domain: &str, + identity_key: &[u8], + ) -> anyhow::Result>> { + let conn = self.get_or_connect(domain).await?; + let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?; + + let (reader, writer) = ( + tokio_util::compat::TokioAsyncReadCompatExt::compat(recv), + tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send), + ); + + let rpc_network = capnp_rpc::twoparty::VatNetwork::new( + reader, + writer, + capnp_rpc::rpc_twoparty_capnp::Side::Client, + Default::default(), + ); + + let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None); + let client: quicproquo_proto::federation_capnp::federation_service::Client = + rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server); + + tokio::task::spawn_local(rpc_system); + + let mut req = client.proxy_fetch_hybrid_key_request(); + { + let mut builder = req.get(); + builder.set_identity_key(identity_key); + let mut auth = builder.init_auth(); + auth.set_origin(&self.local_domain); + } + + let response = req.send().promise.await + .map_err(|e| anyhow::anyhow!("federation proxy_fetch_hybrid_key failed: {e}"))?; + let pk = response.get() + .map_err(|e| anyhow::anyhow!("read proxy_fetch_hybrid_key response: {e}"))? + .get_hybrid_public_key() + .map_err(|e| anyhow::anyhow!("get hybrid_public_key: {e}"))?; + + if pk.is_empty() { + Ok(None) + } else { + Ok(Some(pk.to_vec())) + } + } + + /// Proxy a user resolution to a remote peer. + pub async fn proxy_resolve_user( + &self, + domain: &str, + username: &str, + ) -> anyhow::Result>> { + let conn = self.get_or_connect(domain).await?; + let (send, recv) = conn.open_bi().await.context("open bi stream to peer")?; + + let (reader, writer) = ( + tokio_util::compat::TokioAsyncReadCompatExt::compat(recv), + tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send), + ); + + let rpc_network = capnp_rpc::twoparty::VatNetwork::new( + reader, + writer, + capnp_rpc::rpc_twoparty_capnp::Side::Client, + Default::default(), + ); + + let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(rpc_network), None); + let client: quicproquo_proto::federation_capnp::federation_service::Client = + rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server); + + tokio::task::spawn_local(rpc_system); + + let mut req = client.proxy_resolve_user_request(); + { + let mut builder = req.get(); + builder.set_username(username); + let mut auth = builder.init_auth(); + auth.set_origin(&self.local_domain); + } + + let response = req.send().promise.await + .map_err(|e| anyhow::anyhow!("federation proxy_resolve_user failed: {e}"))?; + let key = response.get() + .map_err(|e| anyhow::anyhow!("read proxy_resolve_user response: {e}"))? + .get_identity_key() + .map_err(|e| anyhow::anyhow!("get identity_key: {e}"))?; + + if key.is_empty() { + Ok(None) + } else { + Ok(Some(key.to_vec())) + } + } + + /// Get an existing connection or create a new one to a peer domain. + async fn get_or_connect(&self, domain: &str) -> anyhow::Result { + // Check for cached connection that's still alive. + if let Some(conn) = self.connections.get(domain) { + if conn.close_reason().is_none() { + return Ok(conn.clone()); + } + } + + let addr = self.peer_addresses.get(domain).ok_or_else(|| { + anyhow::anyhow!("no federation peer configured for domain '{domain}'") + })?; + + tracing::info!(domain = domain, addr = %addr, "connecting to federation peer"); + + let conn = self + .endpoint + .connect(*addr, domain) + .map_err(|e| anyhow::anyhow!("federation connect to {domain}: {e}"))? + .await + .with_context(|| format!("federation QUIC handshake with {domain}"))?; + + self.connections.insert(domain.to_string(), conn.clone()); + Ok(conn) + } +} diff --git a/crates/quicproquo-server/src/federation/mod.rs b/crates/quicproquo-server/src/federation/mod.rs new file mode 100644 index 0000000..04a666e --- /dev/null +++ b/crates/quicproquo-server/src/federation/mod.rs @@ -0,0 +1,16 @@ +//! Federation subsystem: server-to-server message relay over mutual TLS + QUIC. +//! +//! When federation is enabled, the server binds a second QUIC endpoint on a +//! dedicated port (default 7001) that only accepts connections from known peers +//! authenticated via mTLS. Inbound requests are handled by [`service::FederationServiceImpl`], +//! which delegates to the local [`Store`]. Outbound relay uses [`client::FederationClient`]. + +pub mod address; +pub mod client; +pub mod routing; +pub mod service; +pub mod tls; + +pub use address::FederatedAddress; +pub use client::FederationClient; +pub use routing::Destination; diff --git a/crates/quicproquo-server/src/federation/routing.rs b/crates/quicproquo-server/src/federation/routing.rs new file mode 100644 index 0000000..484566a --- /dev/null +++ b/crates/quicproquo-server/src/federation/routing.rs @@ -0,0 +1,44 @@ +//! Federation routing: determine whether a recipient is local or remote. + +use std::sync::Arc; + +use crate::storage::Store; + +/// Where a message should be delivered. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Destination { + /// Recipient is on this server. + Local, + /// Recipient's home server is the given domain. + Remote(String), +} + +/// Resolve a recipient identity key to a routing destination. +/// +/// 1. Check the `identity_home_servers` table for an explicit mapping. +/// 2. If no mapping exists, assume local (backwards compatible with single-server deployments). +pub fn resolve_destination( + store: &Arc, + recipient_key: &[u8], + local_domain: &str, +) -> Destination { + match store.get_identity_home_server(recipient_key) { + Ok(Some(domain)) if domain != local_domain => Destination::Remote(domain), + _ => Destination::Local, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unknown_identity_routes_local() { + let store: Arc = + Arc::new(crate::storage::FileBackedStore::open( + tempfile::tempdir().unwrap().path(), + ).unwrap()); + let dest = resolve_destination(&store, &[1u8; 32], "local.example.com"); + assert_eq!(dest, Destination::Local); + } +} diff --git a/crates/quicproquo-server/src/federation/service.rs b/crates/quicproquo-server/src/federation/service.rs new file mode 100644 index 0000000..a041565 --- /dev/null +++ b/crates/quicproquo-server/src/federation/service.rs @@ -0,0 +1,201 @@ +//! Inbound federation handler: implements `FederationService` Cap'n Proto interface. +//! +//! Delegates all operations to the local [`Store`], acting as a trusted relay +//! from authenticated peer servers. + +use std::sync::Arc; + +use capnp::capability::Promise; +use quicproquo_proto::federation_capnp::federation_service; +use tokio::sync::Notify; +use dashmap::DashMap; + +use crate::storage::Store; + +/// Inbound federation RPC handler. +pub struct FederationServiceImpl { + pub store: Arc, + pub waiters: Arc, Arc>>, + pub local_domain: String, +} + +impl federation_service::Server for FederationServiceImpl { + fn relay_enqueue( + &mut self, + params: federation_service::RelayEnqueueParams, + mut results: federation_service::RelayEnqueueResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))), + }; + + let recipient_key = match p.get_recipient_key() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_key: {e}"))), + }; + let payload = match p.get_payload() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad payload: {e}"))), + }; + let channel_id = p.get_channel_id().unwrap_or_default().to_vec(); + + if let Ok(a) = p.get_auth() { + if let Ok(origin) = a.get_origin() { + let origin = origin.to_str().unwrap_or("?"); + tracing::debug!(origin = origin, "federation relay_enqueue"); + } + } + + if recipient_key.len() != 32 { + return Promise::err(capnp::Error::failed("recipient_key must be 32 bytes".into())); + } + if payload.is_empty() { + return Promise::err(capnp::Error::failed("payload must not be empty".into())); + } + + let seq = match self.store.enqueue(&recipient_key, &channel_id, payload) { + Ok(s) => s, + Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))), + }; + + results.get().set_seq(seq); + + // Wake any waiting fetchWait clients. + if let Some(waiter) = self.waiters.get(&recipient_key) { + waiter.notify_waiters(); + } + + tracing::info!( + recipient_prefix = %hex::encode(&recipient_key[..4]), + seq = seq, + "federation: relayed enqueue" + ); + + Promise::ok(()) + } + + fn relay_batch_enqueue( + &mut self, + params: federation_service::RelayBatchEnqueueParams, + mut results: federation_service::RelayBatchEnqueueResults, + ) -> Promise<(), capnp::Error> { + let p = match params.get() { + Ok(p) => p, + Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))), + }; + + let recipient_keys = match p.get_recipient_keys() { + Ok(v) => v, + Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_keys: {e}"))), + }; + let payload = match p.get_payload() { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad payload: {e}"))), + }; + let channel_id = p.get_channel_id().unwrap_or_default().to_vec(); + + let mut seqs = Vec::with_capacity(recipient_keys.len() as usize); + for i in 0..recipient_keys.len() { + let rk = match recipient_keys.get(i) { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad key[{i}]: {e}"))), + }; + if rk.len() != 32 { + return Promise::err(capnp::Error::failed( + format!("recipient_key[{i}] must be 32 bytes"), + )); + } + let seq = match self.store.enqueue(&rk, &channel_id, payload.clone()) { + Ok(s) => s, + Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))), + }; + seqs.push(seq); + if let Some(waiter) = self.waiters.get(&rk) { + waiter.notify_waiters(); + } + } + + let mut list = results.get().init_seqs(seqs.len() as u32); + for (i, seq) in seqs.iter().enumerate() { + list.set(i as u32, *seq); + } + + tracing::info!( + recipient_count = recipient_keys.len(), + "federation: relayed batch_enqueue" + ); + + Promise::ok(()) + } + + fn proxy_fetch_key_package( + &mut self, + params: federation_service::ProxyFetchKeyPackageParams, + mut results: federation_service::ProxyFetchKeyPackageResults, + ) -> Promise<(), capnp::Error> { + let identity_key = match params.get().and_then(|p| p.get_identity_key()) { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))), + }; + + match self.store.fetch_key_package(&identity_key) { + Ok(Some(pkg)) => results.get().set_package(&pkg), + Ok(None) => results.get().set_package(&[]), + Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))), + } + + Promise::ok(()) + } + + fn proxy_fetch_hybrid_key( + &mut self, + params: federation_service::ProxyFetchHybridKeyParams, + mut results: federation_service::ProxyFetchHybridKeyResults, + ) -> Promise<(), capnp::Error> { + let identity_key = match params.get().and_then(|p| p.get_identity_key()) { + Ok(v) => v.to_vec(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))), + }; + + match self.store.fetch_hybrid_key(&identity_key) { + Ok(Some(pk)) => results.get().set_hybrid_public_key(&pk), + Ok(None) => results.get().set_hybrid_public_key(&[]), + Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))), + } + + Promise::ok(()) + } + + fn proxy_resolve_user( + &mut self, + params: federation_service::ProxyResolveUserParams, + mut results: federation_service::ProxyResolveUserResults, + ) -> Promise<(), capnp::Error> { + let username = match params.get().and_then(|p| p.get_username()) { + Ok(u) => match u.to_str() { + Ok(s) => s.to_string(), + Err(e) => return Promise::err(capnp::Error::failed(format!("bad utf-8: {e}"))), + }, + Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))), + }; + + match self.store.get_user_identity_key(&username) { + Ok(Some(key)) => results.get().set_identity_key(&key), + Ok(None) => results.get().set_identity_key(&[]), + Err(e) => return Promise::err(capnp::Error::failed(format!("store error: {e}"))), + } + + Promise::ok(()) + } + + fn federation_health( + &mut self, + _params: federation_service::FederationHealthParams, + mut results: federation_service::FederationHealthResults, + ) -> Promise<(), capnp::Error> { + results.get().set_status("ok"); + results.get().set_server_domain(&self.local_domain); + Promise::ok(()) + } +} diff --git a/crates/quicproquo-server/src/federation/tls.rs b/crates/quicproquo-server/src/federation/tls.rs new file mode 100644 index 0000000..94d1dd7 --- /dev/null +++ b/crates/quicproquo-server/src/federation/tls.rs @@ -0,0 +1,85 @@ +//! Build mTLS server/client configs for the federation endpoint. +//! +//! Federation uses a separate CA from the public-facing QUIC endpoint. +//! Both server and client present certificates; the server verifies the client +//! cert is signed by the federation CA. + +use std::path::Path; +use std::sync::Arc; + +use anyhow::Context; +use quinn::ServerConfig; +use quinn_proto::crypto::rustls::QuicServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use rustls::version::TLS13; + +/// Build a QUIC server config for the federation listener with mutual TLS. +/// +/// `cert`/`key`: this server's federation certificate and private key. +/// `ca`: the federation CA certificate used to verify peer certificates. +pub fn build_federation_server_config( + cert_path: &Path, + key_path: &Path, + ca_path: &Path, +) -> anyhow::Result { + let cert_bytes = std::fs::read(cert_path) + .with_context(|| format!("read federation cert: {:?}", cert_path))?; + let key_bytes = std::fs::read(key_path) + .with_context(|| format!("read federation key: {:?}", key_path))?; + let ca_bytes = std::fs::read(ca_path) + .with_context(|| format!("read federation CA: {:?}", ca_path))?; + + let cert_chain = vec![CertificateDer::from(cert_bytes)]; + let key = PrivateKeyDer::try_from(key_bytes) + .map_err(|_| anyhow::anyhow!("invalid federation private key"))?; + + // Build a root cert store with the federation CA for client verification. + let mut root_store = rustls::RootCertStore::empty(); + root_store + .add(CertificateDer::from(ca_bytes)) + .context("add federation CA to root store")?; + + let client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) + .build() + .context("build client cert verifier")?; + + let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13]) + .with_client_cert_verifier(client_verifier) + .with_single_cert(cert_chain, key)?; + tls.alpn_protocols = vec![b"qnpc-fed".to_vec()]; + + let crypto = QuicServerConfig::try_from(tls) + .map_err(|e| anyhow::anyhow!("invalid federation server TLS config: {e}"))?; + + Ok(ServerConfig::with_crypto(Arc::new(crypto))) +} + +/// Build a QUIC client config for connecting to a federation peer with mutual TLS. +pub fn build_federation_client_config( + cert_path: &Path, + key_path: &Path, + ca_path: &Path, +) -> anyhow::Result { + let cert_bytes = std::fs::read(cert_path) + .with_context(|| format!("read federation cert: {:?}", cert_path))?; + let key_bytes = std::fs::read(key_path) + .with_context(|| format!("read federation key: {:?}", key_path))?; + let ca_bytes = std::fs::read(ca_path) + .with_context(|| format!("read federation CA: {:?}", ca_path))?; + + let cert_chain = vec![CertificateDer::from(cert_bytes)]; + let key = PrivateKeyDer::try_from(key_bytes) + .map_err(|_| anyhow::anyhow!("invalid federation client private key"))?; + + let mut root_store = rustls::RootCertStore::empty(); + root_store + .add(CertificateDer::from(ca_bytes)) + .context("add federation CA to root store")?; + + let tls = rustls::ClientConfig::builder_with_protocol_versions(&[&TLS13]) + .with_root_certificates(root_store) + .with_client_auth_cert(cert_chain, key) + .context("set client auth cert")?; + + Ok(tls) +} diff --git a/crates/quicnprotochat-server/src/main.rs b/crates/quicproquo-server/src/main.rs similarity index 53% rename from crates/quicnprotochat-server/src/main.rs rename to crates/quicproquo-server/src/main.rs index 7336f40..21fac18 100644 --- a/crates/quicnprotochat-server/src/main.rs +++ b/crates/quicproquo-server/src/main.rs @@ -1,4 +1,4 @@ -//! quicnprotochat-server — unified Authentication + Delivery service. +//! qpq-server — unified Authentication + Delivery service. //! //! The server hosts Authentication + Delivery services over QUIC + Cap'n Proto. @@ -8,7 +8,7 @@ use anyhow::Context; use clap::Parser; use dashmap::DashMap; use opaque_ke::ServerSetup; -use quicnprotochat_core::opaque_auth::OpaqueSuite; +use quicproquo_core::opaque_auth::OpaqueSuite; use quinn::Endpoint; use rand::rngs::OsRng; use tokio::sync::Notify; @@ -17,6 +17,7 @@ use tokio::task::LocalSet; mod auth; mod config; mod error_codes; +mod federation; mod metrics; mod node_service; mod sql_store; @@ -37,62 +38,74 @@ use tls::build_server_config; #[derive(Debug, Parser)] #[command( - name = "quicnprotochat-server", - about = "quicnprotochat Delivery Service + Authentication Service", + name = "qpq-server", + about = "quicproquo Delivery Service + Authentication Service", version )] struct Args { /// Optional path to a TOML config file (fields map to CLI flags). - #[arg(long, env = "QUICNPROTOCHAT_CONFIG")] + #[arg(long, env = "QPQ_CONFIG")] config: Option, /// QUIC listen address (host:port). - #[arg(long, default_value = DEFAULT_LISTEN, env = "QUICNPROTOCHAT_LISTEN")] + #[arg(long, default_value = DEFAULT_LISTEN, env = "QPQ_LISTEN")] listen: String, /// Directory for persisted server data (KeyPackages + delivery queues). - #[arg(long, default_value = DEFAULT_DATA_DIR, env = "QUICNPROTOCHAT_DATA_DIR")] + #[arg(long, default_value = DEFAULT_DATA_DIR, env = "QPQ_DATA_DIR")] data_dir: String, /// TLS certificate path (generated automatically if missing). - #[arg(long, default_value = DEFAULT_TLS_CERT, env = "QUICNPROTOCHAT_TLS_CERT")] + #[arg(long, default_value = DEFAULT_TLS_CERT, env = "QPQ_TLS_CERT")] tls_cert: PathBuf, /// TLS private key path (generated automatically if missing). - #[arg(long, default_value = DEFAULT_TLS_KEY, env = "QUICNPROTOCHAT_TLS_KEY")] + #[arg(long, default_value = DEFAULT_TLS_KEY, env = "QPQ_TLS_KEY")] tls_key: PathBuf, /// Required bearer token for auth.version=1 requests. Use --allow-insecure-auth to run without it (dev only). - #[arg(long, env = "QUICNPROTOCHAT_AUTH_TOKEN")] + #[arg(long, env = "QPQ_AUTH_TOKEN")] auth_token: Option, - /// Allow running without QUICNPROTOCHAT_AUTH_TOKEN (development only). - #[arg(long, env = "QUICNPROTOCHAT_ALLOW_INSECURE_AUTH", default_value_t = false)] + /// Allow running without QPQ_AUTH_TOKEN (development only). + #[arg(long, env = "QPQ_ALLOW_INSECURE_AUTH", default_value_t = false)] allow_insecure_auth: bool, /// Enable Sealed Sender: enqueue does not require identity-bound session, only a valid token. - #[arg(long, env = "QUICNPROTOCHAT_SEALED_SENDER", default_value_t = false)] + #[arg(long, env = "QPQ_SEALED_SENDER", default_value_t = false)] sealed_sender: bool, /// Storage backend: "file" (bincode) or "sql" (SQLCipher-encrypted). - #[arg(long, default_value = DEFAULT_STORE_BACKEND, env = "QUICNPROTOCHAT_STORE_BACKEND")] + #[arg(long, default_value = DEFAULT_STORE_BACKEND, env = "QPQ_STORE_BACKEND")] store_backend: String, /// Path to the SQLCipher database file (only used when --store-backend=sql). - #[arg(long, default_value = DEFAULT_DB_PATH, env = "QUICNPROTOCHAT_DB_PATH")] + #[arg(long, default_value = DEFAULT_DB_PATH, env = "QPQ_DB_PATH")] db_path: PathBuf, /// SQLCipher encryption key. Empty string disables encryption. - #[arg(long, default_value = "", env = "QUICNPROTOCHAT_DB_KEY")] + #[arg(long, default_value = "", env = "QPQ_DB_KEY")] db_key: String, /// Metrics HTTP listen address (e.g. 0.0.0.0:9090). If set and metrics enabled, /metrics is served. - #[arg(long, env = "QUICNPROTOCHAT_METRICS_LISTEN")] + #[arg(long, env = "QPQ_METRICS_LISTEN")] metrics_listen: Option, /// Enable metrics server when metrics_listen is set. - #[arg(long, env = "QUICNPROTOCHAT_METRICS_ENABLED")] + #[arg(long, env = "QPQ_METRICS_ENABLED")] metrics_enabled: Option, + + /// Enable federation (server-to-server message relay). + #[arg(long, env = "QPQ_FEDERATION_ENABLED", default_value_t = false)] + federation_enabled: bool, + + /// This server's domain for federation addressing (e.g. "chat.example.com"). + #[arg(long, env = "QPQ_FEDERATION_DOMAIN")] + federation_domain: Option, + + /// Federation QUIC listen address (default: 0.0.0.0:7001). + #[arg(long, env = "QPQ_FEDERATION_LISTEN")] + federation_listen: Option, } // ── Entry point ─────────────────────────────────────────────────────────────── @@ -112,7 +125,7 @@ async fn main() -> anyhow::Result<()> { let file_cfg = load_config(args.config.as_deref())?; let effective = merge_config(&args, &file_cfg); - let production = std::env::var("QUICNPROTOCHAT_PRODUCTION") + let production = std::env::var("QPQ_PRODUCTION") .map(|v| matches!(v.to_lowercase().as_str(), "1" | "true" | "yes")) .unwrap_or(false); if production { @@ -143,7 +156,7 @@ async fn main() -> anyhow::Result<()> { && !effective.allow_insecure_auth { anyhow::bail!( - "missing QUICNPROTOCHAT_AUTH_TOKEN; set one or pass --allow-insecure-auth for development" + "missing QPQ_AUTH_TOKEN; set one or pass --allow-insecure-auth for development" ); } @@ -154,7 +167,7 @@ async fn main() -> anyhow::Result<()> { .map(|s| s.is_empty()) .unwrap_or(true) { - tracing::warn!("running without QUICNPROTOCHAT_AUTH_TOKEN (allow-insecure-auth enabled); development only"); + tracing::warn!("running without QPQ_AUTH_TOKEN (allow-insecure-auth enabled); development only"); } let listen: SocketAddr = effective @@ -246,10 +259,174 @@ async fn main() -> anyhow::Result<()> { "accepting QUIC connections" ); + // ── Federation setup ───────────────────────────────────────────────────── + let federation_client: Option> = + if let Some(fed_cfg) = &effective.federation { + tracing::info!( + domain = %fed_cfg.domain, + listen = %fed_cfg.listen, + peers = fed_cfg.peers.len(), + "federation enabled" + ); + + // Build a client endpoint for outbound federation connections. + // For now we create a simple endpoint; full mTLS is used when certs are provided. + let client_config = if fed_cfg.federation_cert.exists() + && fed_cfg.federation_key.exists() + && fed_cfg.federation_ca.exists() + { + let tls_cfg = federation::tls::build_federation_client_config( + &fed_cfg.federation_cert, + &fed_cfg.federation_key, + &fed_cfg.federation_ca, + ) + .context("build federation client TLS config")?; + + let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(tls_cfg) + .map_err(|e| anyhow::anyhow!("invalid federation client QUIC config: {e}"))?; + let mut qc = quinn::ClientConfig::new(Arc::new(crypto)); + let mut transport = quinn::TransportConfig::default(); + transport.max_idle_timeout(Some( + std::time::Duration::from_secs(120) + .try_into() + .expect("120s is valid"), + )); + qc.transport_config(Arc::new(transport)); + Some(qc) + } else { + tracing::warn!("federation cert/key/CA not found; outbound federation connections will fail"); + None + }; + + let fed_bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + let mut fed_endpoint = Endpoint::client(fed_bind) + .context("create federation client endpoint")?; + if let Some(cc) = client_config { + fed_endpoint.set_default_client_config(cc); + } + + let client = federation::FederationClient::new(fed_cfg, fed_endpoint) + .context("create federation client")?; + + // Register configured peers in storage. + for peer in &fed_cfg.peers { + if let Err(e) = store.upsert_federation_peer(&peer.domain, true) { + tracing::warn!(domain = %peer.domain, error = %e, "failed to register federation peer"); + } + } + + Some(Arc::new(client)) + } else { + None + }; + + let local_domain: Option = effective.federation.as_ref().map(|f| f.domain.clone()); + + // ── Federation listener ────────────────────────────────────────────────── + let federation_endpoint: Option = + if let Some(fed_cfg) = &effective.federation { + if fed_cfg.federation_cert.exists() + && fed_cfg.federation_key.exists() + && fed_cfg.federation_ca.exists() + { + let fed_server_config = federation::tls::build_federation_server_config( + &fed_cfg.federation_cert, + &fed_cfg.federation_key, + &fed_cfg.federation_ca, + ) + .context("build federation server TLS config")?; + + let fed_listen: SocketAddr = fed_cfg + .listen + .parse() + .context("federation listen must be host:port")?; + + let ep = Endpoint::server(fed_server_config, fed_listen) + .context("bind federation QUIC endpoint")?; + + tracing::info!(addr = %fed_cfg.listen, "federation endpoint listening"); + Some(ep) + } else { + tracing::warn!("federation certs not found; federation listener not started"); + None + } + } else { + None + }; + // capnp-rpc is !Send (Rc internals), so all RPC tasks must stay on a LocalSet. let local = LocalSet::new(); local .run_until(async move { + // Spawn federation acceptor if enabled. + if let Some(fed_ep) = federation_endpoint { + let fed_store = Arc::clone(&store); + let fed_waiters = Arc::clone(&waiters); + let fed_domain = local_domain.clone().unwrap_or_default(); + tokio::task::spawn_local(async move { + loop { + let incoming = match fed_ep.accept().await { + Some(i) => i, + None => break, + }; + let connecting = match incoming.accept() { + Ok(c) => c, + Err(e) => { + tracing::warn!(error = %e, "federation: accept error"); + continue; + } + }; + let store = Arc::clone(&fed_store); + let waiters = Arc::clone(&fed_waiters); + let domain = fed_domain.clone(); + tokio::task::spawn_local(async move { + match connecting.await { + Ok(conn) => { + tracing::info!( + peer = %conn.remote_address(), + "federation: peer connected" + ); + let (send, recv) = match conn.accept_bi().await { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "federation: accept bi error"); + return; + } + }; + let reader = tokio_util::compat::TokioAsyncReadCompatExt::compat(recv); + let writer = tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(send); + + let network = capnp_rpc::twoparty::VatNetwork::new( + reader, + writer, + capnp_rpc::rpc_twoparty_capnp::Side::Server, + Default::default(), + ); + + let service_impl = federation::service::FederationServiceImpl { + store, + waiters, + local_domain: domain, + }; + let client: quicproquo_proto::federation_capnp::federation_service::Client = + capnp_rpc::new_client(service_impl); + + if let Err(e) = capnp_rpc::RpcSystem::new( + Box::new(network), + Some(client.client), + ).await { + tracing::warn!(error = %e, "federation: RPC error"); + } + } + Err(e) => { + tracing::warn!(error = %e, "federation: connection error"); + } + } + }); + } + }); + } + loop { tokio::select! { biased; @@ -284,6 +461,8 @@ async fn main() -> anyhow::Result<()> { let sessions = Arc::clone(&sessions); let rate_limits = Arc::clone(&rate_limits); let sealed_sender = effective.sealed_sender; + let fed_client = federation_client.clone(); + let local_dom = local_domain.clone(); tokio::task::spawn_local(async move { if let Err(e) = handle_node_connection( @@ -296,6 +475,8 @@ async fn main() -> anyhow::Result<()> { sessions, rate_limits, sealed_sender, + fed_client, + local_dom, ) .await { diff --git a/crates/quicnprotochat-server/src/metrics.rs b/crates/quicproquo-server/src/metrics.rs similarity index 100% rename from crates/quicnprotochat-server/src/metrics.rs rename to crates/quicproquo-server/src/metrics.rs diff --git a/crates/quicnprotochat-server/src/node_service/auth_ops.rs b/crates/quicproquo-server/src/node_service/auth_ops.rs similarity index 99% rename from crates/quicnprotochat-server/src/node_service/auth_ops.rs rename to crates/quicproquo-server/src/node_service/auth_ops.rs index 7d3b99a..3d16b49 100644 --- a/crates/quicnprotochat-server/src/node_service/auth_ops.rs +++ b/crates/quicproquo-server/src/node_service/auth_ops.rs @@ -3,8 +3,8 @@ use opaque_ke::{ CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload, ServerLogin, ServerRegistration, }; -use quicnprotochat_core::opaque_auth::OpaqueSuite; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_core::opaque_auth::OpaqueSuite; +use quicproquo_proto::node_capnp::node_service; use crate::auth::{coded_error, current_timestamp, PendingLogin, SESSION_TTL_SECS}; use crate::error_codes::*; diff --git a/crates/quicnprotochat-server/src/node_service/channel_ops.rs b/crates/quicproquo-server/src/node_service/channel_ops.rs similarity index 97% rename from crates/quicnprotochat-server/src/node_service/channel_ops.rs rename to crates/quicproquo-server/src/node_service/channel_ops.rs index 51bff1e..419ca5f 100644 --- a/crates/quicnprotochat-server/src/node_service/channel_ops.rs +++ b/crates/quicproquo-server/src/node_service/channel_ops.rs @@ -1,7 +1,7 @@ //! createChannel RPC: create or look up a 1:1 DM channel. use capnp::capability::Promise; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use crate::auth::{coded_error, require_identity, validate_auth_context}; use crate::error_codes::*; diff --git a/crates/quicnprotochat-server/src/node_service/delivery.rs b/crates/quicproquo-server/src/node_service/delivery.rs similarity index 99% rename from crates/quicnprotochat-server/src/node_service/delivery.rs rename to crates/quicproquo-server/src/node_service/delivery.rs index 4d17dbb..183e26f 100644 --- a/crates/quicnprotochat-server/src/node_service/delivery.rs +++ b/crates/quicproquo-server/src/node_service/delivery.rs @@ -3,7 +3,7 @@ use std::time::Duration; use capnp::capability::Promise; use dashmap::DashMap; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use tokio::sync::Notify; use tokio::time::timeout; diff --git a/crates/quicnprotochat-server/src/node_service/key_ops.rs b/crates/quicproquo-server/src/node_service/key_ops.rs similarity index 98% rename from crates/quicnprotochat-server/src/node_service/key_ops.rs rename to crates/quicproquo-server/src/node_service/key_ops.rs index 8ff65c5..e8cf3b3 100644 --- a/crates/quicnprotochat-server/src/node_service/key_ops.rs +++ b/crates/quicproquo-server/src/node_service/key_ops.rs @@ -1,5 +1,5 @@ use capnp::capability::Promise; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use crate::auth::{coded_error, fmt_hex, require_identity_or_request, validate_auth_context}; use crate::error_codes::*; @@ -63,7 +63,7 @@ impl NodeServiceImpl { return Promise::err(e); } - if let Err(e) = quicnprotochat_core::validate_keypackage_ciphersuite(&package) { + if let Err(e) = quicproquo_core::validate_keypackage_ciphersuite(&package) { return Promise::err(coded_error( E021_CIPHERSUITE_NOT_ALLOWED, format!("KeyPackage ciphersuite not allowed: {e}"), diff --git a/crates/quicnprotochat-server/src/node_service/mod.rs b/crates/quicproquo-server/src/node_service/mod.rs similarity index 94% rename from crates/quicnprotochat-server/src/node_service/mod.rs rename to crates/quicproquo-server/src/node_service/mod.rs index 3b83e7d..aad5229 100644 --- a/crates/quicnprotochat-server/src/node_service/mod.rs +++ b/crates/quicproquo-server/src/node_service/mod.rs @@ -4,8 +4,8 @@ use std::time::Duration; use capnp_rpc::RpcSystem; use dashmap::DashMap; use opaque_ke::ServerSetup; -use quicnprotochat_core::opaque_auth::OpaqueSuite; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_core::opaque_auth::OpaqueSuite; +use quicproquo_proto::node_capnp::node_service; use tokio::sync::Notify; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; @@ -207,6 +207,10 @@ pub struct NodeServiceImpl { pub rate_limits: Arc, RateEntry>>, /// When true, enqueue does not require identity-bound session (Sealed Sender). pub sealed_sender: bool, + /// Outbound federation client for relaying to remote servers (None if federation disabled). + pub federation_client: Option>, + /// This server's federation domain (empty if federation disabled). + pub local_domain: Option, } impl NodeServiceImpl { @@ -219,6 +223,8 @@ impl NodeServiceImpl { sessions: Arc, SessionInfo>>, rate_limits: Arc, RateEntry>>, sealed_sender: bool, + federation_client: Option>, + local_domain: Option, ) -> Self { Self { store, @@ -229,6 +235,8 @@ impl NodeServiceImpl { sessions, rate_limits, sealed_sender, + federation_client, + local_domain, } } } @@ -243,6 +251,8 @@ pub async fn handle_node_connection( sessions: Arc, SessionInfo>>, rate_limits: Arc, RateEntry>>, sealed_sender: bool, + federation_client: Option>, + local_domain: Option, ) -> Result<(), anyhow::Error> { let connection = connecting.await?; @@ -272,6 +282,8 @@ pub async fn handle_node_connection( sessions, rate_limits, sealed_sender, + federation_client, + local_domain, )); RpcSystem::new(Box::new(network), Some(service.client)) diff --git a/crates/quicnprotochat-server/src/node_service/p2p_ops.rs b/crates/quicproquo-server/src/node_service/p2p_ops.rs similarity index 98% rename from crates/quicnprotochat-server/src/node_service/p2p_ops.rs rename to crates/quicproquo-server/src/node_service/p2p_ops.rs index 52978bf..8007860 100644 --- a/crates/quicnprotochat-server/src/node_service/p2p_ops.rs +++ b/crates/quicproquo-server/src/node_service/p2p_ops.rs @@ -1,5 +1,5 @@ use capnp::capability::Promise; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use crate::auth::{ coded_error, fmt_hex, require_identity_or_request, validate_auth, validate_auth_context, diff --git a/crates/quicnprotochat-server/src/node_service/user_ops.rs b/crates/quicproquo-server/src/node_service/user_ops.rs similarity index 63% rename from crates/quicnprotochat-server/src/node_service/user_ops.rs rename to crates/quicproquo-server/src/node_service/user_ops.rs index 256109e..dce63b4 100644 --- a/crates/quicnprotochat-server/src/node_service/user_ops.rs +++ b/crates/quicproquo-server/src/node_service/user_ops.rs @@ -1,7 +1,7 @@ //! resolveUser / resolveIdentity RPCs: bidirectional username ↔ identity key lookup. use capnp::capability::Promise; -use quicnprotochat_proto::node_capnp::node_service; +use quicproquo_proto::node_capnp::node_service; use crate::auth::{coded_error, validate_auth_context}; use crate::error_codes::*; @@ -41,7 +41,44 @@ impl NodeServiceImpl { return Promise::err(coded_error(E020_BAD_PARAMS, "username must not be empty")); } - match self.store.get_user_identity_key(username_str) { + // Federation: parse user@domain format. + let addr = crate::federation::address::FederatedAddress::parse(username_str); + let is_remote = match (&addr.domain, &self.local_domain) { + (Some(d), Some(ld)) => d != ld, + (Some(_), None) => true, + _ => false, + }; + + if is_remote { + // Proxy to remote server via federation. + if let (Some(ref fed_client), Some(ref domain)) = (&self.federation_client, &addr.domain) { + if fed_client.has_peer(domain) { + let fed = fed_client.clone(); + let user = addr.username.clone(); + let dom = domain.clone(); + return Promise::from_future(async move { + match fed.proxy_resolve_user(&dom, &user).await { + Ok(Some(key)) => { + results.get().set_identity_key(&key); + } + Ok(None) => { + // Not found on remote — return empty. + } + Err(e) => { + tracing::warn!(error = %e, "federation proxy_resolve_user failed"); + // Fall through — return empty (not found). + } + } + Ok(()) + }); + } + } + // No federation client or unknown peer — return empty (not found). + return Promise::ok(()); + } + + // Local resolution. + match self.store.get_user_identity_key(&addr.username) { Ok(Some(key)) => { results.get().set_identity_key(&key); } diff --git a/crates/quicnprotochat-server/src/sql_store.rs b/crates/quicproquo-server/src/sql_store.rs similarity index 90% rename from crates/quicnprotochat-server/src/sql_store.rs rename to crates/quicproquo-server/src/sql_store.rs index 8722b33..3151c36 100644 --- a/crates/quicnprotochat-server/src/sql_store.rs +++ b/crates/quicproquo-server/src/sql_store.rs @@ -9,13 +9,14 @@ use rusqlite::{params, Connection}; use crate::storage::{StorageError, Store}; /// Schema version after introducing the migration runner (existing DBs had 1). -const SCHEMA_VERSION: i32 = 4; +const SCHEMA_VERSION: i32 = 5; /// Migrations: (migration_number, SQL). Files named NNN_name.sql, applied in order when N > user_version. const MIGRATIONS: &[(i32, &str)] = &[ (1, include_str!("../migrations/001_initial.sql")), (3, include_str!("../migrations/002_add_seq.sql")), (4, include_str!("../migrations/003_channels.sql")), + (5, include_str!("../migrations/004_federation.sql")), ]; /// Runs pending migrations on an open connection: applies any migration whose number is greater @@ -494,6 +495,71 @@ impl Store for SqlStore { .optional() .map_err(|e| StorageError::Db(e.to_string())) } + + fn store_identity_home_server( + &self, + identity_key: &[u8], + home_server: &str, + ) -> Result<(), StorageError> { + let conn = self.lock_conn()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + conn.execute( + "INSERT OR REPLACE INTO identity_home_servers (identity_key, home_server, updated_at) VALUES (?1, ?2, ?3)", + params![identity_key, home_server, now], + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(()) + } + + fn get_identity_home_server( + &self, + identity_key: &[u8], + ) -> Result, StorageError> { + let conn = self.lock_conn()?; + let mut stmt = conn + .prepare("SELECT home_server FROM identity_home_servers WHERE identity_key = ?1") + .map_err(|e| StorageError::Db(e.to_string()))?; + stmt.query_row(params![identity_key], |row| row.get(0)) + .optional() + .map_err(|e| StorageError::Db(e.to_string())) + } + + fn upsert_federation_peer( + &self, + domain: &str, + is_active: bool, + ) -> Result<(), StorageError> { + let conn = self.lock_conn()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + conn.execute( + "INSERT INTO federation_peers (domain, last_seen, is_active) VALUES (?1, ?2, ?3) + ON CONFLICT(domain) DO UPDATE SET last_seen = ?2, is_active = ?3", + params![domain, now, is_active as i32], + ) + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(()) + } + + fn list_federation_peers(&self) -> Result, StorageError> { + let conn = self.lock_conn()?; + let mut stmt = conn + .prepare("SELECT domain, is_active FROM federation_peers WHERE is_active = 1") + .map_err(|e| StorageError::Db(e.to_string()))?; + let rows = stmt + .query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)? != 0)) + }) + .map_err(|e| StorageError::Db(e.to_string()))? + .collect::, _>>() + .map_err(|e| StorageError::Db(e.to_string()))?; + Ok(rows) + } } /// Convenience extension for `rusqlite::OptionalExtension`. diff --git a/crates/quicnprotochat-server/src/storage.rs b/crates/quicproquo-server/src/storage.rs similarity index 94% rename from crates/quicnprotochat-server/src/storage.rs rename to crates/quicproquo-server/src/storage.rs index 3784e85..4d67b18 100644 --- a/crates/quicnprotochat-server/src/storage.rs +++ b/crates/quicproquo-server/src/storage.rs @@ -133,6 +133,31 @@ pub trait Store: Send + Sync { /// Get the two members of a channel by channel_id (16 bytes). Returns (member_a, member_b) in sorted order. fn get_channel_members(&self, channel_id: &[u8]) -> Result, Vec)>, StorageError>; + + // ── Federation ────────────────────────────────────────────────────────── + + /// Store the home server domain for an identity key. + fn store_identity_home_server( + &self, + identity_key: &[u8], + home_server: &str, + ) -> Result<(), StorageError>; + + /// Get the home server domain for an identity key. + fn get_identity_home_server( + &self, + identity_key: &[u8], + ) -> Result, StorageError>; + + /// Insert or update a federation peer. + fn upsert_federation_peer( + &self, + domain: &str, + is_active: bool, + ) -> Result<(), StorageError>; + + /// List all active federation peers. + fn list_federation_peers(&self) -> Result, StorageError>; } // ── ChannelKey ─────────────────────────────────────────────────────────────── @@ -644,6 +669,34 @@ impl Store for FileBackedStore { let map = lock(&self.channels)?; Ok(map.get(channel_id).cloned()) } + + fn store_identity_home_server( + &self, + _identity_key: &[u8], + _home_server: &str, + ) -> Result<(), StorageError> { + // File-backed store: federation mappings are ephemeral (in-memory only). + Ok(()) + } + + fn get_identity_home_server( + &self, + _identity_key: &[u8], + ) -> Result, StorageError> { + Ok(None) + } + + fn upsert_federation_peer( + &self, + _domain: &str, + _is_active: bool, + ) -> Result<(), StorageError> { + Ok(()) + } + + fn list_federation_peers(&self) -> Result, StorageError> { + Ok(vec![]) + } } #[cfg(test)] diff --git a/crates/quicnprotochat-server/src/tls.rs b/crates/quicproquo-server/src/tls.rs similarity index 100% rename from crates/quicnprotochat-server/src/tls.rs rename to crates/quicproquo-server/src/tls.rs diff --git a/docker-compose.yml b/docker-compose.yml index eb94c94..c48e146 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - "7000:7000" environment: RUST_LOG: "info" - QUICNPROTOCHAT_LISTEN: "0.0.0.0:7000" + QPQ_LISTEN: "0.0.0.0:7000" # Healthcheck: attempt a TCP connection to port 7000. # Uses bash /dev/tcp — available in debian:bookworm-slim without extra packages. healthcheck: diff --git a/docker/Dockerfile b/docker/Dockerfile index 941c253..a3c0d2e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,44 +12,44 @@ WORKDIR /build # Copy manifests first so dependency layers are cached independently of source. COPY Cargo.toml Cargo.lock ./ -COPY crates/quicnprotochat-core/Cargo.toml crates/quicnprotochat-core/Cargo.toml -COPY crates/quicnprotochat-proto/Cargo.toml crates/quicnprotochat-proto/Cargo.toml -COPY crates/quicnprotochat-server/Cargo.toml crates/quicnprotochat-server/Cargo.toml -COPY crates/quicnprotochat-client/Cargo.toml crates/quicnprotochat-client/Cargo.toml -COPY crates/quicnprotochat-p2p/Cargo.toml crates/quicnprotochat-p2p/Cargo.toml +COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml +COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml +COPY crates/qpq-server/Cargo.toml crates/qpq-server/Cargo.toml +COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml +COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml # Create dummy source files so `cargo build` can resolve the dependency graph # and cache the compiled dependencies before copying real source. RUN mkdir -p \ - crates/quicnprotochat-core/src \ - crates/quicnprotochat-proto/src \ - crates/quicnprotochat-server/src \ - crates/quicnprotochat-client/src \ - crates/quicnprotochat-p2p/src \ - && echo 'fn main() {}' > crates/quicnprotochat-server/src/main.rs \ - && echo 'fn main() {}' > crates/quicnprotochat-client/src/main.rs \ - && touch crates/quicnprotochat-core/src/lib.rs \ - && touch crates/quicnprotochat-proto/src/lib.rs \ - && touch crates/quicnprotochat-p2p/src/lib.rs + crates/quicproquo-core/src \ + crates/quicproquo-proto/src \ + crates/qpq-server/src \ + crates/quicproquo-client/src \ + crates/quicproquo-p2p/src \ + && echo 'fn main() {}' > crates/qpq-server/src/main.rs \ + && echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \ + && touch crates/quicproquo-core/src/lib.rs \ + && touch crates/quicproquo-proto/src/lib.rs \ + && touch crates/quicproquo-p2p/src/lib.rs # Schemas must exist before the proto crate's build.rs runs. COPY schemas/ schemas/ # Build dependencies only (source stubs mean this layer is cache-friendly). -RUN cargo build --release --bin quicnprotochat-server 2>/dev/null || true +RUN cargo build --release --bin qpq-server 2>/dev/null || true # Copy real source and build for real. COPY crates/ crates/ # Touch source to force re-compilation after copying real crates. RUN touch \ - crates/quicnprotochat-core/src/lib.rs \ - crates/quicnprotochat-proto/src/lib.rs \ - crates/quicnprotochat-p2p/src/lib.rs \ - crates/quicnprotochat-server/src/main.rs \ - crates/quicnprotochat-client/src/main.rs + crates/quicproquo-core/src/lib.rs \ + crates/quicproquo-proto/src/lib.rs \ + crates/quicproquo-p2p/src/lib.rs \ + crates/qpq-server/src/main.rs \ + crates/quicproquo-client/src/main.rs -RUN cargo build --release --bin quicnprotochat-server +RUN cargo build --release --bin qpq-server # ── Stage 2: Runtime ────────────────────────────────────────────────────────── # @@ -62,14 +62,14 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /build/target/release/quicnprotochat-server /usr/local/bin/quicnprotochat-server +COPY --from=builder /build/target/release/qpq-server /usr/local/bin/qpq-server EXPOSE 7000 ENV RUST_LOG=info \ - QUICNPROTOCHAT_LISTEN=0.0.0.0:7000 + QPQ_LISTEN=0.0.0.0:7000 # Run as a non-root user. USER nobody -CMD ["quicnprotochat-server"] +CMD ["qpq-server"] diff --git a/docker/Dockerfile.chat-test b/docker/Dockerfile.chat-test index a39c279..4470b41 100644 --- a/docker/Dockerfile.chat-test +++ b/docker/Dockerfile.chat-test @@ -12,45 +12,45 @@ WORKDIR /build # Copy manifests first so dependency layers are cached independently of source. COPY Cargo.toml Cargo.lock ./ -COPY crates/quicnprotochat-core/Cargo.toml crates/quicnprotochat-core/Cargo.toml -COPY crates/quicnprotochat-proto/Cargo.toml crates/quicnprotochat-proto/Cargo.toml -COPY crates/quicnprotochat-server/Cargo.toml crates/quicnprotochat-server/Cargo.toml -COPY crates/quicnprotochat-client/Cargo.toml crates/quicnprotochat-client/Cargo.toml -COPY crates/quicnprotochat-p2p/Cargo.toml crates/quicnprotochat-p2p/Cargo.toml +COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml +COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml +COPY crates/qpq-server/Cargo.toml crates/qpq-server/Cargo.toml +COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml +COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml # Create dummy source files so `cargo build` can resolve the dependency graph # and cache the compiled dependencies before copying real source. RUN mkdir -p \ - crates/quicnprotochat-core/src \ - crates/quicnprotochat-proto/src \ - crates/quicnprotochat-server/src \ - crates/quicnprotochat-client/src \ - crates/quicnprotochat-p2p/src \ - && echo 'fn main() {}' > crates/quicnprotochat-server/src/main.rs \ - && echo 'fn main() {}' > crates/quicnprotochat-client/src/main.rs \ - && touch crates/quicnprotochat-core/src/lib.rs \ - && touch crates/quicnprotochat-proto/src/lib.rs \ - && touch crates/quicnprotochat-p2p/src/lib.rs + crates/quicproquo-core/src \ + crates/quicproquo-proto/src \ + crates/qpq-server/src \ + crates/quicproquo-client/src \ + crates/quicproquo-p2p/src \ + && echo 'fn main() {}' > crates/qpq-server/src/main.rs \ + && echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \ + && touch crates/quicproquo-core/src/lib.rs \ + && touch crates/quicproquo-proto/src/lib.rs \ + && touch crates/quicproquo-p2p/src/lib.rs # Schemas must exist before the proto crate's build.rs runs. COPY schemas/ schemas/ # Build dependencies only (source stubs mean this layer is cache-friendly). # The GUI crate is not included, so workspace resolution may fail — || true handles it. -RUN cargo build --release --bin quicnprotochat-server --bin quicnprotochat 2>/dev/null || true +RUN cargo build --release --bin qpq-server --bin qpq 2>/dev/null || true # Copy real source and build for real. COPY crates/ crates/ # Touch source to force re-compilation after copying real crates. RUN touch \ - crates/quicnprotochat-core/src/lib.rs \ - crates/quicnprotochat-proto/src/lib.rs \ - crates/quicnprotochat-p2p/src/lib.rs \ - crates/quicnprotochat-server/src/main.rs \ - crates/quicnprotochat-client/src/main.rs + crates/quicproquo-core/src/lib.rs \ + crates/quicproquo-proto/src/lib.rs \ + crates/quicproquo-p2p/src/lib.rs \ + crates/qpq-server/src/main.rs \ + crates/quicproquo-client/src/main.rs -RUN cargo build --release --bin quicnprotochat-server --bin quicnprotochat +RUN cargo build --release --bin qpq-server --bin qpq # ── Stage 2: Runtime ────────────────────────────────────────────────────────── # @@ -61,8 +61,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /build/target/release/quicnprotochat-server /usr/local/bin/quicnprotochat-server -COPY --from=builder /build/target/release/quicnprotochat /usr/local/bin/quicnprotochat +COPY --from=builder /build/target/release/qpq-server /usr/local/bin/qpq-server +COPY --from=builder /build/target/release/qpq /usr/local/bin/qpq RUN mkdir -p /chat diff --git a/docker/docker-compose.chat-test.yml b/docker/docker-compose.chat-test.yml index 005074f..f599860 100644 --- a/docker/docker-compose.chat-test.yml +++ b/docker/docker-compose.chat-test.yml @@ -14,7 +14,7 @@ services: context: .. dockerfile: docker/Dockerfile.chat-test command: >- - quicnprotochat-server + qpq-server --listen 0.0.0.0:7000 --data-dir /data --tls-cert /data/server-cert.der @@ -43,10 +43,10 @@ services: entrypoint: ["sleep", "infinity"] environment: RUST_LOG: warn - QUICNPROTOCHAT_ACCESS_TOKEN: devtoken - QUICNPROTOCHAT_CA_CERT: /data/server-cert.der - QUICNPROTOCHAT_SERVER_NAME: localhost - QUICNPROTOCHAT_SERVER: "server:7000" + QPQ_ACCESS_TOKEN: devtoken + QPQ_CA_CERT: /data/server-cert.der + QPQ_SERVER_NAME: localhost + QPQ_SERVER: "server:7000" volumes: - server-data:/data:ro working_dir: /chat @@ -65,10 +65,10 @@ services: entrypoint: ["sleep", "infinity"] environment: RUST_LOG: warn - QUICNPROTOCHAT_ACCESS_TOKEN: devtoken - QUICNPROTOCHAT_CA_CERT: /data/server-cert.der - QUICNPROTOCHAT_SERVER_NAME: localhost - QUICNPROTOCHAT_SERVER: "server:7000" + QPQ_ACCESS_TOKEN: devtoken + QPQ_CA_CERT: /data/server-cert.der + QPQ_SERVER_NAME: localhost + QPQ_SERVER: "server:7000" volumes: - server-data:/data:ro working_dir: /chat diff --git a/docs/FUTURE-IMPROVEMENTS.md b/docs/FUTURE-IMPROVEMENTS.md index 897f17c..651e6bb 100644 --- a/docs/FUTURE-IMPROVEMENTS.md +++ b/docs/FUTURE-IMPROVEMENTS.md @@ -1,6 +1,6 @@ # Future Improvements -This document consolidates suggested improvements for quicnprotochat, drawn from the [roadmap](src/roadmap/milestones.md), [production readiness WBS](src/roadmap/production-readiness.md), [security audit](SECURITY-AUDIT.md), [production readiness audit](PRODUCTION-READINESS-AUDIT.md), and [future research](src/roadmap/future-research.md). Items are grouped by theme and ordered by impact and dependency. +This document consolidates suggested improvements for quicproquo, drawn from the [roadmap](src/roadmap/milestones.md), [production readiness WBS](src/roadmap/production-readiness.md), [security audit](SECURITY-AUDIT.md), [production readiness audit](PRODUCTION-READINESS-AUDIT.md), and [future research](src/roadmap/future-research.md). Items are grouped by theme and ordered by impact and dependency. --- @@ -98,7 +98,7 @@ This document consolidates suggested improvements for quicnprotochat, drawn from ### 4.5 Docker user and writable paths - **Current:** Image runs as `nobody`; data dir may not be writable. -- **Improve:** Create a dedicated user/group in the image and set `QUICNPROTOCHAT_DATA_DIR` (and cert paths) to a directory writable by that user; document in deployment docs. +- **Improve:** Create a dedicated user/group in the image and set `QPQ_DATA_DIR` (and cert paths) to a directory writable by that user; document in deployment docs. - **Ref:** [Production readiness audit § 15](PRODUCTION-READINESS-AUDIT.md). --- @@ -133,7 +133,7 @@ This document consolidates suggested improvements for quicnprotochat, drawn from ### 6.1 P2P / NAT traversal (iroh, LibP2P) - **Goal:** Direct peer-to-peer when possible; server as optional relay/rendezvous. Reduces single-point-of-failure and can improve latency. -- **Ref:** [Future research § LibP2P / iroh](src/roadmap/future-research.md). The `quicnprotochat-p2p` crate is a starting point. +- **Ref:** [Future research § LibP2P / iroh](src/roadmap/future-research.md). The `quicproquo-p2p` crate is a starting point. ### 6.2 WebTransport (browser client) @@ -175,8 +175,10 @@ This document consolidates suggested improvements for quicnprotochat, drawn from ## Related documents +- **[ROADMAP.md](../ROADMAP.md)** — phased execution plan (Phases 1–8) incorporating all items below - [Milestones](src/roadmap/milestones.md) — M7 and beyond - [Production readiness WBS](src/roadmap/production-readiness.md) — phased hardening - [Future research](src/roadmap/future-research.md) — technologies and options - [Security audit](SECURITY-AUDIT.md) — recommendations and status - [Production readiness audit](PRODUCTION-READINESS-AUDIT.md) — checklist and fixes +- [ADR-006: SDK-First Adoption](src/design-rationale/adr-006-rest-gateway.md) — no REST gateway, native QUIC SDKs diff --git a/docs/MULTI-AGENT-WORK-PLAN.md b/docs/MULTI-AGENT-WORK-PLAN.md index bbecb99..6f65541 100644 --- a/docs/MULTI-AGENT-WORK-PLAN.md +++ b/docs/MULTI-AGENT-WORK-PLAN.md @@ -9,21 +9,21 @@ This document splits work for **Future Improvements §1 (Security and hardening) **Owns:** Server auth/OPAQUE, TLS config, core crypto (identity, keypackage, hybrid_kem), docs under `docs/src/cryptography/` and TLS/cert docs. ### A1. 1.2 CA-signed TLS / certificate lifecycle -- **Files:** `docs/src/getting-started/` (new or existing), `crates/quicnprotochat-server/src/tls.rs` (optional env), `README.md`. +- **Files:** `docs/src/getting-started/` (new or existing), `crates/quicproquo-server/src/tls.rs` (optional env), `README.md`. - **Tasks:** 1. Add **Certificate lifecycle** doc: using CA-issued certs (e.g. Let's Encrypt), cert rotation, OCSP/CRL optional. Recommend pinning for single-server. - 2. Optional: server config or env to prefer CA-signed cert path (e.g. `QUICNPROTOCHAT_USE_CA_CERT=1` and read from a different path). Low priority if docs suffice. + 2. Optional: server config or env to prefer CA-signed cert path (e.g. `QPQ_USE_CA_CERT=1` and read from a different path). Low priority if docs suffice. - **Deliverable:** `docs/src/getting-started/certificate-lifecycle.md` (or section in running-the-server) + README link. ### A2. 1.4 Username enumeration (OPAQUE) -- **Files:** `crates/quicnprotochat-server/src/node_service/auth_ops.rs`, `docs/SECURITY-AUDIT.md`. +- **Files:** `crates/quicproquo-server/src/node_service/auth_ops.rs`, `docs/SECURITY-AUDIT.md`. - **Tasks:** 1. Document the risk in SECURITY-AUDIT (already mentioned). 2. Optional mitigation: ensure `get_user_record` is always called before `ServerLogin::start` (already true). If desired, add a constant-time delay or dummy work when user not found so response timing does not leak existence. Keep OPAQUE security unchanged. - **Deliverable:** Doc update; optional small code change in `handle_opaque_login_start`. ### A3. 1.1 M7 — Post-quantum MLS -- **Files:** `crates/quicnprotochat-core/src/` (new or modified crypto provider), `crates/quicnprotochat-core/src/group.rs`, `crates/quicnprotochat-core/src/hybrid_kem.rs`, `crates/quicnprotochat-core/src/hybrid_crypto.rs`. +- **Files:** `crates/quicproquo-core/src/` (new or modified crypto provider), `crates/quicproquo-core/src/group.rs`, `crates/quicproquo-core/src/hybrid_kem.rs`, `crates/quicproquo-core/src/hybrid_crypto.rs`. - **Tasks:** 1. Implement a custom `OpenMlsCryptoProvider` (or adapter) that uses hybrid X25519 + ML-KEM-768 for MLS KEM (HPKE layer). 2. Wire hybrid shared secret derivation (see milestones M7) into the provider. @@ -42,7 +42,7 @@ This document splits work for **Future Improvements §1 (Security and hardening) **Owns:** Cap'n Proto schema (node.capnp delivery/channel methods), server storage (Store trait, FileBackedStore, SqlStore), `node_service/delivery.rs`, `node_service/key_ops.rs` (if createChannel lives there), client commands for channels. ### B1. 5.1 Private 1:1 channels (DM) -- **Files:** `schemas/node.capnp`, `crates/quicnprotochat-server/src/storage.rs`, `crates/quicnprotochat-server/src/sql_store.rs`, `crates/quicnprotochat-server/src/node_service/delivery.rs`, new `crates/quicnprotochat-server/src/node_service/channel_ops.rs` (or add to delivery), migrations for channels table. +- **Files:** `schemas/node.capnp`, `crates/quicproquo-server/src/storage.rs`, `crates/quicproquo-server/src/sql_store.rs`, `crates/quicproquo-server/src/node_service/delivery.rs`, new `crates/quicproquo-server/src/node_service/channel_ops.rs` (or add to delivery), migrations for channels table. - **Tasks:** 1. **Schema:** Add `createChannel @N (auth :Auth, peerKey :Data) -> (channelId :Data);` to `node.capnp`. Rebuild proto. 2. **Store trait:** Add `create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result, StorageError>`, `get_channel_members(&self, channel_id: &[u8]) -> Result, Vec)>, StorageError>`. Implement in FileBackedStore (in-memory map channel_id -> (a, b)) and SqlStore (channels table, unique on sorted (a,b)). @@ -53,7 +53,7 @@ This document splits work for **Future Improvements §1 (Security and hardening) - **Ref:** [DM channels design](src/roadmap/dm-channels.md). ### B2. 5.2 MLS lifecycle (remove, update, proposals) -- **Files:** `crates/quicnprotochat-core/src/group.rs`, client commands that use GroupMember. +- **Files:** `crates/quicproquo-core/src/group.rs`, client commands that use GroupMember. - **Tasks:** 1. Add `remove_member` (by index or identity) and `update_credential` / rekey using openmls APIs. 2. Handle incoming MLS proposals (Remove, Update) in `receive_message` path and apply to group state. @@ -62,7 +62,7 @@ This document splits work for **Future Improvements §1 (Security and hardening) - **Ref:** OpenMLS API for `MlsGroup::remove_member`, `MlsGroup::process_pending_proposals`, etc. ### B3. 5.3 Sealed Sender and 5.4 Traffic analysis -- **Files:** Docs; optionally `crates/quicnprotochat-server`, `crates/quicnprotochat-client` for padding. +- **Files:** Docs; optionally `crates/quicproquo-server`, `crates/quicproquo-client` for padding. - **Tasks:** 1. Document current `sealed_sender` behaviour (enqueue without identity binding) and that full “sender in ciphertext” is a future protocol change. 2. Optional: add optional payload padding (e.g. pad to next 256 bytes) or random delay in client send path for 5.4. @@ -75,12 +75,12 @@ This document splits work for **Future Improvements §1 (Security and hardening) | Area | Agent A | Agent B | |------|---------|---------| | `schemas/node.capnp` | — | Add createChannel | -| `crates/quicnprotochat-server/src/node_service/auth_ops.rs` | 1.4 username enum | — | -| `crates/quicnprotochat-server/src/node_service/delivery.rs` | — | 5.1 channel authz | -| `crates/quicnprotochat-server/src/storage.rs` | — | 5.1 Store channel methods | -| `crates/quicnprotochat-server/src/sql_store.rs` | — | 5.1 channels table + impl | -| `crates/quicnprotochat-server/src/tls.rs` | 1.2 optional | — | -| `crates/quicnprotochat-core/` | 1.1 M7, 1.3 doc | 5.2 group.rs | +| `crates/quicproquo-server/src/node_service/auth_ops.rs` | 1.4 username enum | — | +| `crates/quicproquo-server/src/node_service/delivery.rs` | — | 5.1 channel authz | +| `crates/quicproquo-server/src/storage.rs` | — | 5.1 Store channel methods | +| `crates/quicproquo-server/src/sql_store.rs` | — | 5.1 channels table + impl | +| `crates/quicproquo-server/src/tls.rs` | 1.2 optional | — | +| `crates/quicproquo-core/` | 1.1 M7, 1.3 doc | 5.2 group.rs | | `docs/` | 1.2, 1.3, 1.4, 5.3/5.4 | — (or shared) | **Shared:** `docs/`, `README.md`. Prefer non-overlapping files (e.g. A adds `certificate-lifecycle.md`, B does not edit it). diff --git a/docs/PRODUCTION-READINESS-AUDIT.md b/docs/PRODUCTION-READINESS-AUDIT.md index 9cf8f3f..f465173 100644 --- a/docs/PRODUCTION-READINESS-AUDIT.md +++ b/docs/PRODUCTION-READINESS-AUDIT.md @@ -1,6 +1,6 @@ # Production Readiness Audit -This document summarizes issues and fixes needed to get quicnprotochat production-ready, based on a codebase review. It aligns with the existing [Production Readiness WBS](src/roadmap/production-readiness.md) and [Coding Standards](src/contributing/coding-standards.md). +This document summarizes issues and fixes needed to get quicproquo production-ready, based on a codebase review. It aligns with the existing [Production Readiness WBS](src/roadmap/production-readiness.md) and [Coding Standards](src/contributing/coding-standards.md). --- @@ -10,7 +10,7 @@ This document summarizes issues and fixes needed to get quicnprotochat productio - **README and example config** use `auth_token = "devtoken"` and `db_key = ""`. - **Risk:** Deploying with default/example config allows weak or no auth and unencrypted DB. -- **Fix:** Require explicit `QUICNPROTOCHAT_AUTH_TOKEN` (or config) in production; reject empty or `"devtoken"` when a production mode/env is set. Document that `db_key` empty disables SQLCipher and is not acceptable for production. +- **Fix:** Require explicit `QPQ_AUTH_TOKEN` (or config) in production; reject empty or `"devtoken"` when a production mode/env is set. Document that `db_key` empty disables SQLCipher and is not acceptable for production. ### 2. **Database encryption optional** @@ -19,15 +19,15 @@ This document summarizes issues and fixes needed to get quicnprotochat productio ### 3. **Secrets and generated files not ignored** -- **`.gitignore`** does not include `data/`, so `data/server-cert.der`, `data/server-key.der`, and `data/quicnprotochat.db` could be committed. +- **`.gitignore`** does not include `data/`, so `data/server-cert.der`, `data/server-key.der`, and `data/qpq.db` could be committed. - **Fix:** Add `data/` (and any other dirs that hold certs, keys, or DBs) to `.gitignore`. Consider adding `*.der` and `*.db` if used only for local/dev. ### 4. **Dockerfile out of sync with workspace** -- **Workspace** has 5 members including `crates/quicnprotochat-p2p`. -- **Dockerfile** only copies 4 crate manifests and creates stub dirs for those 4; it never copies `quicnprotochat-p2p`. -- **Result:** `cargo build --release --bin quicnprotochat-server` can fail (missing workspace member) or behave inconsistently. -- **Fix:** Add `COPY crates/quicnprotochat-p2p/Cargo.toml` and a stub `crates/quicnprotochat-p2p/src` (or equivalent) in the dependency-cache layer so the workspace resolves. Ensure the final `COPY crates/ crates/` still brings in real p2p source. +- **Workspace** has 5 members including `crates/quicproquo-p2p`. +- **Dockerfile** only copies 4 crate manifests and creates stub dirs for those 4; it never copies `quicproquo-p2p`. +- **Result:** `cargo build --release --bin quicproquo-server` can fail (missing workspace member) or behave inconsistently. +- **Fix:** Add `COPY crates/quicproquo-p2p/Cargo.toml` and a stub `crates/quicproquo-p2p/src` (or equivalent) in the dependency-cache layer so the workspace resolves. Ensure the final `COPY crates/ crates/` still brings in real p2p source. ### 5. **E2E test failing (rustls CryptoProvider)** @@ -41,7 +41,7 @@ This document summarizes issues and fixes needed to get quicnprotochat productio ### 6. **Panic risk in client RPC path** -- **`quicnprotochat-client/src/lib.rs`:** `set_auth()` uses `.expect("init_auth must be called with a non-empty token before RPCs")`. If RPC is called without `init_auth`, the process panics. +- **`quicproquo-client/src/lib.rs`:** `set_auth()` uses `.expect("init_auth must be called with a non-empty token before RPCs")`. If RPC is called without `init_auth`, the process panics. - **Fix:** Replace with a `Result` or an error return (e.g. a dedicated error type) so callers get a recoverable error instead of a panic. Document that `init_auth` must be called before RPCs. ### 7. **Mutex `.unwrap()` in production paths** @@ -95,7 +95,7 @@ This document summarizes issues and fixes needed to get quicnprotochat productio ### 15. **Docker image runs as `nobody`** - **Dockerfile** uses `USER nobody`. Good for not running as root, but `nobody` may not have a writable home or data dir. -- **Fix:** Ensure `QUICNPROTOCHAT_DATA_DIR` (and cert paths) point to a directory writable by `nobody`, or create a dedicated user/group with a known UID and use that in the Dockerfile and docs. +- **Fix:** Ensure `QPQ_DATA_DIR` (and cert paths) point to a directory writable by `nobody`, or create a dedicated user/group with a known UID and use that in the Dockerfile and docs. --- diff --git a/docs/SECURITY-AUDIT.md b/docs/SECURITY-AUDIT.md index 879184e..090abe7 100644 --- a/docs/SECURITY-AUDIT.md +++ b/docs/SECURITY-AUDIT.md @@ -1,6 +1,6 @@ # Security Audit -This document is a security audit of the quicnprotochat 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. +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. --- @@ -24,19 +24,19 @@ This document is a security audit of the quicnprotochat codebase as of the audit ### 1.1 Token comparison -- **Location:** `crates/quicnprotochat-server/src/auth.rs` +- **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/quicnprotochat-server/src/node_service/auth_ops.rs` (login finish) +- **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/quicnprotochat-core/src/opaque_auth.rs`, server `auth_ops.rs` +- **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. @@ -52,19 +52,19 @@ This document is a security audit of the quicnprotochat codebase as of the audit ### 2.1 MLS and identity -- **Location:** `quicnprotochat-core` (group, identity, keypackage) +- **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/quicnprotochat-core/src/hybrid_kem.rs` -- **Finding:** Hybrid keypair and shared secrets use `Zeroizing` where appropriate. HKDF domain separation (`quicnprotochat-hybrid-v1`). ChaCha20-Poly1305 for AEAD. Versioned envelope. +- **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/quicnprotochat-client/src/client/state.rs` +- **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. @@ -75,13 +75,13 @@ This document is a security audit of the quicnprotochat codebase as of the audit ### 3.1 Server TLS -- **Location:** `crates/quicnprotochat-server/src/tls.rs` +- **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/quicnprotochat-client/src/client/rpc.rs` +- **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. @@ -106,7 +106,7 @@ This document is a security audit of the quicnprotochat codebase as of the audit ### 4.2 Rate limiting -- **Location:** `crates/quicnprotochat-server/src/auth.rs`, `delivery.rs` +- **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. @@ -156,11 +156,11 @@ These remain as documented, not new findings: ### 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 `quicnprotochat-client` so they are auditable. +- **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 `QUICNPROTOCHAT_CA_CERT`). Do not use a general CA unless you intend to trust that CA for all servers. See [Certificate pinning](#certificate-pinning) below. +- **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 @@ -208,7 +208,7 @@ Fix or document any reported issues. Running `cargo audit` in CI (e.g. GitHub Ac ## Certificate pinning -The client trusts the server certificate(s) in the file given by `--ca-cert` (or `QUICNPROTOCHAT_CA_CERT`). To **pin** a specific server: +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). diff --git a/docs/book.toml b/docs/book.toml index 1acb9d2..0236a8f 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -1,7 +1,7 @@ [book] -title = "quicnprotochat" +title = "quicproquo" description = "End-to-end encrypted group messaging over QUIC + TLS 1.3 + MLS (RFC 9420)" -authors = ["quicnprotochat contributors"] +authors = ["quicproquo contributors"] language = "en" src = "src" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a7b88f7..2ec6e5f 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,7 +4,7 @@ --- -# Why quicnprotochat? +# Why quicproquo? - [Comparison with Classical Chat Protocols](design-rationale/protocol-comparison.md) - [Why This Design, Not Signal/Matrix/...](design-rationale/why-not-signal.md) @@ -71,6 +71,7 @@ - [ADR-002: Cap'n Proto over MessagePack](design-rationale/adr-002-capnproto.md) - [ADR-004: MLS-Unaware Delivery Service](design-rationale/adr-004-mls-unaware-ds.md) - [ADR-005: Single-Use KeyPackages](design-rationale/adr-005-single-use-keypackages.md) +- [ADR-006: SDK-First Adoption (No REST Gateway)](design-rationale/adr-006-rest-gateway.md) --- @@ -92,6 +93,7 @@ - [Auth, Devices, and Tokens](roadmap/authz-plan.md) - [1:1 Channel Design](roadmap/dm-channels.md) - [Future Research Directions](roadmap/future-research.md) +- [Full Roadmap (Phases 1–8)](../../ROADMAP.md) --- diff --git a/docs/src/appendix/glossary.md b/docs/src/appendix/glossary.md index dfcf85d..959154c 100644 --- a/docs/src/appendix/glossary.md +++ b/docs/src/appendix/glossary.md @@ -1,18 +1,18 @@ # Glossary -Alphabetical glossary of terms used throughout the quicnprotochat documentation. +Alphabetical glossary of terms used throughout the quicproquo documentation. Each entry includes a brief definition and, where applicable, a reference to the relevant specification or documentation page. --- **AEAD** -- Authenticated Encryption with Associated Data. A symmetric encryption -scheme that provides both confidentiality and integrity. quicnprotochat uses +scheme that provides both confidentiality and integrity. quicproquo uses AES-128-GCM (in the MLS ciphersuite). See [Cryptography Overview](../cryptography/overview.md). **ALPN** -- Application-Layer Protocol Negotiation. A TLS extension that allows the client and server to agree on an application protocol during the TLS -handshake. quicnprotochat uses the ALPN token `b"capnp"` to identify Cap'n Proto +handshake. quicproquo uses the ALPN token `b"capnp"` to identify Cap'n Proto RPC connections. See [QUIC + TLS 1.3](../protocol-layers/quic-tls.md). **AS** -- Authentication Service. The server component that stores and @@ -21,7 +21,7 @@ generation; peers fetch them to add new members to a group. See [Architecture Overview](../architecture/overview.md). **Cap'n Proto** -- A zero-copy serialisation format with a built-in RPC system. -quicnprotochat uses Cap'n Proto for all wire messages and service RPCs. Schemas +quicproquo uses Cap'n Proto for all wire messages and service RPCs. Schemas live in `schemas/*.capnp` and are compiled to Rust at build time. See [Cap'n Proto Serialisation and RPC](../protocol-layers/capn-proto.md). @@ -32,13 +32,13 @@ forward secrecy and post-compromise security. See [MLS (RFC 9420)](../protocol-layers/mls.md). **Credential** -- An MLS identity binding that associates a member's signing key -with their identity. quicnprotochat uses `BasicCredential`, which contains the +with their identity. quicproquo uses `BasicCredential`, which contains the raw Ed25519 public key bytes. See [Ed25519 Identity Keys](../cryptography/identity-keys.md). **DER** -- Distinguished Encoding Rules. A binary encoding format for ASN.1 structures, used for X.509 certificates and TLS certificate chains. The -self-signed TLS certificate generated by quicnprotochat is DER-encoded. +self-signed TLS certificate generated by quicproquo is DER-encoded. **DS** -- Delivery Service. The server component that provides store-and-forward relay for opaque MLS payloads. The DS never inspects ciphertext -- it routes @@ -47,7 +47,7 @@ See [Architecture Overview](../architecture/overview.md). **Ed25519** -- Edwards-curve Digital Signature Algorithm on Curve25519. Used for MLS identity credentials and signing (KeyPackages, Commits, group operations). -quicnprotochat uses the `ed25519-dalek` crate. +quicproquo uses the `ed25519-dalek` crate. See [Ed25519 Identity Keys](../cryptography/identity-keys.md). **Epoch** -- The version number of an MLS group's key state. Each Commit @@ -61,11 +61,11 @@ the epoch ratchet: key material from earlier epochs is deleted when the epoch advances. See [Forward Secrecy](../cryptography/forward-secrecy.md). **HKDF** -- HMAC-based Key Derivation Function. Used in MLS to derive symmetric -keys from shared secrets. quicnprotochat uses HKDF-SHA256. +keys from shared secrets. quicproquo uses HKDF-SHA256. **HPKE** -- Hybrid Public Key Encryption. The public-key encryption scheme used in MLS for key exchange (encrypting to a KeyPackage's init key). Defined in -RFC 9180. In quicnprotochat, HPKE uses DHKEM(X25519, HKDF-SHA256). +RFC 9180. In quicproquo, HPKE uses DHKEM(X25519, HKDF-SHA256). See [Hybrid KEM](../protocol-layers/hybrid-kem.md). **KEM** -- Key Encapsulation Mechanism. A cryptographic primitive that generates @@ -80,7 +80,7 @@ is consumed on fetch. See **ML-KEM-768** -- Module-Lattice-based Key Encapsulation Mechanism, security level 3 (NIST FIPS 203). A post-quantum KEM based on the hardness of the -module learning-with-errors (MLWE) problem. quicnprotochat plans to use ML-KEM-768 +module learning-with-errors (MLWE) problem. quicproquo plans to use ML-KEM-768 in a hybrid construction with X25519 at milestone M7. See [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md). @@ -104,7 +104,7 @@ See [Future Research](../roadmap/future-research.md). **QUIC** -- A UDP-based, multiplexed, encrypted transport protocol defined in RFC 9000. QUIC integrates TLS 1.3 for authentication and confidentiality and provides 0-RTT connection establishment, stream multiplexing, and built-in -congestion control. quicnprotochat uses the `quinn` crate. +congestion control. quicproquo uses the `quinn` crate. See [QUIC + TLS 1.3](../protocol-layers/quic-tls.md). **Ratchet Tree** -- The binary tree data structure used in MLS for efficient @@ -113,7 +113,7 @@ hold derived key material. Updates propagate along the path from a leaf to the root, giving O(log N) cost for key updates in a group of N members. **TLS 1.3** -- Transport Layer Security version 1.3, defined in RFC 8446. The -standard for authenticated, encrypted transport. quicnprotochat uses TLS 1.3 +standard for authenticated, encrypted transport. quicproquo uses TLS 1.3 exclusively (via `rustls` with `TLS13` cipher suites only) as part of the QUIC transport. See [QUIC + TLS 1.3](../protocol-layers/quic-tls.md). @@ -125,11 +125,11 @@ KeyPackage. See [MLS (RFC 9420)](../protocol-layers/mls.md). **X25519** -- Elliptic curve Diffie-Hellman key exchange on Curve25519 (using the Montgomery form). Used as the classical component of DHKEM in MLS HPKE and in the hybrid KEM (X25519 + ML-KEM-768). -quicnprotochat uses the `x25519-dalek` crate. +quicproquo uses the `x25519-dalek` crate. See [Cryptography Overview](../cryptography/overview.md). **Zeroize** -- The practice of securely clearing sensitive data (private keys, -shared secrets) from memory when it is no longer needed. quicnprotochat uses the +shared secrets) from memory when it is no longer needed. quicproquo uses the `zeroize` crate with the `ZeroizeOnDrop` derive macro to ensure that key material is overwritten on drop. See [Key Lifecycle and Zeroization](../cryptography/key-lifecycle.md). diff --git a/docs/src/appendix/references.md b/docs/src/appendix/references.md index 075397d..d63f8f1 100644 --- a/docs/src/appendix/references.md +++ b/docs/src/appendix/references.md @@ -1,7 +1,7 @@ # References and Further Reading This page collects the standards, crate documentation, and research papers -referenced throughout the quicnprotochat documentation. Entries are organised by +referenced throughout the quicproquo documentation. Entries are organised by category. --- @@ -10,21 +10,21 @@ category. | Reference | Description | |-----------|-------------| -| [RFC 9420 -- The Messaging Layer Security (MLS) Protocol](https://datatracker.ietf.org/doc/rfc9420/) | The group key agreement protocol used by quicnprotochat. Defines KeyPackages, Welcome messages, Commits, the ratchet tree, epoch advancement, and the security properties (forward secrecy, post-compromise security). See [MLS (RFC 9420)](../protocol-layers/mls.md). | -| [RFC 9000 -- QUIC: A UDP-Based Multiplexed and Secure Transport](https://datatracker.ietf.org/doc/rfc9000/) | The transport protocol underlying quicnprotochat's primary connection layer. Provides multiplexed streams, 0-RTT connection establishment, and built-in congestion control. See [QUIC + TLS 1.3](../protocol-layers/quic-tls.md). | -| [RFC 9001 -- Using TLS to Secure QUIC](https://datatracker.ietf.org/doc/rfc9001/) | Defines how TLS 1.3 is integrated into QUIC for authentication and key exchange. quicnprotochat uses this via the `quinn` + `rustls` stack. | -| [RFC 8446 -- The Transport Layer Security (TLS) Protocol Version 1.3](https://datatracker.ietf.org/doc/rfc8446/) | The TLS version used exclusively by quicnprotochat (no TLS 1.2 fallback). Provides the handshake, key schedule, and record layer for QUIC transport security. | -| [RFC 9180 -- Hybrid Public Key Encryption (HPKE)](https://datatracker.ietf.org/doc/rfc9180/) | The public-key encryption scheme used internally by MLS for encrypting to KeyPackage init keys. quicnprotochat's MLS ciphersuite uses DHKEM(X25519, HKDF-SHA256) with AES-128-GCM. | -| [NIST FIPS 203 -- Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM)](https://csrc.nist.gov/pubs/fips/203/final) | The post-quantum KEM standard. quicnprotochat plans to use ML-KEM-768 in a hybrid construction with X25519 at milestone M7. See [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md). | -| [Cap'n Proto specification](https://capnproto.org/) | The zero-copy serialisation format and RPC system used for all quicnprotochat wire messages and service interfaces. See [Cap'n Proto Serialisation and RPC](../protocol-layers/capn-proto.md). | -| [draft-ietf-tls-hybrid-design -- Hybrid Key Exchange in TLS 1.3](https://datatracker.ietf.org/doc/draft-ietf-tls-hybrid-design/) | The combiner approach used by quicnprotochat's hybrid KEM construction (X25519 shared secret concatenated with ML-KEM-768 shared secret, fed through HKDF). See [Hybrid KEM](../protocol-layers/hybrid-kem.md). | +| [RFC 9420 -- The Messaging Layer Security (MLS) Protocol](https://datatracker.ietf.org/doc/rfc9420/) | The group key agreement protocol used by quicproquo. Defines KeyPackages, Welcome messages, Commits, the ratchet tree, epoch advancement, and the security properties (forward secrecy, post-compromise security). See [MLS (RFC 9420)](../protocol-layers/mls.md). | +| [RFC 9000 -- QUIC: A UDP-Based Multiplexed and Secure Transport](https://datatracker.ietf.org/doc/rfc9000/) | The transport protocol underlying quicproquo's primary connection layer. Provides multiplexed streams, 0-RTT connection establishment, and built-in congestion control. See [QUIC + TLS 1.3](../protocol-layers/quic-tls.md). | +| [RFC 9001 -- Using TLS to Secure QUIC](https://datatracker.ietf.org/doc/rfc9001/) | Defines how TLS 1.3 is integrated into QUIC for authentication and key exchange. quicproquo uses this via the `quinn` + `rustls` stack. | +| [RFC 8446 -- The Transport Layer Security (TLS) Protocol Version 1.3](https://datatracker.ietf.org/doc/rfc8446/) | The TLS version used exclusively by quicproquo (no TLS 1.2 fallback). Provides the handshake, key schedule, and record layer for QUIC transport security. | +| [RFC 9180 -- Hybrid Public Key Encryption (HPKE)](https://datatracker.ietf.org/doc/rfc9180/) | The public-key encryption scheme used internally by MLS for encrypting to KeyPackage init keys. quicproquo's MLS ciphersuite uses DHKEM(X25519, HKDF-SHA256) with AES-128-GCM. | +| [NIST FIPS 203 -- Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM)](https://csrc.nist.gov/pubs/fips/203/final) | The post-quantum KEM standard. quicproquo plans to use ML-KEM-768 in a hybrid construction with X25519 at milestone M7. See [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md). | +| [Cap'n Proto specification](https://capnproto.org/) | The zero-copy serialisation format and RPC system used for all quicproquo wire messages and service interfaces. See [Cap'n Proto Serialisation and RPC](../protocol-layers/capn-proto.md). | +| [draft-ietf-tls-hybrid-design -- Hybrid Key Exchange in TLS 1.3](https://datatracker.ietf.org/doc/draft-ietf-tls-hybrid-design/) | The combiner approach used by quicproquo's hybrid KEM construction (X25519 shared secret concatenated with ML-KEM-768 shared secret, fed through HKDF). See [Hybrid KEM](../protocol-layers/hybrid-kem.md). | | [RFC 9497 -- OPAQUE](https://datatracker.ietf.org/doc/rfc9497/) | Asymmetric password-authenticated key exchange. Considered for future authentication (see [Future Research](../roadmap/future-research.md)). | --- ## Rust Crate Documentation -| Crate | docs.rs | Role in quicnprotochat | +| Crate | docs.rs | Role in quicproquo | |-------|---------|----------------------| | `openmls` | [docs.rs/openmls](https://docs.rs/openmls/) | MLS protocol implementation: group creation, member addition, Welcome processing, application message encryption/decryption. See [MLS (RFC 9420)](../protocol-layers/mls.md). | | `openmls_rust_crypto` | [docs.rs/openmls_rust_crypto](https://docs.rs/openmls_rust_crypto/) | Pure-Rust cryptographic backend for openmls. Provides the `OpenMlsRustCrypto` provider used by `GroupMember`. | @@ -57,7 +57,7 @@ Katriel Cohn-Gordon, Cas Cremers, Luke Garratt, Jon Millican, and Kevin Milner. This paper analyses the security properties of group messaging protocols and motivates the design of MLS. It defines the security goals (forward secrecy, post-compromise security, asynchronous operation) that MLS formalises into a -protocol. Essential background for understanding why quicnprotochat uses MLS +protocol. Essential background for understanding why quicproquo uses MLS rather than extending the Signal protocol to groups. ### Signal Protocol @@ -67,7 +67,7 @@ Trevor Perrin and Moxie Marlinspike. [signal.org/docs/specifications/doubleratchet](https://signal.org/docs/specifications/doubleratchet/) Defines the double ratchet used in Signal's 1:1 messaging. Relevant as a -potential optimisation for quicnprotochat's 1:1 channels (see +potential optimisation for quicproquo's 1:1 channels (see [Future Research: Double-Ratchet DM Layer](../roadmap/future-research.md#double-ratchet-dm-layer)) and as background for understanding how MLS generalises ratcheting to groups. @@ -86,7 +86,7 @@ Roberto Avanzi et al. [NIST PQC Round 3 submission](https://pq-crystals.org/kyber/) The predecessor to ML-KEM (NIST FIPS 203). CRYSTALS-Kyber was selected by NIST -and standardised as ML-KEM. quicnprotochat uses the `ml-kem` crate which +and standardised as ML-KEM. quicproquo uses the `ml-kem` crate which implements the final FIPS 203 standard. ### Metadata Resistance @@ -96,7 +96,7 @@ Signal Blog. [signal.org/blog/sealed-sender](https://signal.org/blog/sealed-sender/) Describes Signal's approach to hiding sender identity from the server. Relevant -to quicnprotochat's future research on metadata resistance (see +to quicproquo's future research on metadata resistance (see [Future Research](../roadmap/future-research.md)). --- @@ -104,7 +104,7 @@ to quicnprotochat's future research on metadata resistance (see ## Cross-references - [Glossary](glossary.md) -- definitions of terms used in these references -- [Protocol Layers Overview](../protocol-layers/overview.md) -- how the protocols layer in quicnprotochat +- [Protocol Layers Overview](../protocol-layers/overview.md) -- how the protocols layer in quicproquo - [Cryptography Overview](../cryptography/overview.md) -- cryptographic properties and threat model - [Future Research](../roadmap/future-research.md) -- technologies under consideration - [Milestones](../roadmap/milestones.md) -- current project status diff --git a/docs/src/architecture/crate-responsibilities.md b/docs/src/architecture/crate-responsibilities.md index 20e0341..1cefe24 100644 --- a/docs/src/architecture/crate-responsibilities.md +++ b/docs/src/architecture/crate-responsibilities.md @@ -1,9 +1,9 @@ # Crate Responsibilities -The quicnprotochat workspace contains six crates. The main four (proto, core, +The quicproquo 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 +only on the crates below it. The workspace also includes **quicproquo-gui** +(Tauri desktop app) and **quicproquo-p2p** (P2P endpoint resolution). This page documents what each crate provides, what it explicitly avoids, and how the crates relate to one another. @@ -13,7 +13,7 @@ crates relate to one another. ```text ┌──────────────────────────┐ - │ quicnprotochat-client │ + │ quicproquo-client │ │ (CLI, QUIC client, │ │ GroupMember orchestr.) │ └─────────┬───────┬────────┘ @@ -21,7 +21,7 @@ crates relate to one another. ┌───────┘ └────────┐ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ - │ quicnprotochat-core │ │ quicnprotochat-server │ + │ quicproquo-core │ │ quicproquo-server │ │ (crypto, MLS, │ │ (QUIC listener, │ │ hybrid KEM) │ │ NodeService RPC, │ │ │ │ storage) │ @@ -30,7 +30,7 @@ crates relate to one another. │ ┌───────────────────┘ ▼ ▼ ┌────────────────────────┐ - │ quicnprotochat-proto │ + │ quicproquo-proto │ │ (Cap'n Proto schemas, │ │ codegen, helpers) │ └────────────────────────┘ @@ -42,7 +42,7 @@ serialisation. The server and client crates both depend on core and proto. --- -## quicnprotochat-core +## quicproquo-core **Role:** Pure cryptographic logic. No network I/O. No async runtime dependency. @@ -70,12 +70,12 @@ dependency. `ed25519-dalek`, `openmls`, `openmls_rust_crypto`, `openmls_traits`, `tls_codec`, `ml-kem`, `x25519-dalek`, `chacha20poly1305`, -`hkdf`, `sha2`, `zeroize`, `capnp`, `quicnprotochat-proto`, `tokio`, +`hkdf`, `sha2`, `zeroize`, `capnp`, `quicproquo-proto`, `tokio`, `serde`, `bincode`, `serde_json`, `thiserror`. --- -## quicnprotochat-proto +## quicproquo-proto **Role:** Cap'n Proto schema definitions, compile-time code generation, and pure-synchronous serialisation helpers. This crate is the single source of truth @@ -110,7 +110,7 @@ for the wire format. --- -## quicnprotochat-server +## quicproquo-server **Role:** Network-facing server binary. Accepts QUIC + TLS 1.3 connections, dispatches Cap'n Proto RPC calls to `NodeServiceImpl`, and persists state to @@ -143,19 +143,19 @@ is handled by `spawn_local`. ### What this crate does NOT do -- No direct crypto operations (it delegates to `quicnprotochat-core` types +- No direct crypto operations (it delegates to `quicproquo-core` types for fingerprinting and storage only). - No MLS processing -- all payloads are opaque byte strings. ### Key dependencies -`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`, +`quicproquo-core`, `quicproquo-proto`, `quinn`, `quinn-proto`, `rustls`, `rcgen`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `dashmap`, `sha2`, `clap`, `tracing`, `anyhow`, `thiserror`, `bincode`, `serde`. --- -## quicnprotochat-client +## quicproquo-client **Role:** CLI client binary. Connects to the server over QUIC + TLS 1.3, orchestrates MLS group operations via `GroupMember`, and persists identity and @@ -167,7 +167,7 @@ group state to disk. |-------------------------|-------------| | `connect_node` | Establishes a QUIC/TLS connection, opens a bidirectional stream, and bootstraps a `capnp-rpc` `RpcSystem` to obtain a `node_service::Client`. | | CLI subcommands (`clap`)| `ping`, `register`, `fetch-key`, `demo-group`, `register-state`, `create-group`, `invite`, `join`, `send`, `recv`. | -| `GroupMember` usage | The client creates a `GroupMember` (from `quicnprotochat-core`), calls `generate_key_package` / `create_group` / `add_member` / `join_group` / `send_message` / `receive_message`. | +| `GroupMember` usage | The client creates a `GroupMember` (from `quicproquo-core`), calls `generate_key_package` / `create_group` / `add_member` / `join_group` / `send_message` / `receive_message`. | | State persistence | `StoredState` holds `identity_seed` (32 bytes) and optional serialised `MlsGroup`. A companion `.ks` file stores the `DiskKeyStore` with HPKE init private keys. | | Auth context | `ClientAuth` bundles an optional bearer token and device ID. Passed to every RPC via the `Auth` struct in `node.capnp`. | @@ -194,7 +194,7 @@ group state to disk. ### Key dependencies -`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`, +`quicproquo-core`, `quicproquo-proto`, `quinn`, `quinn-proto`, `rustls`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `clap`, `sha2`, `serde`, `bincode`, `anyhow`, `thiserror`, `tracing`. @@ -204,8 +204,8 @@ group state to disk. | 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. | +| **quicproquo-gui** | Tauri 2 desktop application; provides a GUI on top of the client/core stack. | +| **quicproquo-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. @@ -220,12 +220,12 @@ These crates are optional for building and running the server and CLI client. 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 +6. **quicproquo-gui** and **quicproquo-p2p** are optional; they depend on client/core/proto as needed and do not change the core layering. This layering ensures that: -- Crypto code can be tested in isolation (`cargo test -p quicnprotochat-core`). +- Crypto code can be tested in isolation (`cargo test -p quicproquo-core`). - Schema changes propagate automatically through codegen. - The server binary contains no client-side MLS orchestration logic. - The client binary contains no server-side storage or listener logic. diff --git a/docs/src/architecture/data-flow.md b/docs/src/architecture/data-flow.md index 4f837ff..39a70e2 100644 --- a/docs/src/architecture/data-flow.md +++ b/docs/src/architecture/data-flow.md @@ -1,6 +1,6 @@ # End-to-End Data Flow -This page traces the three core data flows through the quicnprotochat system: +This page traces the three core data flows through the quicproquo system: registration, group creation, and message exchange. Each flow is illustrated with an ASCII sequence diagram showing control-plane (AS) and data-plane (DS) traffic. diff --git a/docs/src/architecture/overview.md b/docs/src/architecture/overview.md index 139cb3f..098c486 100644 --- a/docs/src/architecture/overview.md +++ b/docs/src/architecture/overview.md @@ -1,6 +1,6 @@ # Architecture Overview -quicnprotochat is an end-to-end encrypted group messaging system built in Rust. +quicproquo is an end-to-end encrypted group messaging system built in Rust. This page describes the high-level architecture: the services that compose the system, the dual-key cryptographic model, and how the pieces fit together. @@ -28,11 +28,11 @@ connection lifecycle, and the long-polling `fetchWait` mechanism. ## Identity Key Model -Each quicnprotochat client holds a single Ed25519 signing keypair that serves +Each quicproquo client holds a single Ed25519 signing keypair that serves as its long-term identity: ```text - quicnprotochat Key Model + quicproquo Key Model ┌──────────────────────────────────────────────────┐ │ │ │ Ed25519 signing keypair (MLS identity) │ @@ -135,10 +135,10 @@ The implementation is split across four workspace crates: | Crate | Role | |----------------------------|-------------------------------------------------------------------| -| `quicnprotochat-core` | Crypto primitives, MLS state machine, hybrid KEM | -| `quicnprotochat-proto` | Cap'n Proto schemas, codegen, and serialisation helpers | -| `quicnprotochat-server` | QUIC listener, NodeService RPC, storage | -| `quicnprotochat-client` | QUIC client, CLI subcommands, state persistence | +| `quicproquo-core` | Crypto primitives, MLS state machine, hybrid KEM | +| `quicproquo-proto` | Cap'n Proto schemas, codegen, and serialisation helpers | +| `quicproquo-server` | QUIC listener, NodeService RPC, storage | +| `quicproquo-client` | QUIC client, CLI subcommands, state persistence | See [Crate Responsibilities](crate-responsibilities.md) for a full breakdown and dependency diagram. diff --git a/docs/src/architecture/protocol-stack.md b/docs/src/architecture/protocol-stack.md index b36259d..4fc9efb 100644 --- a/docs/src/architecture/protocol-stack.md +++ b/docs/src/architecture/protocol-stack.md @@ -1,6 +1,6 @@ # Protocol Stack -quicnprotochat layers three protocol stages to move a plaintext message from +quicproquo layers three protocol stages to move a plaintext message from sender to recipient with end-to-end encryption, typed RPC framing, and authenticated transport. This page describes each layer and provides a comparison table. diff --git a/docs/src/architecture/service-architecture.md b/docs/src/architecture/service-architecture.md index daf87e2..7d5264a 100644 --- a/docs/src/architecture/service-architecture.md +++ b/docs/src/architecture/service-architecture.md @@ -1,6 +1,6 @@ # Service Architecture -The quicnprotochat server exposes a single **NodeService** RPC endpoint that +The quicproquo server exposes a single **NodeService** RPC endpoint that combines Authentication and Delivery operations. This page documents the RPC interface, per-connection lifecycle, storage model, long-polling mechanism, and authentication context. @@ -234,10 +234,10 @@ The server binary is configured via CLI flags or environment variables: | Flag | Env var | Default | Description | |----------------|----------------------------|----------------------|-------------| -| `--listen` | `QUICNPROTOCHAT_LISTEN` | `0.0.0.0:7000` | QUIC listen address (host:port). | -| `--data-dir` | `QUICNPROTOCHAT_DATA_DIR` | `data` | Directory for persisted KeyPackages, delivery queues, and hybrid keys. | -| `--tls-cert` | `QUICNPROTOCHAT_TLS_CERT` | `data/server-cert.der` | Path to TLS certificate (DER). Auto-generated if missing. | -| `--tls-key` | `QUICNPROTOCHAT_TLS_KEY` | `data/server-key.der` | Path to TLS private key (DER). Auto-generated if missing. | +| `--listen` | `QPQ_LISTEN` | `0.0.0.0:7000` | QUIC listen address (host:port). | +| `--data-dir` | `QPQ_DATA_DIR` | `data` | Directory for persisted KeyPackages, delivery queues, and hybrid keys. | +| `--tls-cert` | `QPQ_TLS_CERT` | `data/server-cert.der` | Path to TLS certificate (DER). Auto-generated if missing. | +| `--tls-key` | `QPQ_TLS_KEY` | `data/server-key.der` | Path to TLS private key (DER). Auto-generated if missing. | If the TLS certificate or key files do not exist at startup, the server auto-generates a self-signed certificate for `localhost`, `127.0.0.1`, and diff --git a/docs/src/contributing/coding-standards.md b/docs/src/contributing/coding-standards.md index e969434..18521c0 100644 --- a/docs/src/contributing/coding-standards.md +++ b/docs/src/contributing/coding-standards.md @@ -1,6 +1,6 @@ # Coding Standards -This page defines the engineering standards for quicnprotochat. These are +This page defines the engineering standards for quicproquo. 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. @@ -86,9 +86,9 @@ pub fn create_group( - 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`). +- Use `thiserror` for library error types (`quicproquo-core`, + `quicproquo-proto`) and `anyhow` for application-level error handling + (`quicproquo-server`, `quicproquo-client`). - `unwrap()` is acceptable only in: - Test code. - Cases where the invariant is provably guaranteed by the type system diff --git a/docs/src/contributing/testing.md b/docs/src/contributing/testing.md index b2394ae..64eca32 100644 --- a/docs/src/contributing/testing.md +++ b/docs/src/contributing/testing.md @@ -1,7 +1,7 @@ # 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 +quicproquo. All tests run with `cargo test --workspace` and must pass before any code is merged. For the coding standards that tests must follow, see @@ -17,7 +17,7 @@ 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:** +**quicproquo-core:** | Module | Tests | What they cover | |--------|-------|----------------| @@ -26,7 +26,7 @@ isolation. | `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:** +**quicproquo-proto:** | Module | Tests | What they cover | |--------|-------|----------------| @@ -34,7 +34,7 @@ isolation. ### Integration Tests -Integration tests live in `crates/quicnprotochat-client/tests/` and test the +Integration tests live in `crates/quicproquo-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. @@ -88,17 +88,17 @@ 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 +cargo test -p quicproquo-core +cargo test -p quicproquo-proto +cargo test -p quicproquo-server +cargo test -p quicproquo-client ``` ### Single Test ```bash -cargo test -p quicnprotochat-core -- codec::tests::test_round_trip -cargo test -p quicnprotochat-client --test mls_group +cargo test -p quicproquo-core -- codec::tests::test_round_trip +cargo test -p quicproquo-client --test mls_group ``` ### With Output @@ -117,10 +117,10 @@ Summary: | Crate | Unit Tests | Integration Tests | Total | |-------|-----------|-------------------|-------| -| `quicnprotochat-core` | 23 | -- | 23 | -| `quicnprotochat-proto` | 3 | -- | 3 | -| `quicnprotochat-server` | 0 | -- | 0 | -| `quicnprotochat-client` | 0 | 5 | 5 | +| `quicproquo-core` | 23 | -- | 23 | +| `quicproquo-proto` | 3 | -- | 3 | +| `quicproquo-server` | 0 | -- | 0 | +| `quicproquo-client` | 0 | 5 | 5 | | **Total** | **26** | **5** | **31** | --- diff --git a/docs/src/cryptography/forward-secrecy.md b/docs/src/cryptography/forward-secrecy.md index e53cd42..0952dfc 100644 --- a/docs/src/cryptography/forward-secrecy.md +++ b/docs/src/cryptography/forward-secrecy.md @@ -6,7 +6,7 @@ compromised, past session keys cannot be recovered.** In other words, an attacker who obtains today's long-term key cannot use it to decrypt messages recorded yesterday. -quicnprotochat provides forward secrecy at two independent layers: the transport +quicproquo provides forward secrecy at two independent layers: the transport layer and the application layer. Even if one layer's FS mechanism is defeated, the other continues to protect message confidentiality. @@ -28,7 +28,7 @@ In each TLS 1.3 handshake: Because the ephemeral keys exist only for the duration of the handshake, compromising the server's long-term TLS certificate key (currently self-signed -in quicnprotochat) does not reveal past session keys. +in quicproquo) does not reveal past session keys. ## Application Layer Forward Secrecy @@ -54,7 +54,7 @@ This deletion is the mechanism that provides forward secrecy: once old epoch keys are erased, messages encrypted under those keys cannot be decrypted, even if the current group state is compromised. -In quicnprotochat, epoch advancement occurs when: +In quicproquo, epoch advancement occurs when: - `add_member()` is called, which creates a Commit and calls `merge_pending_commit()`. @@ -91,7 +91,7 @@ HPKE init keys. ## Layered Forward Secrecy -A distinctive property of quicnprotochat's design is that forward secrecy +A distinctive property of quicproquo's design is that forward secrecy operates at two independent layers: ```text @@ -135,7 +135,7 @@ unless they also break the transport encryption. Signal's Double Ratchet protocol also provides forward secrecy, but the mechanisms differ: -| Property | Signal Double Ratchet | MLS (quicnprotochat) | +| Property | Signal Double Ratchet | MLS (quicproquo) | |----------|----------------------|---------------------| | Scope | Pairwise (1:1 sessions) | Group (n-party) | | Ratchet granularity | Per message (symmetric ratchet) + per DH round (DH ratchet) | Per epoch (Commit) | diff --git a/docs/src/cryptography/identity-keys.md b/docs/src/cryptography/identity-keys.md index aff07f5..5ce5c96 100644 --- a/docs/src/cryptography/identity-keys.md +++ b/docs/src/cryptography/identity-keys.md @@ -1,11 +1,11 @@ # Ed25519 Identity Keys The Ed25519 identity keypair is the long-term cryptographic identity of a -quicnprotochat client. It is generated once, persisted across sessions, and used +quicproquo client. It is generated once, persisted across sessions, and used for MLS credential signing, Authentication Service registration, and delivery queue addressing. -**Source:** `crates/quicnprotochat-core/src/identity.rs` +**Source:** `crates/quicproquo-core/src/identity.rs` ## Structure @@ -37,7 +37,7 @@ A fresh identity keypair is generated from the OS CSPRNG (`OsRng`) via `ed25519-dalek`: ```rust -use quicnprotochat_core::identity::IdentityKeypair; +use quicproquo_core::identity::IdentityKeypair; let identity = IdentityKeypair::generate(); // The signing key seed is generated from OsRng (getrandom on Linux). diff --git a/docs/src/cryptography/key-lifecycle.md b/docs/src/cryptography/key-lifecycle.md index 3f61061..c394a6b 100644 --- a/docs/src/cryptography/key-lifecycle.md +++ b/docs/src/cryptography/key-lifecycle.md @@ -1,6 +1,6 @@ # Key Lifecycle and Zeroization -quicnprotochat uses multiple key types with different lifetimes, creation +quicproquo uses multiple key types with different lifetimes, creation patterns, and destruction guarantees. This page provides a comprehensive lifecycle diagram for every key type in the system, from generation through zeroization. @@ -25,7 +25,7 @@ Hybrid KEM Keys Per peer (future) Public portion X25519+ML-KEM-768 ## Ed25519 Identity Key -**Source:** `crates/quicnprotochat-core/src/identity.rs` +**Source:** `crates/quicproquo-core/src/identity.rs` The Ed25519 identity key is the most long-lived secret in the system. It represents the client's cryptographic identity across all sessions and groups. @@ -92,8 +92,8 @@ zeroization. ## HPKE Init Keys -**Source:** `crates/quicnprotochat-core/src/keystore.rs` and -`crates/quicnprotochat-core/src/group.rs` +**Source:** `crates/quicproquo-core/src/keystore.rs` and +`crates/quicproquo-core/src/group.rs` HPKE init keys are generated by the openmls backend as part of MLS KeyPackage creation. They are single-use: each init key is consumed exactly once when @@ -167,7 +167,7 @@ processing a Welcome message. **Managed by:** `openmls` (internal to the `MlsGroup` state machine) MLS epoch keys are derived internally by the openmls ratchet tree. They are not -directly accessible in quicnprotochat code but are critical to understanding the +directly accessible in quicproquo code but are critical to understanding the system's security properties. ### Lifecycle @@ -216,12 +216,12 @@ system's security properties. - **Deletion:** Old epoch keys are deleted after the Commit is processed. This deletion is what provides [forward secrecy](forward-secrecy.md) at the MLS layer. -- **No direct access:** quicnprotochat code interacts with epoch keys only +- **No direct access:** quicproquo code interacts with epoch keys only indirectly through `send_message()` and `receive_message()`. ## Hybrid KEM Keys (Future -- M5+) -**Source:** `crates/quicnprotochat-core/src/hybrid_kem.rs` +**Source:** `crates/quicproquo-core/src/hybrid_kem.rs` The hybrid KEM keypair combines X25519 (classical) with ML-KEM-768 (post-quantum) for content encryption that resists both classical and quantum diff --git a/docs/src/cryptography/overview.md b/docs/src/cryptography/overview.md index 31ae34f..7bf19e3 100644 --- a/docs/src/cryptography/overview.md +++ b/docs/src/cryptography/overview.md @@ -1,6 +1,6 @@ # Cryptography Overview -quicnprotochat layers multiple cryptographic protocols to provide confidentiality, +quicproquo layers multiple cryptographic protocols to provide confidentiality, integrity, authentication, forward secrecy, and post-compromise security. This page catalogues every algorithm in the system, the crate that supplies it, and the security margin it provides. diff --git a/docs/src/cryptography/post-compromise-security.md b/docs/src/cryptography/post-compromise-security.md index 79849bb..dec1d2b 100644 --- a/docs/src/cryptography/post-compromise-security.md +++ b/docs/src/cryptography/post-compromise-security.md @@ -13,7 +13,7 @@ PCS is the complement of [forward secrecy](forward-secrecy.md): - **Post-compromise security** protects the **future** from a past compromise. MLS (RFC 9420) is specifically designed to provide both properties simultaneously -for group messaging. This is a key differentiator of quicnprotochat's design. +for group messaging. This is a key differentiator of quicproquo's design. ## How MLS Provides PCS @@ -64,7 +64,7 @@ This means: For a group of 1,000 members, the path length is approximately 10 nodes -- making PCS practical even for large groups. -## Epoch Advancement in quicnprotochat +## Epoch Advancement in quicproquo In the current implementation, epoch advancement occurs through the `GroupMember` methods in `group.rs`: @@ -145,7 +145,7 @@ deleted), and future epochs are protected by PCS (new key material generated). Signal's group messaging uses **Sender Keys**, a fundamentally different mechanism from MLS's ratchet tree. The comparison is instructive because it -highlights why MLS was chosen for quicnprotochat: +highlights why MLS was chosen for quicproquo: ### Signal Sender Keys @@ -168,7 +168,7 @@ security. If an attacker compromises a member's Sender Key: membership changes. - There is no automatic healing mechanism analogous to MLS's ratchet tree. -### MLS Ratchet Tree (quicnprotochat) +### MLS Ratchet Tree (quicproquo) In contrast, MLS's ratchet tree provides PCS because: @@ -218,7 +218,7 @@ periodic Updates (planned) will bound the healing window. ### Server compromise does not prevent PCS -The quicnprotochat server is MLS-unaware -- it stores and forwards encrypted +The quicproquo server is MLS-unaware -- it stores and forwards encrypted MLS messages without access to the group state. A compromised server cannot: - Prevent PCS by blocking Commits (it could perform denial-of-service, but diff --git a/docs/src/cryptography/post-quantum-readiness.md b/docs/src/cryptography/post-quantum-readiness.md index ffa8336..eca07a6 100644 --- a/docs/src/cryptography/post-quantum-readiness.md +++ b/docs/src/cryptography/post-quantum-readiness.md @@ -1,15 +1,15 @@ # Post-Quantum Readiness -quicnprotochat includes a fully implemented and tested hybrid key encapsulation +quicproquo includes a fully implemented and tested hybrid key encapsulation mechanism (KEM) combining X25519 (classical) with ML-KEM-768 (post-quantum). This page describes the current implementation, the integration plan, the security rationale, and the known gaps. -**Source:** `crates/quicnprotochat-core/src/hybrid_kem.rs` +**Source:** `crates/quicproquo-core/src/hybrid_kem.rs` ## Current State -The hybrid KEM is **fully implemented and tested** in `quicnprotochat-core`. The +The hybrid KEM is **fully implemented and tested** in `quicproquo-core`. The implementation provides: - `HybridKeypair::generate()` -- generate a combined X25519 + ML-KEM-768 keypair @@ -35,7 +35,7 @@ The test suite in `hybrid_kem.rs` includes 10 tests covering: ## ML-KEM-768 (FIPS 203) ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) is the NIST-standardized -post-quantum KEM, published as FIPS 203. quicnprotochat uses ML-KEM-768, the +post-quantum KEM, published as FIPS 203. quicproquo uses ML-KEM-768, the middle parameter set: | Parameter Set | NIST Level | Security (PQ) | EK Size | CT Size | SS Size | @@ -69,8 +69,8 @@ executed, and their shared secrets are combined through a KDF: ```text ikm = X25519_shared_secret(32 bytes) || ML-KEM_shared_secret(32 bytes) -key = HKDF-SHA256(salt=[], ikm, info="quicnprotochat-hybrid-v1", L=32) -nonce = HKDF-SHA256(salt=[], ikm, info="quicnprotochat-hybrid-nonce-v1", L=12) +key = HKDF-SHA256(salt=[], ikm, info="quicproquo-hybrid-v1", L=32) +nonce = HKDF-SHA256(salt=[], ikm, info="quicproquo-hybrid-nonce-v1", L=12) ``` The combined IKM (input key material) is wrapped in `Zeroizing>` and @@ -165,7 +165,7 @@ hybrid KEM for HPKE init key exchange: ## The PQ Gap -There is an important asymmetry in quicnprotochat's post-quantum protection: +There is an important asymmetry in quicproquo's post-quantum protection: ```text Layer Classical Protection Post-Quantum Protection @@ -191,7 +191,7 @@ This is the **PQ gap**: content is safe, but metadata is not. Post-quantum TLS (via ML-KEM in the TLS 1.3 handshake) is being standardized by the IETF and is supported by some TLS libraries, but `rustls` does not yet -support it in a stable release. When `rustls` adds ML-KEM support, quicnprotochat +support it in a stable release. When `rustls` adds ML-KEM support, quicproquo will adopt it to close the PQ gap at the transport layer. ## Harvest-Now, Decrypt-Later Risk @@ -202,7 +202,7 @@ The "harvest-now, decrypt-later" (HNDL) threat model assumes an adversary who: 2. Waits for a sufficiently powerful quantum computer (years or decades). 3. Decrypts the recorded traffic retroactively. -In quicnprotochat's case: +In quicproquo's case: - **Content is safe from M5 onward.** The hybrid KEM wrapping MLS content uses ML-KEM-768, which resists quantum attacks. Even if the recorded traffic is diff --git a/docs/src/cryptography/threat-model.md b/docs/src/cryptography/threat-model.md index 812c1b2..85ce747 100644 --- a/docs/src/cryptography/threat-model.md +++ b/docs/src/cryptography/threat-model.md @@ -1,6 +1,6 @@ # Threat Model -This page defines the attacker models quicnprotochat is designed to resist, +This page defines the attacker models quicproquo is designed to resist, catalogues what is and is not protected, identifies known gaps in the current implementation, and outlines future mitigations. @@ -41,7 +41,7 @@ state-level adversary). **What they can do:** - Attempt TLS 1.3 MITM: TLS 1.3 prevents this if the client validates the - server's certificate. However, quicnprotochat currently uses **self-signed + server's certificate. However, quicproquo currently uses **self-signed certificates**, which means the client has no CA chain to verify. On the first connection, a MITM could present their own certificate and intercept the session (trust-on-first-use vulnerability). diff --git a/docs/src/design-rationale/adr-002-capnproto.md b/docs/src/design-rationale/adr-002-capnproto.md index eaac4e3..a8e849a 100644 --- a/docs/src/design-rationale/adr-002-capnproto.md +++ b/docs/src/design-rationale/adr-002-capnproto.md @@ -6,7 +6,7 @@ ## Context -quicnprotochat needs an efficient, typed wire format for client-server communication. The format must support: +quicproquo needs an efficient, typed wire format for client-server communication. The format must support: 1. **Typed messages** with compile-time schema enforcement to eliminate hand-rolled serialisation bugs. 2. **Schema evolution** so that new fields and methods can be added without breaking existing clients. @@ -105,7 +105,7 @@ The Cap'n Proto schemas are stored in the `schemas/` directory: ### Costs and trade-offs -- **Build-time code generation.** The `capnpc` compiler must run during the build (via `build.rs` in `quicnprotochat-proto`). This adds a build dependency and increases compile times slightly. +- **Build-time code generation.** The `capnpc` compiler must run during the build (via `build.rs` in `quicproquo-proto`). This adds a build dependency and increases compile times slightly. - **Learning curve.** Cap'n Proto's builder/reader API is different from typical `serde`-based Rust serialisation. Developers must learn the Cap'n Proto programming model (builders for construction, readers for traversal, owned messages for storage). - **Generated code verbosity.** The generated Rust code is verbose and not intended to be read directly. Application code interacts with it through the builder/reader traits. - **Smaller ecosystem than Protobuf.** Cap'n Proto has fewer users, fewer tutorials, and fewer third-party tools than Protobuf. However, the core Rust crates are well-maintained. @@ -114,7 +114,7 @@ The Cap'n Proto schemas are stored in the `schemas/` directory: ### Residual risks - **Crate maintenance.** The `capnp` and `capnp-rpc` crates are maintained primarily by David Renshaw. If maintenance lapses, the project would need to fork or switch serialisation formats. Mitigated by the crates' maturity and the relatively stable Cap'n Proto specification. -- **RPC limitations.** The Rust `capnp-rpc` crate implements Level 1 of the Cap'n Proto RPC protocol. Level 3 features (three-party handoffs) are not supported. This has not been a limitation for quicnprotochat's client-server architecture. +- **RPC limitations.** The Rust `capnp-rpc` crate implements Level 1 of the Cap'n Proto RPC protocol. Level 3 features (three-party handoffs) are not supported. This has not been a limitation for quicproquo's client-server architecture. --- @@ -126,8 +126,8 @@ The Cap'n Proto schemas are stored in the `schemas/` directory: | `schemas/auth.capnp` | AuthenticationService RPC interface | | `schemas/delivery.capnp` | DeliveryService RPC interface | | `schemas/node.capnp` | NodeService unified RPC interface | -| `crates/quicnprotochat-proto/build.rs` | Build script that invokes `capnpc` for code generation | -| `crates/quicnprotochat-proto/src/lib.rs` | Re-exports generated Cap'n Proto modules | +| `crates/quicproquo-proto/build.rs` | Build script that invokes `capnpc` for code generation | +| `crates/quicproquo-proto/src/lib.rs` | Re-exports generated Cap'n Proto modules | --- diff --git a/docs/src/design-rationale/adr-004-mls-unaware-ds.md b/docs/src/design-rationale/adr-004-mls-unaware-ds.md index 89171b0..647ea35 100644 --- a/docs/src/design-rationale/adr-004-mls-unaware-ds.md +++ b/docs/src/design-rationale/adr-004-mls-unaware-ds.md @@ -40,7 +40,7 @@ The RFC explicitly envisions that the DS operates on opaque blobs, not on decryp ## Decision -The quicnprotochat Delivery Service is **MLS-unaware**. It routes opaque byte strings by `(recipientKey, channelId)` without parsing, inspecting, or validating any MLS content. +The quicproquo Delivery Service is **MLS-unaware**. It routes opaque byte strings by `(recipientKey, channelId)` without parsing, inspecting, or validating any MLS content. ### What the DS sees @@ -109,8 +109,8 @@ This means that sending a message to a group of n members requires n-1 enqueue c |---|---| | `schemas/delivery.capnp` | DeliveryService RPC interface (opaque `Data` payloads) | | `schemas/node.capnp` | NodeService: `enqueue`, `fetch`, `fetchWait` methods | -| `crates/quicnprotochat-server/src/storage.rs` | Server-side queue storage (DashMap-based FIFO queues) | -| `crates/quicnprotochat-server/src/main.rs` | NodeService RPC handler implementation | +| `crates/quicproquo-server/src/storage.rs` | Server-side queue storage (DashMap-based FIFO queues) | +| `crates/quicproquo-server/src/main.rs` | NodeService RPC handler implementation | --- diff --git a/docs/src/design-rationale/adr-005-single-use-keypackages.md b/docs/src/design-rationale/adr-005-single-use-keypackages.md index e9de19e..55d16fe 100644 --- a/docs/src/design-rationale/adr-005-single-use-keypackages.md +++ b/docs/src/design-rationale/adr-005-single-use-keypackages.md @@ -100,8 +100,8 @@ This is a defense-in-depth measure. In practice, MLS's own signature verificatio |---|---| | `schemas/auth.capnp` | `AuthenticationService` interface: `uploadKeyPackage`, `fetchKeyPackage` | | `schemas/node.capnp` | `NodeService` interface: same methods with `Auth` parameter | -| `crates/quicnprotochat-server/src/storage.rs` | Server-side KeyPackage storage (DashMap-backed queue) | -| `crates/quicnprotochat-server/src/main.rs` | RPC handler: `fetchKeyPackage` implementation with atomic removal | +| `crates/quicproquo-server/src/storage.rs` | Server-side KeyPackage storage (DashMap-backed queue) | +| `crates/quicproquo-server/src/main.rs` | RPC handler: `fetchKeyPackage` implementation with atomic removal | --- diff --git a/docs/src/design-rationale/adr-006-rest-gateway.md b/docs/src/design-rationale/adr-006-rest-gateway.md new file mode 100644 index 0000000..c3bd8ee --- /dev/null +++ b/docs/src/design-rationale/adr-006-rest-gateway.md @@ -0,0 +1,116 @@ +# ADR-006: SDK-First Adoption — Native QUIC + Cap'n Proto, No REST Gateway + +## Status + +Accepted (supersedes earlier REST gateway proposal) + +## Context + +quicproquo uses QUIC + Cap'n Proto RPC as its native protocol. This +combination delivers zero-copy serialization, multiplexed streams, and +sub-RTT connection establishment — ideal for high-performance clients. + +Cap'n Proto has limited language support compared to Protocol Buffers or +JSON. Adding an HTTP/JSON REST gateway was considered to lower the barrier +to entry. However, this would: + +1. **Contradict the project identity.** The name is literally + **quic**n**proto**chat. An HTTP gateway undermines the protocol-native + philosophy. +2. **Add base64 overhead (~33%)** for binary payloads (MLS ciphertext, key + packages) that are already optimal in Cap'n Proto's wire format. +3. **Create a second code path** to maintain, test, and secure. +4. **Lose QUIC transport benefits** (0-RTT, multiplexing, congestion control) + for clients that use the HTTP path. + +## Decision + +**No REST/HTTP gateway.** Instead, invest in native QUIC + Cap'n Proto +SDKs for every viable language, plus WASM/FFI for the crypto layer. + +The `.capnp` schema files ARE the interface definition — they serve the +same role as an OpenAPI spec, but for the native protocol. + +### SDK strategy + +| Language | QUIC | Cap'n Proto | Approach | +|----------|------|-------------|----------| +| **Rust** | quinn | capnp-rpc | Existing reference client | +| **Go** | quic-go | go-capnp | Native SDK, high confidence | +| **Python** | aioquic | pycapnp | Native QUIC, manual RPC framing | +| **C/C++** | msquic/ngtcp2 | capnproto | Reference impl, full RPC | +| **Browser** | WebTransport | WASM bridge | QUIC transport via HTTP/3 | + +### Browser access via WebTransport + +Browsers cannot open raw QUIC connections, but they can use +**WebTransport** — which runs over HTTP/3 (which runs over QUIC). The +server adds a WebTransport listener alongside the Cap'n Proto QUIC +listener. Cap'n Proto RPC is framed over WebTransport bidirectional +streams, identical to the native path. + +``` +Browser → WebTransport (HTTP/3 over QUIC) → Cap'n Proto RPC → Server +Native → QUIC → Cap'n Proto RPC → Server +``` + +Both paths use QUIC transport. The project name stays honest. + +### Crypto layer distribution + +MLS encryption/decryption must happen client-side. The `quicproquo-core` +crate is compiled to: + +- **WASM** — for browsers, Node.js, Deno +- **C FFI** (`libquicproquo`) — for Swift, Kotlin, Python, Go (via cgo) +- **Native Rust** — for Rust clients (existing) + +### Why not REST? + +1. **Protocol purity.** One protocol, one code path, one mental model. +2. **No serialization tax.** No base64, no JSON parsing, no HTTP headers. +3. **QUIC everywhere.** WebTransport gives browsers QUIC access without HTTP + semantics leaking into the protocol. +4. **Schema-driven.** `.capnp` files generate type-safe stubs in every + supported language — the same developer experience as protobuf/gRPC. + +### Why not gRPC? + +1. **We already have Cap'n Proto** with zero-copy deserialization. +2. Adding protobuf would mean three serialization formats in one project. +3. Cap'n Proto's time-travel RPC (promise pipelining) is architecturally + superior to gRPC's request-response model for chained operations. + +gRPC may be reconsidered for server-to-server federation (Phase 7.3). + +## Consequences + +### Positive + +- **Protocol coherence.** Every client speaks the same wire format. +- **Performance.** No translation layer, no base64 overhead, no HTTP framing. +- **Identity.** The project name accurately describes the protocol stack. +- **Security.** One code path to audit, not two. +- **WebTransport.** Browsers get native QUIC with the same RPC interface. + +### Negative + +- **Higher SDK effort.** Each language needs a QUIC + Cap'n Proto + integration, not just an HTTP client. +- **Cap'n Proto ecosystem gaps.** JavaScript and Swift lack mature Cap'n + Proto RPC libraries; these languages rely on WASM bridges. +- **WebTransport maturity.** Browser support is good (Chrome, Edge, Firefox) + but not universal; Safari support is emerging. + +### Neutral + +- SDKs live in separate repositories (e.g., `quicproquo-go`, + `quicproquo-py`) to avoid bloating the core workspace. +- The C FFI crate (`quicproquo-ffi`) bundles both crypto and transport, + so language bindings only need to call C functions. + +## Related + +- [ADR-002: Cap'n Proto over MessagePack](adr-002-capnproto.md) — why Cap'n Proto was chosen +- [ROADMAP Phase 3](../../../ROADMAP.md) — SDK implementation plan +- [FUTURE-IMPROVEMENTS § 6.2](../../../docs/FUTURE-IMPROVEMENTS.md) — WebTransport research diff --git a/docs/src/design-rationale/overview.md b/docs/src/design-rationale/overview.md index 60e2104..f8a8046 100644 --- a/docs/src/design-rationale/overview.md +++ b/docs/src/design-rationale/overview.md @@ -1,6 +1,6 @@ # Design Decisions Overview -This section collects the Architecture Decision Records (ADRs) that document the key design choices in quicnprotochat. Each ADR follows a standard format: context (why the decision was needed), decision (what was chosen), and consequences (trade-offs, benefits, and residual risks). +This section collects the Architecture Decision Records (ADRs) that document the key design choices in quicproquo. Each ADR follows a standard format: context (why the decision was needed), decision (what was chosen), and consequences (trade-offs, benefits, and residual risks). These decisions are not immutable. Each ADR has a status field and can be superseded by a later ADR if circumstances change. The goal is to preserve the reasoning behind each choice so that future contributors understand *why* the system works the way it does, not just *how*. @@ -18,7 +18,7 @@ These decisions are not immutable. Each ADR has a status field and can be supers ## Design comparison -For a broader comparison of quicnprotochat's design against alternative messaging protocols (Signal, Matrix/Olm/Megolm), see [Why This Design, Not Signal/Matrix/...](why-not-signal.md). +For a broader comparison of quicproquo's design against alternative messaging protocols (Signal, Matrix/Olm/Megolm), see [Why This Design, Not Signal/Matrix/...](why-not-signal.md). --- diff --git a/docs/src/design-rationale/protocol-comparison.md b/docs/src/design-rationale/protocol-comparison.md index c49c528..6411414 100644 --- a/docs/src/design-rationale/protocol-comparison.md +++ b/docs/src/design-rationale/protocol-comparison.md @@ -1,6 +1,6 @@ # Comparison with Classical Chat Protocols -This page compares quicnprotochat against **classical and legacy chat protocols** -- IRC+SSL, XMPP (with and without OMEMO), Telegram's MTProto, and plain TCP/TLS chat systems -- to demonstrate what a modern, cryptographically rigorous design provides over protocols that were designed before end-to-end encryption, post-compromise security, and post-quantum readiness were practical concerns. +This page compares quicproquo against **classical and legacy chat protocols** -- IRC+SSL, XMPP (with and without OMEMO), Telegram's MTProto, and plain TCP/TLS chat systems -- to demonstrate what a modern, cryptographically rigorous design provides over protocols that were designed before end-to-end encryption, post-compromise security, and post-quantum readiness were practical concerns. For a comparison against modern E2E-encrypted protocols (Signal, Matrix/Olm/Megolm), see [Why This Design, Not Signal/Matrix/...](why-not-signal.md). @@ -9,7 +9,7 @@ For a comparison against modern E2E-encrypted protocols (Signal, Matrix/Olm/Mego ## At a glance ``` - Classical IRC+SSL quicnprotochat + Classical IRC+SSL quicproquo ───────────────── ────────────── You ──TLS──▶ Server ──TLS──▶ Bob You ──QUIC/TLS──▶ Server ──QUIC/TLS──▶ Bob @@ -19,13 +19,13 @@ For a comparison against modern E2E-encrypted protocols (Signal, Matrix/Olm/Mego messages (cannot decrypt) ``` -The fundamental difference: **classical protocols trust the server with your plaintext**. quicnprotochat's server is cryptographically excluded from reading message content. +The fundamental difference: **classical protocols trust the server with your plaintext**. quicproquo's server is cryptographically excluded from reading message content. --- ## Protocol comparison matrix -| Property | IRC+SSL | XMPP+TLS | XMPP+OMEMO | Telegram (MTProto) | quicnprotochat | +| Property | IRC+SSL | XMPP+TLS | XMPP+OMEMO | Telegram (MTProto) | quicproquo | |---|---|---|---|---|---| | **Transport encryption** | TLS (server-to-server optional) | STARTTLS / direct TLS | STARTTLS / direct TLS | MTProto 2.0 (custom) | QUIC + TLS 1.3 | | **End-to-end encryption** | None | None | Double Ratchet (1:1) | "Secret chats" only (1:1) | MLS RFC 9420 (groups native) | @@ -41,7 +41,7 @@ The fundamental difference: **classical protocols trust the server with your pla --- -## Deep dive: IRC+SSL vs. quicnprotochat +## Deep dive: IRC+SSL vs. quicproquo IRC (Internet Relay Chat) is the archetypal chat protocol, designed in 1988. Adding SSL/TLS wraps the TCP connection in transport encryption, but the protocol's security model remains fundamentally unchanged. @@ -66,7 +66,7 @@ IRC (Internet Relay Chat) is the archetypal chat protocol, designed in 1988. Add 4. **No post-compromise security.** There is no mechanism to recover from a key compromise. If a server is breached, all messages flowing through it are exposed indefinitely. 5. **No identity binding.** NickServ password authentication is plaintext over the IRC protocol (inside TLS, but visible to the server). There is no cryptographic binding between a user's identity and their messages. -### What happens when Alice sends a message on quicnprotochat +### What happens when Alice sends a message on quicproquo ``` ┌───────┐ ┌────────┐ ┌─────┐ @@ -93,7 +93,7 @@ IRC (Internet Relay Chat) is the archetypal chat protocol, designed in 1988. Add --- -## Deep dive: XMPP+OMEMO vs. quicnprotochat +## Deep dive: XMPP+OMEMO vs. quicproquo XMPP with OMEMO (XEP-0384) adds end-to-end encryption via the Signal Double Ratchet protocol. This is a significant improvement over plain XMPP, but OMEMO inherits the limitations of the Signal Protocol for group messaging. @@ -109,7 +109,7 @@ XMPP with OMEMO (XEP-0384) adds end-to-end encryption via the Signal Double Ratc 3 encryptions per message O(n) cost per send - quicnprotochat MLS group (4 members) + quicproquo MLS group (4 members) Alice encrypts once with group epoch key: ┌───────┐ ── MLS_encrypt(epoch_key) ──▶ Server @@ -120,7 +120,7 @@ XMPP with OMEMO (XEP-0384) adds end-to-end encryption via the Signal Double Ratc (all decrypt with same epoch key) ``` -| Property | XMPP+OMEMO groups | quicnprotochat MLS groups | +| Property | XMPP+OMEMO groups | quicproquo MLS groups | |---|---|---| | **Encryption per message** | O(n) -- encrypt once per recipient | O(1) -- single MLS application message | | **Add member** | O(n) -- distribute sender keys to all | O(log n) -- single MLS Commit | @@ -131,7 +131,7 @@ XMPP with OMEMO (XEP-0384) adds end-to-end encryption via the Signal Double Ratc --- -## Deep dive: Telegram (MTProto) vs. quicnprotochat +## Deep dive: Telegram (MTProto) vs. quicproquo Telegram is often perceived as a "secure" messenger, but its default mode provides **no end-to-end encryption**. Only "Secret Chats" (1:1 only, not available on desktop) use E2E encryption. @@ -163,7 +163,7 @@ Telegram is often perceived as a "secure" messenger, but its default mode provid ### Comparison -| Property | Telegram Cloud Chats | Telegram Secret Chats | quicnprotochat | +| Property | Telegram Cloud Chats | Telegram Secret Chats | quicproquo | |---|---|---|---| | **Server reads plaintext** | Yes | No | No | | **Group E2E** | No | N/A (1:1 only) | Yes (MLS) | @@ -173,7 +173,7 @@ Telegram is often perceived as a "secure" messenger, but its default mode provid | **Open source server** | No | No | Yes (MIT license) | | **Post-quantum** | None | None | Hybrid KEM (X25519 + ML-KEM-768) | -**Critical concern with Telegram:** MTProto is a custom, proprietary cryptographic protocol that has not undergone the same level of independent cryptographic review as standard protocols (TLS, MLS, Signal Protocol). Multiple academic papers have identified weaknesses in earlier versions. quicnprotochat exclusively uses IETF-standardized protocols (TLS 1.3, MLS RFC 9420) and widely reviewed cryptographic primitives. +**Critical concern with Telegram:** MTProto is a custom, proprietary cryptographic protocol that has not undergone the same level of independent cryptographic review as standard protocols (TLS, MLS, Signal Protocol). Multiple academic papers have identified weaknesses in earlier versions. quicproquo exclusively uses IETF-standardized protocols (TLS 1.3, MLS RFC 9420) and widely reviewed cryptographic primitives. --- @@ -208,7 +208,7 @@ An attacker gains root access to the chat server. │ sees metadata (who talks to whom, │ │ when, message sizes). │ │ │ -│ quicnprotochat: │ +│ quicproquo: │ │ Cannot read messages (MLS E2E). │ │ Sees metadata (recipient keys, │ │ timing, sizes). │ @@ -243,7 +243,7 @@ A state-level adversary records all encrypted traffic today, planning to decrypt Telegram (MTProto / custom DH): └── Quantum computer breaks DH → all recorded messages decrypted - quicnprotochat (Hybrid KEM): + quicproquo (Hybrid KEM): └── Transport: QUIC/TLS with ECDHE → quantum computer breaks this layer └── Inner layer: MLS content encrypted with group epoch keys └── Hybrid KEM envelope: X25519 + ML-KEM-768 @@ -252,7 +252,7 @@ A state-level adversary records all encrypted traffic today, planning to decrypt └── Combined key: STILL SECURE (both must be broken) ``` -quicnprotochat's hybrid "belt and suspenders" design means that **even if X25519 falls to a quantum computer, ML-KEM-768 protects the content**. The adversary's recorded ciphertext remains useless. +quicproquo's hybrid "belt and suspenders" design means that **even if X25519 falls to a quantum computer, ML-KEM-768 protects the content**. The adversary's recorded ciphertext remains useless. ### Scenario 3: Device theft / compromise @@ -279,7 +279,7 @@ An attacker steals Alice's unlocked device and extracts her key material. Messages after T: all accessible (cloud sync) Recovery: terminate session from another device - quicnprotochat: + quicproquo: Messages before T: protected (MLS forward secrecy, past epoch keys deleted) Messages after T: exposed only until next MLS epoch advance Recovery: ANY group member issues an MLS Update proposal → @@ -293,7 +293,7 @@ An attacker steals Alice's unlocked device and extracts her key material. ### Why QUIC over TCP -Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) blocking. quicnprotochat uses QUIC, which provides independent streams over UDP. +Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) blocking. quicproquo uses QUIC, which provides independent streams over UDP. ``` TCP (IRC/XMPP): all streams share one ordered byte stream @@ -308,7 +308,7 @@ Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) b └── ALL streams blocked until retransmit - QUIC (quicnprotochat): each stream is independent + QUIC (quicproquo): each stream is independent ────────────────────────────────────────────────── Stream A: ████████░░██████████████ (only A waits) @@ -334,7 +334,7 @@ Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) b ════════════════════════════════════════════════════ Total: 2-3 round trips before first message - quicnprotochat: QUIC integrates crypto into handshake = 1 RTT (or 0-RTT) + quicproquo: QUIC integrates crypto into handshake = 1 RTT (or 0-RTT) ────────────────────────────────────────────────────────────────────────── Client ──Initial(ClientHello)──▶ Server │ Client ◀──Initial(ServerHello)── Server │ 1 RTT total @@ -363,7 +363,7 @@ Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) b Phone number + SMS OTP ← carrier and Telegram see phone number (identity = phone number) ← no cryptographic identity - quicnprotochat (OPAQUE PAKE): + quicproquo (OPAQUE PAKE): Client ──blinded_element──▶ Server │ Server never sees password Client ◀──evaluated_element── Server │ Mutual authentication Client ──finalization──▶ Server │ Session key derived @@ -406,7 +406,7 @@ Classical protocols (IRC, XMPP) use TCP, which suffers from head-of-line (HOL) b │ Schema via XSD exists but rarely enforced at runtime. │ └──────────────────────────────────────────────────────────┘ - Cap'n Proto (quicnprotochat): + Cap'n Proto (quicproquo): ┌──────────────────────────────────────────────────────────┐ │ [8-byte aligned struct with pointers] │ │ │ @@ -434,7 +434,7 @@ The following diagram maps each protocol against the security properties it prov Telegram Cloud · · · · · · · · Telegram Secret △ · ● · · ● · · Signal ● · ● ● △ ● · · - quicnprotochat ● ● ● ● ● ● ● ● + quicproquo ● ● ● ● ● ● ● ● Legend: ● = yes △ = partial · = no FS = forward secrecy PCS = post-compromise security @@ -446,12 +446,12 @@ The following diagram maps each protocol against the security properties it prov --- -## The quicnprotochat advantage: a layered defense +## The quicproquo advantage: a layered defense -Classical protocols rely on a **single layer** of security (transport TLS). quicnprotochat applies defense in depth with **three independent layers**, each of which must be broken separately: +Classical protocols rely on a **single layer** of security (transport TLS). quicproquo applies defense in depth with **three independent layers**, each of which must be broken separately: ``` - IRC+SSL security layers: quicnprotochat security layers: + IRC+SSL security layers: quicproquo security layers: ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ TLS (transport) │ │ Layer 3: Hybrid KEM envelope │ @@ -474,7 +474,7 @@ Classical protocols rely on a **single layer** of security (transport TLS). quic To read a message, attacker must break: IRC+SSL: TLS (1 layer) - quicnprotochat: TLS + MLS + Hybrid KEM (3 layers) + quicproquo: TLS + MLS + Hybrid KEM (3 layers) ``` --- @@ -483,7 +483,7 @@ Classical protocols rely on a **single layer** of security (transport TLS). quic Fairness demands acknowledging where classical protocols genuinely excel: -| Advantage | IRC | quicnprotochat | +| Advantage | IRC | quicproquo | |---|---|---| | **Simplicity** | Telnet-compatible text protocol | Binary protocol requiring client implementation | | **Maturity** | 35+ years of production use | Early-stage research project | @@ -493,7 +493,7 @@ Fairness demands acknowledging where classical protocols genuinely excel: | **Public channels** | Designed for open, unencrypted discussion | Designed for private, encrypted communication | | **Anonymity** | No identity required | Requires Ed25519 identity keypair | -IRC remains an excellent choice for **public, open discussion** where encryption is not needed and simplicity is valued. quicnprotochat is designed for a different threat model: private communication where **confidentiality, forward secrecy, and post-compromise security** are requirements, not luxuries. +IRC remains an excellent choice for **public, open discussion** where encryption is not needed and simplicity is valued. quicproquo is designed for a different threat model: private communication where **confidentiality, forward secrecy, and post-compromise security** are requirements, not luxuries. --- @@ -501,10 +501,10 @@ IRC remains an excellent choice for **public, open discussion** where encryption For users and operators coming from classical chat systems, here is what changes practically: -| Concern | Classical (IRC/XMPP) | quicnprotochat | +| Concern | Classical (IRC/XMPP) | quicproquo | |---|---|---| -| **Server setup** | Install IRCd, configure TLS cert | `cargo build && ./quicnprotochat-server` (auto-generates TLS cert) | -| **Client setup** | Install any IRC client | `./quicnprotochat-client register-user` (generates Ed25519 identity) | +| **Server setup** | Install IRCd, configure TLS cert | `cargo build && ./qpq-server` (auto-generates TLS cert) | +| **Client setup** | Install any IRC client | `./quicproquo-client register-user` (generates Ed25519 identity) | | **Joining a group** | `/join #channel` | Receive MLS Welcome message from group creator | | **Sending a message** | Type and press enter | Same -- client handles MLS encryption transparently | | **Server admin sees messages** | Yes (always) | No (never -- server sees only ciphertext) | @@ -518,7 +518,7 @@ For users and operators coming from classical chat systems, here is what changes - [Why This Design, Not Signal/Matrix/...](why-not-signal.md) -- comparison with modern E2E-encrypted protocols - [Protocol Layers Overview](../protocol-layers/overview.md) -- detailed protocol stack documentation -- [Threat Model](../cryptography/threat-model.md) -- what quicnprotochat does and does not protect against +- [Threat Model](../cryptography/threat-model.md) -- what quicproquo does and does not protect against - [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md) -- hybrid KEM design and rationale - [MLS (RFC 9420)](../protocol-layers/mls.md) -- deep dive into the group key agreement protocol - [Architecture Overview](../architecture/overview.md) -- system-level architecture diff --git a/docs/src/design-rationale/why-not-signal.md b/docs/src/design-rationale/why-not-signal.md index 9a42160..8f1534d 100644 --- a/docs/src/design-rationale/why-not-signal.md +++ b/docs/src/design-rationale/why-not-signal.md @@ -1,6 +1,6 @@ # Why This Design, Not Signal/Matrix/... -This page compares quicnprotochat's protocol choices against two widely deployed secure messaging systems -- the Signal Protocol and the Matrix ecosystem (Olm/Megolm) -- to explain why a different architecture was chosen. The comparison covers four dimensions: group key agreement, transport, serialisation, and overall trade-offs. +This page compares quicproquo's protocol choices against two widely deployed secure messaging systems -- the Signal Protocol and the Matrix ecosystem (Olm/Megolm) -- to explain why a different architecture was chosen. The comparison covers four dimensions: group key agreement, transport, serialisation, and overall trade-offs. --- @@ -26,11 +26,11 @@ The Signal Protocol was designed for **1:1 messaging** and later extended to gro - Group membership changes require O(n) pairwise Sender Key distributions. Adding or removing a member requires the affected member to generate a new Sender Key and distribute it to all n-1 other members. - The pairwise key exchange for initial setup is O(n^2): each of n members must establish a Double Ratchet session with each of the other n-1 members. -**Limitations for quicnprotochat's use case:** +**Limitations for quicproquo's use case:** - O(n^2) pairwise setup cost limits practical group size. - No post-compromise security for groups is a significant gap. -- The protocol requires a central server for X3DH prekey bundle distribution (similar to quicnprotochat's AS, but tightly coupled to the Signal server). +- The protocol requires a central server for X3DH prekey bundle distribution (similar to quicproquo's AS, but tightly coupled to the Signal server). ### Matrix / Olm / Megolm @@ -54,15 +54,15 @@ The Matrix ecosystem uses two distinct cryptographic protocols: - **Eventually consistent state** model means that room membership, key sharing, and message ordering can diverge between homeservers. The client must reconcile these inconsistencies, adding complexity to the state machine. - **Device verification** is a persistent UX challenge. The cross-signing mechanism is powerful but difficult for users to understand. -**Limitations for quicnprotochat's use case:** +**Limitations for quicproquo's use case:** - No post-compromise security for groups (same limitation as Signal's Sender Keys). -- Federation adds latency, metadata exposure, and state management complexity that quicnprotochat does not need. +- Federation adds latency, metadata exposure, and state management complexity that quicproquo does not need. - JSON-based wire format is inefficient (see serialisation comparison below). -### quicnprotochat: MLS (RFC 9420) +### quicproquo: MLS (RFC 9420) -quicnprotochat uses the **Messaging Layer Security (MLS)** protocol, standardized as RFC 9420 by the IETF. +quicproquo uses the **Messaging Layer Security (MLS)** protocol, standardized as RFC 9420 by the IETF. **Key properties:** @@ -74,7 +74,7 @@ quicnprotochat uses the **Messaging Layer Security (MLS)** protocol, standardize **Cost of group operations:** -| Operation | Signal (Sender Keys) | Matrix (Megolm) | MLS (quicnprotochat) | +| Operation | Signal (Sender Keys) | Matrix (Megolm) | MLS (quicproquo) | |---|---|---|---| | Add member | O(n) Sender Key distributions | O(n) Megolm session shares | O(log n) tree update | | Remove member | O(n) Sender Key rotations | O(n) new Megolm session | O(log n) tree update | @@ -87,7 +87,7 @@ quicnprotochat uses the **Messaging Layer Security (MLS)** protocol, standardize The transport layer determines how encrypted payloads reach the server and how client-server authentication is performed. -| Property | Signal | Matrix | quicnprotochat | +| Property | Signal | Matrix | quicproquo | |---|---|---|---| | **Transport protocol** | TLS over TCP (HTTP/2) | HTTPS (TLS over TCP) | QUIC (UDP) + TLS 1.3 | | **Multiplexing** | HTTP/2 stream multiplexing | HTTP/1.1 or HTTP/2 | Native QUIC stream multiplexing | @@ -106,7 +106,7 @@ QUIC eliminates TCP head-of-line blocking, which is particularly important for a The serialisation format determines the overhead of encoding and decoding messages, the type safety of the wire format, and the feasibility of schema evolution. -| Property | Signal (Protobuf) | Matrix (JSON) | quicnprotochat (Cap'n Proto) | +| Property | Signal (Protobuf) | Matrix (JSON) | quicproquo (Cap'n Proto) | |---|---|---|---| | **Format** | Binary, schema-defined | Text, schema-optional (JSON Schema exists but is not enforced by the wire format) | Binary, schema-defined | | **Deserialization cost** | Requires a decode pass (allocates and copies) | Requires a parse pass (allocates, copies, and handles UTF-8) | **Zero-copy**: the wire bytes are the in-memory representation. Readers traverse pointers in-place. | @@ -118,7 +118,7 @@ The serialisation format determines the overhead of encoding and decoding messag **Why Cap'n Proto over Protobuf?** -While Protobuf is a reasonable choice (and Signal uses it successfully), Cap'n Proto provides two features that are particularly valuable for quicnprotochat: +While Protobuf is a reasonable choice (and Signal uses it successfully), Cap'n Proto provides two features that are particularly valuable for quicproquo: 1. **Zero-copy deserialization** eliminates a class of allocation and performance overhead. In a messaging system that processes many small messages, avoiding deserialization copies adds up. 2. **Built-in RPC** means that Cap'n Proto is both the serialisation format and the RPC framework. There is no need for a separate gRPC or HTTP layer. The same `.capnp` schema file defines both the data structures and the service interface. @@ -128,7 +128,7 @@ While Protobuf is a reasonable choice (and Signal uses it successfully), Cap'n P ## Summary comparison table -| Dimension | Signal | Matrix | quicnprotochat | +| Dimension | Signal | Matrix | quicproquo | |---|---|---|---| | **1:1 encryption** | Double Ratchet (FS + PCS) | Olm / Double Ratchet (FS + PCS) | MLS (FS + PCS) | | **Group encryption** | Sender Keys (FS only) | Megolm (FS only) | MLS (FS + PCS) | @@ -143,13 +143,13 @@ While Protobuf is a reasonable choice (and Signal uses it successfully), Cap'n P --- -## What quicnprotochat gives up +## What quicproquo gives up -No design is without trade-offs. Compared to Signal and Matrix, quicnprotochat: +No design is without trade-offs. Compared to Signal and Matrix, quicproquo: - **Has no federation.** A single server per deployment means no decentralized architecture. This is a deliberate simplification -- federation adds significant complexity and metadata exposure. -- **Is less mature.** Signal and Matrix have years of production hardening, formal security audits, and battle-tested implementations. quicnprotochat is in early development. -- **Has a smaller ecosystem.** Signal and Matrix have extensive client libraries, bridges, and integrations. quicnprotochat is a standalone Rust implementation. +- **Is less mature.** Signal and Matrix have years of production hardening, formal security audits, and battle-tested implementations. quicproquo is in early development. +- **Has a smaller ecosystem.** Signal and Matrix have extensive client libraries, bridges, and integrations. quicproquo is a standalone Rust implementation. - **Requires MLS client complexity.** MLS clients must maintain a ratchet tree, process Commits, and handle epoch transitions. This is more complex than a simple symmetric ratchet (Sender Keys / Megolm), though the complexity buys post-compromise security. --- @@ -158,6 +158,6 @@ No design is without trade-offs. Compared to Signal and Matrix, quicnprotochat: - [Design Decisions Overview](overview.md) -- index of all ADRs - [ADR-002: Cap'n Proto over MessagePack](adr-002-capnproto.md) -- serialisation format choice -- [Protocol Layers Overview](../protocol-layers/overview.md) -- how quicnprotochat's layers compose +- [Protocol Layers Overview](../protocol-layers/overview.md) -- how quicproquo's layers compose - [MLS (RFC 9420)](../protocol-layers/mls.md) -- deep dive into the MLS protocol layer - [Architecture Overview](../architecture/overview.md) -- system-level architecture diff --git a/docs/src/getting-started/building.md b/docs/src/getting-started/building.md index f507d80..db83b89 100644 --- a/docs/src/getting-started/building.md +++ b/docs/src/getting-started/building.md @@ -16,10 +16,10 @@ This compiles all four crates: | Crate | Type | Purpose | |---|---|---| -| `quicnprotochat-core` | library | Crypto primitives, MLS `GroupMember` state machine, hybrid KEM | -| `quicnprotochat-proto` | library | Cap'n Proto schemas, generated types, envelope serialisation helpers | -| `quicnprotochat-server` | binary | Unified Authentication + Delivery Service (`NodeService`) | -| `quicnprotochat-client` | binary | CLI client with subcommands (`ping`, `register`, `send`, `recv`, etc.) | +| `quicproquo-core` | library | Crypto primitives, MLS `GroupMember` state machine, hybrid KEM | +| `quicproquo-proto` | library | Cap'n Proto schemas, generated types, envelope serialisation helpers | +| `quicproquo-server` | binary | Unified Authentication + Delivery Service (`NodeService`) | +| `quicproquo-client` | binary | CLI client with subcommands (`ping`, `register`, `send`, `recv`, etc.) | For a release build with LTO, symbol stripping, and single codegen unit: @@ -47,25 +47,25 @@ cargo test --workspace The test suite includes: -- **`quicnprotochat-proto`**: Round-trip serialisation tests for Cap'n Proto `Envelope` messages (Ping, Pong, corrupted-input error handling). -- **`quicnprotochat-core`**: Two-party MLS round-trip (`create_group` / `add_member` / `send_message` / `receive_message`), group ID lifecycle assertions. -- **`quicnprotochat-client`**: Integration tests for MLS group operations and auth service interactions (require a running server or use in-process mocks). +- **`quicproquo-proto`**: Round-trip serialisation tests for Cap'n Proto `Envelope` messages (Ping, Pong, corrupted-input error handling). +- **`quicproquo-core`**: Two-party MLS round-trip (`create_group` / `add_member` / `send_message` / `receive_message`), group ID lifecycle assertions. +- **`quicproquo-client`**: Integration tests for MLS group operations and auth service interactions (require a running server or use in-process mocks). To run tests for a single crate: ```bash -cargo test -p quicnprotochat-core +cargo test -p quicproquo-core ``` --- ## Cap'n Proto code generation -The `quicnprotochat-proto` crate does not contain hand-written Rust types for wire messages. Instead, its `build.rs` script invokes the `capnp` compiler at build time to generate Rust source from the `.capnp` schema files. +The `quicproquo-proto` crate does not contain hand-written Rust types for wire messages. Instead, its `build.rs` script invokes the `capnp` compiler at build time to generate Rust source from the `.capnp` schema files. ### How it works -1. `build.rs` locates the workspace-root `schemas/` directory (two levels above `crates/quicnprotochat-proto/`). +1. `build.rs` locates the workspace-root `schemas/` directory (two levels above `crates/quicproquo-proto/`). 2. It invokes `capnpc::CompilerCommand` on all four schema files: - `schemas/envelope.capnp` -- top-level wire envelope with `MsgType` discriminant - `schemas/auth.capnp` -- `AuthenticationService` RPC interface @@ -82,7 +82,7 @@ The `build.rs` script emits `cargo:rerun-if-changed` directives for each schema The `src_prefix` is set to the `schemas/` directory so that inter-schema imports (e.g., `using Auth = import "auth.capnp".Auth;` inside `node.capnp`) resolve correctly. -### Design constraints of quicnprotochat-proto +### Design constraints of quicproquo-proto The proto crate is intentionally restricted: diff --git a/docs/src/getting-started/certificate-lifecycle.md b/docs/src/getting-started/certificate-lifecycle.md index 221f884..8e56f66 100644 --- a/docs/src/getting-started/certificate-lifecycle.md +++ b/docs/src/getting-started/certificate-lifecycle.md @@ -1,6 +1,6 @@ # 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. +This page describes how to use CA-issued certificates with quicproquo 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). @@ -8,8 +8,8 @@ For basic server TLS setup (self-signed certs, generation), see [Running the Ser ## 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. +- **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 (`QPQ_PRODUCTION=1`) requires existing cert and key files. +- **Client:** Trusts exactly the roots in the file given by `--ca-cert` (or `QPQ_CA_CERT`). Typically this is the server's own certificate (pinning) or a CA that signed the server cert. --- @@ -20,7 +20,7 @@ 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 ... + qpq --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. @@ -43,12 +43,12 @@ To use a certificate issued by a public CA (e.g. Let's Encrypt): ``` 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 + export QPQ_TLS_CERT=/etc/quicproquo/server-cert.der + export QPQ_TLS_KEY=/etc/quicproquo/server-key.der ``` 3. **Configure the client** to trust the CA that signed the server cert. Use the CA’s certificate (or the CA bundle) as `--ca-cert`: ```bash - quicnprotochat --ca-cert /etc/ssl/certs/your-ca.der --server-name your.server.example ... + qpq --ca-cert /etc/ssl/certs/your-ca.der --server-name your.server.example ... ``` The `--server-name` must match the certificate’s SAN (e.g. DNS name). @@ -60,7 +60,7 @@ To use a certificate issued by a public CA (e.g. Let's Encrypt): - **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. - **Let’s 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). +- **OCSP / CRL:** The quicproquo 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). --- @@ -70,6 +70,6 @@ To use a certificate issued by a public CA (e.g. Let's Encrypt): |------------------|-------------|--------------------| | Pinned (single server) | Self-signed or any | Server’s cert file | | CA-issued | Let’s Encrypt (or other CA) | CA cert (or bundle) | -| Production | Always use existing cert/key; set `QUICNPROTOCHAT_PRODUCTION=1` | CA or pinned server cert | +| Production | Always use existing cert/key; set `QPQ_PRODUCTION=1` | CA or pinned server cert | For production, prefer either (a) certificate pinning with the server’s cert or (b) a CA-issued server cert with clients trusting the CA, and plan for rotation and restart (or future reload support). diff --git a/docs/src/getting-started/demo-walkthrough.md b/docs/src/getting-started/demo-walkthrough.md index 2e76e4b..754ca7c 100644 --- a/docs/src/getting-started/demo-walkthrough.md +++ b/docs/src/getting-started/demo-walkthrough.md @@ -75,13 +75,13 @@ You will need **three terminal windows**: one for the server, one for Alice, and In **Terminal 1** (Server): ```bash -cargo run -p quicnprotochat-server +cargo run -p quicproquo-server ``` Wait for the log line confirming it is accepting connections: ``` -INFO quicnprotochat_server: accepting QUIC connections addr="0.0.0.0:7000" +INFO quicproquo_server: accepting QUIC connections addr="0.0.0.0:7000" ``` If this is the first run, you will also see a log line about generating the self-signed TLS certificate. The certificate is written to `data/server-cert.der`, which the client will use for TLS verification. @@ -91,7 +91,7 @@ If this is the first run, you will also see a log line about generating the self In **Terminal 2** (Alice): ```bash -cargo run -p quicnprotochat-client -- register-state \ +cargo run -p quicproquo-client -- register-state \ --state alice.bin \ --server 127.0.0.1:7000 ``` @@ -116,7 +116,7 @@ KeyPackage uploaded successfully. In **Terminal 3** (Bob): ```bash -cargo run -p quicnprotochat-client -- register-state \ +cargo run -p quicproquo-client -- register-state \ --state bob.bin \ --server 127.0.0.1:7000 ``` @@ -137,7 +137,7 @@ In **Terminal 2** (Alice): First, create the group: ```bash -cargo run -p quicnprotochat-client -- create-group \ +cargo run -p quicproquo-client -- create-group \ --state alice.bin \ --group-id "demo-chat" ``` @@ -151,7 +151,7 @@ Alice is now the sole member of the group at epoch 0. Next, invite Bob using his identity key from Step 3: ```bash -cargo run -p quicnprotochat-client -- invite \ +cargo run -p quicproquo-client -- invite \ --state alice.bin \ --peer-key \ --server 127.0.0.1:7000 @@ -173,7 +173,7 @@ Alice's group state has now advanced to epoch 1. In **Terminal 3** (Bob): ```bash -cargo run -p quicnprotochat-client -- join \ +cargo run -p quicproquo-client -- join \ --state bob.bin \ --server 127.0.0.1:7000 ``` @@ -195,7 +195,7 @@ Bob is now a member of the group at epoch 1, sharing the same group secret as Al In **Terminal 2** (Alice): ```bash -cargo run -p quicnprotochat-client -- send \ +cargo run -p quicproquo-client -- send \ --state alice.bin \ --peer-key \ --msg "Hello Bob, this is encrypted with MLS!" \ @@ -215,7 +215,7 @@ message sent In **Terminal 3** (Bob): ```bash -cargo run -p quicnprotochat-client -- recv \ +cargo run -p quicproquo-client -- recv \ --state bob.bin \ --server 127.0.0.1:7000 ``` @@ -233,7 +233,7 @@ This command: In **Terminal 3** (Bob): ```bash -cargo run -p quicnprotochat-client -- send \ +cargo run -p quicproquo-client -- send \ --state bob.bin \ --peer-key \ --msg "Hi Alice, received loud and clear!" \ @@ -249,7 +249,7 @@ message sent In **Terminal 2** (Alice): ```bash -cargo run -p quicnprotochat-client -- recv \ +cargo run -p quicproquo-client -- recv \ --state alice.bin \ --server 127.0.0.1:7000 ``` @@ -266,7 +266,7 @@ If you want to see the entire flow in a single command without managing three te ```bash # Ensure the server is running, then: -cargo run -p quicnprotochat-client -- demo-group --server 127.0.0.1:7000 +cargo run -p quicproquo-client -- demo-group --server 127.0.0.1:7000 ``` ``` diff --git a/docs/src/getting-started/docker.md b/docs/src/getting-started/docker.md index bc30469..4a21204 100644 --- a/docs/src/getting-started/docker.md +++ b/docs/src/getting-started/docker.md @@ -1,6 +1,6 @@ # Docker Deployment -quicnprotochat includes a multi-stage Dockerfile and a Docker Compose configuration for building and running the server in containers. +quicproquo includes a multi-stage Dockerfile and a Docker Compose configuration for building and running the server in containers. --- @@ -40,7 +40,7 @@ services: - "7000:7000" environment: RUST_LOG: "info" - QUICNPROTOCHAT_LISTEN: "0.0.0.0:7000" + QPQ_LISTEN: "0.0.0.0:7000" healthcheck: test: ["CMD", "bash", "-c", "echo '' > /dev/tcp/localhost/7000"] interval: 5s @@ -81,9 +81,9 @@ RUN apt-get update \ Key steps: 1. **Base image**: `rust:bookworm` (Debian Bookworm with the Rust toolchain pre-installed). -2. **Install `capnproto`**: Required by `quicnprotochat-proto/build.rs` to compile `.capnp` schemas at build time. +2. **Install `capnproto`**: Required by `quicproquo-proto/build.rs` to compile `.capnp` schemas at build time. 3. **Copy manifests first**: `Cargo.toml` and `Cargo.lock` are copied before source code. Dummy `main.rs` / `lib.rs` stubs are created so that `cargo build` can resolve and cache the dependency graph. This ensures that dependency compilation is cached in a separate Docker layer -- subsequent builds that only change source code skip the dependency compilation step entirely. -4. **Copy schemas**: The `schemas/` directory is copied before the dependency build because `quicnprotochat-proto/build.rs` requires the `.capnp` files during compilation. +4. **Copy schemas**: The `schemas/` directory is copied before the dependency build because `quicproquo-proto/build.rs` requires the `.capnp` files during compilation. 5. **Copy real source and build**: After the dependency cache layer, real source files are copied in and `cargo build --release` is run. ### Stage 2: Runtime (`debian:bookworm-slim`) @@ -95,16 +95,16 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /build/target/release/quicnprotochat-server /usr/local/bin/quicnprotochat-server +COPY --from=builder /build/target/release/qpq-server /usr/local/bin/qpq-server EXPOSE 7000 ENV RUST_LOG=info \ - QUICNPROTOCHAT_LISTEN=0.0.0.0:7000 + QPQ_LISTEN=0.0.0.0:7000 USER nobody -CMD ["quicnprotochat-server"] +CMD ["qpq-server"] ``` Key characteristics: @@ -112,9 +112,9 @@ Key characteristics: - **Minimal image**: No Rust toolchain, no `capnp` compiler, no build artifacts. - **`ca-certificates`**: Included for future HTTPS calls (e.g., ACME certificate provisioning or key sync endpoints). - **Non-root execution**: The container runs as `nobody` for defense in depth. -- **Default port**: The Dockerfile defaults to port `7000` via `QUICNPROTOCHAT_LISTEN`, but the `docker-compose.yml` overrides this to `7000` for consistency with the development workflow. +- **Default port**: The Dockerfile defaults to port `7000` via `QPQ_LISTEN`, but the `docker-compose.yml` overrides this to `7000` for consistency with the development workflow. -> **Note**: The `EXPOSE 7000` directive in the Dockerfile and the `QUICNPROTOCHAT_LISTEN=0.0.0.0:7000` override in `docker-compose.yml` mean the effective listen port is `7000` when using Compose. If you run the Docker image directly without Compose, the server will listen on `7000` by default. +> **Note**: The `EXPOSE 7000` directive in the Dockerfile and the `QPQ_LISTEN=0.0.0.0:7000` override in `docker-compose.yml` mean the effective listen port is `7000` when using Compose. If you run the Docker image directly without Compose, the server will listen on `7000` by default. --- @@ -129,7 +129,7 @@ services: volumes: - server-data:/data environment: - QUICNPROTOCHAT_DATA_DIR: "/data" + QPQ_DATA_DIR: "/data" volumes: server-data: @@ -140,7 +140,7 @@ Or use a bind mount for easier inspection: ```bash docker compose run \ -v $(pwd)/server-data:/data \ - -e QUICNPROTOCHAT_DATA_DIR=/data \ + -e QPQ_DATA_DIR=/data \ server ``` @@ -153,18 +153,18 @@ Without a volume, all server state (including TLS certificates and message queue To build the Docker image without starting a container: ```bash -docker build -t quicnprotochat-server -f docker/Dockerfile . +docker build -t qpq-server -f docker/Dockerfile . ``` To run it manually: ```bash docker run -d \ - --name quicnprotochat \ + --name quicproquo \ -p 7000:7000/udp \ - -e QUICNPROTOCHAT_LISTEN=0.0.0.0:7000 \ + -e QPQ_LISTEN=0.0.0.0:7000 \ -e RUST_LOG=info \ - quicnprotochat-server + qpq-server ``` Note the `/udp` suffix on the port mapping -- QUIC runs over UDP. @@ -180,7 +180,7 @@ When the server runs in Docker with `docker compose up`, the client can connect docker compose cp server:/data/server-cert.der ./data/server-cert.der # Connect -cargo run -p quicnprotochat-client -- ping \ +cargo run -p quicproquo-client -- ping \ --ca-cert ./data/server-cert.der \ --server-name localhost ``` diff --git a/docs/src/getting-started/prerequisites.md b/docs/src/getting-started/prerequisites.md index 3bd9ae8..aaab6aa 100644 --- a/docs/src/getting-started/prerequisites.md +++ b/docs/src/getting-started/prerequisites.md @@ -1,6 +1,6 @@ # Prerequisites -Before building quicnprotochat you need a Rust toolchain and the Cap'n Proto schema compiler. Docker is optional but useful for reproducible builds and deployment. +Before building quicproquo you need a Rust toolchain and the Cap'n Proto schema compiler. Docker is optional but useful for reproducible builds and deployment. --- @@ -8,7 +8,7 @@ Before building quicnprotochat you need a Rust toolchain and the Cap'n Proto sch **Minimum supported Rust version: 1.77+ (stable)** -quicnprotochat uses the 2021 edition and workspace resolver v2. Any stable Rust release from 1.77 onward should work. Install or update via [rustup](https://rustup.rs/): +quicproquo uses the 2021 edition and workspace resolver v2. Any stable Rust release from 1.77 onward should work. Install or update via [rustup](https://rustup.rs/): ```bash # Install rustup (if not already present) @@ -29,7 +29,7 @@ The workspace depends on several crates that use procedural macros (`serde_deriv ## Cap'n Proto compiler (`capnp`) -The `quicnprotochat-proto` crate runs a `build.rs` script that invokes the `capnp` binary at compile time to generate Rust types from the `.capnp` schema files in `schemas/`. The `capnp` binary must be on your `PATH`. +The `quicproquo-proto` crate runs a `build.rs` script that invokes the `capnp` binary at compile time to generate Rust types from the `.capnp` schema files in `schemas/`. The `capnp` binary must be on your `PATH`. ### Debian / Ubuntu @@ -74,7 +74,7 @@ See [Building from Source -- Troubleshooting](building.md#troubleshooting) for m ## Optional: Docker and Docker Compose -If you prefer to build and run quicnprotochat in containers, you will need: +If you prefer to build and run quicproquo in containers, you will need: - **Docker Engine** 20.10+ (or Docker Desktop) - **Docker Compose** v2+ (the `docker compose` plugin, not the legacy `docker-compose` binary) diff --git a/docs/src/getting-started/running-the-client.md b/docs/src/getting-started/running-the-client.md index 0b62a73..a3dfa00 100644 --- a/docs/src/getting-started/running-the-client.md +++ b/docs/src/getting-started/running-the-client.md @@ -1,6 +1,6 @@ # Running the Client -The quicnprotochat CLI client provides subcommands for connectivity testing, identity registration, KeyPackage exchange, and persistent group messaging. All commands connect to the server over QUIC + TLS 1.3 and issue Cap'n Proto RPC calls against the `NodeService` endpoint. +The quicproquo CLI client provides subcommands for connectivity testing, identity registration, KeyPackage exchange, and persistent group messaging. All commands connect to the server over QUIC + TLS 1.3 and issue Cap'n Proto RPC calls against the `NodeService` endpoint. --- @@ -10,8 +10,8 @@ These flags apply to every subcommand: | Flag | Env var | Default | Purpose | |---|---|---|---| -| `--ca-cert` | `QUICNPROTOCHAT_CA_CERT` | `data/server-cert.der` | Path to the server's TLS certificate (DER format). The client uses this to verify the server's identity during the TLS handshake. | -| `--server-name` | `QUICNPROTOCHAT_SERVER_NAME` | `localhost` | Expected TLS server name. Must match a SAN in the server's certificate. | +| `--ca-cert` | `QPQ_CA_CERT` | `data/server-cert.der` | Path to the server's TLS certificate (DER format). The client uses this to verify the server's identity during the TLS handshake. | +| `--server-name` | `QPQ_SERVER_NAME` | `localhost` | Expected TLS server name. Must match a SAN in the server's certificate. | Most subcommands also accept `--server` (default `127.0.0.1:7000`) to specify the server address. @@ -24,11 +24,11 @@ Most subcommands also accept `--server` (default `127.0.0.1:7000`) to specify th Send a health probe to the server and print the round-trip time. ```bash -cargo run -p quicnprotochat-client -- ping +cargo run -p quicproquo-client -- ping ``` ```bash -cargo run -p quicnprotochat-client -- ping --server 192.168.1.10:7000 +cargo run -p quicproquo-client -- ping --server 192.168.1.10:7000 ``` **Output:** @@ -49,7 +49,7 @@ These commands generate a fresh identity keypair in memory each time they run. T Generate a fresh Ed25519 identity, create an MLS KeyPackage, and upload it to the Authentication Service. ```bash -cargo run -p quicnprotochat-client -- register +cargo run -p quicproquo-client -- register ``` **Output:** @@ -66,7 +66,7 @@ Share the `identity_key` value with peers who want to add you to a group. They w Fetch a peer's KeyPackage from the Authentication Service by their Ed25519 public key. ```bash -cargo run -p quicnprotochat-client -- fetch-key a1b2c3d4e5f6... +cargo run -p quicproquo-client -- fetch-key a1b2c3d4e5f6... ``` The `identity_key` argument must be exactly 64 lowercase hex characters (32 bytes). @@ -90,7 +90,7 @@ KeyPackages are single-use: fetching a KeyPackage atomically removes it from the Run a complete Alice-and-Bob MLS round-trip against a live server. Both identities are created in-process; both communicate through the server's AS and DS. ```bash -cargo run -p quicnprotochat-client -- demo-group --server 127.0.0.1:7000 +cargo run -p quicproquo-client -- demo-group --server 127.0.0.1:7000 ``` **Output:** @@ -106,21 +106,21 @@ This is the fastest way to verify that the entire stack (QUIC + TLS + Cap'n Prot ## Persistent group commands -These commands use a state file (`--state`, default `quicnprotochat-state.bin`) to persist the Ed25519 identity seed and MLS group state between invocations. A companion key store file (same path with `.ks` extension) holds HPKE init private keys. +These commands use a state file (`--state`, default `qpq-state.bin`) to persist the Ed25519 identity seed and MLS group state between invocations. A companion key store file (same path with `.ks` extension) holds HPKE init private keys. All persistent commands share the `--state` flag: | Flag | Env var | Default | |---|---|---| -| `--state` | `QUICNPROTOCHAT_STATE` | `quicnprotochat-state.bin` | -| `--server` | `QUICNPROTOCHAT_SERVER` | `127.0.0.1:7000` | +| `--state` | `QPQ_STATE` | `qpq-state.bin` | +| `--server` | `QPQ_SERVER` | `127.0.0.1:7000` | ### `register-state` Create or load a persistent identity, generate a KeyPackage, and upload it to the AS. ```bash -cargo run -p quicnprotochat-client -- register-state \ +cargo run -p quicproquo-client -- register-state \ --state alice.bin \ --server 127.0.0.1:7000 ``` @@ -141,10 +141,10 @@ Refresh the KeyPackage on the server using your **existing** state file. Does no - Your KeyPackage has expired (server TTL, e.g. 24h). - Your KeyPackage was consumed (someone invited you) and you want to be invitable again. -Run with the same `--access-token` (or `QUICNPROTOCHAT_ACCESS_TOKEN`) as for other commands. +Run with the same `--access-token` (or `QPQ_ACCESS_TOKEN`) as for other commands. ```bash -cargo run -p quicnprotochat-client -- refresh-keypackage \ +cargo run -p quicproquo-client -- refresh-keypackage \ --state alice.bin \ --server 127.0.0.1:7000 ``` @@ -163,7 +163,7 @@ If you are told "no key" when someone tries to invite you, have them wait and ru Create a new MLS group. The caller becomes the sole member at epoch 0. ```bash -cargo run -p quicnprotochat-client -- create-group \ +cargo run -p quicproquo-client -- create-group \ --state alice.bin \ --group-id "project-chat" ``` @@ -180,7 +180,7 @@ The group state is saved to the state file. You can now invite peers with `invit Fetch a peer's KeyPackage from the AS, add them to the group, and deliver the Welcome message via the DS. ```bash -cargo run -p quicnprotochat-client -- invite \ +cargo run -p quicproquo-client -- invite \ --state alice.bin \ --peer-key b9a8c7d6e5f4... \ --server 127.0.0.1:7000 @@ -202,7 +202,7 @@ invited peer (welcome queued) Join a group by consuming a Welcome message from the DS. ```bash -cargo run -p quicnprotochat-client -- join \ +cargo run -p quicproquo-client -- join \ --state bob.bin \ --server 127.0.0.1:7000 ``` @@ -219,7 +219,7 @@ joined group successfully Encrypt and send an application message to a peer via the DS. ```bash -cargo run -p quicnprotochat-client -- send \ +cargo run -p quicproquo-client -- send \ --state alice.bin \ --peer-key b9a8c7d6e5f4... \ --msg "hello from alice" \ @@ -238,7 +238,7 @@ message sent Receive and decrypt all pending messages from the DS. ```bash -cargo run -p quicnprotochat-client -- recv \ +cargo run -p quicproquo-client -- recv \ --state bob.bin \ --server 127.0.0.1:7000 ``` @@ -257,12 +257,12 @@ Additional flags: ```bash # Wait up to 5 seconds for messages -cargo run -p quicnprotochat-client -- recv \ +cargo run -p quicproquo-client -- recv \ --state bob.bin \ --wait-ms 5000 # Stream messages continuously -cargo run -p quicnprotochat-client -- recv \ +cargo run -p quicproquo-client -- recv \ --state bob.bin \ --stream --wait-ms 10000 ``` @@ -271,7 +271,7 @@ cargo run -p quicnprotochat-client -- recv \ ## HPKE init key lifecycle warning -The MLS protocol requires that the HPKE init private key generated during KeyPackage creation is available when processing the corresponding Welcome message. In quicnprotochat, this private key is stored in the key store file (`.ks` extension alongside the state file). +The MLS protocol requires that the HPKE init private key generated during KeyPackage creation is available when processing the corresponding Welcome message. In quicproquo, this private key is stored in the key store file (`.ks` extension alongside the state file). **The same state file and key store must be used for both `register-state` and `join`.** If you: diff --git a/docs/src/getting-started/running-the-server.md b/docs/src/getting-started/running-the-server.md index 4cc70c8..e53cf69 100644 --- a/docs/src/getting-started/running-the-server.md +++ b/docs/src/getting-started/running-the-server.md @@ -1,13 +1,13 @@ # Running the Server -The quicnprotochat server is a single binary (`quicnprotochat-server`) that exposes a unified **NodeService** endpoint combining Authentication Service (KeyPackage management) and Delivery Service (message relay) operations over a single QUIC + TLS 1.3 connection. +The quicproquo server is a single binary (`qpq-server`) that exposes a unified **NodeService** endpoint combining Authentication Service (KeyPackage management) and Delivery Service (message relay) operations over a single QUIC + TLS 1.3 connection. --- ## Quick start ```bash -cargo run -p quicnprotochat-server +cargo run -p quicproquo-server ``` On first launch the server will: @@ -20,8 +20,8 @@ On first launch the server will: You should see output similar to: ``` -2025-01-01T00:00:00.000000Z INFO quicnprotochat_server: generated self-signed TLS certificate cert="data/server-cert.der" key="data/server-key.der" -2025-01-01T00:00:00.000000Z INFO quicnprotochat_server: accepting QUIC connections addr="0.0.0.0:7000" +2025-01-01T00:00:00.000000Z INFO quicproquo_server: generated self-signed TLS certificate cert="data/server-cert.der" key="data/server-key.der" +2025-01-01T00:00:00.000000Z INFO quicproquo_server: accepting QUIC connections addr="0.0.0.0:7000" ``` --- @@ -32,36 +32,36 @@ All configuration is available via CLI flags and environment variables. Environm | Purpose | CLI flag | Env var | Default | |---|---|---|---| -| QUIC listen address | `--listen` | `QUICNPROTOCHAT_LISTEN` | `0.0.0.0:7000` | -| TLS certificate (DER) | `--tls-cert` | `QUICNPROTOCHAT_TLS_CERT` | `data/server-cert.der` | -| TLS private key (DER) | `--tls-key` | `QUICNPROTOCHAT_TLS_KEY` | `data/server-key.der` | -| Data directory | `--data-dir` | `QUICNPROTOCHAT_DATA_DIR` | `data` | +| QUIC listen address | `--listen` | `QPQ_LISTEN` | `0.0.0.0:7000` | +| TLS certificate (DER) | `--tls-cert` | `QPQ_TLS_CERT` | `data/server-cert.der` | +| TLS private key (DER) | `--tls-key` | `QPQ_TLS_KEY` | `data/server-key.der` | +| Data directory | `--data-dir` | `QPQ_DATA_DIR` | `data` | | Log level | -- | `RUST_LOG` | `info` | ### Examples ```bash # Listen on a custom port -cargo run -p quicnprotochat-server -- --listen 0.0.0.0:9000 +cargo run -p quicproquo-server -- --listen 0.0.0.0:9000 # Use pre-existing TLS credentials -cargo run -p quicnprotochat-server -- \ - --tls-cert /etc/quicnprotochat/cert.der \ - --tls-key /etc/quicnprotochat/key.der +cargo run -p quicproquo-server -- \ + --tls-cert /etc/quicproquo/cert.der \ + --tls-key /etc/quicproquo/key.der # Via environment variables -QUICNPROTOCHAT_LISTEN=0.0.0.0:9000 \ +QPQ_LISTEN=0.0.0.0:9000 \ RUST_LOG=debug \ -cargo run -p quicnprotochat-server +cargo run -p quicproquo-server ``` ### Production deployment -Set `QUICNPROTOCHAT_PRODUCTION=1` (or `true` / `yes`) so the server enforces production checks: +Set `QPQ_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. +- **Auth:** A non-empty `QPQ_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. +- **SQL store:** When `--store-backend=sql`, a non-empty `QPQ_DB_KEY` is required. An empty key leaves the database unencrypted on disk and is not acceptable for production. --- @@ -88,7 +88,7 @@ To use a certificate issued by a CA or a custom self-signed certificate: ``` 2. Point the server at them: ```bash - cargo run -p quicnprotochat-server -- \ + cargo run -p quicproquo-server -- \ --tls-cert cert.der \ --tls-key key.der ``` @@ -128,7 +128,7 @@ The server persists its state to the data directory (`--data-dir`, default `data | `data/deliveries.bin` | `bincode`-serialised map of `(channelId, recipientKey)` to message queues | | `data/hybridkeys.bin` | `bincode`-serialised map of identity keys to hybrid (X25519 + ML-KEM-768) public keys | -Storage is implemented by the `FileBackedStore` in `crates/quicnprotochat-server/src/storage.rs`. Every mutation (upload, enqueue, fetch) flushes the entire map to disk synchronously. This is suitable for proof-of-concept workloads but not production traffic. See [Storage Backend](../internals/storage-backend.md) for details. +Storage is implemented by the `FileBackedStore` in `crates/quicproquo-server/src/storage.rs`. Every mutation (upload, enqueue, fetch) flushes the entire map to disk synchronously. This is suitable for proof-of-concept workloads but not production traffic. See [Storage Backend](../internals/storage-backend.md) for details. --- @@ -153,16 +153,16 @@ The server uses `tracing` with `tracing-subscriber` and respects the `RUST_LOG` ```bash # Default: info level -RUST_LOG=info cargo run -p quicnprotochat-server +RUST_LOG=info cargo run -p quicproquo-server # Debug level for detailed RPC tracing -RUST_LOG=debug cargo run -p quicnprotochat-server +RUST_LOG=debug cargo run -p quicproquo-server # Trace level for maximum verbosity -RUST_LOG=trace cargo run -p quicnprotochat-server +RUST_LOG=trace cargo run -p quicproquo-server # Filter to specific crates -RUST_LOG=quicnprotochat_server=debug,quinn=warn cargo run -p quicnprotochat-server +RUST_LOG=quicproquo_server=debug,quinn=warn cargo run -p quicproquo-server ``` --- diff --git a/docs/src/internals/authentication-service.md b/docs/src/internals/authentication-service.md index d449365..0627d6d 100644 --- a/docs/src/internals/authentication-service.md +++ b/docs/src/internals/authentication-service.md @@ -9,8 +9,8 @@ This page covers the server-side implementation of KeyPackage storage, the `Auth` struct validation logic, and the hybrid key endpoints. **Sources:** -- `crates/quicnprotochat-server/src/main.rs` (RPC handlers, auth validation) -- `crates/quicnprotochat-server/src/storage.rs` (FileBackedStore) +- `crates/quicproquo-server/src/main.rs` (RPC handlers, auth validation) +- `crates/quicproquo-server/src/storage.rs` (FileBackedStore) - `schemas/node.capnp` (wire schema) --- @@ -148,8 +148,8 @@ Configured via CLI flags / environment variables: | Flag / Env Var | Default | Purpose | |-----------------------------------|---------|---------| -| `--auth-token` / `QUICNPROTOCHAT_AUTH_TOKEN` | None | Required bearer token. If unset, any non-empty token is accepted for version 1. | -| `--allow-auth-v0` / `QUICNPROTOCHAT_ALLOW_AUTH_V0` | `true` | Whether to accept `auth.version=0` (legacy, unauthenticated) requests. | +| `--auth-token` / `QPQ_AUTH_TOKEN` | None | Required bearer token. If unset, any non-empty token is accepted for version 1. | +| `--allow-auth-v0` / `QPQ_ALLOW_AUTH_V0` | `true` | Whether to accept `auth.version=0` (legacy, unauthenticated) requests. | ### Version Semantics diff --git a/docs/src/internals/delivery-service.md b/docs/src/internals/delivery-service.md index 9a7a1c0..85214b9 100644 --- a/docs/src/internals/delivery-service.md +++ b/docs/src/internals/delivery-service.md @@ -6,8 +6,8 @@ recipient identity key and channel identifier. The DS exposes three operations through the `NodeService` RPC interface: `enqueue`, `fetch`, and `fetchWait`. **Sources:** -- `crates/quicnprotochat-server/src/main.rs` (RPC handlers) -- `crates/quicnprotochat-server/src/storage.rs` (queue storage) +- `crates/quicproquo-server/src/main.rs` (RPC handlers) +- `crates/quicproquo-server/src/storage.rs` (queue storage) - `schemas/node.capnp` (wire schema) --- diff --git a/docs/src/internals/group-member-lifecycle.md b/docs/src/internals/group-member-lifecycle.md index 745c140..a72298a 100644 --- a/docs/src/internals/group-member-lifecycle.md +++ b/docs/src/internals/group-member-lifecycle.md @@ -1,12 +1,12 @@ # GroupMember Lifecycle -The `GroupMember` struct in `quicnprotochat-core` is the core MLS state machine +The `GroupMember` struct in `quicproquo-core` is the core MLS state machine that manages a single client's membership in an MLS group. It wraps an openmls `MlsGroup`, a persistent crypto backend, and the long-term Ed25519 identity keypair. Every MLS operation -- key package generation, group creation, member addition, joining, sending, and receiving -- flows through this struct. -**Source:** `crates/quicnprotochat-core/src/group.rs` +**Source:** `crates/quicproquo-core/src/group.rs` --- diff --git a/docs/src/internals/keypackage-exchange.md b/docs/src/internals/keypackage-exchange.md index 69d6b1b..9492ee1 100644 --- a/docs/src/internals/keypackage-exchange.md +++ b/docs/src/internals/keypackage-exchange.md @@ -3,7 +3,7 @@ MLS KeyPackages are single-use tokens that enable a group creator to add a new member. The KeyPackage contains the member's HPKE init public key, their MLS credential (Ed25519 public key), and a signature proving ownership. The -quicnprotochat Authentication Service (AS) provides a simple upload/fetch +quicproquo Authentication Service (AS) provides a simple upload/fetch interface for distributing KeyPackages between clients. **Expiry and refresh:** KeyPackages are consumed on fetch (single-use). The server may also enforce a TTL (e.g. 24h). Clients should upload a fresh KeyPackage periodically or on demand so they remain invitable. The CLI provides `refresh-keypackage`: load existing state, generate a new KeyPackage, upload to the AS. See [Running the Client](../getting-started/running-the-client.md#refresh-keypackage). @@ -12,10 +12,10 @@ This page describes the end-to-end flow: from client-side generation through server-side storage to peer-side retrieval and consumption. **Sources:** -- `crates/quicnprotochat-core/src/group.rs` (client-side generation) -- `crates/quicnprotochat-server/src/main.rs` (server-side handlers) -- `crates/quicnprotochat-server/src/storage.rs` (server-side persistence) -- `crates/quicnprotochat-client/src/lib.rs` (client-side RPC calls) +- `crates/quicproquo-core/src/group.rs` (client-side generation) +- `crates/quicproquo-server/src/main.rs` (server-side handlers) +- `crates/quicproquo-server/src/storage.rs` (server-side persistence) +- `crates/quicproquo-client/src/lib.rs` (client-side RPC calls) - `schemas/node.capnp` (wire schema) --- @@ -274,10 +274,10 @@ identity; `register-state` loads from (or initializes) a persistent state file. ```bash # Ephemeral registration (for testing) -quicnprotochat register --server 127.0.0.1:7000 +qpq register --server 127.0.0.1:7000 # Persistent registration (production) -quicnprotochat register-state --state alice.bin --server 127.0.0.1:7000 +qpq register-state --state alice.bin --server 127.0.0.1:7000 ``` Output: @@ -292,7 +292,7 @@ KeyPackage uploaded successfully. Fetches a peer's KeyPackage by their hex-encoded Ed25519 public key: ```bash -quicnprotochat fetch-key --server 127.0.0.1:7000 7a3f... +qpq fetch-key --server 127.0.0.1:7000 7a3f... ``` --- diff --git a/docs/src/internals/storage-backend.md b/docs/src/internals/storage-backend.md index aac5fab..f549ed7 100644 --- a/docs/src/internals/storage-backend.md +++ b/docs/src/internals/storage-backend.md @@ -1,14 +1,14 @@ # Storage Backend -quicnprotochat uses two storage backends: `FileBackedStore` on the server side +quicproquo uses two storage backends: `FileBackedStore` on the server side for KeyPackages and delivery queues, and `DiskKeyStore` on the client side for MLS cryptographic key material. Both follow the same pattern: in-memory data structures backed by optional file persistence, with full serialization on every write. **Sources:** -- `crates/quicnprotochat-server/src/storage.rs` (FileBackedStore) -- `crates/quicnprotochat-core/src/keystore.rs` (DiskKeyStore, StoreCrypto) +- `crates/quicproquo-server/src/storage.rs` (FileBackedStore) +- `crates/quicproquo-core/src/keystore.rs` (DiskKeyStore, StoreCrypto) --- @@ -52,7 +52,7 @@ File paths: - `{dir}/hybridkeys.bin` -- Hybrid public keys The default data directory is `data/`, configurable via `--data-dir` / -`QUICNPROTOCHAT_DATA_DIR`. +`QPQ_DATA_DIR`. ### Flush-on-Every-Write @@ -233,7 +233,7 @@ fn keystore_path(state_path: &Path) -> PathBuf { } ``` -So `quicnprotochat-state.bin` produces a key store at `quicnprotochat-state.ks`. +So `qpq-state.bin` produces a key store at `quicproquo-state.ks`. ### Persistence Format diff --git a/docs/src/introduction.md b/docs/src/introduction.md index 3980a60..7e0dc5a 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -1,6 +1,6 @@ # Introduction -**quicnprotochat** is a research-oriented, end-to-end encrypted group messaging system written in Rust. It layers the Messaging Layer Security protocol (MLS, [RFC 9420](https://datatracker.ietf.org/doc/rfc9420/)) on top of QUIC + TLS 1.3 transport (via [quinn](https://github.com/quinn-rs/quinn) and [rustls](https://github.com/rustls/rustls)), with all service RPCs and wire messages framed using [Cap'n Proto](https://capnproto.org/). The project exists to explore how modern transport encryption (QUIC), a formally specified group key agreement protocol (MLS), and a zero-copy serialisation format (Cap'n Proto) compose in practice -- and to provide a readable, auditable reference implementation for security researchers, protocol designers, and Rust developers who want to study or extend the design. +**quicproquo** is a research-oriented, end-to-end encrypted group messaging system written in Rust. It layers the Messaging Layer Security protocol (MLS, [RFC 9420](https://datatracker.ietf.org/doc/rfc9420/)) on top of QUIC + TLS 1.3 transport (via [quinn](https://github.com/quinn-rs/quinn) and [rustls](https://github.com/rustls/rustls)), with all service RPCs and wire messages framed using [Cap'n Proto](https://capnproto.org/). The project exists to explore how modern transport encryption (QUIC), a formally specified group key agreement protocol (MLS), and a zero-copy serialisation format (Cap'n Proto) compose in practice -- and to provide a readable, auditable reference implementation for security researchers, protocol designers, and Rust developers who want to study or extend the design. --- @@ -20,7 +20,7 @@ Each layer addresses a distinct concern: 1. **QUIC + TLS 1.3** provides authenticated, confidential transport with 0-RTT connection establishment and multiplexed streams. The server presents a TLS 1.3 certificate (self-signed by default); the client verifies it against a local trust anchor. ALPN negotiation uses the token `b"capnp"`. -2. **Cap'n Proto RPC** defines the wire schema for all service operations (KeyPackage upload/fetch, message enqueue/fetch, health probes). Schemas live in `schemas/*.capnp` and are compiled to Rust at build time. Because Cap'n Proto uses a pointer-based layout, messages can be read without an unpacking step -- though quicnprotochat currently uses the unpacked wire format for simplicity. +2. **Cap'n Proto RPC** defines the wire schema for all service operations (KeyPackage upload/fetch, message enqueue/fetch, health probes). Schemas live in `schemas/*.capnp` and are compiled to Rust at build time. Because Cap'n Proto uses a pointer-based layout, messages can be read without an unpacking step -- though quicproquo currently uses the unpacked wire format for simplicity. 3. **MLS (RFC 9420)** provides the group key agreement layer. Each participant holds an Ed25519 identity keypair and generates single-use HPKE KeyPackages. The MLS epoch ratchet delivers forward secrecy and post-compromise security: compromising a member's state at epoch *n* does not reveal plaintext from epochs *< n* (forward secrecy) or *> n+1* (post-compromise security, once the compromised member updates). @@ -49,7 +49,7 @@ For a deeper discussion of the cryptographic guarantees, threat model, and known **Security researchers** studying how MLS composes with QUIC transport and Cap'n Proto framing. The codebase is intentionally small (four crates, ~2 500 lines of non-generated Rust) so that every cryptographic boundary is auditable. -**Protocol designers** evaluating MLS deployment patterns. quicnprotochat implements a concrete Authentication Service (AS) and Delivery Service (DS) pair, demonstrating single-use KeyPackage lifecycle, Welcome routing, and epoch advancement in a live system. +**Protocol designers** evaluating MLS deployment patterns. quicproquo implements a concrete Authentication Service (AS) and Delivery Service (DS) pair, demonstrating single-use KeyPackage lifecycle, Welcome routing, and epoch advancement in a live system. **Rust developers** looking for a working example of: @@ -64,7 +64,7 @@ For a deeper discussion of the cryptographic guarantees, threat model, and known | Section | What you will find | |---|---| -| **[Comparison with Classical Protocols](design-rationale/protocol-comparison.md)** | **Why quicnprotochat? IRC+SSL, XMPP, Telegram vs. our design** | +| **[Comparison with Classical Protocols](design-rationale/protocol-comparison.md)** | **Why quicproquo? IRC+SSL, XMPP, Telegram vs. our design** | | [Prerequisites](getting-started/prerequisites.md) | Toolchain and system dependencies | | [Building from Source](getting-started/building.md) | `cargo build`, Cap'n Proto codegen, troubleshooting | | [Running the Server](getting-started/running-the-server.md) | Server startup, configuration, TLS cert generation | @@ -82,7 +82,7 @@ For a deeper discussion of the cryptographic guarantees, threat model, and known ## Current status -quicnprotochat is a **proof of concept**. It has not been audited by a third party. +quicproquo is a **proof of concept**. It has not been audited by a third party. Known limitations: @@ -99,4 +99,4 @@ For the full milestone tracker, see [Milestones](roadmap/milestones.md). ## License -quicnprotochat is released under the **MIT** license. See `LICENSE` in the repository root. +quicproquo is released under the **MIT** license. See `LICENSE` in the repository root. diff --git a/docs/src/protocol-layers/capn-proto.md b/docs/src/protocol-layers/capn-proto.md index da732f0..55bfe7a 100644 --- a/docs/src/protocol-layers/capn-proto.md +++ b/docs/src/protocol-layers/capn-proto.md @@ -1,6 +1,6 @@ # Cap'n Proto Serialisation and RPC -quicnprotochat uses [Cap'n Proto](https://capnproto.org/) for both message serialisation and remote procedure calls. The serialisation layer encodes structured messages (Envelopes, Auth tokens, delivery payloads) into a compact binary format. The RPC layer provides the client-server interface for the Authentication Service, Delivery Service, and health checks -- all exposed through a single `NodeService` interface. +quicproquo uses [Cap'n Proto](https://capnproto.org/) for both message serialisation and remote procedure calls. The serialisation layer encodes structured messages (Envelopes, Auth tokens, delivery payloads) into a compact binary format. The RPC layer provides the client-server interface for the Authentication Service, Delivery Service, and health checks -- all exposed through a single `NodeService` interface. This page covers why Cap'n Proto was chosen, how schemas are compiled, the owned `ParsedEnvelope` type, serialisation helpers, and ALPN integration with QUIC. @@ -23,7 +23,7 @@ Cap'n Proto was selected for the following reasons: 3. **Canonical serialisation**: Cap'n Proto can produce deterministic byte representations of messages. This is critical for MLS, where Commits and KeyPackages must be signed -- the signature must cover exactly the same bytes that the verifier will see. -4. **Built-in async RPC**: The `capnp-rpc` crate provides a capability-based RPC system with promise pipelining. quicnprotochat uses it for the `NodeService` interface (KeyPackage upload/fetch, message enqueue/fetch, health checks, hybrid key operations). This avoids the need to hand-roll a request/response protocol. +4. **Built-in async RPC**: The `capnp-rpc` crate provides a capability-based RPC system with promise pipelining. quicproquo uses it for the `NodeService` interface (KeyPackage upload/fetch, message enqueue/fetch, health checks, hybrid key operations). This avoids the need to hand-roll a request/response protocol. 5. **Compact wire format**: Cap'n Proto's wire format is more compact than JSON or XML and comparable to Protocol Buffers, with the advantage of no decode step. @@ -41,7 +41,7 @@ schemas/ ### build.rs -The `quicnprotochat-proto` crate compiles these schemas at build time via `build.rs`: +The `quicproquo-proto` crate compiles these schemas at build time via `build.rs`: ```rust capnpc::CompilerCommand::new() @@ -63,7 +63,7 @@ Key details: ### Generated module inclusion -The generated code is spliced into the `quicnprotochat-proto` crate via `include!` macros: +The generated code is spliced into the `quicproquo-proto` crate via `include!` macros: ```rust pub mod envelope_capnp { @@ -84,7 +84,7 @@ Consumers import types from these modules. For example, `node_capnp::node_servic ## The Envelope schema -The `Envelope` is the top-level wire message for all quicnprotochat traffic. Every frame exchanged between peers is serialised as an Envelope: +The `Envelope` is the top-level wire message for all quicproquo traffic. Every frame exchanged between peers is serialised as an Envelope: ```capnp struct Envelope { @@ -114,7 +114,7 @@ The Delivery Service routes by `(groupId, msgType)` without inspecting `payload` Cap'n Proto readers (`envelope_capnp::envelope::Reader`) borrow from the original byte buffer and cannot be sent across async task boundaries (`!Send`). This is a fundamental limitation of zero-copy reads. -To bridge this gap, `quicnprotochat-proto` defines `ParsedEnvelope`: +To bridge this gap, `quicproquo-proto` defines `ParsedEnvelope`: ```rust pub struct ParsedEnvelope { @@ -256,11 +256,11 @@ MessagePack is untyped -- there is no schema file, and type errors are caught at FlatBuffers supports zero-copy reads (like Cap'n Proto) but lacks a built-in RPC framework. The ecosystem and tooling are also less mature for Rust. -## Design constraints of `quicnprotochat-proto` +## Design constraints of `quicproquo-proto` -The `quicnprotochat-proto` crate enforces three design constraints: +The `quicproquo-proto` crate enforces three design constraints: -1. **No crypto**: Key material never enters this crate. All encryption and signing happens in `quicnprotochat-core`. +1. **No crypto**: Key material never enters this crate. All encryption and signing happens in `quicproquo-core`. 2. **No I/O**: Callers own the transport. This crate only converts between bytes and types. 3. **No async**: Pure synchronous data-layer code. Async is the caller's responsibility. diff --git a/docs/src/protocol-layers/hybrid-kem.md b/docs/src/protocol-layers/hybrid-kem.md index 37c0853..696927e 100644 --- a/docs/src/protocol-layers/hybrid-kem.md +++ b/docs/src/protocol-layers/hybrid-kem.md @@ -1,8 +1,8 @@ # Hybrid KEM: X25519 + ML-KEM-768 -quicnprotochat implements a hybrid Key Encapsulation Mechanism that combines classical X25519 Diffie-Hellman with post-quantum ML-KEM-768 (FIPS 203). The hybrid construction ensures that the system remains secure even if one of the two components is broken: X25519 protects against failures in ML-KEM, and ML-KEM protects against quantum computers breaking X25519. +quicproquo implements a hybrid Key Encapsulation Mechanism that combines classical X25519 Diffie-Hellman with post-quantum ML-KEM-768 (FIPS 203). The hybrid construction ensures that the system remains secure even if one of the two components is broken: X25519 protects against failures in ML-KEM, and ML-KEM protects against quantum computers breaking X25519. -The implementation lives in `quicnprotochat-core/src/hybrid_kem.rs`. It is fully implemented and tested but **not yet integrated into the MLS ciphersuite** -- integration is planned for the M5 milestone. Currently, the module can be used as a standalone envelope encryption layer to wrap MLS payloads in an outer post-quantum-resistant encryption before they transit the network. +The implementation lives in `quicproquo-core/src/hybrid_kem.rs`. It is fully implemented and tested but **not yet integrated into the MLS ciphersuite** -- integration is planned for the M5 milestone. Currently, the module can be used as a standalone envelope encryption layer to wrap MLS payloads in an outer post-quantum-resistant encryption before they transit the network. ## Design approach @@ -70,8 +70,8 @@ The two shared secrets are combined via HKDF-SHA256 with domain separation: ikm = X25519_shared_secret(32 bytes) || ML-KEM_shared_secret(32 bytes) salt = [] (empty) -key = HKDF-SHA256(salt, ikm, info="quicnprotochat-hybrid-v1", L=32) -nonce = HKDF-SHA256(salt, ikm, info="quicnprotochat-hybrid-nonce-v1", L=12) +key = HKDF-SHA256(salt, ikm, info="quicproquo-hybrid-v1", L=32) +nonce = HKDF-SHA256(salt, ikm, info="quicproquo-hybrid-nonce-v1", L=12) ``` The implementation in `derive_aead_material()`: @@ -85,10 +85,10 @@ fn derive_aead_material(x25519_ss: &[u8], mlkem_ss: &[u8]) -> (Key, Nonce) { let hk = Hkdf::::new(None, &ikm); let mut key_bytes = Zeroizing::new([0u8; 32]); - hk.expand(b"quicnprotochat-hybrid-v1", &mut *key_bytes).unwrap(); + hk.expand(b"quicproquo-hybrid-v1", &mut *key_bytes).unwrap(); let mut nonce_bytes = [0u8; 12]; - hk.expand(b"quicnprotochat-hybrid-nonce-v1", &mut nonce_bytes).unwrap(); + hk.expand(b"quicproquo-hybrid-nonce-v1", &mut nonce_bytes).unwrap(); (*Key::from_slice(&*key_bytes), *Nonce::from_slice(&nonce_bytes)) } @@ -273,7 +273,7 @@ The AEAD nonce is derived deterministically from the shared secrets via HKDF. Si ## Further reading -- [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md) -- Broader discussion of quicnprotochat's PQ strategy. +- [Post-Quantum Readiness](../cryptography/post-quantum-readiness.md) -- Broader discussion of quicproquo's PQ strategy. - [MLS (RFC 9420)](mls.md) -- The MLS layer that the hybrid KEM will wrap. - [Key Lifecycle and Zeroization](../cryptography/key-lifecycle.md) -- How hybrid key material is managed and cleared. - [Threat Model](../cryptography/threat-model.md) -- Where hybrid KEM fits in the overall threat model. diff --git a/docs/src/protocol-layers/mls.md b/docs/src/protocol-layers/mls.md index c8f9397..d85441d 100644 --- a/docs/src/protocol-layers/mls.md +++ b/docs/src/protocol-layers/mls.md @@ -1,8 +1,8 @@ # MLS (RFC 9420) -The Messaging Layer Security protocol (RFC 9420) is the core cryptographic layer in quicnprotochat. It provides authenticated group key agreement with forward secrecy and post-compromise security -- properties that distinguish quicnprotochat from a simple transport-encrypted relay. This is the most detailed page in the Protocol Deep Dives section because MLS is the most complex layer in the stack. +The Messaging Layer Security protocol (RFC 9420) is the core cryptographic layer in quicproquo. It provides authenticated group key agreement with forward secrecy and post-compromise security -- properties that distinguish quicproquo from a simple transport-encrypted relay. This is the most detailed page in the Protocol Deep Dives section because MLS is the most complex layer in the stack. -The implementation lives in `quicnprotochat-core/src/group.rs` and `quicnprotochat-core/src/keystore.rs`, using the `openmls 0.5` crate. +The implementation lives in `quicproquo-core/src/group.rs` and `quicproquo-core/src/keystore.rs`, using the `openmls 0.5` crate. ## Background: what problem MLS solves @@ -21,7 +21,7 @@ MLS takes a fundamentally different approach: it uses a **ratchet tree** (a bina ## Ciphersuite -quicnprotochat uses: +quicproquo uses: ```text MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 @@ -38,7 +38,7 @@ This ciphersuite provides 128-bit classical security. Post-quantum protection is ## The `GroupMember` state machine -The central type is `GroupMember`, defined in `quicnprotochat-core/src/group.rs`. It wraps an openmls `MlsGroup`, a persistent crypto backend (`StoreCrypto`), and the user's long-term Ed25519 identity keypair. +The central type is `GroupMember`, defined in `quicproquo-core/src/group.rs`. It wraps an openmls `MlsGroup`, a persistent crypto backend (`StoreCrypto`), and the user's long-term Ed25519 identity keypair. ### Lifecycle diagram @@ -135,7 +135,7 @@ pub fn create_group(&mut self, group_id: &[u8]) -> Result<(), CoreError> Creates a new MLS group at epoch 0 with the caller as the sole member. **Parameters:** -- `group_id`: Any non-empty byte string. By convention, quicnprotochat uses the SHA-256 digest of a human-readable group name. +- `group_id`: Any non-empty byte string. By convention, quicproquo uses the SHA-256 digest of a human-readable group name. **What happens internally:** @@ -259,7 +259,7 @@ Processes an incoming TLS-encoded MLS message. ## The `StoreCrypto` backend -The `StoreCrypto` struct (in `quicnprotochat-core/src/keystore.rs`) implements `OpenMlsCryptoProvider`, which openmls requires for all cryptographic operations: +The `StoreCrypto` struct (in `quicproquo-core/src/keystore.rs`) implements `OpenMlsCryptoProvider`, which openmls requires for all cryptographic operations: ```rust pub struct StoreCrypto { @@ -318,11 +318,11 @@ KeyPackageIn::tls_deserialize(&mut bytes.as_ref())? ### Feature-gated methods -Several convenient methods (`into_welcome()`, `into_protocol_message()`) are feature-gated behind openmls feature flags that quicnprotochat does not enable. The workaround is to use `msg_in.extract()` and pattern-match on the `MlsMessageInBody` enum variants. +Several convenient methods (`into_welcome()`, `into_protocol_message()`) are feature-gated behind openmls feature flags that quicproquo does not enable. The workaround is to use `msg_in.extract()` and pattern-match on the `MlsMessageInBody` enum variants. ### MlsGroup is not Send -`MlsGroup` holds internal state that may not be `Send` depending on the crypto backend. In quicnprotochat, `StoreCrypto` uses `RwLock` (which is `Send + Sync`), so `GroupMember` is `Send`. However, all MLS operations must use the same backend instance, so `GroupMember` should not be cloned across tasks. +`MlsGroup` holds internal state that may not be `Send` depending on the crypto backend. In quicproquo, `StoreCrypto` uses `RwLock` (which is `Send + Sync`), so `GroupMember` is `Send`. However, all MLS operations must use the same backend instance, so `GroupMember` should not be cloned across tasks. ## Ratchet tree embedding @@ -335,7 +335,7 @@ The trade-off: - **Pro**: No need for a separate tree distribution service or additional round-trips. - **Con**: Welcome messages grow with the group size (O(n log n) for a balanced tree of n members). -For quicnprotochat's target group sizes (2-100 members), this trade-off is acceptable. +For quicproquo's target group sizes (2-100 members), this trade-off is acceptable. ## Wire format @@ -386,7 +386,7 @@ The following sequence shows a complete Alice-and-Bob scenario, matching the `tw ## Credential model -quicnprotochat uses MLS `Basic` credentials. The credential body is the raw Ed25519 public key bytes (32 bytes), and the `signature_key` is the same public key: +quicproquo uses MLS `Basic` credentials. The credential body is the raw Ed25519 public key bytes (32 bytes), and the `signature_key` is the same public key: ```rust let credential = Credential::new( diff --git a/docs/src/protocol-layers/overview.md b/docs/src/protocol-layers/overview.md index e7a69b0..51536b4 100644 --- a/docs/src/protocol-layers/overview.md +++ b/docs/src/protocol-layers/overview.md @@ -1,6 +1,6 @@ # Protocol Layers Overview -quicnprotochat composes four distinct protocol layers into a single security stack. Each layer addresses a specific class of threat and delegates everything else to the layers above or below it. No single layer is sufficient on its own; the composition is what delivers end-to-end confidentiality, server authentication, forward secrecy, post-compromise security, and post-quantum resistance. +quicproquo composes four distinct protocol layers into a single security stack. Each layer addresses a specific class of threat and delegates everything else to the layers above or below it. No single layer is sufficient on its own; the composition is what delivers end-to-end confidentiality, server authentication, forward secrecy, post-compromise security, and post-quantum resistance. This page provides a high-level comparison and a suggested reading order. The deep-dive pages that follow contain implementation details drawn directly from the source code. @@ -47,7 +47,7 @@ The pages in this section are ordered to build understanding incrementally: 1. **[QUIC + TLS 1.3](quic-tls.md)** -- Start here. This is the transport layer that every client-server connection uses. Understanding QUIC stream multiplexing and the TLS 1.3 handshake is prerequisite to understanding how Cap'n Proto RPC rides on top. -2. **[MLS (RFC 9420)](mls.md)** -- The core cryptographic innovation. MLS provides the group key agreement that makes quicnprotochat an E2E encrypted group messenger rather than just a transport-encrypted relay. This is the longest and most detailed page. +2. **[MLS (RFC 9420)](mls.md)** -- The core cryptographic innovation. MLS provides the group key agreement that makes quicproquo an E2E encrypted group messenger rather than just a transport-encrypted relay. This is the longest and most detailed page. 3. **[Cap'n Proto Serialisation and RPC](capn-proto.md)** -- The serialisation and RPC layer that bridges MLS application data with the transport. Understanding the Envelope schema, the ParsedEnvelope owned type, and the NodeService RPC interface is essential for reading the server and client source code. @@ -70,9 +70,9 @@ Each protocol layer maps to one or more workspace crates: | Layer | Primary Crate | Source File(s) | |---|---|---| -| QUIC + TLS 1.3 | `quicnprotochat-server`, `quicnprotochat-client` | `main.rs` (server and client entry points) | -| Cap'n Proto | `quicnprotochat-proto` | `src/lib.rs`, `build.rs`, `schemas/*.capnp` | -| MLS | `quicnprotochat-core` | `src/group.rs`, `src/keystore.rs` | -| Hybrid KEM | `quicnprotochat-core` | `src/hybrid_kem.rs` | +| QUIC + TLS 1.3 | `quicproquo-server`, `quicproquo-client` | `main.rs` (server and client entry points) | +| Cap'n Proto | `quicproquo-proto` | `src/lib.rs`, `build.rs`, `schemas/*.capnp` | +| MLS | `quicproquo-core` | `src/group.rs`, `src/keystore.rs` | +| Hybrid KEM | `quicproquo-core` | `src/hybrid_kem.rs` | For a full crate responsibility breakdown, see [Crate Responsibilities](../architecture/crate-responsibilities.md). diff --git a/docs/src/protocol-layers/quic-tls.md b/docs/src/protocol-layers/quic-tls.md index 1fcd89a..8c09d07 100644 --- a/docs/src/protocol-layers/quic-tls.md +++ b/docs/src/protocol-layers/quic-tls.md @@ -1,6 +1,6 @@ # QUIC + TLS 1.3 -quicnprotochat uses QUIC (RFC 9000) with mandatory TLS 1.3 (RFC 9001) as its transport layer. This page explains how the `quinn` and `rustls` crates are integrated and what security properties the transport provides. +quicproquo uses QUIC (RFC 9000) with mandatory TLS 1.3 (RFC 9001) as its transport layer. This page explains how the `quinn` and `rustls` crates are integrated and what security properties the transport provides. ## Why QUIC @@ -14,16 +14,16 @@ QUIC provides several advantages over traditional TCP-based transports: ## Crate integration -quicnprotochat uses the following crates for QUIC and TLS: +quicproquo uses the following crates for QUIC and TLS: - **`quinn 0.11`** -- The async QUIC implementation for Tokio. Provides `Endpoint`, `Connection`, and bidirectional stream types. - **`quinn-proto 0.11`** -- The protocol-level types, including `QuicServerConfig` and `QuicClientConfig` wrappers that bridge `rustls` into `quinn`. -- **`rustls 0.23`** -- The TLS implementation. quicnprotochat uses it in strict TLS 1.3 mode with no fallback to TLS 1.2. +- **`rustls 0.23`** -- The TLS implementation. quicproquo uses it in strict TLS 1.3 mode with no fallback to TLS 1.2. - **`rcgen 0.13`** -- Self-signed certificate generation for development and testing. ### Server configuration -The server builds its QUIC endpoint configuration in `build_server_config()` (in `quicnprotochat-server/src/main.rs`): +The server builds its QUIC endpoint configuration in `build_server_config()` (in `quicproquo-server/src/main.rs`): ```rust let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13]) @@ -37,7 +37,7 @@ Ok(ServerConfig::with_crypto(Arc::new(crypto))) Key points: -1. **TLS 1.3 strict mode**: `builder_with_protocol_versions(&[&TLS13])` ensures no TLS 1.2 fallback. This is a hard requirement: TLS 1.2 lacks the 0-RTT and full forward secrecy guarantees that quicnprotochat relies on. +1. **TLS 1.3 strict mode**: `builder_with_protocol_versions(&[&TLS13])` ensures no TLS 1.2 fallback. This is a hard requirement: TLS 1.2 lacks the 0-RTT and full forward secrecy guarantees that quicproquo relies on. 2. **No client certificate authentication**: `with_no_client_auth()` means the server does not verify client certificates at the TLS layer. Client authentication is handled at the application layer via Ed25519 identity keys and MLS credentials. This is a deliberate design choice -- MLS provides stronger authentication properties than TLS client certificates. @@ -82,11 +82,11 @@ Because `capnp-rpc` uses `Rc>` internally (making it `!Send`), all RPC ## Certificate trust model -quicnprotochat currently uses a **trust-on-first-use (TOFU)** model with self-signed certificates: +quicproquo currently uses a **trust-on-first-use (TOFU)** model with self-signed certificates: 1. On first start, the server generates a self-signed certificate using `rcgen::generate_simple_self_signed` with SANs for `localhost`, `127.0.0.1`, and `::1`. 2. The certificate and private key are persisted to disk as DER files (default: `data/server-cert.der` and `data/server-key.der`). -3. Clients must obtain the server's certificate file out-of-band and reference it via the `--ca-cert` flag or `QUICNPROTOCHAT_CA_CERT` environment variable. +3. Clients must obtain the server's certificate file out-of-band and reference it via the `--ca-cert` flag or `QPQ_CA_CERT` environment variable. This model is adequate for development and single-server deployments. The roadmap includes: @@ -136,18 +136,18 @@ The QUIC + TLS 1.3 layer provides: | Environment Variable | CLI Flag | Default | Description | |---|---|---|---| -| `QUICNPROTOCHAT_LISTEN` | `--listen` | `0.0.0.0:7000` | QUIC listen address | -| `QUICNPROTOCHAT_TLS_CERT` | `--tls-cert` | `data/server-cert.der` | TLS certificate path | -| `QUICNPROTOCHAT_TLS_KEY` | `--tls-key` | `data/server-key.der` | TLS private key path | -| `QUICNPROTOCHAT_DATA_DIR` | `--data-dir` | `data` | Persistent storage directory | +| `QPQ_LISTEN` | `--listen` | `0.0.0.0:7000` | QUIC listen address | +| `QPQ_TLS_CERT` | `--tls-cert` | `data/server-cert.der` | TLS certificate path | +| `QPQ_TLS_KEY` | `--tls-key` | `data/server-key.der` | TLS private key path | +| `QPQ_DATA_DIR` | `--data-dir` | `data` | Persistent storage directory | ### Client | Environment Variable | CLI Flag | Default | Description | |---|---|---|---| -| `QUICNPROTOCHAT_CA_CERT` | `--ca-cert` | `data/server-cert.der` | Server certificate to trust | -| `QUICNPROTOCHAT_SERVER_NAME` | `--server-name` | `localhost` | Expected TLS server name (must match certificate SAN) | -| `QUICNPROTOCHAT_SERVER` | `--server` | `127.0.0.1:7000` | Server address (per-subcommand) | +| `QPQ_CA_CERT` | `--ca-cert` | `data/server-cert.der` | Server certificate to trust | +| `QPQ_SERVER_NAME` | `--server-name` | `localhost` | Expected TLS server name (must match certificate SAN) | +| `QPQ_SERVER` | `--server` | `127.0.0.1:7000` | Server address (per-subcommand) | ## Further reading diff --git a/docs/src/roadmap/authz-plan.md b/docs/src/roadmap/authz-plan.md index 6e213a6..1277ccc 100644 --- a/docs/src/roadmap/authz-plan.md +++ b/docs/src/roadmap/authz-plan.md @@ -1,7 +1,7 @@ # Auth, Devices, and Tokens This page describes the authentication, device management, and authorisation -design for quicnprotochat. It introduces account and device identities, gates +design for quicproquo. It introduces account and device identities, gates server operations by authenticated identity, enforces rate and size limits, and binds MLS identity keys to accounts. diff --git a/docs/src/roadmap/dm-channels.md b/docs/src/roadmap/dm-channels.md index 839893d..f14f2d3 100644 --- a/docs/src/roadmap/dm-channels.md +++ b/docs/src/roadmap/dm-channels.md @@ -1,7 +1,7 @@ # 1:1 Channel Design This page describes the design for first-class 1:1 (direct message) channels in -quicnprotochat. Channels provide per-conversation authorisation, MLS-encrypted +quicproquo. Channels provide per-conversation authorisation, MLS-encrypted payloads, message retention with TTL eviction, and backward compatibility with the legacy delivery model. diff --git a/docs/src/roadmap/fully-operational-checklist.md b/docs/src/roadmap/fully-operational-checklist.md index 5bb1be9..3bcacb2 100644 --- a/docs/src/roadmap/fully-operational-checklist.md +++ b/docs/src/roadmap/fully-operational-checklist.md @@ -109,7 +109,7 @@ Not strictly required for “operational” but expected for production deployme ## 3. Roadmap and Documentation Updates - **Milestones doc:** Mark M4 as **Complete** (CLI subcommands exist). Mark M6 as **Complete** (migrations + runner; server and client persistence in place). Leave M5 as **Next** and M7 as **Planned**. -- **README:** Update milestone table to reflect M4 and M6 complete; add one line on migrations (e.g. “Server supports SQL migrations under `quicnprotochat-server/migrations/`”). +- **README:** Update milestone table to reflect M4 and M6 complete; add one line on migrations (e.g. “Server supports SQL migrations under `quicproquo-server/migrations/`”). - **Migration convention:** Document in README or a dev doc: add new migrations as `NNN_name.sql`, add to `MIGRATIONS` in `sql_store.rs`, bump `SCHEMA_VERSION`. --- diff --git a/docs/src/roadmap/future-research.md b/docs/src/roadmap/future-research.md index 1f1ffc5..07d63e5 100644 --- a/docs/src/roadmap/future-research.md +++ b/docs/src/roadmap/future-research.md @@ -1,7 +1,7 @@ # Future Research Directions This page catalogues technologies and research directions that could strengthen -quicnprotochat beyond the current [milestone plan](milestones.md). Each entry +quicproquo beyond the current [milestone plan](milestones.md). Each entry includes a brief description, the problem it solves, relevant crates or specifications, and how it maps to the project architecture. @@ -21,7 +21,7 @@ delivery. **Solution:** [LibP2P](https://libp2p.io/) and [iroh](https://iroh.computer/) (from n0) provide peer discovery, NAT traversal (hole-punching), and relay fallback. iroh is particularly interesting because it is Rust-native and built on -QUIC, aligning with quicnprotochat's existing transport layer. +QUIC, aligning with quicproquo's existing transport layer. **Architecture impact:** Move from pure client-server to a hybrid topology where peers communicate directly when possible and fall back to server relay when NAT @@ -68,7 +68,7 @@ significantly, so this should be optional. ### SQLCipher / libsql (Turso) -**Problem:** At M6, quicnprotochat needs persistent storage for group state, key +**Problem:** At M6, quicproquo needs persistent storage for group state, key material, and message queues. Storing private keys in a plaintext SQLite database is insufficient. @@ -129,7 +129,7 @@ vulnerable to harvest-now-decrypt-later attacks. hybrid Ed25519 + ML-DSA-65 for credential signatures. The `ml-kem` crate is already vendored in the workspace. -**Architecture impact:** Custom `OpenMlsCryptoProvider` in `quicnprotochat-core` +**Architecture impact:** Custom `OpenMlsCryptoProvider` in `quicproquo-core` implementing the hybrid combiner. This is the M7 milestone -- see [Milestones](milestones.md#m7----post-quantum-planned) and [Hybrid KEM](../protocol-layers/hybrid-kem.md). @@ -202,7 +202,7 @@ DID URIs. The server resolves DIDs to public keys for routing. ### OPAQUE (aPAKE) -**Problem:** If quicnprotochat adds password-based account registration, the +**Problem:** If quicproquo adds password-based account registration, the server must never see the password -- not even a hash. **Solution:** [OPAQUE](https://datatracker.ietf.org/doc/rfc9497/) is an @@ -253,7 +253,7 @@ admin could require proof of organization membership before allowing join. **Problem:** A single server is a single point of failure and a single point of trust. Users on different servers cannot communicate. -**Solution:** Federation allows multiple quicnprotochat servers to exchange +**Solution:** Federation allows multiple quicproquo servers to exchange messages, similar to [Matrix](https://matrix.org/) homeserver federation. Each server manages its own users and relays messages to peer servers. @@ -345,10 +345,10 @@ the user base for testing and demonstration. **Solution:** [Tauri](https://tauri.app/) or [Dioxus](https://dioxuslabs.com/) provide native cross-platform GUI frameworks in Rust. The -`quicnprotochat-core` crate can be shared directly with the GUI client. +`quicproquo-core` crate can be shared directly with the GUI client. -**Architecture impact:** Add a `quicnprotochat-gui` crate that depends on -`quicnprotochat-core` and `quicnprotochat-proto`. The GUI drives the same +**Architecture impact:** Add a `quicproquo-gui` crate that depends on +`quicproquo-core` and `quicproquo-proto`. The GUI drives the same `GroupMember` and RPC logic as the CLI client. **Crates:** `tauri`, `dioxus` @@ -361,7 +361,7 @@ provide native cross-platform GUI frameworks in Rust. The [diplomat](https://github.com/nickelc/diplomat) generate idiomatic Swift and Kotlin bindings from Rust definitions. -**Architecture impact:** Expose `quicnprotochat-core` through a C-compatible FFI +**Architecture impact:** Expose `quicproquo-core` through a C-compatible FFI layer. Mobile apps call into the Rust crypto and protocol logic. **Crates:** `uniffi`, `diplomat` @@ -387,10 +387,10 @@ considering the current state of the codebase and the [milestone plan](milestone | Priority | Technology | Why | Unlocks | |----------|-----------|-----|---------| -| 1 | **Post-quantum hybrid KEM** | `ml-kem` is already vendored in the workspace. Completing the hybrid `OpenMlsCryptoProvider` makes quicnprotochat one of the first PQ MLS implementations. | M7 | +| 1 | **Post-quantum hybrid KEM** | `ml-kem` is already vendored in the workspace. Completing the hybrid `OpenMlsCryptoProvider` makes quicproquo one of the first PQ MLS implementations. | M7 | | 2 | **SQLCipher persistence** | Encrypted-at-rest storage is the prerequisite for multi-device support, offline usage, and server restart survival. | M6 | | 3 | **OPAQUE auth** | Zero-knowledge password authentication is a massive security uplift for the account system. The server never sees or stores passwords. | Phase 3 (authz) | -| 4 | **iroh / LibP2P** | NAT traversal and optional P2P mesh makes quicnprotochat deployable without centralised infrastructure. Aligns with the existing QUIC transport. | Beyond M7 | +| 4 | **iroh / LibP2P** | NAT traversal and optional P2P mesh makes quicproquo deployable without centralised infrastructure. Aligns with the existing QUIC transport. | Beyond M7 | | 5 | **Sealed Sender + PIR** | Content encryption is table stakes. Metadata resistance (hiding who talks to whom) is the frontier of private messaging research. | Beyond M7 | --- diff --git a/docs/src/roadmap/milestones.md b/docs/src/roadmap/milestones.md index 2c9b8d5..9ee2e45 100644 --- a/docs/src/roadmap/milestones.md +++ b/docs/src/roadmap/milestones.md @@ -1,6 +1,6 @@ # Milestone Tracker -This page tracks the project milestones for quicnprotochat, from initial transport +This page tracks the project milestones for quicproquo, from initial transport layer through post-quantum cryptography. Each milestone produces production-ready, tested, deployable code -- see [Coding Standards](../contributing/coding-standards.md) for what that means in practice. @@ -29,13 +29,13 @@ typed Cap'n Proto frames. **Deliverables:** - `schemas/envelope.capnp`: `Envelope` struct with `MsgType` enum (Ping/Pong at this stage) -- `quicnprotochat-proto`: `build.rs` invoking `capnpc`, generated type re-exports, +- `quicproquo-proto`: `build.rs` invoking `capnpc`, generated type re-exports, canonical serialisation helpers -- `quicnprotochat-core`: Ed25519 identity keypair generation, +- `quicproquo-core`: Ed25519 identity keypair generation, Cap'n Proto frame codec (Tokio `Encoder`/`Decoder`) -- `quicnprotochat-server`: QUIC listener with TLS 1.3 (quinn/rustls), Ping to Pong +- `quicproquo-server`: QUIC listener with TLS 1.3 (quinn/rustls), Ping to Pong handler, one tokio task per connection -- `quicnprotochat-client`: connects over QUIC, sends Ping, receives Pong, exits 0 +- `quicproquo-client`: connects over QUIC, sends Ping, receives Pong, exits 0 - Integration test: server and client in same test binary using `tokio::spawn` - `docker-compose.yml` running the server @@ -54,10 +54,10 @@ via Cap'n Proto RPC. - `schemas/auth.capnp`: `AuthenticationService` interface (`uploadKeyPackage`, `fetchKeyPackage`) -- `quicnprotochat-core`: Ed25519 identity keypair generation, MLS KeyPackage +- `quicproquo-core`: Ed25519 identity keypair generation, MLS KeyPackage generation via `openmls` -- `quicnprotochat-server`: AS RPC server with `DashMap` store, atomic consume-on-fetch -- `quicnprotochat-client`: `register-state` and `fetch-key` CLI subcommands +- `quicproquo-server`: AS RPC server with `DashMap` store, atomic consume-on-fetch +- `quicproquo-client`: `register-state` and `fetch-key` CLI subcommands - Integration test: Alice uploads KeyPackage, Bob fetches it, fingerprints match **Tests:** auth\_service.rs integration tests (upload, fetch, consume semantics). @@ -97,7 +97,7 @@ group\_id lifecycle, MLS integration. 3. **openmls 0.5 API gotchas.** Several `openmls` methods changed signatures between 0.4 and 0.5 (e.g., `MlsGroup::new` vs `MlsGroup::new_with_group_id`, `BasicCredential::new` taking `Vec` directly). These differences are - documented inline in `quicnprotochat-core/src/group.rs`. + documented inline in `quicproquo-core/src/group.rs`. **Branch:** `feat/m1-noise-transport` @@ -143,13 +143,13 @@ providing post-quantum confidentiality for all group key material. **Deliverables:** -- Custom `OpenMlsCryptoProvider` with hybrid KEM in `quicnprotochat-core` +- Custom `OpenMlsCryptoProvider` with hybrid KEM in `quicproquo-core` - Hybrid shared secret derivation: ``` SharedSecret = HKDF-SHA256( ikm = X25519_ss || ML-KEM-768_ss, - info = "quicnprotochat-hybrid-v1", + info = "quicproquo-hybrid-v1", len = 32 ) ``` diff --git a/docs/src/roadmap/phase2-and-m4-m6.md b/docs/src/roadmap/phase2-and-m4-m6.md index 26b4b56..0181ce4 100644 --- a/docs/src/roadmap/phase2-and-m4-m6.md +++ b/docs/src/roadmap/phase2-and-m4-m6.md @@ -21,7 +21,7 @@ The following legacy behaviour has been removed; only current behaviour is suppo | Task | Status | Notes | |------|--------|-------| -| **Ciphersuite allowlist** | **Done** | Server rejects KeyPackages whose ciphersuite is not `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`. See `quicnprotochat_core::validate_keypackage_ciphersuite` and `upload_key_package` (E021). | +| **Ciphersuite allowlist** | **Done** | Server rejects KeyPackages whose ciphersuite is not `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`. See `quicproquo_core::validate_keypackage_ciphersuite` and `upload_key_package` (E021). | | **ALPN enforcement** | **Done** | Server TLS config sets `alpn_protocols = [b"capnp"]`; handshake completes only if client offers `capnp`. | | **Connection draining** | **Done** | On `Ctrl+C`, server calls `endpoint.close(0, b"server shutdown")` and exits the accept loop. | | **Wire versioning** | **Done** | `enqueue`, `fetch`, `fetchWait` require `version == CURRENT_WIRE_VERSION` (1). Other RPCs use auth version. | diff --git a/docs/src/roadmap/production-readiness.md b/docs/src/roadmap/production-readiness.md index ea31e61..380bc55 100644 --- a/docs/src/roadmap/production-readiness.md +++ b/docs/src/roadmap/production-readiness.md @@ -1,6 +1,6 @@ # Production Readiness WBS -This page defines the work breakdown structure (WBS) for taking quicnprotochat +This page defines the work breakdown structure (WBS) for taking quicproquo from a proof-of-concept to a production-hardened system. It covers feature scope, security policy, phased delivery, and a planning checklist. @@ -11,7 +11,7 @@ document focuses on the cross-cutting concerns that span multiple milestones. ## Feature Scope (Must-Have) -These are the feature areas that must be addressed before quicnprotochat can be +These are the feature areas that must be addressed before quicproquo can be considered production-ready. Each area maps to one or more milestones or phases in the WBS below. @@ -30,7 +30,7 @@ in the WBS below. ## Security Plan (By Design) -quicnprotochat follows a security-by-design philosophy. The standards below are +quicproquo follows a security-by-design philosophy. The standards below are non-negotiable -- see [Coding Standards](../contributing/coding-standards.md) for how they are enforced in code. diff --git a/docs/src/wire-format/envelope-schema.md b/docs/src/wire-format/envelope-schema.md index f20d052..55863ce 100644 --- a/docs/src/wire-format/envelope-schema.md +++ b/docs/src/wire-format/envelope-schema.md @@ -3,7 +3,7 @@ **Schema file:** `schemas/envelope.capnp` **File ID:** `@0xe4a7f2c8b1d63509` -The Envelope is the legacy top-level wire message used in M1 for all quicnprotochat traffic. Every frame exchanged between peers was serialised as an Envelope, with the Delivery Service routing by `(groupId, msgType)` without inspecting the payload. +The Envelope is the legacy top-level wire message used in M1 for all quicproquo traffic. Every frame exchanged between peers was serialised as an Envelope, with the Delivery Service routing by `(groupId, msgType)` without inspecting the payload. > **Note:** The Envelope is the M1-era framing format. The current M3+ architecture uses Cap'n Proto RPC directly via the [NodeService](node-service-schema.md) interface. The Envelope schema remains in the codebase for backward compatibility and for use in integration tests. @@ -12,7 +12,7 @@ The Envelope is the legacy top-level wire message used in M1 for all quicnprotoc ## Full schema listing ```capnp -# envelope.capnp -- top-level wire message for all quicnprotochat traffic. +# envelope.capnp -- top-level wire message for all quicproquo traffic. # # Every frame is serialised as an Envelope. # The Delivery Service routes by (groupId, msgType) without inspecting payload. diff --git a/docs/src/wire-format/node-service-schema.md b/docs/src/wire-format/node-service-schema.md index b872268..c194864 100644 --- a/docs/src/wire-format/node-service-schema.md +++ b/docs/src/wire-format/node-service-schema.md @@ -3,14 +3,14 @@ **Schema file:** `schemas/node.capnp` **File ID:** `@0xd5ca5648a9cc1c28` -The `NodeService` interface is the unified Cap'n Proto RPC surface that every quicnprotochat client talks to. It combines the Authentication Service and Delivery Service into a single interface, adds long-polling support (`fetchWait`), a health probe (`health`), and hybrid KEM key management. Every method that mutates state or accesses per-user data accepts an `Auth` struct for versioned authentication. +The `NodeService` interface is the unified Cap'n Proto RPC surface that every quicproquo client talks to. It combines the Authentication Service and Delivery Service into a single interface, adds long-polling support (`fetchWait`), a health probe (`health`), and hybrid KEM key management. Every method that mutates state or accesses per-user data accepts an `Auth` struct for versioned authentication. --- ## Full schema listing ```capnp -# node.capnp -- Unified quicnprotochat node RPC interface. +# node.capnp -- Unified quicproquo node RPC interface. # # Combines Authentication and Delivery operations into a single service. # diff --git a/docs/src/wire-format/overview.md b/docs/src/wire-format/overview.md index c3bedfb..3053d64 100644 --- a/docs/src/wire-format/overview.md +++ b/docs/src/wire-format/overview.md @@ -1,6 +1,6 @@ # Wire Format Overview -This section documents the serialisation pipeline that transforms application-level data structures into encrypted bytes on the wire. Every byte exchanged between quicnprotochat clients and the server passes through this pipeline, so understanding it is prerequisite to reading the protocol deep dives or the server/client source code. +This section documents the serialisation pipeline that transforms application-level data structures into encrypted bytes on the wire. Every byte exchanged between quicproquo clients and the server passes through this pipeline, so understanding it is prerequisite to reading the protocol deep dives or the server/client source code. --- diff --git a/master-prompt.md b/master-prompt.md index 2e6d385..0caf143 100644 --- a/master-prompt.md +++ b/master-prompt.md @@ -1,8 +1,8 @@ -# quicnprotochat — Master Project Prompt +# quicproquo — Master Project Prompt ## Project Identity -You are building **quicnprotochat**, a production-grade end-to-end encrypted group messenger in Rust. It uses the MLS protocol (RFC 9420) for group key agreement, ML-KEM-768 (NIST FIPS 203) hybrid post-quantum key exchange, the Noise Protocol Framework (Noise_XX pattern) over raw TCP as the transport layer, and Cap'n Proto for wire serialisation and RPC. There is no TLS, no HTTP, no WebSocket, no MessagePack. +You are building **quicproquo**, a production-grade end-to-end encrypted group messenger in Rust. It uses the MLS protocol (RFC 9420) for group key agreement, ML-KEM-768 (NIST FIPS 203) hybrid post-quantum key exchange, the Noise Protocol Framework (Noise_XX pattern) over raw TCP as the transport layer, and Cap'n Proto for wire serialisation and RPC. There is no TLS, no HTTP, no WebSocket, no MessagePack. This is not a prototype. Every milestone produces production-ready, tested, deployable code. @@ -35,13 +35,13 @@ This is not a prototype. Every milestone produces production-ready, tested, depl ### Workspace Layout ``` -quicnprotochat/ +quicproquo/ ├── Cargo.toml # workspace root ├── crates/ -│ ├── quicnprotochat-core/ # crypto primitives, MLS wrapper, Noise framing codec -│ ├── quicnprotochat-proto/ # Cap'n Proto schemas + generated types, no crypto, no I/O -│ ├── quicnprotochat-server/ # Delivery Service (DS) + Authentication Service (AS) -│ └── quicnprotochat-client/ # CLI client +│ ├── quicproquo-core/ # crypto primitives, MLS wrapper, Noise framing codec +│ ├── quicproquo-proto/ # Cap'n Proto schemas + generated types, no crypto, no I/O +│ ├── quicproquo-server/ # Delivery Service (DS) + Authentication Service (AS) +│ └── quicproquo-client/ # CLI client ├── schemas/ # .capnp schema files (canonical source of truth) │ ├── envelope.capnp │ ├── auth.capnp @@ -55,31 +55,31 @@ quicnprotochat/ ### Crate Responsibilities -**quicnprotochat-core** +**quicproquo-core** - Noise_XX handshake initiator and responder (via `snow`) - Length-prefixed Cap'n Proto frame codec (Tokio `Encoder`/`Decoder` traits) - MLS group state machine wrapper around `openmls` - Hybrid PQ ciphersuite (X25519 + ML-KEM-768) - Key generation and zeroize-on-drop key types -**quicnprotochat-proto** +**quicproquo-proto** - Cap'n Proto `.capnp` schemas in `schemas/` (workspace root, shared) - `build.rs` invokes `capnpc` to generate Rust types into `src/generated/` - Re-exports generated types with ergonomic builder/reader helpers - Canonical serialisation helpers for signing (uses `capnp::message::Builder::canonicalize()`) - No crypto, no I/O, no async -**quicnprotochat-server** +**quicproquo-server** - Authentication Service: KeyPackage store (DashMap → SQLite at M6) - Delivery Service: Cap'n Proto RPC interface, fan-out router, per-group append-only message log - Tokio TCP listener, Noise handshake per connection, then Cap'n Proto RPC over the encrypted channel - Structured logging (tracing) -**quicnprotochat-client** +**quicproquo-client** - Tokio TCP connection to server - Noise handshake, then Cap'n Proto RPC client stub - CLI interface (clap) -- Drives quicnprotochat-core for all crypto operations +- Drives quicproquo-core for all crypto operations - Displays received messages to stdout ### Transport Stack @@ -174,11 +174,11 @@ Hybrid KEM construction: ``` SharedSecret = HKDF-SHA256( ikm = X25519_ss || ML-KEM-768_ss, - info = "quicnprotochat-hybrid-v1", + info = "quicproquo-hybrid-v1", len = 32 ) ``` -Follows the combiner approach from draft-ietf-tls-hybrid-design. Implemented as a custom `openmls` `OpenMlsCryptoProvider` trait implementation in `quicnprotochat-core`. +Follows the combiner approach from draft-ietf-tls-hybrid-design. Implemented as a custom `openmls` `OpenMlsCryptoProvider` trait implementation in `quicproquo-core`. --- @@ -189,10 +189,10 @@ Follows the combiner approach from draft-ietf-tls-hybrid-design. Implemented as Deliverables: - `schemas/envelope.capnp`: `Envelope` + `MsgType` (Ping/Pong only needed at this stage) -- `quicnprotochat-proto`: `build.rs` with `capnpc`, generated type re-exports, canonical helper -- `quicnprotochat-core`: static X25519 keypair generation, Noise_XX initiator + responder, length-prefixed Cap'n Proto frame codec -- `quicnprotochat-server`: TCP listener, Noise handshake, Ping→Pong handler, one tokio task per connection -- `quicnprotochat-client`: connects, Noise handshake, sends Ping, receives Pong, exits 0 +- `quicproquo-proto`: `build.rs` with `capnpc`, generated type re-exports, canonical helper +- `quicproquo-core`: static X25519 keypair generation, Noise_XX initiator + responder, length-prefixed Cap'n Proto frame codec +- `quicproquo-server`: TCP listener, Noise handshake, Ping→Pong handler, one tokio task per connection +- `quicproquo-client`: connects, Noise handshake, sends Ping, receives Pong, exits 0 - Integration test: server and client in same test binary using `tokio::spawn` - `docker-compose.yml` running the server @@ -201,10 +201,10 @@ Deliverables: Deliverables: - `schemas/auth.capnp`: `AuthenticationService` interface -- `quicnprotochat-proto`: generated RPC stubs + client/server bootstrap helpers -- `quicnprotochat-core`: MLS KeyPackage generation (openmls) -- `quicnprotochat-server`: AS RPC server implementation with DashMap store -- `quicnprotochat-client`: `register` and `fetch-key` CLI subcommands +- `quicproquo-proto`: generated RPC stubs + client/server bootstrap helpers +- `quicproquo-core`: MLS KeyPackage generation (openmls) +- `quicproquo-server`: AS RPC server implementation with DashMap store +- `quicproquo-client`: `register` and `fetch-key` CLI subcommands - Test: Alice uploads KeyPackage, Bob fetches it, fingerprints match ### M3 — MLS Group Create + Welcome @@ -212,25 +212,25 @@ Deliverables: Deliverables: - `schemas/delivery.capnp`: `DeliveryService` + `MessageStream` interfaces -- `quicnprotochat-core`: group create, add member, process Welcome -- `quicnprotochat-server`: DS RPC server, Welcome routing by identity -- `quicnprotochat-client`: `create-group` and `join` CLI subcommands +- `quicproquo-core`: group create, add member, process Welcome +- `quicproquo-server`: DS RPC server, Welcome routing by identity +- `quicproquo-client`: `create-group` and `join` CLI subcommands - Test: two clients reach identical epoch 1 group state, verified by comparing group context hashes ### M4 — Encrypted Group Messaging **Goal:** Alice and Bob exchange MLS Application messages through the DS. Deliverables: -- `quicnprotochat-core`: send/receive application message, epoch rotation on Commit -- `quicnprotochat-server`: DS fan-out via `MessageStream` capability stream, per-group ordered log (in-memory) -- `quicnprotochat-client`: `send` subcommand, live receive loop via `MessageStream.next()` +- `quicproquo-core`: send/receive application message, epoch rotation on Commit +- `quicproquo-server`: DS fan-out via `MessageStream` capability stream, per-group ordered log (in-memory) +- `quicproquo-client`: `send` subcommand, live receive loop via `MessageStream.next()` - Test: round-trip message integrity, forward secrecy verified by confirming distinct key material across epochs ### M5 — Hybrid PQ Ciphersuite **Goal:** Replace MLS crypto backend with X25519 + ML-KEM-768 hybrid. Deliverables: -- `quicnprotochat-core`: custom `OpenMlsCryptoProvider` with hybrid KEM +- `quicproquo-core`: custom `OpenMlsCryptoProvider` with hybrid KEM - All M3/M4 tests pass unchanged with new ciphersuite - Criterion benchmarks: key generation, encap/decap, group-add latency (10/100/1000 members) @@ -238,7 +238,7 @@ Deliverables: **Goal:** Server survives restart. Full containerised deployment. Deliverables: -- `quicnprotochat-server`: SQLite via `sqlx` for AS key store and DS message log, `migrations/` directory +- `quicproquo-server`: SQLite via `sqlx` for AS key store and DS message log, `migrations/` directory - `docker/Dockerfile`: multi-stage build (rust:bookworm builder → debian:bookworm-slim runtime) - `docker-compose.yml`: server + SQLite volume, healthcheck - Client reconnect with session resume (re-handshake + rejoin group epoch from DS log) @@ -266,7 +266,7 @@ capnp = "0.19" capnp-rpc = "0.19" # Build-time only -capnpc = "0.19" # build-dependency in quicnprotochat-proto +capnpc = "0.19" # build-dependency in quicproquo-proto # Async / networking tokio = { version = "1", features = ["full"] } @@ -310,7 +310,7 @@ The MLS content layer is PQ-protected from M5. The Noise transport (X25519) rema ## How to Use This Prompt -Paste this document at the start of any session working on quicnprotochat. Then state which milestone you are working on and what specific task you need. The assistant will: +Paste this document at the start of any session working on quicproquo. Then state which milestone you are working on and what specific task you need. The assistant will: 1. Confirm the current milestone and task. 2. State any design decisions being made (ADR format if significant). @@ -325,5 +325,5 @@ When asking for code, always specify: --- -*quicnprotochat — MLS + Post-Quantum + Noise/TCP + Cap'n Proto messenger in Rust* +*quicproquo — MLS + Post-Quantum + Noise/TCP + Cap'n Proto messenger in Rust* *Architecture version: 1.1 | Last updated: 2026-02-19* diff --git a/schemas/federation.capnp b/schemas/federation.capnp new file mode 100644 index 0000000..e7d22e5 --- /dev/null +++ b/schemas/federation.capnp @@ -0,0 +1,54 @@ +# federation.capnp — Server-to-server federation RPC interface. +# +# Enables multiple quicproquo servers to relay messages across instances +# via mutual TLS over QUIC. Each method carries a FederationAuth struct whose +# `origin` field is validated against the mTLS certificate's SAN. +# +# ID generated with: capnp id +@0xb7e3f4a1c9d82056; + +interface FederationService { + # Relay an enqueued payload to a recipient on this server. + relayEnqueue @0 ( + recipientKey :Data, + payload :Data, + channelId :Data, + version :UInt16, + auth :FederationAuth + ) -> (seq :UInt64); + + # Relay a batch enqueue to multiple recipients on this server. + relayBatchEnqueue @1 ( + recipientKeys :List(Data), + payload :Data, + channelId :Data, + version :UInt16, + auth :FederationAuth + ) -> (seqs :List(UInt64)); + + # Fetch a KeyPackage for an identity hosted on this server. + proxyFetchKeyPackage @2 ( + identityKey :Data, + auth :FederationAuth + ) -> (package :Data); + + # Fetch a hybrid public key for an identity hosted on this server. + proxyFetchHybridKey @3 ( + identityKey :Data, + auth :FederationAuth + ) -> (hybridPublicKey :Data); + + # Resolve a username to identity key on this server. + proxyResolveUser @4 ( + username :Text, + auth :FederationAuth + ) -> (identityKey :Data); + + # Health / readiness probe for the federation link. + federationHealth @5 () -> (status :Text, serverDomain :Text); +} + +struct FederationAuth { + # Origin domain of the requesting server. Validated against mTLS cert SAN. + origin @0 :Text; +} diff --git a/schemas/node.capnp b/schemas/node.capnp index 7bdbea7..0c2d9be 100644 --- a/schemas/node.capnp +++ b/schemas/node.capnp @@ -1,4 +1,4 @@ -# node.capnp — Unified quicnprotochat node RPC interface. +# node.capnp — Unified quicproquo node RPC interface. # # Combines Authentication and Delivery operations into a single service. # diff --git a/scripts/chat-test.sh b/scripts/chat-test.sh index 3ad9725..7f7efd5 100755 --- a/scripts/chat-test.sh +++ b/scripts/chat-test.sh @@ -69,14 +69,14 @@ $COMPOSE up -d --wait # ── Step 3: Verify server reachable via QUIC ────────────────────────────────── step "Verifying QUIC connectivity from alice..." -$COMPOSE exec -T alice quicnprotochat health +$COMPOSE exec -T alice qpq health # ── Step 4: Alice — register identity + upload KeyPackage ───────────────────── step "Alice: register-state..." -$COMPOSE exec -T alice quicnprotochat register-state --state /chat/alice.bin +$COMPOSE exec -T alice qpq register-state --state /chat/alice.bin -ALICE_KEY=$($COMPOSE exec -T alice quicnprotochat whoami --state /chat/alice.bin \ +ALICE_KEY=$($COMPOSE exec -T alice qpq whoami --state /chat/alice.bin \ | grep 'identity_key' | awk '{print $3}' | tr -d '[:space:]') info "Alice identity: ${ALICE_KEY}" @@ -88,9 +88,9 @@ fi # ── Step 5: Bob — register identity + upload KeyPackage ─────────────────────── step "Bob: register-state..." -$COMPOSE exec -T bob quicnprotochat register-state --state /chat/bob.bin +$COMPOSE exec -T bob qpq register-state --state /chat/bob.bin -BOB_KEY=$($COMPOSE exec -T bob quicnprotochat whoami --state /chat/bob.bin \ +BOB_KEY=$($COMPOSE exec -T bob qpq whoami --state /chat/bob.bin \ | grep 'identity_key' | awk '{print $3}' | tr -d '[:space:]') info "Bob identity: ${BOB_KEY}" @@ -102,21 +102,21 @@ fi # ── Step 6: Alice creates group ─────────────────────────────────────────────── step "Alice: create-group 'docker-chat'..." -$COMPOSE exec -T alice quicnprotochat create-group \ +$COMPOSE exec -T alice qpq create-group \ --state /chat/alice.bin \ --group-id docker-chat # ── Step 7: Alice invites Bob ───────────────────────────────────────────────── step "Alice: invite Bob..." -$COMPOSE exec -T alice quicnprotochat invite \ +$COMPOSE exec -T alice qpq invite \ --state /chat/alice.bin \ --peer-key "$BOB_KEY" # ── Step 8: Bob joins ───────────────────────────────────────────────────────── step "Bob: join group..." -$COMPOSE exec -T bob quicnprotochat join --state /chat/bob.bin +$COMPOSE exec -T bob qpq join --state /chat/bob.bin # ── Step 9: Launch tmux ────────────────────────────────────────────────────── @@ -130,8 +130,8 @@ echo " Ctrl+D exits a pane." echo " tmux kill-session -t qpc-chat to stop." echo "" -ALICE_CMD="$COMPOSE exec alice quicnprotochat chat --state /chat/alice.bin" -BOB_CMD="$COMPOSE exec bob quicnprotochat chat --state /chat/bob.bin" +ALICE_CMD="$COMPOSE exec alice qpq chat --state /chat/alice.bin" +BOB_CMD="$COMPOSE exec bob qpq chat --state /chat/bob.bin" # Kill any stale tmux session with the same name. tmux kill-session -t qpc-chat 2>/dev/null || true