From f6bd511599b06aa64e6c4de6ebd85f57901d4b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Feb 2026 13:49:08 +0000 Subject: [PATCH] feat: Lewes Protocol with PSQv2 (#6491) * merging georgio/lp-psqv2-integration * use authenicator on the responder's side * nym-lp crate compiling * moved the e2e test to nym-lp * move key generation to peer * moved principal generation * update KKTResponder * encapsulation key parsing * Adding concrete types within KKT exchange * initiator side of the full handshake * responder side of the handshake and full e2e test * fixed unit-tests within nym-kkt * LpSession cleanup * helpers for Transport * revamp of the transport traits and initial work on client-side transport * compiling nym-crypto * 'working' client-entry dvpn reg * Fix key conversion * Slightly reduce use of rand08 * reverted back to libcrux repo refs * intial telescoping reg * removing dead code * wip * moved data encryption into the state machine * restoring nym-lp tests * update lp api model * Add receiver index derivation * Add receiver index derivation * use derived receiver index * feat: add kem key generation to nodes * generate fresh x25519, mlkem768 and mceliece keys on config migration * add lp peer config * nym-node startup cleanup * removed dependency on pre-rand09 from nym-lp * re-expose LP information on the http API * fixed tests compilation * add peer config happy path tests * formatting * add more tests and fix bug * better docs * clippy and formatting issues * return error on mceliece within NestedSession * wasm fixes * removed legacy nym-vpn-lib-wasm * fixing wasm for real this time * additional fixes * add payload to kkt * make clippy happy * moved LP to nym-node crate * cargo fmt * integrate lpconfig payload * fix response size trait impl * Migrate receiver index * Change receiver index to u32 and regorganize crates * clippy * hopefully final wasm fixes * simple conversion method from semver to ciphersuite * updated nym-node config template * chore: remove duplicated code --------- Co-authored-by: Georgio Nicolas --- .github/workflows/ci-build-vpn-api-wasm.yml | 42 - Cargo.lock | 248 +- Cargo.toml | 23 +- Makefile | 9 +- common/client-core/Cargo.toml | 4 + common/client-core/src/client/helpers/wasm.rs | 10 + .../validator-client/src/client.rs | 62 +- .../validator-client/src/nym_api/mod.rs | 96 +- .../mixnet/query/query_all_gateways.rs | 2 +- .../mixnet/query/query_all_mixnodes.rs | 2 +- common/crypto/Cargo.toml | 8 +- common/crypto/src/asymmetric/x25519/mod.rs | 103 + .../src/asymmetric/x25519/serde_helpers.rs | 22 + common/crypto/src/hkdf.rs | 149 ++ common/crypto/src/kdf.rs | 98 - common/crypto/src/lib.rs | 2 - common/nym-kkt-ciphersuite/Cargo.toml | 4 +- common/nym-kkt-ciphersuite/src/lib.rs | 84 +- .../Cargo.toml | 12 +- .../context.rs => nym-kkt-context/src/lib.rs} | 144 +- common/nym-kkt/Cargo.toml | 21 +- common/nym-kkt/benches/benches.rs | 480 ---- common/nym-kkt/src/carrier.rs | 188 ++ common/nym-kkt/src/ciphersuite.rs | 74 - common/nym-kkt/src/encryption.rs | 253 -- common/nym-kkt/src/error.rs | 43 +- common/nym-kkt/src/frame.rs | 185 +- common/nym-kkt/src/initiator.rs | 188 ++ common/nym-kkt/src/key_utils.rs | 85 +- common/nym-kkt/src/keys.rs | 440 ++++ common/nym-kkt/src/kkt.rs | 449 ---- common/nym-kkt/src/lib.rs | 636 ++--- common/nym-kkt/src/masked_byte.rs | 189 ++ common/nym-kkt/src/message.rs | 265 ++ common/nym-kkt/src/rekey.rs | 257 ++ common/nym-kkt/src/responder.rs | 196 ++ common/nym-kkt/src/session.rs | 230 -- common/nym-lp-common/Cargo.toml | 8 - common/nym-lp-transport/src/traits.rs | 139 -- common/nym-lp/Cargo.toml | 32 +- common/nym-lp/benches/replay_protection.rs | 23 +- common/nym-lp/src/codec.rs | 1473 ++--------- common/nym-lp/src/config.rs | 12 +- common/nym-lp/src/error.rs | 114 +- common/nym-lp/src/kkt_orchestrator.rs | 492 ---- common/nym-lp/src/lib.rs | 394 +-- common/nym-lp/src/message.rs | 892 ------- common/nym-lp/src/noise_protocol.rs | 337 --- common/nym-lp/src/packet.rs | 289 --- common/nym-lp/src/packet/error.rs | 33 + common/nym-lp/src/packet/header.rs | 165 ++ common/nym-lp/src/packet/message.rs | 501 ++++ common/nym-lp/src/packet/mod.rs | 126 + common/nym-lp/src/packet/replay.rs | 33 + .../src/lib.rs => nym-lp/src/packet/utils.rs} | 6 +- common/nym-lp/src/peer.rs | 155 +- common/nym-lp/src/peer_config.rs | 482 ++++ common/nym-lp/src/psk.rs | 792 ------ common/nym-lp/src/psq/handshake_message.rs | 145 ++ common/nym-lp/src/psq/helpers.rs | 52 +- common/nym-lp/src/psq/initiator.rs | 651 +++-- common/nym-lp/src/psq/mod.rs | 548 ++-- common/nym-lp/src/psq/responder.rs | 734 +++--- common/nym-lp/src/replay/validator.rs | 47 +- common/nym-lp/src/session.rs | 802 ++---- common/nym-lp/src/session_integration/mod.rs | 1009 +++----- common/nym-lp/src/session_manager.rs | 148 +- common/nym-lp/src/state_machine.rs | 1109 ++------- common/nym-lp/src/transport/error.rs | 55 + .../lib.rs => nym-lp/src/transport/mod.rs} | 5 + common/nym-lp/src/transport/traits.rs | 302 +++ common/registration/src/lib.rs | 13 +- common/store-cipher/Cargo.toml | 2 +- common/test-utils/Cargo.toml | 1 + common/test-utils/src/helpers.rs | 47 + common/wasm/client-core/.cargo/config.toml | 1 + common/wasm/storage/.cargo/config.toml | 1 + common/wasm/storage/Cargo.toml | 2 - common/wasm/utils/.cargo/config.toml | 1 + contracts/Cargo.lock | 4 +- gateway/Cargo.toml | 8 +- gateway/src/config.rs | 4 - gateway/src/error.rs | 15 - gateway/src/node/lp_listener/data_handler.rs | 261 -- gateway/src/node/lp_listener/handler.rs | 1427 ----------- gateway/src/node/mod.rs | 52 +- .../wireguard/new_peer_registration/lp.rs | 11 +- .../wireguard/new_peer_registration/mod.rs | 24 +- .../new_peer_registration/pending.rs | 11 +- integration-tests/Cargo.toml | 6 +- integration-tests/src/lp_registration.rs | 278 ++- nym-api/nym-api-requests/Cargo.toml | 1 - nym-api/nym-api-requests/src/ecash/models.rs | 10 +- .../src/models/described/type_translation.rs | 151 +- .../src/models/described/v2.rs | 52 +- nym-api/src/nym_nodes/handlers/v1.rs | 2 +- nym-api/src/nym_nodes/handlers/v2.rs | 34 +- .../storage/.cargo/config.toml | 4 +- .../vpn-api-lib-wasm/Cargo.toml | 38 - .../vpn-api-lib-wasm/Makefile | 4 - .../internal-dev/bootstrap.js | 2 - .../vpn-api-lib-wasm/internal-dev/index.html | 17 - .../vpn-api-lib-wasm/internal-dev/index.js | 49 - .../internal-dev/package.json | 40 - .../internal-dev/webpack.config.js | 33 - .../vpn-api-lib-wasm/internal-dev/yarn.lock | 2204 ----------------- .../vpn-api-lib-wasm/src/error.rs | 35 - .../vpn-api-lib-wasm/src/lib.rs | 277 --- nym-gateway-probe/Cargo.toml | 1 + nym-gateway-probe/src/common/helpers.rs | 10 - nym-gateway-probe/src/common/nodes.rs | 70 +- nym-gateway-probe/src/common/probe_tests.rs | 64 +- nym-node/Cargo.toml | 11 +- nym-node/nym-node-requests/Cargo.toml | 3 +- nym-node/nym-node-requests/src/api/client.rs | 20 +- nym-node/nym-node-requests/src/api/mod.rs | 39 +- .../src/api/v1/lewes_protocol/models.rs | 33 +- nym-node/src/cli/commands/run/args.rs | 9 +- nym-node/src/cli/helpers.rs | 71 +- nym-node/src/config/gateway_tasks.rs | 4 - nym-node/src/config/helpers.rs | 4 - nym-node/src/config/lp.rs | 153 ++ nym-node/src/config/mod.rs | 16 +- nym-node/src/config/old_configs/mod.rs | 2 + .../src/config/old_configs/old_config_v11.rs | 416 +--- .../src/config/old_configs/old_config_v12.rs | 1001 ++++++++ nym-node/src/config/persistence.rs | 55 + nym-node/src/config/template.rs | 40 + nym-node/src/config/upgrade_helpers.rs | 3 +- nym-node/src/env.rs | 8 +- nym-node/src/error.rs | 16 +- nym-node/src/lib.rs | 10 + nym-node/src/main.rs | 8 +- nym-node/src/node/helpers.rs | 83 + .../http/router/api/v1/lewes_protocol/mod.rs | 6 +- .../http/router/api/v1/lewes_protocol/root.rs | 17 +- .../router/api/v1/node/host_information.rs | 3 +- nym-node/src/node/http/router/mod.rs | 8 +- nym-node/src/node/key_rotation/key.rs | 7 +- nym-node/src/node/lp/data_handler.rs | 237 ++ nym-node/src/node/lp/error.rs | 72 + nym-node/src/node/lp/handler.rs | 821 ++++++ .../src/node/lp}/mod.rs | 278 +-- .../src/node/lp}/registration.rs | 10 +- nym-node/src/node/mod.rs | 183 +- nym-node/src/wireguard/mod.rs | 3 + nym-outfox/Cargo.toml | 5 - nym-registration-client/Cargo.toml | 12 +- nym-registration-client/src/clients/lp.rs | 31 +- .../src/lp_client/client.rs | 408 +-- .../src/lp_client/error.rs | 76 +- .../src/lp_client/helpers.rs | 37 +- .../lp_client/nested_session/connection.rs | 154 +- .../src/lp_client/nested_session/mod.rs | 221 +- .../src/lp_client/state_machine_helpers.rs | 64 +- nym-wallet/Cargo.lock | 384 ++- tools/nym-lp-client/Cargo.toml | 1 + tools/nym-lp-client/src/client.rs | 142 +- tools/nym-lp-client/src/topology.rs | 10 +- wasm/client/.cargo/config.toml | 2 + wasm/client/Cargo.toml | 1 + wasm/full-nym-wasm/.cargo/config.toml | 1 + wasm/mix-fetch/.cargo/config.toml | 1 + wasm/mix-fetch/src/helpers.rs | 1 + wasm/node-tester/.cargo/config.toml | 1 + wasm/zknym-lib/.cargo/config.toml | 1 + wasm/zknym-lib/Cargo.toml | 10 - 167 files changed, 10977 insertions(+), 16911 deletions(-) delete mode 100644 .github/workflows/ci-build-vpn-api-wasm.yml delete mode 100644 common/crypto/src/kdf.rs rename common/{nym-lp-transport => nym-kkt-context}/Cargo.toml (54%) rename common/{nym-kkt/src/context.rs => nym-kkt-context/src/lib.rs} (62%) delete mode 100644 common/nym-kkt/benches/benches.rs create mode 100644 common/nym-kkt/src/carrier.rs delete mode 100644 common/nym-kkt/src/ciphersuite.rs delete mode 100644 common/nym-kkt/src/encryption.rs create mode 100644 common/nym-kkt/src/initiator.rs create mode 100644 common/nym-kkt/src/keys.rs delete mode 100644 common/nym-kkt/src/kkt.rs create mode 100644 common/nym-kkt/src/masked_byte.rs create mode 100644 common/nym-kkt/src/message.rs create mode 100644 common/nym-kkt/src/rekey.rs create mode 100644 common/nym-kkt/src/responder.rs delete mode 100644 common/nym-kkt/src/session.rs delete mode 100644 common/nym-lp-common/Cargo.toml delete mode 100644 common/nym-lp-transport/src/traits.rs delete mode 100644 common/nym-lp/src/kkt_orchestrator.rs delete mode 100644 common/nym-lp/src/message.rs delete mode 100644 common/nym-lp/src/noise_protocol.rs delete mode 100644 common/nym-lp/src/packet.rs create mode 100644 common/nym-lp/src/packet/error.rs create mode 100644 common/nym-lp/src/packet/header.rs create mode 100644 common/nym-lp/src/packet/message.rs create mode 100644 common/nym-lp/src/packet/mod.rs create mode 100644 common/nym-lp/src/packet/replay.rs rename common/{nym-lp-common/src/lib.rs => nym-lp/src/packet/utils.rs} (84%) create mode 100644 common/nym-lp/src/peer_config.rs delete mode 100644 common/nym-lp/src/psk.rs create mode 100644 common/nym-lp/src/psq/handshake_message.rs create mode 100644 common/nym-lp/src/transport/error.rs rename common/{nym-lp-transport/src/lib.rs => nym-lp/src/transport/mod.rs} (52%) create mode 100644 common/nym-lp/src/transport/traits.rs delete mode 100644 gateway/src/node/lp_listener/data_handler.rs delete mode 100644 gateway/src/node/lp_listener/handler.rs delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/Makefile delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/src/error.rs delete mode 100644 nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs create mode 100644 nym-node/src/config/lp.rs create mode 100644 nym-node/src/config/old_configs/old_config_v12.rs create mode 100644 nym-node/src/lib.rs create mode 100644 nym-node/src/node/lp/data_handler.rs create mode 100644 nym-node/src/node/lp/error.rs create mode 100644 nym-node/src/node/lp/handler.rs rename {gateway/src/node/lp_listener => nym-node/src/node/lp}/mod.rs (64%) rename {gateway/src/node/lp_listener => nym-node/src/node/lp}/registration.rs (96%) diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml deleted file mode 100644 index 8de8b02c80..0000000000 --- a/.github/workflows/ci-build-vpn-api-wasm.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: ci-build-vpn-api-wasm - -on: - pull_request: - paths: - - 'common/**' - - 'nym-credential-proxy/**' - - '.github/workflows/ci-build-vpn-api-wasm.yml' - -jobs: - wasm: - runs-on: arc-linux-latest - env: - CARGO_TERM_COLOR: always - RUSTUP_PERMIT_COPY_RENAME: 1 - steps: - - name: Check out repository code - uses: actions/checkout@v6 - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }} - target: wasm32-unknown-unknown - override: true - components: rustfmt, clippy - - - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - - name: Install wasm-opt - uses: ./.github/actions/install-wasm-opt - with: - version: '116' - - - name: Install wasm-bindgen-cli - run: cargo install wasm-bindgen-cli - - - name: "Build" - run: make - working-directory: nym-credential-proxy/vpn-api-lib-wasm diff --git a/Cargo.lock b/Cargo.lock index 96d2beb24a..5d6366ccc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,10 +1351,11 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "classic-mceliece-rust" -version = "3.2.0" -source = "git+https://github.com/georgio/classic-mceliece-rust#f2f27048b621df103bbe64369a18174ffec04ae1" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a9b6d27e553269a76625911aa8cf6afaa8659f1b0c85b410cb5f51a87183d9" dependencies = [ - "rand 0.9.2", + "rand 0.8.5", "sha3", "zeroize", ] @@ -1563,11 +1564,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-models" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.5" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "hax-lib", - "pastey", + "pastey 0.2.1", "rand 0.9.2", ] @@ -3307,9 +3308,9 @@ dependencies = [ [[package]] name = "hax-lib" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" dependencies = [ "hax-lib-macros", "num-bigint", @@ -3318,9 +3319,9 @@ dependencies = [ [[package]] name = "hax-lib-macros" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" dependencies = [ "hax-lib-macros-types", "proc-macro-error2", @@ -3331,9 +3332,9 @@ dependencies = [ [[package]] name = "hax-lib-macros-types" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" dependencies = [ "proc-macro2", "quote", @@ -4120,8 +4121,10 @@ dependencies = [ "nym-credential-verification", "nym-credentials-interface", "nym-crypto", - "nym-gateway", - "nym-lp-transport", + "nym-kkt", + "nym-kkt-ciphersuite", + "nym-lp", + "nym-node", "nym-registration-client", "nym-test-utils", "nym-wireguard", @@ -4450,10 +4453,21 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libcrux-aesgcm" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + [[package]] name = "libcrux-chacha20poly1305" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4464,8 +4478,8 @@ dependencies = [ [[package]] name = "libcrux-curve25519" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4475,8 +4489,8 @@ dependencies = [ [[package]] name = "libcrux-ecdh" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-curve25519", "libcrux-p256", @@ -4486,8 +4500,8 @@ dependencies = [ [[package]] name = "libcrux-ed25519" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4499,15 +4513,15 @@ dependencies = [ [[package]] name = "libcrux-hacl-rs" version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-macros", ] [[package]] name = "libcrux-hkdf" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-hmac", @@ -4516,8 +4530,8 @@ dependencies = [ [[package]] name = "libcrux-hmac" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4526,8 +4540,8 @@ dependencies = [ [[package]] name = "libcrux-intrinsics" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "core-models", "hax-lib", @@ -4535,8 +4549,8 @@ dependencies = [ [[package]] name = "libcrux-kem" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-curve25519", "libcrux-ecdh", @@ -4551,16 +4565,30 @@ dependencies = [ [[package]] name = "libcrux-macros" version = "0.0.3" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "quote", "syn 2.0.106", ] +[[package]] +name = "libcrux-ml-dsa" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "core-models", + "hax-lib", + "libcrux-intrinsics", + "libcrux-macros", + "libcrux-platform", + "libcrux-sha3", + "tls_codec", +] + [[package]] name = "libcrux-ml-kem" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "hax-lib", "libcrux-intrinsics", @@ -4574,8 +4602,8 @@ dependencies = [ [[package]] name = "libcrux-p256" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4586,8 +4614,8 @@ dependencies = [ [[package]] name = "libcrux-platform" -version = "0.0.2" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.3" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libc", ] @@ -4595,7 +4623,7 @@ dependencies = [ [[package]] name = "libcrux-poly1305" version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4603,34 +4631,38 @@ dependencies = [ [[package]] name = "libcrux-psq" -version = "0.0.5" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ + "classic-mceliece-rust", + "libcrux-aesgcm", "libcrux-chacha20poly1305", "libcrux-ecdh", "libcrux-ed25519", "libcrux-hkdf", "libcrux-hmac", "libcrux-kem", + "libcrux-ml-dsa", "libcrux-ml-kem", "libcrux-sha2", "libcrux-traits", + "rand 0.8.5", "rand 0.9.2", "tls_codec", ] [[package]] name = "libcrux-secrets" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.5" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "hax-lib", ] [[package]] name = "libcrux-sha2" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -4639,8 +4671,8 @@ dependencies = [ [[package]] name = "libcrux-sha3" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "hax-lib", "libcrux-intrinsics", @@ -4650,8 +4682,8 @@ dependencies = [ [[package]] name = "libcrux-traits" -version = "0.0.4" -source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" dependencies = [ "libcrux-secrets", "rand 0.9.2", @@ -5075,7 +5107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3176f18d11a1ae46053e59ec89d46ba318ae1343615bd3f8c908bfc84edae35c" dependencies = [ "byteorder", - "pastey", + "pastey 0.1.1", "thiserror 2.0.12", ] @@ -5422,7 +5454,6 @@ dependencies = [ "nym-serde-helpers", "nym-test-utils", "nym-ticketbooks-merkle", - "rand_chacha 0.3.1", "schemars 0.8.22", "serde", "serde_json", @@ -5689,6 +5720,7 @@ dependencies = [ "clap", "comfy-table", "futures", + "getrandom 0.3.3", "gloo-timers", "http-body-util", "humantime", @@ -6192,10 +6224,13 @@ dependencies = [ "hkdf", "hmac", "jwt-simple", + "libcrux-curve25519", + "libcrux-psq", "nym-pemstore", "nym-sphinx-types", "nym-test-utils", "rand 0.8.5", + "rand 0.9.2", "rand_chacha 0.3.1", "serde", "serde_bytes", @@ -6357,7 +6392,6 @@ dependencies = [ "bincode", "bip39", "bs58", - "bytes", "dashmap", "defguard_wireguard_rs", "fastrand", @@ -6377,9 +6411,7 @@ dependencies = [ "nym-gateway-storage", "nym-id", "nym-ip-packet-router", - "nym-kcp", "nym-lp", - "nym-lp-transport", "nym-metrics", "nym-mixnet-client", "nym-network-defaults", @@ -6486,6 +6518,7 @@ dependencies = [ "nym-validator-client", "pnet_packet", "rand 0.8.5", + "rand 0.9.2", "reqwest 0.13.1", "serde", "serde_json", @@ -6790,15 +6823,16 @@ name = "nym-kkt" version = "0.1.0" dependencies = [ "anyhow", - "blake3", - "classic-mceliece-rust", - "criterion", "libcrux-chacha20poly1305", "libcrux-ecdh", "libcrux-kem", + "libcrux-ml-kem", + "libcrux-psq", "num_enum", "nym-crypto", "nym-kkt-ciphersuite", + "nym-kkt-context", + "nym-pemstore", "rand 0.9.2", "rand_chacha 0.9.0", "strum", @@ -6813,11 +6847,21 @@ dependencies = [ "blake3", "libcrux-sha3", "num_enum", + "semver 1.0.26", "strum", "strum_macros", "thiserror 2.0.12", ] +[[package]] +name = "nym-kkt-context" +version = "1.20.4" +dependencies = [ + "num_enum", + "nym-kkt-ciphersuite", + "thiserror 2.0.12", +] + [[package]] name = "nym-ledger" version = "1.20.4" @@ -6836,25 +6880,14 @@ dependencies = [ "anyhow", "bs58", "bytes", - "chacha20poly1305", "criterion", - "dashmap", - "libcrux-kem", "libcrux-psq", - "libcrux-traits", - "mock_instant", "num_enum", "nym-crypto", "nym-kkt", - "nym-lp-common", - "nym-lp-transport", + "nym-kkt-ciphersuite", "nym-test-utils", - "parking_lot", - "rand 0.8.5", "rand 0.9.2", - "serde", - "sha2 0.10.9", - "snow", "thiserror 2.0.12", "tls_codec", "tokio", @@ -6886,6 +6919,7 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.8.5", + "rand 0.9.2", "rand_chacha 0.3.1", "serde", "serde_json", @@ -6897,19 +6931,6 @@ dependencies = [ "url", ] -[[package]] -name = "nym-lp-common" -version = "0.1.0" - -[[package]] -name = "nym-lp-transport" -version = "0.1.0" -dependencies = [ - "nym-test-utils", - "tokio", - "tracing", -] - [[package]] name = "nym-metrics" version = "1.20.4" @@ -7112,6 +7133,7 @@ dependencies = [ "arrayref", "async-trait", "axum 0.7.9", + "bincode", "bip39", "blake2 0.8.1", "bloomfilter", @@ -7126,6 +7148,7 @@ dependencies = [ "criterion", "csv", "cupid", + "dashmap", "futures", "hex", "hkdf", @@ -7145,6 +7168,7 @@ dependencies = [ "nym-http-api-common", "nym-ip-packet-router", "nym-kkt", + "nym-lp", "nym-metrics", "nym-mixnet-client", "nym-network-requester", @@ -7154,6 +7178,7 @@ dependencies = [ "nym-noise-keys", "nym-nonexhaustive-delayqueue", "nym-pemstore", + "nym-registration-common", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-forwarding", @@ -7163,6 +7188,7 @@ dependencies = [ "nym-sphinx-types", "nym-statistics-common", "nym-task", + "nym-test-utils", "nym-topology", "nym-types", "nym-validator-client", @@ -7172,7 +7198,7 @@ dependencies = [ "opentelemetry", "opentelemetry_sdk", "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", "serde", "serde_json", "sha2 0.10.9", @@ -7220,9 +7246,9 @@ dependencies = [ "nym-http-api-client", "nym-kkt-ciphersuite", "nym-noise-keys", + "nym-test-utils", "nym-upgrade-mode-check", "nym-wireguard-types", - "rand_chacha 0.3.1", "schemars 0.8.22", "serde", "serde_json", @@ -7445,12 +7471,7 @@ dependencies = [ "blake3", "chacha20", "chacha20poly1305", - "criterion", "fastrand", - "getrandom 0.2.16", - "log", - "rand 0.8.5", - "rayon", "sphinx-packet", "thiserror 2.0.12", "x25519-dalek", @@ -7496,6 +7517,7 @@ dependencies = [ name = "nym-registration-client" version = "1.20.4" dependencies = [ + "bincode", "bytes", "futures", "nym-authenticator-client", @@ -7504,19 +7526,19 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-ip-packet-client", + "nym-kkt", "nym-lp", - "nym-lp-transport", "nym-registration-common", "nym-sdk", + "nym-test-utils", "nym-validator-client", "nym-wireguard-types", - "rand 0.8.5", + "rand 0.9.2", "thiserror 2.0.12", "tokio", "tokio-util", "tracing", "typed-builder", - "url", ] [[package]] @@ -8018,6 +8040,7 @@ dependencies = [ "futures", "nym-bin-common", "rand_chacha 0.3.1", + "rand_chacha 0.9.0", "tokio", "tracing", ] @@ -8251,31 +8274,6 @@ dependencies = [ "ts-rs", ] -[[package]] -name = "nym-vpn-api-lib-wasm" -version = "1.20.4" -dependencies = [ - "bs58", - "getrandom 0.2.16", - "js-sys", - "nym-bin-common", - "nym-compact-ecash", - "nym-credential-proxy-requests", - "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", - "nym-wasm-utils", - "serde", - "serde-wasm-bindgen 0.6.5", - "serde_json", - "thiserror 2.0.12", - "time", - "tsify", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "nym-wallet-types" version = "1.0.0" @@ -8335,9 +8333,7 @@ name = "nym-wasm-storage" version = "1.20.4" dependencies = [ "async-trait", - "getrandom 0.2.16", "indexed_db_futures", - "js-sys", "nym-store-cipher", "nym-wasm-utils", "serde", @@ -8829,6 +8825,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "peg" version = "0.8.5" @@ -14094,22 +14096,14 @@ dependencies = [ name = "zknym-lib" version = "1.20.4" dependencies = [ - "anyhow", "async-trait", - "bs58", - "getrandom 0.2.16", "js-sys", "nym-bin-common", "nym-compact-ecash", - "nym-credentials", - "nym-crypto", "nym-http-api-client", "nym-wasm-utils", - "rand 0.8.5", - "reqwest 0.13.1", "serde", "thiserror 2.0.12", - "tokio", "tsify", "uuid", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 99451a3484..4f7ebfae70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,6 @@ members = [ "common/nym-id", "common/nym-kcp", "common/nym-lp", - "common/nym-lp-common", "common/nym-kkt", "common/nym-metrics", "common/nym_offline_compact_ecash", @@ -129,7 +128,6 @@ members = [ "nym-browser-extension/storage", "nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy-requests", - "nym-credential-proxy/vpn-api-lib-wasm", "nym-data-observatory", "nym-ip-packet-client", "nym-network-monitor", @@ -173,8 +171,9 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", - "nym-gateway-probe", - "integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite", + # "nym-gateway-probe", + "integration-tests", + "common/nym-kkt-ciphersuite", "common/nym-kkt-context", ] default-members = [ @@ -274,6 +273,7 @@ futures = "0.3.31" futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" +getrandom03 = { package = "getrandom", version = "=0.3.3" } glob = "0.3" handlebars = "3.5.5" hex = "0.4.3" @@ -324,6 +324,7 @@ quote = "1" rand = "0.8.5" rand09 = { package = "rand", version = "=0.9.2" } rand_chacha = "0.3" +rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" } rand_core = "0.6.3" rand_distr = "0.4" rayon = "1.5.1" @@ -392,6 +393,17 @@ zeroize = "1.7.0" prometheus = { version = "0.14.0" } + +# libcrux +libcrux-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-curve25519 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-psq = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } +libcrux-traits = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" } + # Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" } nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" } @@ -436,7 +448,8 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def nym-id = { version = "1.20.4", path = "common/nym-id" } nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" } nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" } -nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" } +nym-kkt = { version = "0.1.0", path = "common/nym-kkt" } +nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" } nym-metrics = { version = "1.20.4", path = "common/nym-metrics" } nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" } nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/Makefile b/Makefile index eb58f2ab5c..b88a99d79f 100644 --- a/Makefile +++ b/Makefile @@ -104,11 +104,11 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet)) sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint sdk-wasm-build: - $(MAKE) -C nym-browser-extension/storage wasm-pack +# $(MAKE) -C nym-browser-extension/storage wasm-pack $(MAKE) -C wasm/client $(MAKE) -C wasm/node-tester $(MAKE) -C wasm/mix-fetch - $(MAKE) -C wasm/zknym-lib +# $(MAKE) -C wasm/zknym-lib # $(MAKE) -C wasm/full-nym-wasm # run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo @@ -119,13 +119,14 @@ sdk-typescript-build: yarn --cwd sdk/typescript/codegen/contract-clients build # NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown) -WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib +# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib +WASM_CRATES = nym-client-wasm nym-node-tester-wasm sdk-wasm-test: #cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings sdk-wasm-lint: - cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings + RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings $(MAKE) -C wasm/mix-fetch check-fmt # Add to top-level targets diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 77cdf19c98..c6275532e6 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -121,6 +121,10 @@ features = ["wasm-bindgen"] workspace = true features = ["full"] +[target."cfg(target_arch = \"wasm32\")".dependencies.getrandom03] +workspace = true +features = ["wasm_js"] + [dev-dependencies] tempfile = { workspace = true } diff --git a/common/client-core/src/client/helpers/wasm.rs b/common/client-core/src/client/helpers/wasm.rs index 59f1ec9e75..ef04028396 100644 --- a/common/client-core/src/client/helpers/wasm.rs +++ b/common/client-core/src/client/helpers/wasm.rs @@ -15,3 +15,13 @@ pub(crate) fn get_time_now() -> Instant { pub(crate) fn new_interval_stream(polling_rate: Duration) -> IntervalStream { gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32) } + +#[unsafe(no_mangle)] +unsafe extern "Rust" fn __getrandom_v03_custom( + dest: *mut u8, + len: usize, +) -> Result<(), getrandom03::Error> { + let _ = dest; + let _ = len; + Err(getrandom03::Error::UNSUPPORTED) +} diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index d461157f52..43ab5808a7 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -20,7 +20,7 @@ use nym_api_requests::ecash::{ }; use nym_api_requests::models::{ ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse, - MixnodeCoreStatusResponse, NymNodeDescriptionV1, + MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2, }; use nym_api_requests::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, @@ -273,18 +273,18 @@ impl Client { Ok(history) } - // #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")] + #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")] pub async fn get_all_cached_described_nodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_all_described_nodes().await?) } - // pub async fn get_all_cached_described_nodes_v2( - // &self, - // ) -> Result, ValidatorClientError> { - // Ok(self.nym_api.get_all_described_nodes_v2().await?) - // } + pub async fn get_all_cached_described_nodes_v2( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_all_described_nodes_v2().await?) + } pub async fn get_all_cached_bonded_nym_nodes( &self, @@ -473,7 +473,7 @@ impl NymApiClient { Ok(self.nym_api.health().await?) } - // #[deprecated(note = "use .get_all_described_nodes_v2 instead")] + #[deprecated(note = "use .get_all_described_nodes_v2 instead")] pub async fn get_all_described_nodes( &self, ) -> Result, ValidatorClientError> { @@ -495,29 +495,29 @@ impl NymApiClient { Ok(descriptions) } - // pub async fn get_all_described_nodes_v2( - // &self, - // ) -> Result, ValidatorClientError> { - // // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - // let mut page = 0; - // let mut descriptions = Vec::new(); - // - // loop { - // let mut res = self - // .nym_api - // .get_nodes_described_v2(Some(page), None) - // .await?; - // - // descriptions.append(&mut res.data); - // if descriptions.len() < res.pagination.total { - // page += 1 - // } else { - // break; - // } - // } - // - // Ok(descriptions) - // } + pub async fn get_all_described_nodes_v2( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_nodes_described_v2(Some(page), None) + .await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } pub async fn get_all_bonded_nym_nodes( &self, diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 08bcd7e7f3..cf71f706a4 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody, - NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse, + NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse, }; use nym_api_requests::nym_nodes::{ @@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient { } #[tracing::instrument(level = "debug", skip_all)] - // #[deprecated(note = "use .get_nodes_described_v2 instead")] + #[deprecated(note = "use .get_nodes_described_v2 instead")] async fn get_nodes_described( &self, page: Option, @@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient { .await } - // #[tracing::instrument(level = "debug", skip_all)] - // async fn get_nodes_described_v2( - // &self, - // page: Option, - // per_page: Option, - // ) -> Result, NymAPIError> { - // let mut params = Vec::new(); - // - // if let Some(page) = page { - // params.push(("page", page.to_string())) - // } - // - // if let Some(per_page) = per_page { - // params.push(("per_page", per_page.to_string())) - // } - // - // self.get_json( - // &[ - // routes::V2_API_VERSION, - // routes::NYM_NODES_ROUTES, - // routes::NYM_NODES_DESCRIBED, - // ], - // ¶ms, - // ) - // .await - // } + #[tracing::instrument(level = "debug", skip_all)] + async fn get_nodes_described_v2( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::V2_API_VERSION, + routes::NYM_NODES_ROUTES, + routes::NYM_NODES_DESCRIBED, + ], + ¶ms, + ) + .await + } async fn get_current_rewarded_set(&self) -> Result { self.get_rewarded_set().await @@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient { Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } - // #[deprecated(note = "use .get_all_described_nodes_v2 instead")] - // #[allow(deprecated)] + #[deprecated(note = "use .get_all_described_nodes_v2 instead")] + #[allow(deprecated)] async fn get_all_described_nodes(&self) -> Result, NymAPIError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient { Ok(descriptions) } - // async fn (&self) -> Result, NymAPIError> { - // // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - // let mut page = 0; - // let mut descriptions = Vec::new(); - // - // loop { - // let mut res = self.get_nodes_described_v2(Some(page), None).await?; - // - // descriptions.append(&mut res.data); - // if descriptions.len() < res.pagination.total { - // page += 1 - // } else { - // break; - // } - // } - // - // Ok(descriptions) - // } + async fn get_all_described_nodes_v2(&self) -> Result, NymAPIError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.get_nodes_described_v2(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index f03fb61d28..516a533315 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -14,7 +14,7 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.get_all_cached_described_nodes().await { + match client.get_all_cached_described_nodes_v2().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index ef5b4a9548..b1a81780b9 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -14,7 +14,7 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.get_all_cached_described_nodes().await { + match client.get_all_cached_described_nodes_v2().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 1b5d62f85e..7d037f1a97 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -21,10 +21,13 @@ generic-array = { workspace = true, optional = true } hkdf = { workspace = true, optional = true } hmac = { workspace = true, optional = true } jwt-simple = { workspace = true, optional = true } +libcrux-psq = { workspace = true, optional = true } +libcrux-curve25519 = { workspace = true, optional = true } cipher = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } rand = { workspace = true, optional = true } +rand09 = { workspace = true, optional = true } serde_bytes = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } sha2 = { workspace = true, optional = true } @@ -39,17 +42,18 @@ nym-pemstore = { workspace = true } [dev-dependencies] anyhow = { workspace = true } rand_chacha = { workspace = true } -serde_json = { workspace = true } nym-test-utils = { workspace = true } +serde_json = { workspace = true } [features] default = [] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] naive_jwt = ["asymmetric", "jwt-simple"] +libcrux_x25519 = ["libcrux-psq", "libcrux-curve25519"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"] -hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"] +hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize", "rand09"] stream_cipher = ["aes", "ctr", "cipher", "generic-array"] sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"] diff --git a/common/crypto/src/asymmetric/x25519/mod.rs b/common/crypto/src/asymmetric/x25519/mod.rs index e717df3d92..8b88f6325a 100644 --- a/common/crypto/src/asymmetric/x25519/mod.rs +++ b/common/crypto/src/asymmetric/x25519/mod.rs @@ -17,6 +17,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(feature = "serde")] pub mod serde_helpers; +#[cfg(feature = "libcrux_x25519")] +pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; + /// Size of a X25519 private key pub const PRIVATE_KEY_SIZE: usize = 32; @@ -45,6 +48,9 @@ pub enum KeyRecoveryError { #[source] source: bs58::decode::Error, }, + + #[error("the x25519 private key could not be converted to its PSQ representation")] + IncompatiblePSQPrivateKey, } #[derive(Zeroize, ZeroizeOnDrop)] @@ -413,6 +419,88 @@ impl AsRef<[u8]> for PrivateKey { } } +// libcrux-psq conversion +#[cfg(feature = "libcrux_x25519")] +impl TryFrom for libcrux_psq::handshake::types::DHPrivateKey { + type Error = KeyRecoveryError; + + fn try_from( + key: PrivateKey, + ) -> Result { + Self::try_from(&key) + } +} + +#[cfg(feature = "libcrux_x25519")] +impl From for PrivateKey { + fn from(key: libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey { + // SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length + #[allow(clippy::unwrap_used)] + PrivateKey::from_bytes(key.as_ref()).unwrap() + } +} + +#[cfg(feature = "libcrux_x25519")] +impl TryFrom<&PrivateKey> for libcrux_psq::handshake::types::DHPrivateKey { + type Error = KeyRecoveryError; + + fn try_from( + key: &PrivateKey, + ) -> Result { + let mut private_key_bytes = zeroize::Zeroizing::new(key.to_bytes()); + libcrux_curve25519::clamp(&mut private_key_bytes); + match libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&private_key_bytes) { + Ok(key) => Ok(key), + Err(_) => Err(KeyRecoveryError::IncompatiblePSQPrivateKey), + } + } +} + +#[cfg(feature = "libcrux_x25519")] +impl From<&libcrux_psq::handshake::types::DHPrivateKey> for PrivateKey { + fn from(key: &libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey { + // SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length + #[allow(clippy::unwrap_used)] + PrivateKey::from_bytes(key.as_ref()).unwrap() + } +} + +#[cfg(feature = "libcrux_x25519")] +impl From for libcrux_psq::handshake::types::DHPublicKey { + fn from(key: PublicKey) -> libcrux_psq::handshake::types::DHPublicKey { + libcrux_psq::handshake::types::DHPublicKey::from_bytes(key.as_bytes()) + } +} + +#[cfg(feature = "libcrux_x25519")] +impl From for PublicKey { + fn from(key: libcrux_psq::handshake::types::DHPublicKey) -> PublicKey { + // SAFETY: the DHPublicKey is guaranteed to be 32 bytes in length + #[allow(clippy::unwrap_used)] + PublicKey::from_bytes(key.as_ref()).unwrap() + } +} + +#[cfg(feature = "libcrux_x25519")] +impl TryFrom for libcrux_psq::handshake::types::DHKeyPair { + type Error = KeyRecoveryError; + + fn try_from( + key: KeyPair, + ) -> Result { + Ok(libcrux_psq::handshake::types::DHKeyPair::from( + libcrux_psq::handshake::types::DHPrivateKey::try_from(&key.private_key)?, + )) + } +} + +#[cfg(feature = "libcrux_x25519")] +impl From for KeyPair { + fn from(key: libcrux_psq::handshake::types::DHKeyPair) -> KeyPair { + KeyPair::from(PrivateKey::from(key.sk())) + } +} + #[cfg(test)] mod tests { use super::*; @@ -421,6 +509,21 @@ mod tests { fn assert_zeroize() {} + #[test] + fn test_key_conversion() { + let dalek_kp = super::KeyPair::new(&mut rand::thread_rng()); + + let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned(); + + libcrux_curve25519::clamp(&mut dalek_private_key_bytes); + let libcrux_private_key = + libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes) + .unwrap(); + let libcrux_public_key = libcrux_private_key.to_public(); + + assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes()); + } + #[test] fn private_key_is_zeroized() { assert_zeroize::(); diff --git a/common/crypto/src/asymmetric/x25519/serde_helpers.rs b/common/crypto/src/asymmetric/x25519/serde_helpers.rs index 02a5282cdd..14fe273f10 100644 --- a/common/crypto/src/asymmetric/x25519/serde_helpers.rs +++ b/common/crypto/src/asymmetric/x25519/serde_helpers.rs @@ -44,3 +44,25 @@ pub mod option_bs58_x25519_pubkey { } } } + +#[cfg(feature = "libcrux_x25519")] +pub mod bs58_dh_public_key { + use crate::asymmetric::x25519; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + key: &libcrux_psq::handshake::types::DHPublicKey, + serializer: S, + ) -> Result { + let x25519: x25519::PublicKey = (*key).into(); + serializer.serialize_str(&x25519.to_base58_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let s = String::deserialize(deserializer)?; + let x25519 = x25519::PublicKey::from_base58_string(s).map_err(serde::de::Error::custom)?; + Ok(x25519.into()) + } +} diff --git a/common/crypto/src/hkdf.rs b/common/crypto/src/hkdf.rs index c343002430..0cd4650083 100644 --- a/common/crypto/src/hkdf.rs +++ b/common/crypto/src/hkdf.rs @@ -109,3 +109,152 @@ impl DerivationMaterial { } } } + +pub mod blake3 { + + //! Key Derivation Functions using Blake3. + + use blake3::Hasher; + + use rand09::{RngCore, rng}; + use zeroize::Zeroize; + + pub fn derive_key_blake3_multi_input( + info: &str, + input_key_material: &[&[u8]], + salt: &[u8], + ) -> [u8; 32] { + let mut hasher = Hasher::new_derive_key(info); + + for input_key in input_key_material { + hasher.update(input_key); + } + + hasher.update(salt); + + hasher.finalize().as_bytes().to_owned() + } + + /// Derives a 32-byte key using Blake3's key derivation mode. + /// + /// Uses Blake3's built-in `derive_key` function with domain separation via context string. + /// + /// # Arguments + /// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1") + /// * `input_key_material` - Input key material (shared secret from ECDH, etc.) + /// * `salt` - Additional salt for freshness (nonce) + /// + /// # Returns + /// 32-byte derived key suitable for use as PSK + /// + /// # Example + /// ```ignore + /// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt); + /// ``` + pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] { + derive_key_blake3_multi_input(info, &[input_key_material], salt) + } + + pub fn derive_fresh_key_blake3_multi_input( + info: &str, + input_key_material: &[&[u8]], + ) -> [u8; 32] { + let mut salt = [0u8; 32]; + rng().fill_bytes(&mut salt); + + let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt); + + // Zeroize salt + salt.zeroize(); + + derived_key + } + + /// Derives a fresh 32-byte key using Blake3's key derivation mode. + /// The function calls a random number generator to generate a fresh salt. + /// Uses Blake3's built-in `derive_key` function with domain separation via context string. + /// + /// # Arguments + /// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1") + /// * `input_key_material` - Input key material (shared secret from ECDH, etc.) + /// + /// # Returns + /// 32-byte derived key suitable for use as PSK + /// + /// # Example + /// ```ignore + /// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes()); + /// ``` + pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] { + derive_fresh_key_blake3_multi_input(info, &[input_key_material]) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_deterministic_derivation() { + let context = "test-context"; + let key_material = b"shared_secret_12345"; + let salt = b"salt_67890"; + + let key1 = derive_key_blake3(context, key_material, salt); + let key2 = derive_key_blake3(context, key_material, salt); + + assert_eq!(key1, key2, "Same inputs should produce same output"); + } + + #[test] + fn test_different_contexts_produce_different_keys() { + let key_material = b"shared_secret"; + let salt = b"salt"; + + let key1 = derive_key_blake3("context1", key_material, salt); + let key2 = derive_key_blake3("context2", key_material, salt); + + assert_ne!( + key1, key2, + "Different contexts should produce different keys" + ); + } + + #[test] + fn test_different_salts_produce_different_keys() { + let context = "test-context"; + let key_material = b"shared_secret"; + + let key1 = derive_key_blake3(context, key_material, b"salt1"); + let key2 = derive_key_blake3(context, key_material, b"salt2"); + + assert_ne!(key1, key2, "Different salts should produce different keys"); + } + + #[test] + fn test_different_key_material_produces_different_keys() { + let context = "test-context"; + let salt = b"salt"; + + let key1 = derive_key_blake3(context, b"secret1", salt); + let key2 = derive_key_blake3(context, b"secret2", salt); + + assert_ne!( + key1, key2, + "Different key material should produce different keys" + ); + } + + #[test] + fn test_output_length() { + let key = derive_key_blake3("test", b"key", b"salt"); + assert_eq!(key.len(), 32, "Output should be exactly 32 bytes"); + } + + #[test] + fn test_empty_inputs() { + // Should not panic with empty inputs + let key = derive_key_blake3("test", b"", b""); + assert_eq!(key.len(), 32); + } + } +} diff --git a/common/crypto/src/kdf.rs b/common/crypto/src/kdf.rs deleted file mode 100644 index 3edb257299..0000000000 --- a/common/crypto/src/kdf.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Key Derivation Functions using Blake3. - -/// Derives a 32-byte key using Blake3's key derivation mode. -/// -/// Uses Blake3's built-in `derive_key` function with domain separation via context string. -/// -/// # Arguments -/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1") -/// * `key_material` - Input key material (shared secret from ECDH, etc.) -/// * `salt` - Additional salt for freshness (timestamp + nonce) -/// -/// # Returns -/// 32-byte derived key suitable for use as PSK -/// -/// # Example -/// ```ignore -/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt); -/// ``` -pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] { - // Concatenate key_material and salt as input - let input = [key_material, salt].concat(); - - // Use Blake3's derive_key with context for domain separation - // blake3::derive_key returns [u8; 32] directly - blake3::derive_key(context, &input) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_deterministic_derivation() { - let context = "test-context"; - let key_material = b"shared_secret_12345"; - let salt = b"salt_67890"; - - let key1 = derive_key_blake3(context, key_material, salt); - let key2 = derive_key_blake3(context, key_material, salt); - - assert_eq!(key1, key2, "Same inputs should produce same output"); - } - - #[test] - fn test_different_contexts_produce_different_keys() { - let key_material = b"shared_secret"; - let salt = b"salt"; - - let key1 = derive_key_blake3("context1", key_material, salt); - let key2 = derive_key_blake3("context2", key_material, salt); - - assert_ne!( - key1, key2, - "Different contexts should produce different keys" - ); - } - - #[test] - fn test_different_salts_produce_different_keys() { - let context = "test-context"; - let key_material = b"shared_secret"; - - let key1 = derive_key_blake3(context, key_material, b"salt1"); - let key2 = derive_key_blake3(context, key_material, b"salt2"); - - assert_ne!(key1, key2, "Different salts should produce different keys"); - } - - #[test] - fn test_different_key_material_produces_different_keys() { - let context = "test-context"; - let salt = b"salt"; - - let key1 = derive_key_blake3(context, b"secret1", salt); - let key2 = derive_key_blake3(context, b"secret2", salt); - - assert_ne!( - key1, key2, - "Different key material should produce different keys" - ); - } - - #[test] - fn test_output_length() { - let key = derive_key_blake3("test", b"key", b"salt"); - assert_eq!(key.len(), 32, "Output should be exactly 32 bytes"); - } - - #[test] - fn test_empty_inputs() { - // Should not panic with empty inputs - let key = derive_key_blake3("test", b"", b""); - assert_eq!(key.len(), 32); - } -} diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index 3875fa7f81..1dff7b82be 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -10,8 +10,6 @@ pub mod crypto_hash; pub mod hkdf; #[cfg(feature = "hashing")] pub mod hmac; -#[cfg(feature = "hashing")] -pub mod kdf; #[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))] pub mod shared_key; pub mod symmetric; diff --git a/common/nym-kkt-ciphersuite/Cargo.toml b/common/nym-kkt-ciphersuite/Cargo.toml index 079325f625..ef1b8e7738 100644 --- a/common/nym-kkt-ciphersuite/Cargo.toml +++ b/common/nym-kkt-ciphersuite/Cargo.toml @@ -9,15 +9,17 @@ license.workspace = true rust-version.workspace = true readme.workspace = true version.workspace = true +publish = false [dependencies] thiserror = { workspace = true } num_enum = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } +semver = { workspace = true } blake3 = { workspace = true, optional = true } -libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true } +libcrux-sha3 = { workspace = true, optional = true } [features] digests = ["blake3", "libcrux-sha3"] diff --git a/common/nym-kkt-ciphersuite/src/lib.rs b/common/nym-kkt-ciphersuite/src/lib.rs index e72d873a87..9e5c382dc6 100644 --- a/common/nym-kkt-ciphersuite/src/lib.rs +++ b/common/nym-kkt-ciphersuite/src/lib.rs @@ -3,10 +3,12 @@ use crate::error::KKTCiphersuiteError; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fmt::Display; use strum_macros::{Display, EnumIter, EnumString}; +pub use strum::IntoEnumIterator; + pub mod error; pub const DEFAULT_HASH_LEN: usize = 32; @@ -45,10 +47,13 @@ pub mod xwing { pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH; } -pub type KEMKeyDigests = KeyDigests; -pub type SigningKeyDigests = KeyDigests; +pub type KEMKeyDigests = BTreeMap>; -pub type KeyDigests = HashMap>; +pub mod node_compatibility { + /// Indicates the initial version where kkt has been introduced + /// 1.27.0 Raclette release + pub const INTRODUCTION: semver::Version = semver::Version::new(1, 27, 0); +} #[derive( Clone, @@ -62,6 +67,8 @@ pub type KeyDigests = HashMap>; EnumIter, EnumString, Display, + Ord, + PartialOrd, )] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "lowercase")] @@ -204,23 +211,26 @@ impl SignatureScheme { EnumIter, EnumString, Display, + Default, + Ord, + PartialOrd, )] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "lowercase")] #[repr(u8)] pub enum KEM { - XWing = 0, + // unsupported + // XWing = 0, + #[default] MlKem768 = 1, McEliece = 2, - X25519 = 255, } impl KEM { - pub fn encapsulation_key_length(&self) -> usize { + pub const fn encapsulation_key_length(&self) -> usize { match self { KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH, - KEM::XWing => xwing::PUBLIC_KEY_LENGTH, - KEM::X25519 => x25519::PUBLIC_KEY_LENGTH, + // KEM::XWing => xwing::PUBLIC_KEY_LENGTH, KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH, } } @@ -238,6 +248,17 @@ pub struct Ciphersuite { signature_length: usize, } +impl Default for Ciphersuite { + fn default() -> Self { + Ciphersuite::new( + KEM::MlKem768, + HashFunction::Blake3, + SignatureScheme::Ed25519, + HashLength::Default, + ) + } +} + impl Ciphersuite { pub fn new( kem: KEM, @@ -257,6 +278,51 @@ impl Ciphersuite { } } + /// Determine optimal `Ciphersuite` based on remote's node's version + pub fn from_node_version(semver: semver::Version) -> Option { + if semver < node_compatibility::INTRODUCTION { + // node can't possibly support any Ciphersuite + return None; + } + // currently there are no other branches known to the client + // once changes to defaults are introduced, follow pattern similar to the one implemented in + // `common/authenticator-requests/src/version.rs` + Some(Ciphersuite::new( + KEM::MlKem768, + HashFunction::Blake3, + SignatureScheme::Ed25519, + HashLength::Default, + )) + } + + #[must_use] + pub fn with_kem(mut self, kem: KEM) -> Self { + self.kem = kem; + self.encapsulation_key_length = kem.encapsulation_key_length(); + self + } + + #[must_use] + pub fn with_signature_scheme(mut self, signature_scheme: SignatureScheme) -> Self { + self.signature_scheme = signature_scheme; + self.signing_key_length = signature_scheme.signing_key_length(); + self.verification_key_length = signature_scheme.verification_key_length(); + self.signature_length = signature_scheme.signature_length(); + self + } + + #[must_use] + pub fn with_hash_function(mut self, hash_function: HashFunction) -> Self { + self.hash_function = hash_function; + self + } + + #[must_use] + pub fn with_hash_length(mut self, hash_length: HashLength) -> Self { + self.hash_length = hash_length; + self + } + pub fn kem_key_len(&self) -> usize { self.encapsulation_key_length } diff --git a/common/nym-lp-transport/Cargo.toml b/common/nym-kkt-context/Cargo.toml similarity index 54% rename from common/nym-lp-transport/Cargo.toml rename to common/nym-kkt-context/Cargo.toml index d4448e49c8..9ab1c8e123 100644 --- a/common/nym-lp-transport/Cargo.toml +++ b/common/nym-kkt-context/Cargo.toml @@ -1,6 +1,5 @@ [package] -name = "nym-lp-transport" -version = "0.1.0" +name = "nym-kkt-context" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -9,15 +8,14 @@ edition.workspace = true license.workspace = true rust-version.workspace = true readme.workspace = true +version.workspace = true publish = false [dependencies] -tokio = { workspace = true, features = ["net", "io-util"] } -nym-test-utils = { path = "../test-utils", optional = true } -tracing = { workspace = true } +num_enum = { workspace = true } +thiserror = { workspace = true } -[features] -io-mocks = ["nym-test-utils"] +nym-kkt-ciphersuite = { path = "../nym-kkt-ciphersuite" } [lints] workspace = true diff --git a/common/nym-kkt/src/context.rs b/common/nym-kkt-context/src/lib.rs similarity index 62% rename from common/nym-kkt/src/context.rs rename to common/nym-kkt-context/src/lib.rs index 8034c52af7..7e7e1f7b6b 100644 --- a/common/nym-kkt/src/context.rs +++ b/common/nym-kkt-context/src/lib.rs @@ -1,13 +1,38 @@ -// Copyright 2025 - Nym Technologies SA +// Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN; -use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN}; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use nym_kkt_ciphersuite::{CIPHERSUITE_ENCODING_LEN, Ciphersuite}; use std::fmt::Display; +use thiserror::Error; + +// This must be less than 4 bits +pub const KKT_VERSION: u8 = 1; +const _: () = assert!(KKT_VERSION < 1 << 4); pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN; +#[derive(Debug, Error)] +pub enum KKTContextEncodingError { + #[error("KKT Message Count Limit Reached")] + MessageCountLimitReached, + + #[error("{version} is not a valid KKT version")] + InvalidVersion { version: u8 }, + + #[error("{raw} is not a valid KKTStatus")] + InvalidStatus { raw: u8 }, + + #[error("{raw} is not a valid KKTRole")] + InvalidRole { raw: u8 }, + + #[error("{raw} is not a valid KKTMode")] + InvalidMode { raw: u8 }, + + #[error(transparent)] + InvalidCiphersuite(#[from] nym_kkt_ciphersuite::error::KKTCiphersuiteError), +} + // bitmask used: 0b1110_0000 #[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] @@ -15,11 +40,11 @@ pub enum KKTStatus { Ok = 0b0000_0000, InvalidRequestFormat = 0b0010_0000, InvalidResponseFormat = 0b0100_0000, - InvalidSignature = 0b0110_0000, - UnsupportedCiphersuite = 0b1000_0000, - UnsupportedKKTVersion = 0b1010_0000, - InvalidKey = 0b1100_0000, - Timeout = 0b1110_0000, + UnsupportedCiphersuite = 0b0110_0000, + UnsupportedKKTVersion = 0b1000_0000, + InvalidKey = 0b1010_0000, + Timeout = 0b1100_0000, + UnverifiedKEMKey = 0b1110_0000, } impl Display for KKTStatus { @@ -28,10 +53,10 @@ impl Display for KKTStatus { KKTStatus::Ok => "Ok", KKTStatus::InvalidRequestFormat => "Invalid Request Format", KKTStatus::InvalidResponseFormat => "Invalid Response Format", - KKTStatus::InvalidSignature => "Invalid Signature", KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite", KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version", KKTStatus::InvalidKey => "Invalid Key", + KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key", KKTStatus::Timeout => "Timeout", }) } @@ -43,7 +68,16 @@ impl Display for KKTStatus { pub enum KKTRole { Initiator = 0b0000_0000, Responder = 0b0000_0001, - AnonymousInitiator = 0b0000_0010, +} + +impl KKTRole { + pub const fn is_initiator(&self) -> bool { + matches!(self, KKTRole::Initiator) + } + + pub const fn is_responder(&self) -> bool { + matches!(self, KKTRole::Responder) + } } // bitmask used: 0b0001_1100 @@ -54,6 +88,16 @@ pub enum KKTMode { Mutual = 0b0000_0100, } +impl KKTMode { + pub const fn is_one_way(&self) -> bool { + matches!(self, KKTMode::OneWay) + } + + pub const fn is_mutual(&self) -> bool { + matches!(self, KKTMode::Mutual) + } +} + #[derive(Copy, Clone, Debug, PartialEq)] pub struct KKTContext { version: u8, @@ -63,24 +107,20 @@ pub struct KKTContext { role: KKTRole, ciphersuite: Ciphersuite, } + impl KKTContext { - pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result { - if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay { - return Err(KKTError::IncompatibilityError { - info: "Anonymous Initiator can only use OneWay mode", - }); - } - Ok(Self { + pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Self { + Self { version: KKT_VERSION, message_sequence: 0, status: KKTStatus::Ok, mode, role, ciphersuite, - }) + } } - pub fn derive_responder_header(&self) -> Result { + pub fn derive_responder_header(&self) -> Result { let mut responder_header = *self; responder_header.increment_message_sequence_count()?; @@ -89,12 +129,12 @@ impl KKTContext { Ok(responder_header) } - pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> { + pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTContextEncodingError> { if self.message_sequence + 1 < (1 << 4) { self.message_sequence += 1; Ok(()) } else { - Err(KKTError::MessageCountLimitReached) + Err(KKTContextEncodingError::MessageCountLimitReached) } } @@ -118,9 +158,10 @@ impl KKTContext { } pub fn body_len(&self) -> usize { - if self.status != KKTStatus::Ok - || (self.mode == KKTMode::OneWay - && (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator)) + if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey) + || + // no payload + (self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator) { 0 } else { @@ -128,37 +169,18 @@ impl KKTContext { } } - pub fn signature_len(&self) -> usize { - match self.role { - KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(), - KKTRole::AnonymousInitiator => 0, - } - } - pub const fn header_len(&self) -> usize { KKT_CONTEXT_LEN } - pub const fn session_id_len(&self) -> usize { - // note: if anyone decides to update this function and changes the constant value, - // you will have to adjust encoding/decoding functions - - // match self.role { - // KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH, - // It doesn't make sense to send a session_id if we send messages in the clear - // KKTRole::AnonymousInitiator => 0, - // } - KKT_SESSION_ID_LEN - } - pub fn full_message_len(&self) -> usize { - self.body_len() + self.signature_len() + self.header_len() + self.session_id_len() + self.body_len() + self.header_len() } - pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> { + pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTContextEncodingError> { let mut header_bytes = [0u8; KKT_CONTEXT_LEN]; if self.message_sequence >= 1 << 4 { - return Err(KKTError::MessageCountLimitReached); + return Err(KKTContextEncodingError::MessageCountLimitReached); } let ciphersuite_bytes = self.ciphersuite.encode(); @@ -175,15 +197,17 @@ impl KKTContext { Ok(header_bytes) } - pub fn try_decode(header_bytes: [u8; KKT_CONTEXT_LEN]) -> Result { + pub fn try_decode( + header_bytes: [u8; KKT_CONTEXT_LEN], + ) -> Result { let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4; let message_sequence_counter = header_bytes[0] & 0b0000_1111; // We only check if stuff is valid here, not necessarily if it's compatible if kkt_version > KKT_VERSION { - return Err(KKTError::FrameDecodingError { - info: format!("Header - Invalid KKT Version: {kkt_version}"), + return Err(KKTContextEncodingError::InvalidVersion { + version: kkt_version, }); } @@ -191,16 +215,15 @@ impl KKTContext { let raw_kkt_role = header_bytes[1] & 0b0000_0011; let raw_kkt_mode = header_bytes[1] & 0b0001_1100; - let status = - KKTStatus::try_from(raw_kkt_status).map_err(|_| KKTError::FrameDecodingError { - info: format!("Header - Invalid KKT Status: {raw_kkt_status}"), - })?; - let role = KKTRole::try_from(raw_kkt_role).map_err(|_| KKTError::FrameDecodingError { - info: format!("Header - Invalid KKT Role: {raw_kkt_role}"), - })?; - let mode = KKTMode::try_from(raw_kkt_mode).map_err(|_| KKTError::FrameDecodingError { - info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"), + let status = KKTStatus::try_from(raw_kkt_status).map_err(|_| { + KKTContextEncodingError::InvalidStatus { + raw: raw_kkt_status, + } })?; + let role = KKTRole::try_from(raw_kkt_role) + .map_err(|_| KKTContextEncodingError::InvalidRole { raw: raw_kkt_role })?; + let mode = KKTMode::try_from(raw_kkt_mode) + .map_err(|_| KKTContextEncodingError::InvalidMode { raw: raw_kkt_mode })?; // SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes #[allow(clippy::unwrap_used)] @@ -228,9 +251,8 @@ mod tests { let valid_context = KKTContext::new( KKTRole::Initiator, KKTMode::Mutual, - Ciphersuite::decode([255, 1, 0, 0]).unwrap(), - ) - .unwrap(); + Ciphersuite::decode([1, 1, 0, 0]).unwrap(), + ); let encoded = valid_context.encode().unwrap(); let decoded = KKTContext::try_decode(encoded).unwrap(); diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml index 705395f712..207b1f00ed 100644 --- a/common/nym-kkt/Cargo.toml +++ b/common/nym-kkt/Cargo.toml @@ -7,35 +7,30 @@ license.workspace = true publish = false [dependencies] -blake3 = { workspace = true } thiserror = { workspace = true } num_enum = { workspace = true } strum = { workspace = true } - # internal -nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] } +nym-crypto = { path = "../crypto", features = ["hashing"] } nym-kkt-ciphersuite = { workspace = true, features = ["digests"] } +nym-kkt-context = { path = "../nym-kkt-context" } +nym-pemstore = { workspace = true } -libcrux-kem = { git = "https://github.com/cryspen/libcrux" } -libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] } -libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" } +libcrux-kem = { workspace = true } +libcrux-ecdh = { workspace = true, features = ["codec"] } +libcrux-chacha20poly1305 = { workspace = true } -# rand 0.9 for libcrux integration (libcrux uses rand 0.9) rand09 = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } -classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] } +libcrux-psq = { workspace = true, features = ["classic-mceliece"] } +libcrux-ml-kem = { workspace = true } [dev-dependencies] rand_chacha = "0.9.0" anyhow = { workspace = true } -criterion = { workspace = true } -[[bench]] -name = "benches" -harness = false - [lints] workspace = true diff --git a/common/nym-kkt/benches/benches.rs b/common/nym-kkt/benches/benches.rs deleted file mode 100644 index 4d4f0f7e13..0000000000 --- a/common/nym-kkt/benches/benches.rs +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -// fine in benchmarking code -#![allow(clippy::expect_used)] -#![allow(clippy::unwrap_used)] - -use criterion::{Criterion, criterion_group, criterion_main}; - -use nym_crypto::asymmetric::ed25519; -use nym_kkt::{ - ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme}, - context::KKTMode, - frame::KKTFrame, - key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key}, - session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, - }, -}; -use rand09::prelude::*; - -pub fn gen_ed25519_keypair(c: &mut Criterion) { - c.bench_function("Generate Ed25519 Keypair", |b| { - b.iter(|| { - let mut s: [u8; 32] = [0u8; 32]; - rand09::rng().fill_bytes(&mut s); - ed25519::KeyPair::from_secret(s, 0) - }); - }); -} - -pub fn gen_mlkem768_keypair(c: &mut Criterion) { - c.bench_function("Generate MlKem768 Keypair", |b| { - b.iter(|| { - libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap() - }); - }); -} - -pub fn kkt_benchmark(c: &mut Criterion) { - let mut rng = rand09::rng(); - - // generate ed25519 keys - let mut secret_initiator: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_initiator); - let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0); - - let mut secret_responder: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_responder); - - let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1); - for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { - for hash_function in [ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ] { - let ciphersuite = Ciphersuite::resolve_ciphersuite( - kem, - hash_function, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // generate kem public keys - - let (responder_kem_public_key, initiator_kem_public_key) = match kem { - KEM::MlKem768 => ( - EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::XWing => ( - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::X25519 => ( - EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::McEliece => ( - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - ), - }; - - let i_kem_key_bytes = initiator_kem_public_key.encode(); - - let r_kem_key_bytes = responder_kem_public_key.encode(); - - let i_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &i_kem_key_bytes, - ); - - let r_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &r_kem_key_bytes, - ); - - // Anonymous Initiator, OneWay - { - c.bench_function( - &format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",), - |b| { - b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap()); - }, - ); - - let (i_context, i_frame) = - anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Encode Frame - Request", - ), - |b| b.iter(|| i_frame.to_bytes()), - ); - - let i_frame_bytes = i_frame.to_bytes(); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Decode Frame - Request", - ), - |b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()), - ); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame", - ), - |b| { - b.iter(|| { - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap() - }); - }, - ); - - let (r_context, _) = - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Responder Generate Response", - ), - |b| { - b.iter(|| { - responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap() - }); - }, - ); - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Responder Encode Frame", - ), - |b| b.iter(|| r_frame.to_bytes()), - ); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Anonymous Initiator: Initiator Ingest Response", - ), - |b| { - b.iter(|| { - initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap() - }); - }, - ); - - let obtained_key = initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(obtained_key.encode(), r_kem_key_bytes) - } - // Initiator, OneWay - { - let (i_context, i_frame) = initiator_process( - &mut rng, - KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",), - |b| { - b.iter(|| { - initiator_process( - &mut rng, - KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap() - }); - }, - ); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator OneWay: Encode Frame - Request",), - |b| b.iter(|| i_frame.to_bytes()), - ); - - let i_frame_bytes = i_frame.to_bytes(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator OneWay: Decode Frame - Request",), - |b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()), - ); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",), - |b| { - b.iter(|| { - responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap() - }); - }, - ); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); - - assert!(r_obtained_key.is_none()); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Initiator OneWay: Responder Generate Response", - ), - |b| { - b.iter(|| { - responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap() - }); - }, - ); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator OneWay: Responder Encode Frame",), - |b| { - b.iter(|| r_frame.to_bytes()); - }, - ); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Initiator OneWay: Initiator Ingest Response", - ), - |b| { - b.iter(|| { - initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap() - }); - }, - ); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - - // Initiator, Mutual - { - c.bench_function( - &format!("{kem}, {hash_function} | Initiator Mutual: Generate Request",), - |b| { - b.iter(|| { - initiator_process( - &mut rng, - KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap() - }); - }, - ); - - let (i_context, i_frame) = initiator_process( - &mut rng, - KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator Mutual: Encode Frame - Request",), - |b| { - b.iter(|| i_frame.to_bytes()); - }, - ); - - let i_frame_bytes = i_frame.to_bytes(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator Mutual: Decode Frame - Request",), - |b| { - b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()); - }, - ); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",), - |b| { - b.iter(|| { - responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap() - }); - }, - ); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); - - assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Initiator Mutual: Responder Generate Response", - ), - |b| { - b.iter(|| { - responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap() - }); - }, - ); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - c.bench_function( - &format!("{kem}, {hash_function} | Initiator Mutual: Responder Encode Frame",), - |b| { - b.iter(|| { - r_frame.to_bytes(); - }); - }, - ); - - c.bench_function( - &format!( - "{kem}, {hash_function} | Initiator Mutual: Initiator Ingest Response", - ), - |b| { - b.iter(|| { - initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap() - }); - }, - ); - - let obtained_key = initiator_ingest_response( - &i_context, - &r_frame, - &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(obtained_key.encode(), r_kem_key_bytes) - } - } - } -} - -criterion_group!( - benches, - gen_ed25519_keypair, - gen_mlkem768_keypair, - kkt_benchmark -); -criterion_main!(benches); diff --git a/common/nym-kkt/src/carrier.rs b/common/nym-kkt/src/carrier.rs new file mode 100644 index 0000000000..001c10b609 --- /dev/null +++ b/common/nym-kkt/src/carrier.rs @@ -0,0 +1,188 @@ +use libcrux_chacha20poly1305::TAG_LEN; +use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey}; +use nym_crypto::hkdf::blake3::derive_key_blake3; +use rand09::{CryptoRng, RngCore}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use crate::error::KKTError; + +// This is arbitrary +pub const MAX_PAYLOAD_LEN: usize = 1_000_000; +const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX"; +const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX"; + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct Carrier { + tx_key: [u8; 32], + rx_key: [u8; 32], + tx_counter: u64, + rx_counter: u64, +} + +pub enum CarrierRole { + Initiator, + Responder, +} + +fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> { + match nonce.checked_add(1) { + Some(incremented_nonce) => { + *nonce = incremented_nonce; + Ok(()) + } + None => Err(KKTError::AEADError { + info: "Nonce maxed out.", + }), + } +} + +fn as_nonce_bytes(nonce: u64) -> [u8; 12] { + let mut bytes = [0u8; 12]; + let nonce_bytes = nonce.to_le_bytes(); + bytes[4..].clone_from_slice(&nonce_bytes); + bytes +} + +impl Carrier { + fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self { + Self { + tx_key, + rx_key, + tx_counter: 1, + rx_counter: 1, + } + } + + pub fn new( + rng: &mut R, + remote_public_key: &DHPublicKey, + context: &[u8], + is_initiator: bool, + ) -> Result<(Self, DHPublicKey), KKTError> + where + R: RngCore + CryptoRng, + { + let ephemeral_keypair = DHKeyPair::new(rng); + let shared_secret = ephemeral_keypair + .sk() + .diffie_hellman(remote_public_key) + .map_err(|_| KKTError::X25519Error { + info: "Key Derivation Error", + })?; + + Ok(( + Self::from_secret_slice(shared_secret.as_ref(), context, is_initiator), + ephemeral_keypair.pk, + )) + } + + pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8], is_initiator: bool) -> Self { + let (tx_key, rx_key) = if is_initiator { + ( + derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context), + derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context), + ) + } else { + ( + derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context), + derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context), + ) + }; + + Self::init(tx_key, rx_key) + } + + pub fn from_secret(secret: [u8; 32], context: &[u8], is_initiator: bool) -> Self { + Self::from_secret_slice(Zeroizing::new(secret).as_slice(), context, is_initiator) + } + + pub fn encrypt(&mut self, plaintext: &[u8]) -> Result, KKTError> { + if plaintext.len() > MAX_PAYLOAD_LEN { + return Err(KKTError::AEADError { + info: "Plaintext too large", + }); + } + let mut output_buffer = vec![0; plaintext.len() + TAG_LEN]; + libcrux_chacha20poly1305::encrypt( + &self.tx_key, + plaintext, + &mut output_buffer, + b"kkt-carrier-v1", + &as_nonce_bytes(self.tx_counter), + )?; + + increment_nonce(&mut self.tx_counter)?; + + Ok(output_buffer) + } + pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result, KKTError> { + if ciphertext.len() > MAX_PAYLOAD_LEN + TAG_LEN { + return Err(KKTError::AEADError { + info: "Ciphertext too large", + }); + } + let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN]; + libcrux_chacha20poly1305::decrypt( + &self.rx_key, + &mut output_buffer, + ciphertext, + b"kkt-carrier-v1", + &as_nonce_bytes(self.rx_counter), + )?; + + increment_nonce(&mut self.rx_counter)?; + + Ok(output_buffer) + } +} + +#[cfg(test)] +mod tests { + use crate::{carrier::Carrier, key_utils::generate_lp_keypair_x25519}; + use rand09::RngCore; + + #[test] + fn test_e2e() { + let mut rng = rand09::rng(); + + // generate responder x25519 keys + let r_x25519 = generate_lp_keypair_x25519(&mut rng); + + let mut context: [u8; 32] = [0u8; 32]; + rng.fill_bytes(&mut context); + + let ephemeral_keypair = generate_lp_keypair_x25519(&mut rng); + + let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap(); + + let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap(); + + let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context, true); + let mut r_carrier = Carrier::from_secret_slice(r_shared_secret.as_ref(), &context, false); + + let test1 = b"test1: i>r #1"; + let ct1 = i_carrier.encrypt(test1).unwrap(); + let pt1 = r_carrier.decrypt(&ct1).unwrap(); + assert_eq!(pt1, test1); + + let test2 = b"test2: r>i #1"; + let ct2 = i_carrier.encrypt(test2).unwrap(); + let pt2 = r_carrier.decrypt(&ct2).unwrap(); + assert_eq!(pt2, test2); + let test3 = b"test3: i>r #2"; + + let ct3 = i_carrier.encrypt(test3).unwrap(); + let pt3 = r_carrier.decrypt(&ct3).unwrap(); + assert_eq!(pt3, test3); + + let test4 = b"test4: i>r #3"; + let ct4 = i_carrier.encrypt(test4).unwrap(); + let pt4 = r_carrier.decrypt(&ct4).unwrap(); + assert_eq!(pt4, test4); + + let test5 = b"test5: r>i #2"; + let ct5 = i_carrier.encrypt(test5).unwrap(); + let pt5 = r_carrier.decrypt(&ct5).unwrap(); + assert_eq!(pt5, test5); + } +} diff --git a/common/nym-kkt/src/ciphersuite.rs b/common/nym-kkt/src/ciphersuite.rs deleted file mode 100644 index c87915881c..0000000000 --- a/common/nym-kkt/src/ciphersuite.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::KKTError; -use libcrux_kem::Algorithm; - -pub use nym_kkt_ciphersuite::*; - -pub enum EncapsulationKey<'a> { - MlKem768(libcrux_kem::PublicKey), - XWing(libcrux_kem::PublicKey), - X25519(libcrux_kem::PublicKey), - McEliece(classic_mceliece_rust::PublicKey<'a>), -} - -pub enum DecapsulationKey<'a> { - MlKem768(libcrux_kem::PrivateKey), - XWing(libcrux_kem::PrivateKey), - X25519(libcrux_kem::PrivateKey), - McEliece(classic_mceliece_rust::SecretKey<'a>), -} -impl<'a> EncapsulationKey<'a> { - pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result { - match kem { - KEM::McEliece => { - if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES { - Err(KKTError::KEMError { - info: "Received McEliece Encapsulation Key with Invalid Length", - }) - } else { - let mut public_key_bytes = - Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]); - // Size must be correct due to KKTFrame::from_bytes(message_bytes)? - public_key_bytes.clone_from_slice(bytes); - Ok(EncapsulationKey::McEliece( - classic_mceliece_rust::PublicKey::from(public_key_bytes), - )) - } - } - KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - } - } - - pub fn encode(&self) -> Vec { - match self { - EncapsulationKey::XWing(public_key) - | EncapsulationKey::MlKem768(public_key) - | EncapsulationKey::X25519(public_key) => public_key.encode(), - EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()), - } - } -} - -pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result { - match kem { - KEM::MlKem768 => Ok(Algorithm::MlKem768), - KEM::XWing => Ok(Algorithm::XWingKemDraft06), - KEM::X25519 => Ok(Algorithm::X25519), - KEM::McEliece => Err(KKTError::KEMMapping { - info: "attempted to map McEliece KEM to libcrux_kem", - }), - } -} diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs deleted file mode 100644 index c4189f9552..0000000000 --- a/common/nym-kkt/src/encryption.rs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2025-2026 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame}; -use blake3::Hasher; -use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN}; -use nym_crypto::asymmetric::x25519; -use rand09::{CryptoRng, RngCore}; -use zeroize::Zeroize; - -#[derive(Clone, Copy, Zeroize)] -pub struct KKTSessionSecret([u8; 32]); - -impl KKTSessionSecret { - pub fn new(rng: &mut R, remote_public_key: &x25519::PublicKey) -> (Self, x25519::PublicKey) - where - R: RngCore + CryptoRng, - { - let mut private_key_bytes = [0u8; x25519::PRIVATE_KEY_SIZE]; - rng.fill_bytes(&mut private_key_bytes); - - let ephemeral_private_key = x25519::PrivateKey::from_secret(private_key_bytes); - let ephemeral_public_key = x25519::PublicKey::from(&ephemeral_private_key); - - ( - Self::derive(&ephemeral_private_key, remote_public_key), - ephemeral_public_key, - ) - } - pub fn from_bytes(secret: [u8; 32]) -> Self { - Self(secret) - } - - fn try_derive(private_key: &x25519::PrivateKey, public_key: &[u8]) -> Result { - let mut pub_key: [u8; 32] = [0u8; 32]; - pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]); - - // Todo: check validity of pk... - let pk = x25519::PublicKey::from(pub_key); - Ok(Self::derive(private_key, &pk)) - } - - pub fn derive(private_key: &x25519::PrivateKey, public_key: &x25519::PublicKey) -> Self { - let mut shared_secret = private_key.diffie_hellman(public_key); - - let mut hasher = Hasher::new(); - - hasher.update(&shared_secret); - shared_secret.zeroize(); - - Self(hasher.finalize().as_bytes().to_owned()) - } - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -pub fn encrypt_initial_kkt_frame( - rng: &mut R, - remote_public_key: &x25519::PublicKey, - kkt_frame: &KKTFrame, -) -> Result<(KKTSessionSecret, Vec), KKTError> -where - R: CryptoRng + RngCore, -{ - let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(rng, remote_public_key); - - let mut encrypted_frame = - encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?; - - let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE); - output_buffer.extend_from_slice(ephemeral_public_key.as_bytes()); - output_buffer.append(&mut encrypted_frame); - - // [ 32 | 12 | ciphertext | 16]; - // [eph_pub_key | nonce | ciphertext | tag]; - Ok((session_secret_key, output_buffer)) -} - -pub fn decrypt_initial_kkt_frame( - responder_private_key: &x25519::PrivateKey, - encrypted_frame_bytes: &[u8], -) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> { - if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN { - Err(KKTError::AEADError { - info: "Encrypted KKT Frame is too short.", - }) - } else { - let shared_secret = KKTSessionSecret::try_derive( - responder_private_key, - &encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE], - )?; - - let (kkt_frame, kkt_context) = decrypt_kkt_frame( - &shared_secret, - &encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..], - KKT_INITIAL_FRAME_AAD, - )?; - Ok((shared_secret, kkt_frame, kkt_context)) - } -} - -pub fn encrypt_kkt_frame( - rng: &mut R, - secret_key: &KKTSessionSecret, - kkt_frame: &KKTFrame, - aad: &[u8], -) -> Result, KKTError> -where - R: CryptoRng + RngCore, -{ - let kkt_frame_bytes = kkt_frame.to_bytes(); - - // generate nonce - let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN]; - rng.fill_bytes(&mut nonce); - - let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?; - - // [ 12 | ciphertext | 16]; - // [nonce | ciphertext | tag]; - let mut output_buffer: Vec = - Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN); - - output_buffer.extend_from_slice(&nonce); - output_buffer.append(&mut ciphertext); - - Ok(output_buffer) -} - -// kkt_frame_bytes should look like this -// [ 12 | ciphertext | 16]; -// [nonce | ciphertext | tag]; -pub fn decrypt_kkt_frame( - secret_key: &KKTSessionSecret, - kkt_frame_bytes: &[u8], - aad: &[u8], -) -> Result<(KKTFrame, KKTContext), KKTError> { - let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN]; - nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]); - - let plaintext = decrypt( - secret_key.as_bytes(), - &kkt_frame_bytes[NONCE_LEN..], - aad, - &nonce, - )?; - - KKTFrame::from_bytes(&plaintext) -} - -fn encrypt( - secret_key: &[u8; 32], - plaintext: &[u8], - aad: &[u8], - nonce: &[u8; NONCE_LEN], -) -> Result, KKTError> { - let mut output_buffer = vec![0; plaintext.len() + TAG_LEN]; - libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?; - Ok(output_buffer) -} - -fn decrypt( - secret_key: &[u8; 32], - ciphertext: &[u8], - aad: &[u8], - nonce: &[u8; NONCE_LEN], -) -> Result, KKTError> { - let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN]; - libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?; - Ok(output_buffer) -} - -#[cfg(test)] -mod test { - use crate::ciphersuite::Ciphersuite; - use crate::context::{KKTContext, KKTMode, KKTRole}; - use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame}; - use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame}; - use crate::{ - ciphersuite::DEFAULT_HASH_LEN, - encryption::{KKTSessionSecret, decrypt, encrypt}, - key_utils::generate_keypair_x25519, - }; - use rand09::{RngCore, SeedableRng, rng}; - - #[test] - fn test_keygen() { - let mut rng = rng(); - let responder_x25519_keypair = generate_keypair_x25519(&mut rng); - - let (session_secret_key, ephemeral_public_key) = - KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key()); - - let shared_secret = KKTSessionSecret::try_derive( - responder_x25519_keypair.private_key(), - ephemeral_public_key.as_bytes().as_slice(), - ) - .unwrap(); - - assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes()) - } - - #[test] - fn test_encryption() { - let mut rng = rng(); - - let mut secret_key = [0u8; DEFAULT_HASH_LEN]; - rng.fill_bytes(&mut secret_key); - - let mut plaintext = vec![0; 100]; - rng.fill_bytes(&mut plaintext); - - let mut nonce = [0; 12]; - rng.fill_bytes(&mut nonce); - - let mut aad = vec![0; 124]; - rng.fill_bytes(&mut aad); - - let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap(); - - let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap(); - - assert_eq!(o_plaintext, plaintext) - } - - #[test] - fn kkt_frame_encryption() -> anyhow::Result<()> { - let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42); - let session_key = KKTSessionSecret::from_bytes([42u8; 32]); - let aad = b"my-amazing-aad"; - - let valid_context = KKTContext::new( - KKTRole::Initiator, - KKTMode::Mutual, - Ciphersuite::decode([255, 1, 0, 0])?, - )?; - let dummy_frame = KKTFrame::new( - valid_context.encode()?, - &[2u8; 32], - [3u8; KKT_SESSION_ID_LEN], - &[4u8; 64], - ); - - let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?; - - let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?; - - assert_eq!(dummy_frame, frame); - assert_eq!(context, valid_context); - Ok(()) - } -} diff --git a/common/nym-kkt/src/error.rs b/common/nym-kkt/src/error.rs index 1defc1b1e0..5751b096ba 100644 --- a/common/nym-kkt/src/error.rs +++ b/common/nym-kkt/src/error.rs @@ -3,18 +3,18 @@ use crate::context::KKTStatus; use nym_kkt_ciphersuite::error::KKTCiphersuiteError; +use nym_kkt_context::KKTContextEncodingError; use std::fmt::Debug; use thiserror::Error; #[derive(Error, Debug)] pub enum KKTError { - #[error("Signature constructor error")] - SigConstructorError, - #[error("Signature verification error")] - SigVerifError, #[error(transparent)] CiphersuiteDecodingError(#[from] KKTCiphersuiteError), + #[error(transparent)] + MaskedByteError(#[from] MaskedByteError), + #[error("KEM mapping failure: {}", info)] KEMMapping { info: &'static str }, @@ -33,9 +33,6 @@ pub enum KKTError { #[error("KKT Responder Flagged Error: {}", status)] ResponderFlaggedError { status: KKTStatus }, - #[error("KKT Message Count Limit Reached")] - MessageCountLimitReached, - #[error("PSQ KEM Error: {}", info)] KEMError { info: &'static str }, @@ -48,8 +45,40 @@ pub enum KKTError { #[error("{}", info)] AEADError { info: &'static str }, + #[error("{}", info)] + DecodingError { info: &'static str }, + + #[error("{}", info)] + UnsupportedAlgorithm { info: &'static str }, + #[error("Generic libcrux error")] LibcruxError, + + #[error("failed to derive shared secret: {inner:?}")] + SharedSecretDerivationFailure { + inner: libcrux_psq::handshake::HandshakeError, + }, + + #[error("the received encapsulation key hash does not match the expected value")] + MismatchedKEMHash, + + #[error(transparent)] + MalformedContext(#[from] KKTContextEncodingError), +} + +impl KKTError { + pub fn shared_secret_derivation_failure(inner: libcrux_psq::handshake::HandshakeError) -> Self { + KKTError::SharedSecretDerivationFailure { inner } + } +} + +#[derive(Error, Debug)] +pub enum MaskedByteError { + #[error("invalid Masked Byte Length: Expected({expected}), Actual({actual})")] + InvalidLength { expected: usize, actual: usize }, + + #[error("failed to Unmask Byte")] + Failure, } impl From for KKTError { diff --git a/common/nym-kkt/src/frame.rs b/common/nym-kkt/src/frame.rs index 6502af882c..a6780336c4 100644 --- a/common/nym-kkt/src/frame.rs +++ b/common/nym-kkt/src/frame.rs @@ -7,90 +7,158 @@ // [2..=5] => Ciphersuite // [6] => Reserved +use crate::context::{KKTMode, KKTRole}; +use crate::message::{ + DecryptedRequestFrame, KKTRequest, KKTRequestEncryptionResult, KKTRequestPlaintext, +}; use crate::{ context::{KKT_CONTEXT_LEN, KKTContext}, error::KKTError, }; +use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey}; +use nym_kkt_ciphersuite::KEM; +use rand09::{CryptoRng, RngCore}; -pub const KKT_SESSION_ID_LEN: usize = 16; - -pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN]; +pub(crate) const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF"; #[derive(Debug, PartialEq, Clone)] pub struct KKTFrame { - context: [u8; KKT_CONTEXT_LEN], - session_id: KKTSessionId, + context: KKTContext, body: Vec, - signature: Vec, + payload: Vec, } -// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes). -// if message coming from anonymous initiator => body is empty, there is no signature. -// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id. -// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id. +// if oneway and message coming from initiator => body is empty. +// if mutual and message coming from initiator => body has the initiator's kem public key. +// if coming from responder => body has the responder's kem public key. impl KKTFrame { - pub fn new( - context: [u8; KKT_CONTEXT_LEN], - body: &[u8], - session_id: [u8; KKT_SESSION_ID_LEN], - signature: &[u8], - ) -> Self { + pub fn new(context: KKTContext, body: &[u8], payload: Vec) -> Self { Self { context, body: Vec::from(body), - session_id, - signature: Vec::from(signature), + payload, } } - pub fn context_ref(&self) -> &[u8] { + + pub const fn size_excluding_payload(role: KKTRole, mode: KKTMode, kem: KEM) -> usize { + match role { + KKTRole::Initiator => { + match mode { + KKTMode::OneWay => { + // if oneway and message coming from initiator => body is empty. + KKT_CONTEXT_LEN + } + KKTMode::Mutual => { + // if mutual and message coming from initiator => body has the initiator's kem public key. + KKT_CONTEXT_LEN + kem.encapsulation_key_length() + } + } + } + KKTRole::Responder => { + // if coming from responder => body has the responder's kem public key. + KKT_CONTEXT_LEN + kem.encapsulation_key_length() + } + } + } + + pub fn size(&self) -> usize { + self.payload.len() + + Self::size_excluding_payload( + self.context.role(), + self.context.mode(), + self.context.ciphersuite().kem(), + ) + } + + pub fn context(&self) -> &KKTContext { &self.context } - pub fn context(&self) -> Result { - KKTContext::try_decode(self.context) + pub fn payload(&self) -> &[u8] { + self.payload.as_ref() } - pub fn signature_ref(&self) -> &[u8] { - &self.signature + pub fn encrypt_initiator_frame( + self, + rng: &mut R, + responder_public_key: &DHPublicKey, + version_byte: u8, + ) -> Result + where + R: CryptoRng + RngCore, + { + let ephemeral_keypair = DHKeyPair::new(rng); + + let plaintext = + KKTRequestPlaintext::new(ephemeral_keypair.pk, responder_public_key, version_byte); + + let mut carrier = + plaintext.derive_initiator_carrier(ephemeral_keypair.sk(), responder_public_key)?; + let full_kkt_message = plaintext.into_request(&mut carrier, self)?; + + Ok(KKTRequestEncryptionResult { + carrier, + request: full_kkt_message, + }) + } + + pub fn decrypt_initiator_frame( + responder_keypair: &DHKeyPair, + message: KKTRequest, + supported_versions: &[u8], + request_payload_len: usize, + ) -> Result { + let mask = message.plaintext.version_mask(&responder_keypair.pk); + + // check mask + // this could be used later when we have multiple versions + // if this call fails, it does before the server has to run a DH + let outer_protocol_version = message + .plaintext + .masked_version_bytes + .unmask_check_version(&mask, supported_versions)?; + + // after verifying the version, we can perform the DH and continue processing the request + let mut carrier = message + .plaintext + .derive_responder_carrier(responder_keypair)?; + + let decrypted_message = carrier.decrypt(&message.encrypted_frame)?; + let frame = KKTFrame::from_bytes(&decrypted_message, request_payload_len)?; + + Ok(DecryptedRequestFrame { + carrier, + remote_frame: frame, + outer_protocol_version, + }) } pub fn body_ref(&self) -> &[u8] { &self.body } - pub fn session_id_ref(&self) -> &[u8] { - &self.session_id - } - pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] { - self.session_id + pub fn body(self) -> Vec { + self.body } - pub fn signature_mut(&mut self) -> &mut [u8] { - &mut self.signature - } pub fn body_mut(&mut self) -> &mut [u8] { &mut self.body } - pub fn session_id_mut(&mut self) -> &mut [u8] { - &mut self.session_id - } - pub fn frame_length(&self) -> usize { - self.context.len() + self.session_id.len() + self.body.len() + self.signature.len() + KKT_CONTEXT_LEN + self.body.len() } - pub fn to_bytes(&self) -> Vec { + pub fn try_to_bytes(&self) -> Result, KKTError> { let mut bytes = Vec::with_capacity(self.frame_length()); - bytes.extend_from_slice(&self.context); + bytes.extend_from_slice(&self.context.encode()?); bytes.extend_from_slice(&self.body); - bytes.extend_from_slice(&self.session_id); - bytes.extend_from_slice(&self.signature); - bytes + bytes.extend_from_slice(&self.payload); + Ok(bytes) } - pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> { + pub fn from_bytes(bytes: &[u8], payload_len: usize) -> Result { let len = bytes.len(); if bytes.len() < KKT_CONTEXT_LEN { return Err(KKTError::FrameDecodingError { @@ -105,7 +173,7 @@ impl KKTFrame { let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap(); let context = KKTContext::try_decode(context_bytes)?; - if bytes.len() != context.full_message_len() { + if bytes.len() != context.full_message_len() + payload_len { return Err(KKTError::FrameDecodingError { info: format!( "Frame is shorter than expected: actual {len} != expected {}", @@ -115,7 +183,6 @@ impl KKTFrame { } let mut body = Vec::new(); - let mut signature = Vec::new(); // decode body if context.body_len() > 0 { @@ -123,33 +190,9 @@ impl KKTFrame { body.extend_from_slice(body_bytes); } - let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() - ..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN]; - // SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes - #[allow(clippy::unwrap_used)] - let session_id = session_bytes.try_into().unwrap(); + // decode payload. this could be empty. + let payload: Vec = Vec::from(&bytes[KKT_CONTEXT_LEN + context.body_len()..]); - // // old code left for reference if session id becomes variable in length: - // if context.session_id_len() > 0 { - // session_id.extend_from_slice( - // &bytes[KKT_CONTEXT_LEN + context.body_len() - // ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()], - // ); - // } - - // decode signature - if context.signature_len() > 0 { - let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN - ..KKT_CONTEXT_LEN - + context.body_len() - + KKT_SESSION_ID_LEN - + context.signature_len()]; - signature.extend_from_slice(signature_bytes); - } - - Ok(( - KKTFrame::new(context_bytes, &body, session_id, &signature), - context, - )) + Ok(KKTFrame::new(context, &body, payload)) } } diff --git a/common/nym-kkt/src/initiator.rs b/common/nym-kkt/src/initiator.rs new file mode 100644 index 0000000000..e764a12375 --- /dev/null +++ b/common/nym-kkt/src/initiator.rs @@ -0,0 +1,188 @@ +// Copyright 2025-2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use libcrux_psq::handshake::types::DHPublicKey; +use nym_kkt_ciphersuite::Ciphersuite; +use rand09::{CryptoRng, RngCore}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::keys::EncapsulationKey; +use crate::message::{KKTRequest, KKTResponse, ProcessedKKTResponse}; +use crate::{ + carrier::Carrier, + context::{KKTContext, KKTMode, KKTRole, KKTStatus}, + error::KKTError, + frame::KKTFrame, + key_utils::validate_encapsulation_key, +}; + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct KKTInitiator<'a> { + carrier: Carrier, + + #[zeroize(skip)] + context: KKTContext, + + #[zeroize(skip)] + expected_hash: &'a [u8], +} + +impl<'a> KKTInitiator<'a> { + // to be used by clients + pub fn generate_one_way_request( + rng: &mut R, + ciphersuite: Ciphersuite, + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + payload: Option>, + ) -> Result<(Self, KKTRequest), KKTError> + where + R: CryptoRng + RngCore, + { + Self::generate_encrypted_request( + rng, + KKTMode::OneWay, + ciphersuite, + None, + responder_dh_public_key, + expected_hash, + outer_protocol_version, + payload, + ) + } + + // to be used by nodes + pub fn generate_mutual_request<'b, R>( + rng: &mut R, + ciphersuite: Ciphersuite, + local_encapsulation_key: &'b [u8], + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + payload: Option>, + ) -> Result<(Self, KKTRequest), KKTError> + where + R: CryptoRng + RngCore, + { + Self::generate_encrypted_request( + rng, + KKTMode::Mutual, + ciphersuite, + Some(local_encapsulation_key), + responder_dh_public_key, + expected_hash, + outer_protocol_version, + payload, + ) + } + + #[allow(clippy::too_many_arguments)] + fn generate_encrypted_request<'b, R>( + rng: &mut R, + mode: KKTMode, + ciphersuite: Ciphersuite, + local_encapsulation_key: Option<&'b [u8]>, + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + payload: Option>, + ) -> Result<(Self, KKTRequest), KKTError> + where + R: CryptoRng + RngCore, + { + let frame = initiator_process(mode, ciphersuite, local_encapsulation_key, payload)?; + let context = *frame.context(); + + let request = + frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?; + + Ok(( + Self { + carrier: request.carrier, + context, + expected_hash, + }, + request.request, + )) + } + + pub fn process_response( + &mut self, + response: KKTResponse, + response_payload_len: usize, + ) -> Result { + let decrypted_response_bytes = self.carrier.decrypt(&response.encrypted_frame)?; + let response_frame = KKTFrame::from_bytes(&decrypted_response_bytes, response_payload_len)?; + initiator_ingest_response(&self.context, &response_frame, self.expected_hash) + } +} + +pub fn initiator_process( + mode: KKTMode, + ciphersuite: Ciphersuite, + own_encapsulation_key: Option<&[u8]>, + payload: Option>, +) -> Result { + let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite); + + let body: &[u8] = match mode { + KKTMode::OneWay => &[], + KKTMode::Mutual => match own_encapsulation_key { + Some(encaps_key) => encaps_key, + + // Missing key + None => { + return Err(KKTError::FunctionInputError { + info: "KEM Key Not Provided", + }); + } + }, + }; + + Ok(KKTFrame::new( + context, + body, + match payload { + Some(payload_vec) => payload_vec, + None => Vec::with_capacity(0), + }, + )) +} + +pub fn initiator_ingest_response( + own_context: &KKTContext, + remote_frame: &KKTFrame, + expected_hash: &[u8], +) -> Result { + let remote_context = remote_frame.context(); + let verified_initiator_kem_key = match remote_context.status() { + KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => { + match validate_encapsulation_key( + own_context.ciphersuite().hash_function(), + own_context.ciphersuite().hash_len(), + remote_frame.body_ref(), + expected_hash, + ) { + true => remote_context.status() != KKTStatus::UnverifiedKEMKey, + + // The key does not match the hash obtained from the directory + false => return Err(KKTError::MismatchedKEMHash), + } + } + _ => { + return Err(KKTError::ResponderFlaggedError { + status: remote_context.status(), + }); + } + }; + + let kem = own_context.ciphersuite().kem(); + let kem_bytes = remote_frame.body_ref(); + let encapsulation_key = EncapsulationKey::try_from_bytes(kem_bytes.to_vec(), kem)?; + Ok(ProcessedKKTResponse { + encapsulation_key, + verified_initiator_kem_key, + response_payload: remote_frame.payload().to_vec(), + }) +} diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs index 2ea88d231b..222f228088 100644 --- a/common/nym-kkt/src/key_utils.rs +++ b/common/nym-kkt/src/key_utils.rs @@ -1,74 +1,35 @@ -use crate::ciphersuite::HashFunction; -use std::collections::HashMap; +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use classic_mceliece_rust::keypair_boxed; - -use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests}; +use libcrux_ml_kem::mlkem768::MlKem768KeyPair; +use libcrux_psq::handshake::types::DHKeyPair; +use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests}; use rand09::{CryptoRng, RngCore}; +use std::collections::BTreeMap; -pub fn generate_keypair_ed25519( - rng: &mut R, - index: Option, -) -> nym_crypto::asymmetric::ed25519::KeyPair +pub fn generate_lp_keypair_x25519(rng: &mut R) -> DHKeyPair where R: RngCore + CryptoRng, { - let mut secret_initiator: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_initiator); - nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0)) + DHKeyPair::new(rng) } -pub fn generate_keypair_x25519(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair +pub fn generate_keypair_mlkem(rng: &mut R) -> MlKem768KeyPair where R: RngCore + CryptoRng, { - let mut secret_initiator: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_initiator); - - let private_key = nym_crypto::asymmetric::x25519::PrivateKey::from_secret(secret_initiator); - private_key.into() + libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng) } -// (decapsulation_key, encapsulation_key) -pub fn generate_keypair_libcrux( - rng: &mut R, - kem: crate::ciphersuite::KEM, -) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError> +pub fn generate_keypair_mceliece(rng: &mut R) -> libcrux_psq::classic_mceliece::KeyPair where R: RngCore + CryptoRng, { - match kem { - crate::ciphersuite::KEM::MlKem768 => { - Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, rng)?) - } - crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen( - libcrux_kem::Algorithm::XWingKemDraft06, - rng, - )?), - crate::ciphersuite::KEM::X25519 => { - Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?) - } - _ => Err(crate::error::KKTError::KEMError { - info: "Key Generation Error: Unsupported Libcrux Algorithm", - }), - } -} -// (decapsulation_key, encapsulation_key) -pub fn generate_keypair_mceliece<'a, R>( - rng: &mut R, -) -> ( - classic_mceliece_rust::SecretKey<'a>, - classic_mceliece_rust::PublicKey<'a>, -) -where - R: RngCore + CryptoRng, -{ - let (encapsulation_key, decapsulation_key) = keypair_boxed(rng); - (decapsulation_key, encapsulation_key) + libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng) } pub fn hash_key_bytes( - hash_function: &HashFunction, + hash_function: HashFunction, hash_length: usize, key_bytes: &[u8], ) -> Vec { @@ -77,9 +38,9 @@ pub fn hash_key_bytes( /// attempt to produce digests of the provided key using all known [HashFunction] with a default /// hash length where variable output is available -pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests { +pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests { use strum::IntoEnumIterator; - let mut digests = HashMap::new(); + let mut digests = BTreeMap::new(); for hash in HashFunction::iter() { digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN)); } @@ -93,7 +54,7 @@ fn compare_hashes(a: &[u8], b: &[u8]) -> bool { } pub fn validate_encapsulation_key( - hash_function: &HashFunction, + hash_function: HashFunction, hash_length: usize, encapsulation_key: &[u8], expected_hash_bytes: &[u8], @@ -104,20 +65,8 @@ pub fn validate_encapsulation_key( ) } -pub fn validate_key_bytes( - hash_function: &HashFunction, - hash_length: usize, - key_bytes: &[u8], - expected_hash_bytes: &[u8], -) -> bool { - compare_hashes( - &hash_key_bytes(hash_function, hash_length, key_bytes), - expected_hash_bytes, - ) -} - pub fn hash_encapsulation_key( - hash_function: &HashFunction, + hash_function: HashFunction, hash_length: usize, encapsulation_key: &[u8], ) -> Vec { diff --git a/common/nym-kkt/src/keys.rs b/common/nym-kkt/src/keys.rs new file mode 100644 index 0000000000..18206de7b8 --- /dev/null +++ b/common/nym-kkt/src/keys.rs @@ -0,0 +1,440 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::KKTError; +use libcrux_psq::handshake::types::PQEncapsulationKey; +use nym_kkt_ciphersuite::{KEM, KEMKeyDigests}; +use std::collections::BTreeMap; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +use crate::key_utils::produce_key_digests; +pub use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey}; +pub use libcrux_psq::classic_mceliece as mceliece; +pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; + +/// Wrapper around keys used for the KEM exchange +/// with cheap clones thanks to Arc wrappers +#[derive(Clone)] +pub struct KEMKeys { + mc_eliece_pk: Arc, + mc_eliece_sk: Arc, + ml_kem768_pk: Arc, + ml_kem768_sk: Arc, +} + +impl Debug for KEMKeys { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KEMKeys") + .field("mc_eliece", &"") + .field("ml_kem768", &"") + .finish() + } +} + +impl KEMKeys { + pub fn new(mc_eliece: mceliece::KeyPair, ml_kem768: MlKem768KeyPair) -> Self { + let (ml_kem768_sk, ml_kem768_pk) = ml_kem768.into_parts(); + Self { + mc_eliece_pk: Arc::new(mc_eliece.pk), + mc_eliece_sk: Arc::new(mc_eliece.sk), + ml_kem768_pk: Arc::new(ml_kem768_pk), + ml_kem768_sk: Arc::new(ml_kem768_sk), + } + } + + pub fn encapsulation_keys_digests(&self) -> BTreeMap { + let mut digests = BTreeMap::new(); + + let mlkem_digests = produce_key_digests(self.ml_kem768_pk.as_slice()); + let mceliece_digests = produce_key_digests(self.mc_eliece_pk.as_ref().as_ref()); + + digests.insert(KEM::MlKem768, mlkem_digests); + digests.insert(KEM::McEliece, mceliece_digests); + + digests + } + + pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> { + match kem { + KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()), + KEM::MlKem768 => Some(self.ml_kem768_pk.as_slice()), + // _ => None, + } + } + + pub fn encapsulation_key(&self, kem: KEM) -> Option { + match kem { + KEM::McEliece => Some(EncapsulationKey::McEliece(self.mc_eliece_pk.clone())), + KEM::MlKem768 => Some(EncapsulationKey::MlKem768(self.ml_kem768_pk.clone())), + // _ => None, + } + } + + pub fn mc_eliece_encapsulation_key(&self) -> &mceliece::PublicKey { + &self.mc_eliece_pk + } + + pub fn ml_kem768_encapsulation_key(&self) -> &MlKem768PublicKey { + self.ml_kem768_pk.as_ref() + } + + pub fn mc_eliece_decapsulation_key(&self) -> &mceliece::SecretKey { + &self.mc_eliece_sk + } + + pub fn ml_kem768_decapsulation_key(&self) -> &MlKem768PrivateKey { + &self.ml_kem768_sk + } +} + +#[derive(Clone)] +pub enum EncapsulationKey { + McEliece(Arc), + MlKem768(Arc), +} + +impl Debug for EncapsulationKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EncapsulationKey::McEliece(_) => write!(f, "EncapsulationKey::McEliece"), + EncapsulationKey::MlKem768(_) => write!(f, "EncapsulationKey::MlKem768"), + } + } +} + +impl EncapsulationKey { + pub fn kem(&self) -> KEM { + match self { + EncapsulationKey::McEliece(_) => KEM::McEliece, + EncapsulationKey::MlKem768(_) => KEM::MlKem768, + } + } + + pub fn as_pq_encapsulation_key(&self) -> PQEncapsulationKey<'_> { + match self { + EncapsulationKey::McEliece(pk) => PQEncapsulationKey::CMC(pk), + EncapsulationKey::MlKem768(pk) => PQEncapsulationKey::MlKem(pk), + } + } + + pub fn try_from_bytes(bytes: Vec, kem: KEM) -> Result { + match kem { + KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(Arc::new( + MlKem768PublicKey::try_from(bytes.as_slice()).map_err(|_| KKTError::KEMError { + info: "mlkem768 key of invalid length", + })?, + ))), + KEM::McEliece => { + let boxed_array: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> = + bytes + .into_boxed_slice() + .try_into() + .map_err(|_| KKTError::KEMError { + info: "mceliece key of invalid length", + })?; + + Ok(EncapsulationKey::McEliece(Arc::new( + mceliece::PublicKey::from(boxed_array), + ))) + } + } + } + + pub fn as_bytes(&self) -> &[u8] { + match self { + EncapsulationKey::McEliece(k) => k.as_ref().as_ref(), + EncapsulationKey::MlKem768(k) => k.as_ref().as_ref(), + } + } +} + +// storage helpers +pub mod storage_wrappers { + use nym_pemstore::traits::PemStorableKey; + use thiserror::Error; + + #[derive(Debug, Error)] + pub enum MalformedStoredKeyError { + #[error("{typ} stored key has an invalid length")] + InvalidKeyLength { typ: &'static str }, + + #[error("{typ} stored key is malformed: {message}")] + MalformedData { typ: &'static str, message: String }, + + #[error("attempted to take ownership of a stored {typ} key representation")] + IllegalStoredConversion { typ: &'static str }, + } + + pub trait StorableKey: Sized { + type StorableRepresentation<'a>: PemStorableKey + + From<&'a Self> + + TryInto + + Sized + where + Self: 'a; + + fn to_storable(&self) -> Self::StorableRepresentation<'_> { + self.into() + } + + fn from_storable( + repr: Self::StorableRepresentation<'_>, + ) -> Result { + repr.try_into() + } + } + + macro_rules! declare_key_wrappers { + ($pub_key_type:ty, $private_key_type:ty) => { + pub enum StorablePublicKey<'a> { + Owned(Box<$pub_key_type>), + Borrowed(&'a $pub_key_type), + } + + impl AsRef<$pub_key_type> for StorablePublicKey<'_> { + fn as_ref(&self) -> &$pub_key_type { + match self { + StorablePublicKey::Owned(k) => k, + StorablePublicKey::Borrowed(k) => k, + } + } + } + + pub enum StorablePrivateKey<'a> { + Owned(Box<$private_key_type>), + Borrowed(&'a $private_key_type), + } + + impl AsRef<$private_key_type> for StorablePrivateKey<'_> { + fn as_ref(&self) -> &$private_key_type { + match self { + StorablePrivateKey::Owned(k) => k, + StorablePrivateKey::Borrowed(k) => k, + } + } + } + + impl<'a> From<&'a $pub_key_type> for StorablePublicKey<'a> { + fn from(value: &'a $pub_key_type) -> Self { + StorablePublicKey::Borrowed(value) + } + } + + impl<'a> TryFrom> for $pub_key_type { + type Error = MalformedStoredKeyError; + + fn try_from(value: StorablePublicKey<'a>) -> Result { + match value { + StorablePublicKey::Owned(value) => Ok(*value), + StorablePublicKey::Borrowed(_) => { + Err(MalformedStoredKeyError::IllegalStoredConversion { + typ: ::pem_type(), + }) + } + } + } + } + + impl<'a> From<&'a $private_key_type> for StorablePrivateKey<'a> { + fn from(value: &'a $private_key_type) -> Self { + StorablePrivateKey::Borrowed(value) + } + } + + impl<'a> TryFrom> for $private_key_type { + type Error = MalformedStoredKeyError; + + fn try_from(value: StorablePrivateKey<'a>) -> Result { + match value { + StorablePrivateKey::Owned(value) => Ok(*value), + StorablePrivateKey::Borrowed(_) => { + Err(MalformedStoredKeyError::IllegalStoredConversion { + typ: ::pem_type(), + }) + } + } + } + } + + impl $crate::keys::storage_wrappers::StorableKey for $pub_key_type { + type StorableRepresentation<'a> = StorablePublicKey<'a>; + } + + impl $crate::keys::storage_wrappers::StorableKey for $private_key_type { + type StorableRepresentation<'a> = StorablePrivateKey<'a>; + } + }; + } + + pub mod mceliece { + use crate::keys::storage_wrappers::MalformedStoredKeyError; + use libcrux_psq::classic_mceliece; + use nym_pemstore::traits::PemStorableKey; + + declare_key_wrappers!(classic_mceliece::PublicKey, classic_mceliece::SecretKey); + + impl<'a> PemStorableKey for StorablePrivateKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "MCELIECE PRIVATE KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_ref().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::SECRET_KEY_LENGTH]> = + bytes.to_vec().into_boxed_slice().try_into().map_err(|_| { + MalformedStoredKeyError::InvalidKeyLength { + typ: Self::pem_type(), + } + })?; + + Ok(StorablePrivateKey::Owned(Box::new( + classic_mceliece::SecretKey::from(bytes), + ))) + } + } + + impl<'a> PemStorableKey for StorablePublicKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "MCELIECE PUBLIC KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_ref().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> = + bytes.to_vec().into_boxed_slice().try_into().map_err(|_| { + MalformedStoredKeyError::InvalidKeyLength { + typ: Self::pem_type(), + } + })?; + + Ok(StorablePublicKey::Owned(Box::new( + classic_mceliece::PublicKey::from(bytes), + ))) + } + } + } + + pub mod mlkem768 { + use crate::keys::storage_wrappers::MalformedStoredKeyError; + use libcrux_ml_kem::mlkem768::{MlKem768PrivateKey, MlKem768PublicKey}; + use nym_pemstore::traits::PemStorableKey; + + declare_key_wrappers!(MlKem768PublicKey, MlKem768PrivateKey); + + impl<'a> PemStorableKey for StorablePrivateKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "MLKEM768 PRIVATE KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_slice().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let inner = MlKem768PrivateKey::try_from(bytes).map_err(|message| { + MalformedStoredKeyError::MalformedData { + typ: Self::pem_type(), + message: message.to_string(), + } + })?; + Ok(StorablePrivateKey::Owned(Box::new(inner))) + } + } + + impl<'a> PemStorableKey for StorablePublicKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "MLKEM768 PUBLIC KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_slice().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let inner = MlKem768PublicKey::try_from(bytes).map_err(|message| { + MalformedStoredKeyError::MalformedData { + typ: Self::pem_type(), + message: message.to_string(), + } + })?; + Ok(StorablePublicKey::Owned(Box::new(inner))) + } + } + } + + pub mod x25519 { + use crate::keys::storage_wrappers::MalformedStoredKeyError; + use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey}; + use nym_pemstore::traits::PemStorableKey; + + declare_key_wrappers!(DHPublicKey, DHPrivateKey); + + impl<'a> PemStorableKey for StorablePrivateKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "LP X25519 PRIVATE KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_ref().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes = + bytes + .try_into() + .map_err(|_| MalformedStoredKeyError::InvalidKeyLength { + typ: Self::pem_type(), + })?; + Ok(StorablePrivateKey::Owned(Box::new( + DHPrivateKey::from_bytes(&bytes).map_err(|err| { + MalformedStoredKeyError::MalformedData { + typ: Self::pem_type(), + message: format!("{err:?}"), + } + })?, + ))) + } + } + + impl<'a> PemStorableKey for StorablePublicKey<'a> { + type Error = MalformedStoredKeyError; + + fn pem_type() -> &'static str { + "LP X25519 PUBLIC KEY" + } + + fn to_bytes(&self) -> Vec { + self.as_ref().as_ref().to_vec() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes = + bytes + .try_into() + .map_err(|_| MalformedStoredKeyError::InvalidKeyLength { + typ: Self::pem_type(), + })?; + Ok(StorablePublicKey::Owned(Box::new(DHPublicKey::from_bytes( + &bytes, + )))) + } + } + } +} diff --git a/common/nym-kkt/src/kkt.rs b/common/nym-kkt/src/kkt.rs deleted file mode 100644 index c1d21982f3..0000000000 --- a/common/nym-kkt/src/kkt.rs +++ /dev/null @@ -1,449 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Convenience wrappers around KKT protocol functions for easier integration. -//! -//! This module provides simplified APIs for the common use case of exchanging -//! KEM public keys between a client (initiator) and gateway (responder). -//! -//! The underlying KKT protocol is implemented in the `session` module. - -use nym_crypto::asymmetric::{ed25519, x25519}; -use rand09::{CryptoRng, RngCore}; - -use crate::{ - ciphersuite::{Ciphersuite, EncapsulationKey}, - context::{KKTContext, KKTMode}, - encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame}, - error::KKTError, -}; - -// Re-export core session functions for advanced use cases -pub use crate::session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, -}; - -use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame}; -use crate::frame::KKTFrame; - -/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode). -/// -/// This is the client-side operation that initiates a KKT exchange. -/// The request will be signed with the provided signing key. -/// -/// # Arguments -/// * `rng` - random number generator -/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms) -/// * `signing_key` - Client's Ed25519 signing key for authentication -/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key -/// -/// # Returns -/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses -/// * `KKTContext` - Context to use when validating the response -/// * `Vec` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder -/// -/// # Example -/// ```ignore -/// let (session_secret, context, request_frame) = request_kem_key( -/// &mut rng, -/// ciphersuite, -/// client_signing_key, -/// responder_dh_public_key, -/// )?; -/// // Send request_frame to gateway -/// ``` -pub fn request_kem_key( - rng: &mut R, - ciphersuite: Ciphersuite, - signing_key: &ed25519::PrivateKey, - responder_dh_public_key: &x25519::PublicKey, -) -> Result<(KKTSessionSecret, KKTContext, Vec), KKTError> { - // OneWay mode: client only wants responder's KEM key - // None: client doesn't send their own KEM key - let (initiator_context, initiator_frame) = - initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?; - - // Generate the session's shared secret and encrypt the Initiator's request - let (session_secret, encrypted_request_bytes) = - encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?; - - Ok((session_secret, initiator_context, encrypted_request_bytes)) -} - -/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key. -/// -/// This is the client-side operation that processes the gateway's response. -/// It verifies the signature and validates the key hash against the expected value -/// (typically retrieved from a directory service). -/// -/// # Arguments -/// * `context` - Context from the initial request -/// * `session_secret` - Session Secret Key (generated with request) -/// * `responder_vk` - Responder's Ed25519 verification key (from directory) -/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory) -/// * `response_bytes` - Serialized response frame from responder -/// -/// # Returns -/// * `EncapsulationKey` - Authenticated KEM public key of the responder -/// -/// # Example -/// ```ignore -/// let gateway_kem_key = validate_kem_response( -/// &context, -/// &session_secret, -/// &gateway_verification_key, -/// &expected_hash_from_directory, -/// &response_bytes, -/// )?; -/// // Use gateway_kem_key for PSQ -/// ``` -pub fn validate_kem_response<'a>( - context: &mut KKTContext, - session_secret: &KKTSessionSecret, - responder_vk: &ed25519::PublicKey, - expected_key_hash: &[u8], - encrypted_response_bytes: &[u8], -) -> Result, KKTError> { - let (responder_frame, responder_context) = - decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?; - - initiator_ingest_response( - context, - &responder_frame, - &responder_context, - responder_vk, - expected_key_hash, - ) -} - -/// Decrypts and validates an *Encrypted* KKT response -/// -/// This is the client-side operation that processes the gateway's response. -pub fn decrypt_kkt_response_frame( - session_secret: &KKTSessionSecret, - frame_ciphertext: &[u8], -) -> Result<(KKTFrame, KKTContext), KKTError> { - decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD) -} - -/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key. -/// -/// This is the gateway-side operation that processes a client's KKT request. -/// It validates the request signature (if authenticated) and responds with -/// the gateway's KEM public key, signed for authenticity. -/// -/// # Arguments -/// * `encrypted_request_bytes` - encrypted KEM request -/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous) -/// * `responder_signing_key` - Gateway's Ed25519 signing key -/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key -/// * `responder_kem_key` - Gateway's KEM public key to send -/// -/// # Returns -/// * `KKTFrame` - Signed response frame containing the KEM public key -/// -/// # Example -/// ```ignore -/// let response_frame = handle_kem_request( -/// &request_frame, -/// Some(client_verification_key), // or None for anonymous -/// gateway_signing_key, -/// &gateway_kem_public_key, -/// )?; -/// // Send response_frame back to client -/// ``` -pub fn handle_kem_request<'a, R>( - rng: &mut R, - encrypted_request_bytes: &[u8], - initiator_vk: Option<&ed25519::PublicKey>, - responder_signing_key: &ed25519::PrivateKey, - responder_dh_private_key: &x25519::PrivateKey, - responder_kem_key: &EncapsulationKey<'a>, -) -> Result, KKTError> -where - R: RngCore + CryptoRng, -{ - // Compute the session's shared secret, decrypt and parse context from the request frame - - let (session_secret, request_frame, initiator_context) = - decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?; - - // Validate the request (verifies signature if initiator_vk provided) - let (mut response_context, _) = responder_ingest_message( - &initiator_context, - initiator_vk, - None, // Not checking initiator's KEM key in OneWay mode - &request_frame, - )?; - - // Generate signed response with our KEM public key - let responder_frame = responder_process( - &mut response_context, - request_frame.session_id(), - responder_signing_key, - responder_kem_key, - )?; - - // Encrypt the responder's response with the session's shared secret - encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - ciphersuite::{HashFunction, KEM, SignatureScheme}, - key_utils::{generate_keypair_libcrux, hash_encapsulation_key}, - }; - - fn random_x25519_key() -> x25519::PrivateKey { - let mut bytes = [0u8; 32]; - let mut rng = rand09::rng(); - rng.fill_bytes(&mut bytes); - x25519::PrivateKey::from_secret(bytes) - } - - #[test] - fn test_kkt_wrappers_oneway_authenticated() { - let mut rng = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - // Generate responder's KEM keypair (X25519 for testing) - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - // Create ciphersuite - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Hash the KEM key (simulating directory storage) - let key_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &responder_kem_key.encode(), - ); - - // Client: Request KEM key - let (session_key, context, request_frame_ciphertext) = request_kem_key( - &mut rng, - ciphersuite, - ed25519_init.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - // Gateway: Handle request - let response_frame_ciphertext = handle_kem_request( - &mut rng, - &request_frame_ciphertext, - Some(ed25519_init.public_key()), // Authenticated - ed25519_resp.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Client: Validate response - let obtained_key = validate_kem_response( - &context, - &session_key, - ed25519_resp.public_key(), - &key_hash, - &response_frame_ciphertext, - ) - .unwrap(); - - // Verify we got the correct KEM key - assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - } - - #[test] - fn test_kkt_wrappers_anonymous() { - let mut rng = rand09::rng(); - - // Only responder has keys - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let key_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &responder_kem_key.encode(), - ); - - // Anonymous initiator - let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); - - // Generate the session's shared secret and encrypt the Initiator's request - let (session_secret, encrypted_request_bytes) = - encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap(); - - // Gateway: Handle anonymous request - let response_frame = handle_kem_request( - &mut rng, - &encrypted_request_bytes, - None, // Anonymous - no verification key - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Initiator: Validate response - let obtained_key = validate_kem_response( - &context, - &session_secret, - responder_keypair.public_key(), - &key_hash, - &response_frame, - ) - .unwrap(); - - assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - } - - #[test] - fn test_invalid_signature_rejected() { - let mut rng = rand09::rng(); - - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - // Different keypair for wrong signature - let mut wrong_secret = [0u8; 32]; - rng.fill_bytes(&mut wrong_secret); - let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let (_session_key, _context, request_frame_ciphertext) = request_kem_key( - &mut rng, - ciphersuite, - initiator_keypair.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - // Gateway handles request but we provide WRONG verification key - let result = handle_kem_request( - &mut rng, - &request_frame_ciphertext, - Some(wrong_keypair.public_key()), // Wrong key! - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ); - - // Should fail signature verification - assert!(result.is_err()); - } - - #[test] - fn test_hash_mismatch_rejected() { - let mut rng = rand09::rng(); - - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Use WRONG hash - let wrong_hash = [0u8; 32]; - - let (session_key, context, request_frame) = request_kem_key( - &mut rng, - ciphersuite, - initiator_keypair.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - let response_frame = handle_kem_request( - &mut rng, - &request_frame, - Some(initiator_keypair.public_key()), - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Client validates with WRONG hash - let result = validate_kem_response( - &context, - &session_key, - responder_keypair.public_key(), - &wrong_hash, // Wrong! - &response_frame, - ); - - // Should fail hash validation - assert!(result.is_err()); - } -} diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs index d879cf2286..d6c5dcbe49 100644 --- a/common/nym-kkt/src/lib.rs +++ b/common/nym-kkt/src/lib.rs @@ -1,498 +1,230 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod ciphersuite; -pub mod context; -pub mod encryption; +pub mod carrier; pub mod error; pub mod frame; +pub mod initiator; pub mod key_utils; -// pub mod kkt; -pub mod session; +pub mod keys; +pub mod masked_byte; +pub mod message; +pub mod rekey; +pub mod responder; -// This must be less than 4 bits -pub const KKT_VERSION: u8 = 1; -const _: () = assert!(KKT_VERSION < 1 << 4); -pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response"; -pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME"; +pub use nym_kkt_context as context; #[cfg(test)] mod test { + use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; + use rand09::RngCore; + + use crate::keys::KEMKeys; use crate::{ - KKT_RESPONSE_AAD, - ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM}, - encryption::{ - decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame, - encrypt_kkt_frame, - }, - frame::KKTFrame, + initiator::KKTInitiator, key_utils::{ - generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece, - generate_keypair_x25519, hash_encapsulation_key, - }, - session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, + generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, + hash_encapsulation_key, }, + responder::KKTResponder, }; #[test] - fn test_kkt_psq_e2e_clear() { + fn test_kkt_psq_e2e_encrypted_carrier() { let mut rng = rand09::rng(); - // generate ed25519 keys - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { - for hash_function in [ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ] { - let ciphersuite = Ciphersuite::resolve_ciphersuite( - kem, - hash_function, - crate::ciphersuite::SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // generate kem public keys - - let (responder_kem_public_key, initiator_kem_public_key) = match kem { - KEM::MlKem768 => ( - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::XWing => ( - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::X25519 => ( - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::McEliece => ( - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - ), - }; - - let i_kem_key_bytes = initiator_kem_public_key.encode(); - - let r_kem_key_bytes = responder_kem_public_key.encode(); - - let i_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &i_kem_key_bytes, - ); - - let r_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &r_kem_key_bytes, - ); - - // Anonymous Initiator, OneWay - { - let (i_context, i_frame) = - anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); - - let i_frame_bytes = i_frame.to_bytes(); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - let (r_context, _) = - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - // Initiator, OneWay - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap(); - - let i_frame_bytes = i_frame.to_bytes(); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); - - assert!(r_obtained_key.is_none()); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - - // Initiator, Mutual - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap(); - - let i_frame_bytes = i_frame.to_bytes(); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); - - assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - } - } - } - #[test] - fn test_kkt_psq_e2e_encrypted() { - let mut rng = rand09::rng(); - - // generate ed25519 keys - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); + let mut payload: Vec = vec![0u8; 900_000]; + rng.fill_bytes(&mut payload); // generate responder x25519 keys - let responder_x25519_keypair = generate_keypair_x25519(&mut rng); + let responder_x25519_keypair = generate_lp_keypair_x25519(&mut rng); - for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { - for hash_function in [ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ] { + for hash_function in [ + HashFunction::Blake3, + HashFunction::SHA256, + HashFunction::Shake128, + HashFunction::Shake256, + ] { + // generate kem public keys + + let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng); + let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng); + + let responder_kem = KEMKeys::new(responder_mceliece_keypair, responder_mlkem_keypair); + + let r_dir_hash_mlkem = hash_encapsulation_key( + hash_function, + HashLength::Default.value(), + responder_kem.ml_kem768_encapsulation_key().as_slice(), + ); + + let r_dir_hash_mceliece = hash_encapsulation_key( + hash_function, + HashLength::Default.value(), + responder_kem.mc_eliece_encapsulation_key().as_ref(), + ); + let initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng); + let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng); + + let _i_dir_hash_mlkem = hash_encapsulation_key( + hash_function, + HashLength::Default.value(), + initiator_mlkem_keypair.public_key().as_slice(), + ); + + let _i_dir_hash_mceliece = hash_encapsulation_key( + hash_function, + HashLength::Default.value(), + initiator_mceliece_keypair.pk.as_ref(), + ); + + let responder = KKTResponder::new( + &responder_x25519_keypair, + &responder_kem, + &[ + HashFunction::Blake3, + HashFunction::SHA256, + HashFunction::Shake128, + HashFunction::Shake256, + ], + &[SignatureScheme::Ed25519], + &[1], + ) + .unwrap(); + + // OneWay - MlKem + { let ciphersuite = Ciphersuite::resolve_ciphersuite( - kem, + KEM::MlKem768, hash_function, - crate::ciphersuite::SignatureScheme::Ed25519, + SignatureScheme::Ed25519, None, ) .unwrap(); + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mlkem, + 1u8, + Some(payload.clone()), + ) + .unwrap(); - // generate kem public keys + let processed_request = responder.process_request(request, payload.len()).unwrap(); - let (responder_kem_public_key, initiator_kem_public_key) = match kem { - KEM::MlKem768 => ( - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::XWing => ( - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::X25519 => ( - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::McEliece => ( - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - ), - }; + assert_eq!(processed_request.request_payload, payload); - let i_kem_key_bytes = initiator_kem_public_key.encode(); - - let r_kem_key_bytes = responder_kem_public_key.encode(); - - let i_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &i_kem_key_bytes, - ); - - let r_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &r_kem_key_bytes, - ); - - // Anonymous Initiator, OneWay - { - let (i_context, i_frame) = - anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); - - // encryption - initiator frame - - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) + let result = initiator + .process_response(processed_request.response, 0) .unwrap(); - // decryption - initiator frame + assert_eq!( + result.encapsulation_key.as_bytes(), + responder_kem.ml_kem768_encapsulation_key().as_slice(), + ) + } + // Mutual - MlKem + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::MlKem768, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mlkem, + 1u8, + Some(payload.clone()), + ) + .unwrap(); - let (r_session_secret, i_frame_r, i_context_r) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); + let processed_request = responder.process_request(request, payload.len()).unwrap(); - let (r_context, _) = - responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap(); + assert_eq!(processed_request.request_payload, payload); - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) + // if we keep unverified keys, this should change + assert!(processed_request.remote_encapsulation_key.is_none()); + + let processed_response = initiator + .process_response(processed_request.response, 0) .unwrap(); - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); + assert_eq!( + processed_response.encapsulation_key.as_bytes(), + responder_kem.ml_kem768_encapsulation_key().as_slice(), + ) + } - // decryption - responder frame + // OneWay - McEliece + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::McEliece, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mceliece, + 1u8, + Some(payload.clone()), + ) + .unwrap(); - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); + let processed_request = responder.process_request(request, payload.len()).unwrap(); + assert_eq!(processed_request.request_payload, payload); - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) + let processed_response = initiator + .process_response(processed_request.response, 0) .unwrap(); - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - // Initiator, OneWay - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) + assert_eq!( + processed_response.encapsulation_key.as_bytes(), + responder_kem.mc_eliece_encapsulation_key().as_ref() + ) + } + // Mutual - MlKem + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::McEliece, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mceliece, + 1u8, + Some(payload.clone()), + ) + .unwrap(); + + let processed_request = responder.process_request(request, payload.len()).unwrap(); + + assert_eq!(processed_request.request_payload, payload); + + // if we keep unverified keys, this should change + assert!(processed_request.remote_encapsulation_key.is_none()); + + let processed_response = initiator + .process_response(processed_request.response, 0) .unwrap(); - // encryption - initiator frame - - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) - .unwrap(); - - // decryption - initiator frame - - let (r_session_secret, i_frame_r, r_context) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); - - assert!(r_obtained_key.is_none()); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); - - // decryption - responder frame - - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - - // Initiator, Mutual - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap(); - - // encryption - initiator frame - - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) - .unwrap(); - - // decryption - initiator frame - - let (r_session_secret, i_frame_r, i_context_r) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &i_context_r, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); - - assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); - - // decryption - responder frame - - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } + assert_eq!( + processed_response.encapsulation_key.as_bytes(), + responder_kem.mc_eliece_encapsulation_key().as_ref() + ) } } } diff --git a/common/nym-kkt/src/masked_byte.rs b/common/nym-kkt/src/masked_byte.rs new file mode 100644 index 0000000000..2bdd29edae --- /dev/null +++ b/common/nym-kkt/src/masked_byte.rs @@ -0,0 +1,189 @@ +use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput}; + +use crate::error::{ + MaskedByteError, + MaskedByteError::{Failure, InvalidLength}, +}; + +pub const MASKED_BYTE_LEN: usize = 16; +pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1"; + +const U8_RANGE: [u8; 256] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, + 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, + 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, + 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, + 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, + 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, + 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, + 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, + 250, 251, 252, 253, 254, 255, +]; + +#[derive(Clone, Copy)] +pub struct MaskedByte([u8; MASKED_BYTE_LEN]); + +impl MaskedByte { + /// Mask a byte by hashing it with some mask. + /// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte) + pub fn new(byte: u8, mask: &[u8]) -> Self { + let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN]; + let mut hasher = blake3::Hasher::new(); + hasher.update(MASKED_BYTE_CONTEXT_STR); + hasher.update(mask); + // avoid zero update + hasher.update(&[0xFF, byte]); + hasher.finalize_xof_into(&mut output); + + Self(output) + } + /// Unmasks a byte by trial hashing. + /// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF). + /// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max). + /// If we find an `i` which yields back the hash input, then we found the masked byte. + /// Otherwise, the function returns an error. + pub fn unmask(&self, mask: &[u8]) -> Result { + self.unmask_check_version(mask, &U8_RANGE) + } + + // This could be more efficient than unmask, + // because we just could check against a smaller list of supported versions. + pub fn unmask_check_version( + &self, + mask: &[u8], + supported_versions: &[u8], + ) -> Result { + let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN]; + let mut hasher = blake3::Hasher::new(); + hasher.update(MASKED_BYTE_CONTEXT_STR); + hasher.update(mask); + // avoid zero update + hasher.update(&[0xFF]); + for i in supported_versions { + let mut t_hasher = hasher.clone(); + t_hasher.update(&[*i]); + t_hasher.finalize_xof_into(&mut buf); + if buf == self.0 { + return Ok(*i); + } + } + Err(Failure) + } + + pub fn as_slice(&self) -> &[u8] { + &self.0 + } + + pub fn to_bytes(self) -> [u8; MASKED_BYTE_LEN] { + self.0 + } +} + +impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte { + fn from(value: [u8; MASKED_BYTE_LEN]) -> Self { + MaskedByte(value) + } +} + +impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte { + fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self { + MaskedByte(*value) + } +} + +impl TryFrom<&[u8]> for MaskedByte { + type Error = MaskedByteError; + + fn try_from(value: &[u8]) -> Result { + let Ok(inner) = value.try_into() else { + return Err(InvalidLength { + expected: MASKED_BYTE_LEN, + actual: value.len(), + }); + }; + Ok(MaskedByte(inner)) + } +} + +#[cfg(test)] +mod test { + + use crate::masked_byte::MASKED_BYTE_LEN; + + use super::MaskedByte; + use rand09::{Rng, RngCore, rng}; + + #[test] + fn test_masking() { + let mut mask: [u8; 256] = [0u8; 256]; + let mut wire_bytes: [u8; MASKED_BYTE_LEN]; + + // why not + for i in 0..=u8::MAX { + // gen mask + rng().fill_bytes(&mut mask); + let masked_byte = MaskedByte::new(i, &mask); + wire_bytes = masked_byte.to_bytes(); + + let decoded_masked_byte = MaskedByte::from(wire_bytes); + let output = decoded_masked_byte.unmask(&mask).unwrap(); + + assert_eq!(i, output); + + // flip bit + let mut with_flipped_bit = decoded_masked_byte.to_bytes(); + + let byte_idx: usize = rng().random_range(0..MASKED_BYTE_LEN); + let bit_idx = rng().random_range(0..8); + with_flipped_bit[byte_idx] ^= 1 << bit_idx; + + let decoded_masked_byte = MaskedByte::from(with_flipped_bit); + assert!(decoded_masked_byte.unmask(&mask).is_err()); + } + } + + #[test] + fn test_decoding() { + let mut mask: [u8; 256] = [0u8; 256]; + + // gen mask + rng().fill_bytes(&mut mask); + let byte = rng().random(); + let masked_byte = MaskedByte::new(byte, &mask); + let wire_bytes: [u8; MASKED_BYTE_LEN] = masked_byte.to_bytes(); + + // should succeed + let decoded_masked_byte = MaskedByte::try_from(wire_bytes.as_slice()).unwrap(); + let output = decoded_masked_byte.unmask(&mask).unwrap(); + + assert_eq!(byte, output); + + let empty_slice: &[u8] = &[]; + // should fail + assert!(MaskedByte::try_from(empty_slice).is_err()); + + let mut wire_bytes_messy = Vec::from(wire_bytes); + + // add more one more byte + wire_bytes_messy.push(0x42); + assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN + 1); + // should fail + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err()); + + // pop the added byte + _ = wire_bytes_messy.pop(); + assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN); + // should succeed + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_ok()); + + // pop one more byte + _ = wire_bytes_messy.pop(); + assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN - 1); + // should fail + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err()); + } +} diff --git a/common/nym-kkt/src/message.rs b/common/nym-kkt/src/message.rs new file mode 100644 index 0000000000..2d2e6f3361 --- /dev/null +++ b/common/nym-kkt/src/message.rs @@ -0,0 +1,265 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::carrier::Carrier; +use crate::context::{KKTContext, KKTMode, KKTRole}; +use crate::error::KKTError; +use crate::frame::KKTFrame; +use crate::keys::EncapsulationKey; +use crate::masked_byte::{MASKED_BYTE_LEN, MaskedByte}; +use libcrux_chacha20poly1305::TAG_LEN; +use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; +use nym_kkt_ciphersuite::{KEM, x25519}; + +pub struct KKTRequest { + /// The plaintext part of the request + pub(crate) plaintext: KKTRequestPlaintext, + + /// Ciphertext of an initial request `KKTFrame` + pub(crate) encrypted_frame: Vec, +} + +impl KKTRequest { + // the size of KKTRequest is the plaintext data followed by the frame and the encryption tag + pub const fn size_excluding_payload(mode: KKTMode, kem: KEM) -> usize { + KKTRequestPlaintext::SIZE + + KKTFrame::size_excluding_payload(KKTRole::Initiator, mode, kem) + + TAG_LEN + } + + pub fn size(&self) -> usize { + self.encrypted_frame.len() + KKTRequestPlaintext::SIZE + } + + pub fn into_bytes(mut self) -> Vec { + let mut out = self.plaintext.to_bytes(); + out.append(&mut self.encrypted_frame); + out + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.len() < x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN { + return Err(KKTError::FrameDecodingError { + info: "the KKTRequest frame has invalid length".to_string(), + }); + } + let plaintext = + KKTRequestPlaintext::try_from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN])?; + + Ok(KKTRequest { + plaintext, + encrypted_frame: b[x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN..].to_vec(), + }) + } +} + +pub(crate) struct KKTRequestPlaintext { + /// Ephemeral Diffie-Hellman public key of the initiator + pub(crate) dh_pubkey: DHPublicKey, + + /// Masked bytes representing the outer protocol version information + pub(crate) masked_version_bytes: MaskedByte, +} + +impl KKTRequestPlaintext { + pub const SIZE: usize = x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN; + + pub(crate) fn new( + initiator_pubkey: DHPublicKey, + responder_pubkey: &DHPublicKey, + outer_protocol_version: u8, + ) -> Self { + let mask = Self::create_version_mask(&initiator_pubkey, responder_pubkey); + let masked_version_bytes = MaskedByte::new(outer_protocol_version, &mask); + KKTRequestPlaintext { + dh_pubkey: initiator_pubkey, + masked_version_bytes, + } + } + + pub(crate) fn into_request( + self, + carrier: &mut Carrier, + frame: KKTFrame, + ) -> Result { + let frame_bytes = frame.try_to_bytes()?; + let frame_ciphertext = carrier.encrypt(&frame_bytes)?; + Ok(KKTRequest { + plaintext: self, + encrypted_frame: frame_ciphertext, + }) + } + + pub(crate) fn create_version_mask( + initiator_pubkey: &DHPublicKey, + responder_pubkey: &DHPublicKey, + ) -> Vec { + let mut mask = Vec::with_capacity(2 * x25519::PUBLIC_KEY_LENGTH); + mask.extend_from_slice(initiator_pubkey.as_ref()); + mask.extend_from_slice(responder_pubkey.as_ref()); + mask + } + + fn create_carrier_ctx( + masked_version: &MaskedByte, + initiator_pubkey: &DHPublicKey, + responder_pubkey: &DHPublicKey, + ) -> Vec { + let mut context = Vec::new(); + context.extend_from_slice(masked_version.as_slice()); + context.extend_from_slice(crate::frame::KKT_CARRIER_CONTEXT); + context.extend_from_slice(initiator_pubkey.as_ref()); + context.extend_from_slice(responder_pubkey.as_ref()); + context + } + + pub(crate) fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN); + out.extend_from_slice(self.dh_pubkey.as_ref()); + out.extend_from_slice(self.masked_version_bytes.as_slice()); + out + } + + pub(crate) fn try_from_bytes(b: &[u8]) -> Result { + if b.len() != x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN { + return Err(KKTError::FrameDecodingError { + info: "the KKTRequest frame has invalid length".to_string(), + }); + } + // SAFETY: we're using exactly 32 byte + #[allow(clippy::unwrap_used)] + let dh_pubkey = + DHPublicKey::from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH].try_into().unwrap()); + let masked_version_bytes = MaskedByte::try_from(&b[x25519::PUBLIC_KEY_LENGTH..])?; + + Ok(KKTRequestPlaintext { + dh_pubkey, + masked_version_bytes, + }) + } + + pub(crate) fn version_mask(&self, responder_pubkey: &DHPublicKey) -> Vec { + Self::create_version_mask(&self.dh_pubkey, responder_pubkey) + } + + pub(crate) fn derive_initiator_carrier( + &self, + initiator_sk: &DHPrivateKey, + responder_pubkey: &DHPublicKey, + ) -> Result { + let ctx = Self::create_carrier_ctx( + &self.masked_version_bytes, + &self.dh_pubkey, + responder_pubkey, + ); + + let shared_secret = initiator_sk + .diffie_hellman(responder_pubkey) + .map_err(KKTError::shared_secret_derivation_failure)?; + + Ok(Carrier::from_secret_slice( + shared_secret.as_ref(), + &ctx, + true, + )) + } + + pub(crate) fn derive_responder_carrier( + &self, + responder_keys: &DHKeyPair, + ) -> Result { + let ctx = Self::create_carrier_ctx( + &self.masked_version_bytes, + &self.dh_pubkey, + &responder_keys.pk, + ); + let shared_secret = responder_keys + .sk() + .diffie_hellman(&self.dh_pubkey) + .map_err(KKTError::shared_secret_derivation_failure)?; + Ok(Carrier::from_secret_slice( + shared_secret.as_ref(), + &ctx, + false, + )) + } +} + +pub struct KKTRequestEncryptionResult { + /// Derived carrier used for decrypting this frame and encrypting the response + pub(crate) carrier: Carrier, + + /// The underlying request that is going to get sent to the remote + pub(crate) request: KKTRequest, +} + +pub struct DecryptedRequestFrame { + /// Derived carrier used for decrypting this frame and encrypting the response + pub(crate) carrier: Carrier, + + /// The remote frame sent in the message + pub(crate) remote_frame: KKTFrame, + + /// The unmasked byte representing the outer protocol version sent by the initiator + pub(crate) outer_protocol_version: u8, +} + +impl DecryptedRequestFrame { + pub(crate) fn remote_context(&self) -> &KKTContext { + self.remote_frame.context() + } +} + +pub struct ProcessedKKTRequest { + pub response: KKTResponse, + + /// The obtained encapsulation key of the remote + pub remote_encapsulation_key: Option, + + /// The KEM key requested in the original request + pub requested_kem: KEM, + + /// The unmasked byte representing the outer protocol version sent by the initiator + pub outer_protocol_version: u8, + + // Request payload data (Could be empty. Contents are unrelated to current KKT execution). + pub request_payload: Vec, +} + +pub struct KKTResponse { + /// Encrypted KKT frame that is going to be sent back to the initiator + pub encrypted_frame: Vec, +} + +impl KKTResponse { + // the size of KKTRequest is the plaintext data followed by the frame and the encryption tag + pub const fn size_excluding_payload(kem: KEM) -> usize { + // `KKTMode` argument makes no difference for the Responder role + KKTFrame::size_excluding_payload(KKTRole::Responder, KKTMode::OneWay, kem) + TAG_LEN + } + + pub fn size(&self) -> usize { + self.encrypted_frame.len() + } + + pub fn from_bytes(bytes: Vec) -> KKTResponse { + KKTResponse { + encrypted_frame: bytes, + } + } + + pub fn into_bytes(self) -> Vec { + self.encrypted_frame + } +} + +pub struct ProcessedKKTResponse { + /// The obtained encapsulation key of the remote + pub encapsulation_key: EncapsulationKey, + + /// Indicates whether responder was able to verify the initiator's kem key, + pub verified_initiator_kem_key: bool, + + /// Optional response payload (Could be empty. Contents are unrelated to current KKT execution). + pub response_payload: Vec, +} diff --git a/common/nym-kkt/src/rekey.rs b/common/nym-kkt/src/rekey.rs new file mode 100644 index 0000000000..dc8fb273df --- /dev/null +++ b/common/nym-kkt/src/rekey.rs @@ -0,0 +1,257 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Post-Quantum Re-Key Protocol + +/// This module implements a stateless post-quantum re-keying protocol in one round-trip. +/// We currently support MlKem768 and XWing. +/// +/// This protocol is safe if it runs under a trusted secure channel. +/// +/// Bandwidth costs: +/// Request (MlKem768): 1216 bytes +/// Response (MlKem768): 1088 bytes +/// Request (XWing): 1248 bytes +/// Response (XWing): 1120 bytes +use libcrux_kem::*; +use nym_crypto::hkdf::blake3::derive_key_blake3; +use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing}; +use rand09::{CryptoRng, RngCore}; +use std::fmt::{Debug, Formatter}; +use zeroize::Zeroize; + +use crate::error::KKTError; + +/// Context string to be used with the Blake3 KDF. +const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1"; + +pub struct RekeyInitiator { + algorithm: Algorithm, + decapsulation_key: PrivateKey, + salt: [u8; 32], +} + +impl Debug for RekeyInitiator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let key_typ = match self.decapsulation_key { + PrivateKey::X25519(_) => "x25519", + PrivateKey::P256(_) => "p256", + PrivateKey::MlKem512(_) => "ml512", + PrivateKey::MlKem768(_) => "mlkem768", + PrivateKey::X25519MlKem768Draft00(_) => "x25519-mlkem768", + PrivateKey::XWingKemDraft06(_) => "xwing", + PrivateKey::MlKem1024(_) => "ml1024", + }; + + f.debug_struct("RekeyInitiator") + .field("algorithm", &self.algorithm) + .field("decapsulation_key", &key_typ) + .field("salt", &self.salt) + .finish() + } +} + +impl RekeyInitiator { + /// The Initiator generates an ephemeral KEM keypair and a 32-byte salt. + /// The Initiator keeps the decapsulation key and generates a request message. + /// The request message contains the salt and an encoding of the encapsulation key as follows + /// salt encapsulation_key + /// [0 ........ 32 | 32 .............. ] + /// + /// Inputs: + /// rng: something that implements CryptoRng + RngCore + /// kem: a KEM algorithm (we currently support MlKem768 and XWing) + /// + /// Outputs: + /// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use. + /// Vec: The request message as explained above. This is to be sent to the responder as-is. + pub fn generate_request(rng: &mut R, kem: KEM) -> Result<(RekeyInitiator, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + let (algorithm, buffer_size) = match kem { + // KEM::XWing => (Algorithm::XWingKemDraft06, 32 + xwing::PUBLIC_KEY_LENGTH), + KEM::MlKem768 => (Algorithm::MlKem768, 32 + ml_kem768::PUBLIC_KEY_LENGTH), + // We don't support McEliece because the keys are massive. + // If this is a deal-breaker, users can start a new session with PSQ which can use McEliece. + KEM::McEliece => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + }; + + // Generate the Initiator's salt + let mut salt = [0u8; 32]; + rng.fill_bytes(&mut salt); + + // Create the buffer for the request message and copy the salt into it. + let mut request_buffer = Vec::with_capacity(buffer_size); + request_buffer.extend_from_slice(&salt); + + // Generate the ephemeral KEM keypair based on the algorithm from the function's input. + let (decapsulation_key, encapsulation_key) = key_gen(algorithm, rng)?; + + // Append the encoding of the KEM encapsulation key to the initiator's randomness. + request_buffer.extend(encapsulation_key.encode()); + + Ok(( + // The Initiator should store this until they use `RekeyInitiator::finalize`. + RekeyInitiator { + algorithm, + decapsulation_key, + salt, + }, + // This is to be sent to the responder. + request_buffer, + )) + } + + /// The Initiator will attempt to decapsulate the `pre_key` generated by the responder + /// secret. This `pre_key` will be combined with the Initiator's previously generated salt + /// as input to a Blake3 KDF call to generate the new shared secret. + /// + /// This function fails if the ciphertext cannot be decoded or decapsulated. + /// + /// Input: + /// response_message: the responder's message which contains an encapsulation of `pre_key`. + /// Output: + /// [u8; 32]: the new shared secret. + pub fn finalize(mut self, response_message: &[u8]) -> Result<[u8; 32], KKTError> { + // Decode the responder's ciphertext. + let ciphertext = Ct::decode(self.algorithm, response_message)?; + // Decapsulate the `pre_key` using the Initiator's decapsulation key. + let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?; + + // Encode the `pre_key` into bytes + let pre_key_bytes = pre_key.encode(); + + let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &self.salt); + + // Zeroize the Initiator's salt + self.salt.zeroize(); + + // TODO: zeroize the decapsulation key + + Ok(new_secret) + } +} + +/// The responder parses the request message. +/// The first 32 bytes are the Initiator's salt, +/// and the remainder is the encoding of the public key. +/// Given that XWing and MlKem768 have different key lengths, +/// we could deduce the algorithm from that. +/// +/// If the message is badly formatted, or the encapsulation received is invalid, +/// this function will produce an error. +/// +/// If everything is alright, the responder generates and encapsulates a key `pre_key` to send to the Initiator. +/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's salt to obtain +/// the new shared secret. +/// +/// Inputs: +/// rng: something that implements CryptoRng + RngCore +/// request_message: the Initiator's request message (contains the salt and encapsulation key) +/// +/// Outputs: +/// [u8; 32]: new shared secret +/// Vec: response which contains an encapsulation of a secret value generated by the responder. +/// This is to be sent back to the Initiator as-is. +pub fn responder_process( + rng: &mut R, + mut request_message: Vec, +) -> Result<([u8; 32], Vec), KKTError> +where + R: CryptoRng + RngCore, +{ + // Deduce the KEM algorithm from the message length + let algorithm = match request_message.len().checked_sub(32) { + // + Some(num) => match num { + // If message length is 1216 (32 + 1184) then the algorithm should be MlKem768 + ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768, + // If message length is 1248 (32 + 1216) then the algorithm should be MlKem768 + xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06, + // We don't support McEliece because the keys are massive. + // If this is a deal-breaker, users can start a new session with PSQ which can use McEliece. + mceliece::PUBLIC_KEY_LENGTH => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + // We don't support X25519 because it's not post-quantum secure. + x25519::PUBLIC_KEY_LENGTH => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + // Reject if the size does not match any of the above. + _ => { + return Err(KKTError::UnsupportedAlgorithm { + info: "Unknown Algorithm", + }); + } + }, + // Reject if message length is less than 32. + None => { + return Err(KKTError::DecodingError { + info: "Invalid rekey request: size is too small", + }); + } + }; + + // Split the message to get the Initiator's salt (first 32 bytes) + // and the encoding of the Initiator's public key. + let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32); + + // Attempt to decode the Initiator's encapsulation key. + let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?; + + // Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`. + let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?; + // Encode the ciphertext into bytes to send back to the initiator. + let message = ciphertext.encode(); + + // Encode the `pre_key` into bytes + let pre_key_bytes = pre_key.encode(); + + let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, remote_salt); + + // Zeroize the Initiator's salt + remote_salt.zeroize(); + + Ok((new_secret, message)) +} + +#[cfg(test)] +mod tests { + use crate::error::KKTError; + use crate::rekey::{RekeyInitiator, responder_process}; + use nym_kkt_ciphersuite::KEM; + + #[test] + fn rekey_test() { + let mut rng = rand09::rng(); + + let (rekey_state, request_message) = + RekeyInitiator::generate_request(&mut rng, KEM::MlKem768).unwrap(); + + let (responder_secret, response_message) = + responder_process(&mut rng, request_message).unwrap(); + + let initiator_secret = rekey_state.finalize(&response_message).unwrap(); + + assert_eq!(initiator_secret, responder_secret); + + // mceliece should fail + let err = RekeyInitiator::generate_request(&mut rng, KEM::McEliece).unwrap_err(); + assert_eq!( + err.to_string(), + KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + } + .to_string() + ) + } +} diff --git a/common/nym-kkt/src/responder.rs b/common/nym-kkt/src/responder.rs new file mode 100644 index 0000000000..f7aac86269 --- /dev/null +++ b/common/nym-kkt/src/responder.rs @@ -0,0 +1,196 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::key_utils::validate_encapsulation_key; +use crate::keys::{EncapsulationKey, KEMKeys}; +use crate::message::{KKTRequest, KKTResponse, ProcessedKKTRequest}; +use crate::{ + context::{KKTContext, KKTMode, KKTRole, KKTStatus}, + error::KKTError, + frame::KKTFrame, +}; +use libcrux_psq::handshake::types::DHKeyPair; +use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, SignatureScheme}; + +/// Representation of a KKT Responder +pub struct KKTResponder<'a> { + /// Long-term x25519 DH key pair of this Responder + x25519_keypair: &'a DHKeyPair, + + /// KEM keys of this responder + kem_keys: &'a KEMKeys, + + /// List of supported Hash Functions by this Responder + supported_hash_functions: Vec, + + /// List of supported Signature Schemes by this Responder + supported_signature_schemes: Vec, + + /// List of supported outer (LP) protocol version by this Responder + supported_outer_protocol_versions: Vec, +} + +impl<'a> KKTResponder<'a> { + pub fn new( + x25519_keypair: &'a DHKeyPair, + kem_keys: &'a KEMKeys, + supported_hash_functions: &[HashFunction], + supported_signature_schemes: &[SignatureScheme], + supported_outer_protocol_versions: &[u8], + ) -> Result { + if supported_hash_functions.is_empty() { + return Err(KKTError::FunctionInputError { + info: "Did not provide a supported HashFunction when instantiating a KKTResponder", + }); + } + + if supported_signature_schemes.is_empty() { + return Err(KKTError::FunctionInputError { + info: "Did not provide a supported SignatureScheme when instantiating a KKTResponder", + }); + } + + if supported_outer_protocol_versions.is_empty() { + return Err(KKTError::FunctionInputError { + info: "Did not provide a supported outer protocol version when instantiating a KKTResponder", + }); + } + + Ok(Self { + x25519_keypair, + kem_keys, + supported_hash_functions: supported_hash_functions.to_vec(), + supported_signature_schemes: supported_signature_schemes.to_vec(), + supported_outer_protocol_versions: supported_outer_protocol_versions.to_vec(), + }) + } + + fn check_ciphersuite_compatiblity( + &self, + remote_ciphersuite: Ciphersuite, + ) -> Result<(), KKTError> { + let r_hash = remote_ciphersuite.hash_function(); + let r_sig = remote_ciphersuite.signature_scheme(); + + if !self.supported_hash_functions.contains(&r_hash) { + return Err(KKTError::IncompatibilityError { + info: "Unsupported HashFunction", + }); + } + + if !self.supported_signature_schemes.contains(&r_sig) { + return Err(KKTError::IncompatibilityError { + info: "Unsupported SignatureScheme", + }); + } + + Ok(()) + } + + // When this function fails, we do that silently (i.e. we don't generate a response to the initiator). + + pub fn process_request( + &self, + request: KKTRequest, + request_payload_len: usize, + ) -> Result { + let processed_req = KKTFrame::decrypt_initiator_frame( + self.x25519_keypair, + request, + &self.supported_outer_protocol_versions, + request_payload_len, + )?; + + let remote_context = *processed_req.remote_context(); + let remote_frame = processed_req.remote_frame; + let request_payload = remote_frame.payload().to_vec(); + let mut carrier = processed_req.carrier; + + self.check_ciphersuite_compatiblity(remote_context.ciphersuite())?; + + let (local_context, remote_encapsulation_key) = match remote_context.mode() { + KKTMode::OneWay => responder_ingest_message(None, remote_frame)?, + KKTMode::Mutual => { + // So we can either fetch the remote hash here using some async call to the directory, + // which might make registration hang or accept the sent key then verify later. + + // If we choose to not accept, the response's status will be KKTStatus::UnverifiedKEMKey. + // The response would still contain the responder's encapsulation key. + responder_ingest_message(None, remote_frame)? + } + }; + + let kem = local_context.ciphersuite().kem(); + let Some(kem_key) = self.kem_keys.encoded_encapsulation_key(kem) else { + return Err(KKTError::IncompatibilityError { + info: "Unsupported KEM", + }); + }; + + // for now the response payload is empty + let response_payload = Vec::with_capacity(0); + + let frame = KKTFrame::new(local_context, kem_key, response_payload); + + // encryption - responder frame + let encrypted_frame = carrier.encrypt(&frame.try_to_bytes()?)?; + Ok(ProcessedKKTRequest { + response: KKTResponse { encrypted_frame }, + remote_encapsulation_key, + requested_kem: remote_context.ciphersuite().kem(), + outer_protocol_version: processed_req.outer_protocol_version, + request_payload, + }) + } +} + +pub fn responder_ingest_message( + expected_hash: Option<&[u8]>, + remote_frame: KKTFrame, +) -> Result<(KKTContext, Option), KKTError> { + let remote_context = remote_frame.context(); + let mut own_context = remote_context.derive_responder_header()?; + let cs = own_context.ciphersuite(); + + match remote_context.role() { + KKTRole::Initiator => { + // using own_context here because maybe for whatever reason we want to ignore the remote kem key + match own_context.mode() { + KKTMode::OneWay => Ok((own_context, None)), + KKTMode::Mutual => { + let Some(expected_hash) = expected_hash else { + own_context.update_status(KKTStatus::UnverifiedKEMKey); + // we don't store an unverified key + // changing the status notifies the initiator that we didn't + + // we could still keep it here and then verify later... + // let received_encapsulation_key = EncapsulationKey::decode( + // own_context.ciphersuite().kem(), + // remote_frame.body_ref(), + // )?; + // Ok((own_context, Some(received_encapsulation_key))) + // + return Ok((own_context, None)); + }; + + if !validate_encapsulation_key( + cs.hash_function(), + cs.hash_len(), + remote_frame.body_ref(), + expected_hash, + ) { + // The key does not match the hash obtained from the directory + return Err(KKTError::MismatchedKEMHash); + } + let remote_key = + EncapsulationKey::try_from_bytes(remote_frame.body(), cs.kem())?; + Ok((own_context, Some(remote_key))) + } + } + } + + KKTRole::Responder => Err(KKTError::IncompatibilityError { + info: "Responder received a request from another responder.", + }), + } +} diff --git a/common/nym-kkt/src/session.rs b/common/nym-kkt/src/session.rs deleted file mode 100644 index 40db3d275d..0000000000 --- a/common/nym-kkt/src/session.rs +++ /dev/null @@ -1,230 +0,0 @@ -use nym_crypto::asymmetric::ed25519::{self, Signature}; -use rand09::{CryptoRng, RngCore}; - -use crate::frame::KKTSessionId; -use crate::{ - ciphersuite::{Ciphersuite, EncapsulationKey}, - context::{KKTContext, KKTMode, KKTRole, KKTStatus}, - error::KKTError, - frame::{KKT_SESSION_ID_LEN, KKTFrame}, - key_utils::validate_encapsulation_key, -}; - -pub fn initiator_process<'a, R>( - rng: &mut R, - mode: KKTMode, - ciphersuite: Ciphersuite, - signing_key: &ed25519::PrivateKey, - own_encapsulation_key: Option<&EncapsulationKey<'a>>, -) -> Result<(KKTContext, KKTFrame), KKTError> -where - R: CryptoRng + RngCore, -{ - let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?; - - let context_bytes = context.encode()?; - - let mut session_id = [0; KKT_SESSION_ID_LEN]; - // Generate Session ID - rng.fill_bytes(&mut session_id); - - let body: &[u8] = match mode { - KKTMode::OneWay => &[], - KKTMode::Mutual => match own_encapsulation_key { - Some(encaps_key) => &encaps_key.encode(), - - // Missing key - None => { - return Err(KKTError::FunctionInputError { - info: "KEM Key Not Provided", - }); - } - }, - }; - - let mut bytes_to_sign = - Vec::with_capacity(context.full_message_len() - context.signature_len()); - bytes_to_sign.extend_from_slice(&context_bytes); - bytes_to_sign.extend_from_slice(body); - bytes_to_sign.extend_from_slice(&session_id); - - let signature = signing_key.sign(bytes_to_sign).to_bytes(); - - Ok(( - context, - KKTFrame::new(context_bytes, body, session_id, &signature), - )) -} - -pub fn anonymous_initiator_process( - rng: &mut R, - ciphersuite: Ciphersuite, -) -> Result<(KKTContext, KKTFrame), KKTError> -where - R: CryptoRng + RngCore, -{ - let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?; - let context_bytes = context.encode()?; - - let mut session_id = [0u8; KKT_SESSION_ID_LEN]; - rng.fill_bytes(&mut session_id); - - Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[]))) -} - -pub fn initiator_ingest_response<'a>( - own_context: &KKTContext, - remote_frame: &KKTFrame, - remote_context: &KKTContext, - remote_verification_key: &ed25519::PublicKey, - expected_hash: &[u8], -) -> Result, KKTError> { - check_compatibility(own_context, remote_context)?; - match remote_context.status() { - KKTStatus::Ok => { - let mut bytes_to_verify: Vec = Vec::with_capacity( - remote_context.full_message_len() - remote_context.signature_len(), - ); - bytes_to_verify.extend_from_slice(&remote_context.encode()?); - bytes_to_verify.extend_from_slice(remote_frame.body_ref()); - bytes_to_verify.extend_from_slice(remote_frame.session_id_ref()); - - match Signature::from_bytes(remote_frame.signature_ref()) { - Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) { - Ok(()) => { - let received_encapsulation_key = EncapsulationKey::decode( - own_context.ciphersuite().kem(), - remote_frame.body_ref(), - )?; - - match validate_encapsulation_key( - &own_context.ciphersuite().hash_function(), - own_context.ciphersuite().hash_len(), - remote_frame.body_ref(), - expected_hash, - ) { - true => Ok(received_encapsulation_key), - - // The key does not match the hash obtained from the directory - false => Err(KKTError::KEMError { - info: "Hash of received encapsulation key does not match the value stored on the directory.", - }), - } - } - Err(_) => Err(KKTError::SigVerifError), - }, - Err(_) => Err(KKTError::SigConstructorError), - } - } - _ => Err(KKTError::ResponderFlaggedError { - status: remote_context.status(), - }), - } -} - -// todo: figure out how to handle errors using status codes - -pub fn responder_ingest_message<'a>( - remote_context: &KKTContext, - remote_verification_key: Option<&ed25519::PublicKey>, - expected_hash: Option<&[u8]>, - remote_frame: &KKTFrame, -) -> Result<(KKTContext, Option>), KKTError> { - let own_context = remote_context.derive_responder_header()?; - - match remote_context.role() { - KKTRole::AnonymousInitiator => Ok((own_context, None)), - - KKTRole::Initiator => { - match remote_verification_key { - Some(remote_verif_key) => { - let mut bytes_to_verify: Vec = Vec::with_capacity( - own_context.full_message_len() - own_context.signature_len(), - ); - bytes_to_verify.extend_from_slice(remote_frame.context_ref()); - bytes_to_verify.extend_from_slice(remote_frame.body_ref()); - bytes_to_verify.extend_from_slice(remote_frame.session_id_ref()); - - match Signature::from_bytes(remote_frame.signature_ref()) { - Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) { - Ok(()) => { - // using own_context here because maybe for whatever reason we want to ignore the remote kem key - match own_context.mode() { - KKTMode::OneWay => Ok((own_context, None)), - KKTMode::Mutual => { - match expected_hash { - Some(expected_hash) => { - let received_encapsulation_key = - EncapsulationKey::decode( - own_context.ciphersuite().kem(), - remote_frame.body_ref(), - )?; - if validate_encapsulation_key( - &own_context.ciphersuite().hash_function(), - own_context.ciphersuite().hash_len(), - remote_frame.body_ref(), - expected_hash, - ) { - Ok(( - own_context, - Some(received_encapsulation_key), - )) - } - // The key does not match the hash obtained from the directory - else { - Err(KKTError::KEMError { - info: "Hash of received encapsulation key does not match the value stored on the directory.", - }) - } - } - None => Err(KKTError::FunctionInputError { - info: "Expected hash of the remote encapsulation key is not provided.", - }), - } - } - } - } - Err(_) => Err(KKTError::SigVerifError), - }, - Err(_) => Err(KKTError::SigConstructorError), - } - } - None => Err(KKTError::FunctionInputError { - info: "Remote Signature Verification Key Not Provided", - }), - } - } - KKTRole::Responder => Err(KKTError::IncompatibilityError { - info: "Responder received a request from another responder.", - }), - } -} - -pub fn responder_process<'a>( - own_context: &KKTContext, - session_id: KKTSessionId, - signing_key: &ed25519::PrivateKey, - encapsulation_key: &EncapsulationKey<'a>, -) -> Result { - let body = encapsulation_key.encode(); - - let context_bytes = own_context.encode()?; - - let mut bytes_to_sign = - Vec::with_capacity(own_context.full_message_len() - own_context.signature_len()); - bytes_to_sign.extend_from_slice(&own_context.encode()?); - bytes_to_sign.extend_from_slice(&body); - bytes_to_sign.extend_from_slice(&session_id); - - let signature = signing_key.sign(bytes_to_sign).to_bytes(); - - Ok(KKTFrame::new(context_bytes, &body, session_id, &signature)) -} - -fn check_compatibility( - _own_context: &KKTContext, - _remote_context: &KKTContext, -) -> Result<(), KKTError> { - // todo: check ciphersuite/context compatibility - Ok(()) -} diff --git a/common/nym-lp-common/Cargo.toml b/common/nym-lp-common/Cargo.toml deleted file mode 100644 index b84b617154..0000000000 --- a/common/nym-lp-common/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "nym-lp-common" -version = "0.1.0" -edition = { workspace = true } -license = { workspace = true } -publish = false - -[dependencies] diff --git a/common/nym-lp-transport/src/traits.rs b/common/nym-lp-transport/src/traits.rs deleted file mode 100644 index 045bf29498..0000000000 --- a/common/nym-lp-transport/src/traits.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2026 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "io-mocks")] -use nym_test_utils::mocks::async_read_write::MockIOStream; -use std::net::SocketAddr; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::net::TcpStream; -use tracing::debug; - -// only used in internal code (and tests) -#[allow(async_fn_in_trait)] -pub trait LpTransport: Sized { - async fn connect(endpoint: SocketAddr) -> std::io::Result; - - fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>; - - /// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing. - /// - /// Format: 4-byte big-endian u32 length + packet bytes - /// - /// # Arguments - /// * `packet_data` - The serialised LP packet to send - /// - /// # Errors - /// Returns an error on network transmission fails. - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>; - - /// Receives an LP packet from a TCP stream with length-prefixed framing. - /// - /// Format: 4-byte big-endian u32 length + packet bytes - /// - /// # Errors - /// Returns an error on network transmission fails. - async fn receive_raw_packet(&mut self) -> std::io::Result>; -} - -async fn send_serialised_packet_async_write( - writer: &mut W, - packet_data: &[u8], -) -> std::io::Result<()> -where - W: AsyncWrite + Unpin, -{ - // Send 4-byte length prefix (u32 big-endian) - let len = packet_data.len() as u32; - writer - .write_all(&len.to_be_bytes()) - .await - .inspect_err(|e| debug!("Failed to send packet length: {e}"))?; - - // Send the actual packet data - writer - .write_all(packet_data) - .await - .inspect_err(|e| debug!("Failed to send packet data: {e}"))?; - - // Flush to ensure data is sent immediately - writer - .flush() - .await - .inspect_err(|e| debug!("Failed to flush stream: {e}"))?; - - tracing::trace!( - "Sent LP packet ({} bytes + 4 byte header)", - packet_data.len() - ); - Ok(()) -} - -async fn receive_raw_packet_async_read(reader: &mut R) -> std::io::Result> -where - R: AsyncRead + Unpin, -{ - // Read 4-byte length prefix (u32 big-endian) - let mut len_buf = [0u8; 4]; - reader - .read_exact(&mut len_buf) - .await - .inspect_err(|e| debug!("Failed to read packet length: {e}"))?; - - let packet_len = u32::from_be_bytes(len_buf) as usize; - - // Sanity check to prevent huge allocations - const MAX_PACKET_SIZE: usize = 65536; // 64KB max - if packet_len > MAX_PACKET_SIZE { - return Err(std::io::Error::other(format!( - "Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}", - ))); - } - - // Read the actual packet data - let mut packet_buf = vec![0u8; packet_len]; - reader - .read_exact(&mut packet_buf) - .await - .inspect_err(|e| debug!("Failed to read packet data: {e}"))?; - - tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)"); - Ok(packet_buf) -} - -impl LpTransport for TcpStream { - async fn connect(endpoint: SocketAddr) -> std::io::Result { - TcpStream::connect(endpoint).await - } - - fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()> { - // Set TCP_NODELAY for low latency - self.set_nodelay(nodelay) - } - - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> { - send_serialised_packet_async_write(self, packet_data).await - } - - async fn receive_raw_packet(&mut self) -> std::io::Result> { - receive_raw_packet_async_read(self).await - } -} - -#[cfg(feature = "io-mocks")] -impl LpTransport for MockIOStream { - async fn connect(_endpoint: SocketAddr) -> std::io::Result { - Ok(MockIOStream::default()) - } - - fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> { - Ok(()) - } - - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> { - send_serialised_packet_async_write(self, packet_data).await - } - - async fn receive_raw_packet(&mut self) -> std::io::Result> { - receive_raw_packet_async_read(self).await - } -} diff --git a/common/nym-lp/Cargo.toml b/common/nym-lp/Cargo.toml index 22f11c279b..cbfaf75bbd 100644 --- a/common/nym-lp/Cargo.toml +++ b/common/nym-lp/Cargo.toml @@ -7,50 +7,36 @@ publish = false [dependencies] thiserror = { workspace = true } -parking_lot = { workspace = true } -snow = { workspace = true } bs58 = { workspace = true } -serde = { workspace = true } bytes = { workspace = true } -dashmap = { workspace = true } -sha2 = { workspace = true } tracing = { workspace = true } -rand = { workspace = true } -# rand 0.9 for KKT integration (nym-kkt uses rand 0.9) rand09 = { workspace = true } +tls_codec = { workspace = true } +tokio = { workspace = true, features = ["net", "io-util"] } -nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] } +nym-crypto = { path = "../crypto", features = ["hashing"] } nym-kkt = { path = "../nym-kkt" } -nym-lp-common = { path = "../nym-lp-common" } -nym-lp-transport = { path = "../nym-lp-transport" } +nym-kkt-ciphersuite = { workspace = true } # libcrux dependencies for PSQ (Post-Quantum PSK derivation) -libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [ - "test-utils", -] } -libcrux-kem = { git = "https://github.com/cryspen/libcrux" } -libcrux-traits = { git = "https://github.com/cryspen/libcrux" } -tls_codec = { workspace = true } +libcrux-psq = { workspace = true, features = ["test-utils"] } num_enum = { workspace = true } -chacha20poly1305 = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } + # needed for the 'mock 'feature nym-test-utils = { workspace = true, optional = true } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } -#rand_chacha = "0.3" -mock_instant = { workspace = true } -nym-crypto = { path = "../crypto", features = ["rand"] } nym-test-utils = { workspace = true } anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] } + [features] -mock = ["nym-test-utils", "nym-crypto/rand"] +mock = ["nym-test-utils"] [[bench]] name = "replay_protection" -harness = false +harness = false \ No newline at end of file diff --git a/common/nym-lp/benches/replay_protection.rs b/common/nym-lp/benches/replay_protection.rs index a21a1092f0..63c1b615d9 100644 --- a/common/nym-lp/benches/replay_protection.rs +++ b/common/nym-lp/benches/replay_protection.rs @@ -1,9 +1,8 @@ use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; use nym_lp::replay::ReceivingKeyCounterValidator; -use nym_test_utils::helpers::u64_seeded_rng; -use parking_lot::Mutex; -use rand::Rng; -use std::sync::Arc; +use nym_test_utils::helpers::deterministic_rng_09; +use rand09::Rng; +use std::sync::{Arc, Mutex}; fn bench_sequential_counters(c: &mut Criterion) { let mut group = c.benchmark_group("replay_sequential"); @@ -47,8 +46,8 @@ fn bench_out_of_order_counters(c: &mut Criterion) { let validator = ReceivingKeyCounterValidator::default(); // Create random counters within a valid window - let mut rng = u64_seeded_rng(42); - let counters: Vec = (0..size).map(|_| rng.gen_range(0..1024)).collect(); + let mut rng = deterministic_rng_09(); + let counters: Vec = (0..size).map(|_| rng.random_range(0..1024)).collect(); b.iter(|| { let mut validator = validator.clone(); @@ -75,19 +74,15 @@ fn bench_thread_safety(c: &mut Criterion) { BenchmarkId::new("thread_safe_validator", size), &size, |b, &size| { - let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default())); + let mut validator = ReceivingKeyCounterValidator::default(); let counters: Vec = (0..size).collect(); b.iter(|| { for &counter in &counters { - let result = { - let guard = validator.lock(); - black_box(guard.will_accept_branchless(counter)) - }; + let result = { black_box(validator.will_accept_branchless(counter)) }; if result.is_ok() { - let mut guard = validator.lock(); - let _ = black_box(guard.mark_did_receive_branchless(counter)); + let _ = black_box(validator.mark_did_receive_branchless(counter)); } } }); @@ -202,7 +197,7 @@ fn bench_concurrency_scaling(c: &mut Criterion) { let mut success_count = 0; for i in 0..100 { let counter = t * 1000 + i; - let mut guard = validator_clone.lock(); + let mut guard = validator_clone.lock().unwrap(); if guard.mark_did_receive_branchless(counter as u64).is_ok() { success_count += 1; } diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index 94eedc1199..276df6c26a 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -2,1324 +2,285 @@ // SPDX-License-Identifier: Apache-2.0 use crate::LpError; -use crate::message::{LpMessage, MessageType}; -use crate::packet::{LpHeader, LpPacket, OuterHeader, TRAILER_LEN}; -use bytes::{BufMut, BytesMut}; -use chacha20poly1305::{ - ChaCha20Poly1305, Key, Nonce, Tag, - aead::{AeadInPlace, KeyInit}, -}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use crate::packet::{EncryptedLpPacket, InnerHeader, LpHeader, LpMessage, LpPacket}; +use bytes::BytesMut; +use libcrux_psq::Channel; -/// Size of outer header (receiver_idx + counter) - always cleartext -pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes +// needs to be equal or above to the actual overhead +pub(crate) const SANE_ENC_OVERHEAD: usize = 32; -/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode -const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3) +// needs to be equal or below the actual overhead +pub(crate) const SANE_DEC_OVERHEAD: usize = 24; -/// Outer AEAD key for LP packet encryption. -/// -/// Derived from PSK using Blake3 KDF with domain separation. -/// Used for opportunistic encryption: before PSK packets are cleartext, -/// after PSK packets have encrypted payload and authenticated header. -/// -/// # Security: Nonce Reuse Prevention -/// -/// ChaCha20-Poly1305 requires unique nonces per key. The counter starts at 0 -/// for each session, which is safe because: -/// -/// 1. **PSK is always fresh**: Each handshake uses PSQ -/// with a client-generated random salt. This ensures a unique -/// PSK for every session, even between the same client-gateway pair. -/// -/// 2. **Key derivation**: `outer_key = Blake3_KDF("lp-outer-aead", PSK)`. -/// Different PSK → different outer_key → nonce reuse impossible. -/// -/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions. -/// Each connection performs fresh KKT+PSQ handshake. -/// -#[derive(Clone, Zeroize, ZeroizeOnDrop)] -pub struct OuterAeadKey { - key: [u8; 32], -} +pub(crate) fn encrypt_data( + plaintext: &[u8], + transport: &mut libcrux_psq::session::Transport, +) -> Result, LpError> { + let mut ciphertext = vec![0u8; plaintext.len() + SANE_ENC_OVERHEAD]; + let n = transport.write_message(plaintext, &mut ciphertext)?; -impl OuterAeadKey { - /// KDF context for outer AEAD key derivation (domain separation) - const KDF_CONTEXT: &'static str = "lp-outer-aead"; - - /// Derive outer AEAD key from PSK. - /// - /// Uses Blake3 KDF with domain separation to avoid key reuse - /// between the outer AEAD layer and the inner Noise layer. - pub fn from_psk(psk: &[u8; 32]) -> Self { - let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]); - Self { key } + if plaintext.len() + SANE_ENC_OVERHEAD != n { + ciphertext.truncate(n); } - /// Get reference to the raw key bytes. - pub fn as_bytes(&self) -> &[u8; 32] { - &self.key + Ok(ciphertext) +} + +pub(crate) fn decrypt_data( + ciphertext: &[u8], + transport: &mut libcrux_psq::session::Transport, +) -> Result, LpError> { + if ciphertext.len() < SANE_DEC_OVERHEAD { + return Err(LpError::InsufficientBufferSize); } -} + let mut plaintext = vec![0u8; ciphertext.len() - SANE_DEC_OVERHEAD]; -impl std::fmt::Debug for OuterAeadKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OuterAeadKey") - .field("key", &"[REDACTED]") - .finish() + let (_, n) = transport.read_message(ciphertext, &mut plaintext)?; + if n != ciphertext.len() - SANE_DEC_OVERHEAD { + plaintext.truncate(n); } + Ok(plaintext) } -/// Build 12-byte nonce from 8-byte counter (zero-padded). -/// -/// Format: counter (8 bytes LE) || 0x00000000 (4 bytes) -fn build_nonce(counter: u64) -> [u8; 12] { - let mut nonce = [0u8; 12]; - nonce[..8].copy_from_slice(&counter.to_le_bytes()); - // bytes 8..12 remain zero (zero-padding) - nonce +pub(crate) fn encrypt_lp_packet( + packet: LpPacket, + transport: &mut libcrux_psq::session::Transport, +) -> Result { + let mut plaintext = BytesMut::with_capacity(InnerHeader::SIZE + packet.message().len()); + packet.header().inner.encode(&mut plaintext); + packet.message().encode_content(&mut plaintext); + + let ciphertext = encrypt_data(plaintext.as_ref(), transport)?; + + Ok(EncryptedLpPacket::new(packet.header().outer, ciphertext)) } -/// Parse message from raw type and content bytes. -/// -/// Used when decrypting outer-encrypted packets where the message type -/// was encrypted along with the content. -fn parse_message_from_type_and_content( - msg_type_raw: u32, - content: &[u8], -) -> Result { - let message_type = MessageType::from_u32(msg_type_raw) - .ok_or_else(|| LpError::invalid_message_type(msg_type_raw))?; - - LpMessage::decode_content(content, message_type) -} - -/// Parse only the outer header from raw packet bytes. -/// -/// Used for routing before session lookup. The outer header (receiver_idx + counter) -/// is always cleartext at bytes 0-12 in the unified packet format. -/// -/// # Arguments -/// * `src` - Raw packet bytes (at least OuterHeader::SIZE bytes) -/// -/// # Errors -/// * `LpError::InsufficientBufferSize` - Packet too small for outer header -pub fn parse_lp_header_only(src: &[u8]) -> Result { - OuterHeader::parse(src) -} - -/// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload). -/// -/// ## Unified Packet Format -/// -/// Both cleartext and encrypted packets have the same structure: -/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext -/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted -/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted) -/// -/// # Arguments -/// * `src` - Raw packet bytes -/// * `outer_key` - None for cleartext parsing, Some for AEAD decryption -/// -/// # Errors -/// * `LpError::AeadTagMismatch` - Tag verification failed (when outer_key provided) -/// * `LpError::InsufficientBufferSize` - Packet too small -pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result { - // Minimum size check: OuterHeader + InnerPrefix + MsgType + Trailer (for 0-payload message) - // 12 + 4 + 2 + 16 = 34 bytes - let min_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + TRAILER_LEN; - if src.len() < min_size { +pub(crate) fn decrypt_lp_packet( + packet: EncryptedLpPacket, + transport: &mut libcrux_psq::session::Transport, +) -> Result { + if packet.ciphertext().len() < InnerHeader::SIZE + SANE_DEC_OVERHEAD { return Err(LpError::InsufficientBufferSize); } - // Parse outer header (always cleartext at bytes 0-12) - let outer_header = OuterHeader::parse(src)?; + let plaintext = decrypt_data(packet.ciphertext(), transport)?; - // Extract trailer (potential AEAD tag) - let trailer_start = src.len() - TRAILER_LEN; - let mut trailer = [0u8; TRAILER_LEN]; - trailer.copy_from_slice(&src[trailer_start..]); + let inner_header = InnerHeader::parse(&plaintext)?; + let payload = &plaintext[InnerHeader::SIZE..]; + let message = LpMessage::decode_content(payload, inner_header.message_type)?; - // Inner payload is everything between outer header and trailer - let inner_bytes = &src[OUTER_HEADER_SIZE..trailer_start]; - - // Handle decryption if outer key provided - match outer_key { - None => { - // Cleartext mode - parse inner directly - // Inner format: proto(1) + reserved(3) + msg_type(4) + content - if inner_bytes.len() < INNER_PREFIX_SIZE + 4 { - return Err(LpError::InsufficientBufferSize); - } - - let protocol_version = inner_bytes[0]; - // reserved bytes [1..4] are ignored - let msg_type = u32::from_le_bytes([ - inner_bytes[4], - inner_bytes[5], - inner_bytes[6], - inner_bytes[7], - ]); - let message_content = &inner_bytes[8..]; - - let header = LpHeader { - protocol_version, - reserved: [0u8; 3], - receiver_idx: outer_header.receiver_idx, - counter: outer_header.counter, - }; - - let message = parse_message_from_type_and_content(msg_type, message_content)?; - - Ok(LpPacket { - header, - message, - trailer, - }) - } - Some(key) => { - // AEAD decryption mode - // AAD is the outer header (12 bytes) - let nonce = build_nonce(outer_header.counter); - let aad = &src[..OUTER_HEADER_SIZE]; - - // Copy inner payload for in-place decryption - let mut decrypted = inner_bytes.to_vec(); - - // Convert trailer to Tag - let tag = Tag::from_slice(&trailer); - - // Decrypt and verify - let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes())); - cipher - .decrypt_in_place_detached(Nonce::from_slice(&nonce), aad, &mut decrypted, tag) - .map_err(|_| LpError::AeadTagMismatch)?; - - // Decrypted format: proto(1) + reserved(3) + msg_type(4) + content - if decrypted.len() < INNER_PREFIX_SIZE + 4 { - return Err(LpError::InsufficientBufferSize); - } - - let protocol_version = decrypted[0]; - // reserved bytes [1..4] are ignored - let msg_type = - u32::from_le_bytes([decrypted[4], decrypted[5], decrypted[6], decrypted[7]]); - let message_content = &decrypted[8..]; - - let header = LpHeader { - protocol_version, - reserved: [0u8; 3], - receiver_idx: outer_header.receiver_idx, - counter: outer_header.counter, - }; - - let message = parse_message_from_type_and_content(msg_type, message_content)?; - - Ok(LpPacket { - header, - message, - trailer, - }) - } - } -} - -/// Serializes an LpPacket into the provided BytesMut buffer. -/// -/// ## Unified Packet Format -/// -/// Both cleartext and encrypted packets have the same structure: -/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext -/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted -/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted) -/// -/// # Arguments -/// * `item` - Packet to serialize -/// * `dst` - Output buffer -/// * `outer_key` - None for cleartext, Some for AEAD encryption -pub fn serialize_lp_packet( - item: &LpPacket, - dst: &mut BytesMut, - outer_key: Option<&OuterAeadKey>, -) -> Result<(), LpError> { - // Total size: outer_header(12) + inner_prefix(4) + msg_type(4) + content + trailer(16) - let total_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 4 + item.message.len() + TRAILER_LEN; - dst.reserve(total_size); - - // 1. Write outer header (always cleartext) - 12 bytes - let outer_header = OuterHeader::new(item.header.receiver_idx, item.header.counter); - outer_header.encode_into(dst); - - match outer_key { - None => { - // Cleartext mode - // 2. Write inner prefix: proto(1) + reserved(3) - dst.put_u8(item.header.protocol_version); - dst.put_slice(&item.header.reserved); // reserved - - // 3. Write message type (4B) + content - dst.put_slice(&(item.message.typ() as u32).to_le_bytes()); - item.message.encode_content(dst); - - // 4. Write zeros trailer - dst.put_slice(&[0u8; TRAILER_LEN]); - - Ok(()) - } - Some(key) => { - // AEAD encryption mode - // AAD is the outer header (first 12 bytes) - let aad = outer_header.encode(); - - // 2. Build plaintext: proto(1) + reserved(3) + msg_type(4) + content - let mut plaintext = BytesMut::new(); - plaintext.put_u8(item.header.protocol_version); - plaintext.put_slice(&item.header.reserved); // reserved - plaintext.put_slice(&(item.message.typ() as u32).to_le_bytes()); - item.message.encode_content(&mut plaintext); - - // 3. Copy plaintext to dst for in-place encryption - let payload_start = dst.len(); - dst.put_slice(&plaintext); - - // 4. Build nonce from counter - let nonce = build_nonce(item.header.counter); - - // 5. Encrypt payload in-place - let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes())); - let tag = cipher - .encrypt_in_place_detached( - Nonce::from_slice(&nonce), - &aad, - &mut dst[payload_start..], - ) - .map_err(|_| LpError::Internal("AEAD encryption failed".to_string()))?; - - // 6. Append tag as trailer - dst.put_slice(&tag); - - Ok(()) - } - } -} - -// Add a new error variant for invalid message types (Moved from previous impl LpError block) -impl LpError { - pub fn invalid_message_type(message_type: u32) -> Self { - LpError::InvalidMessageType(message_type) - } + Ok(LpPacket::new( + LpHeader { + outer: packet.outer_header(), + inner: inner_header, + }, + message, + )) } #[cfg(test)] mod tests { - use std::time::{SystemTime, UNIX_EPOCH}; - - // Import standalone functions - use super::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; - // Keep necessary imports use crate::LpError; - use crate::message::{EncryptedDataPayload, HandshakeData, LpMessage, MessageType}; - use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; - use bytes::BytesMut; - use nym_crypto::asymmetric::{ed25519, x25519}; - use rand::thread_rng; + use crate::codec::{decrypt_data, decrypt_lp_packet, encrypt_data, encrypt_lp_packet}; + use crate::packet::{EncryptedLpPacket, LpHeader, LpMessage, LpPacket, MessageType}; + use crate::peer::mock_peers; + use crate::psq::initiator::{build_psq_ciphersuite, build_psq_principal}; + use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size, responder}; + use libcrux_psq::{Channel, IntoSession}; + use nym_kkt_ciphersuite::KEM; + use nym_test_utils::helpers::u64_seeded_rng_09; - // With unified format, outer header (receiver_idx + counter) is always first - // and is the only cleartext portion for encrypted packets - const OUTER_HDR: usize = super::OUTER_HEADER_SIZE; // 12 bytes + fn mock_transport() -> ( + libcrux_psq::session::Transport, + libcrux_psq::session::Transport, + ) { + let kem = KEM::MlKem768; + let rng1 = u64_seeded_rng_09(1); + let rng2 = u64_seeded_rng_09(2); + let (init, resp) = mock_peers(); + let remote_resp = resp.as_remote(); + let encapsulation_key = resp + .kem_keypairs + .as_ref() + .unwrap() + .encapsulation_key(kem) + .unwrap(); - // === Cleartext Encode/Decode Tests === + let initiator_ciphersuite = + build_psq_ciphersuite(&init, &remote_resp, &encapsulation_key).unwrap(); + let mut psq_initiator = build_psq_principal(rng1, 1, initiator_ciphersuite).unwrap(); - #[test] - fn test_serialize_parse_busy() { - let mut dst = BytesMut::new(); + let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem).unwrap(); + let mut psq_responder = + responder::build_psq_principal(rng2, 1, responder_ciphersuite).unwrap(); - // Create a Busy packet - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 123, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; + // Send first message + let mut buf = vec![0u8; psq_msg1_size(kem)]; - // Serialize the packet (cleartext) - serialize_lp_packet(&packet, &mut dst, None).unwrap(); + let mut payload_buf_responder = vec![0u8; 4096]; + let mut payload_buf_initiator = vec![0u8; 4096]; - // Parse the packet (cleartext) - let decoded = parse_lp_packet(&dst, None).unwrap(); + let len_i = psq_initiator.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_i, buf.len()); - // Verify the packet fields - assert_eq!(decoded.header.protocol_version, 1); - assert_eq!(decoded.header.receiver_idx, 42); - assert_eq!(decoded.header.counter, 123); - assert!(matches!(decoded.message, LpMessage::Busy)); - assert_eq!(decoded.trailer, [0; TRAILER_LEN]); + // Read first message + let (_, _) = psq_responder + .read_message(&buf, &mut payload_buf_responder) + .unwrap(); + + // Respond + let mut buf = [0u8; PSQ_MSG2_SIZE]; + let len_r = psq_responder.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_r, buf.len()); + + // Finalize on registration initiator + let (len_i_deserialized, _) = psq_initiator + .read_message(&buf, &mut payload_buf_initiator) + .unwrap(); + + // We read the same amount of data. + assert_eq!(len_r, len_i_deserialized); + + // Ready for transport mode + assert!(psq_initiator.is_handshake_finished()); + assert!(psq_responder.is_handshake_finished()); + + let transport_initiator = psq_initiator + .into_session() + .unwrap() + .transport_channel() + .unwrap(); + + let transport_responder = psq_responder + .into_session() + .unwrap() + .transport_channel() + .unwrap(); + + (transport_initiator, transport_responder) } #[test] - fn test_serialize_parse_handshake() { - let mut dst = BytesMut::new(); + fn basic_plain_encryption_test() { + let (mut init_transport, mut resp_transport) = mock_transport(); - // Create a Handshake message packet - let payload = vec![42u8; 80]; // Example payload size - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 123, - }, - message: LpMessage::Handshake(HandshakeData(payload.clone())), - trailer: [0; TRAILER_LEN], - }; + for msg_size in [1usize, 10, 100, 1000, 10000, 65535] { + let message1 = vec![42u8; msg_size]; - // Serialize the packet (cleartext) - serialize_lp_packet(&packet, &mut dst, None).unwrap(); + let mut ciphertext = vec![0u8; msg_size + 64]; + let written_init1 = init_transport + .write_message(&message1, &mut ciphertext) + .unwrap(); - // Parse the packet (cleartext) - let decoded = parse_lp_packet(&dst, None).unwrap(); + let init_ciphertext_overhead = written_init1 - msg_size; + let ciphertext_content = &ciphertext[..written_init1]; - // Verify the packet fields - assert_eq!(decoded.header.protocol_version, 1); - assert_eq!(decoded.header.receiver_idx, 42); - assert_eq!(decoded.header.counter, 123); + let mut plaintext = vec![0u8; msg_size + 64]; + let (read_resp1, written_resp1) = resp_transport + .read_message(ciphertext_content, &mut plaintext) + .unwrap(); + let resp_plaintext_overhead = ciphertext_content.len() - written_resp1; - // Verify message type and data - match decoded.message { - LpMessage::Handshake(decoded_payload) => { - assert_eq!(decoded_payload, HandshakeData(payload)); - } - _ => panic!("Expected Handshake message"), - } - assert_eq!(decoded.trailer, [0; TRAILER_LEN]); - } - - #[test] - fn test_serialize_parse_encrypted_data() { - let mut dst = BytesMut::new(); - - // Create an EncryptedData message packet - let payload = vec![43u8; 124]; // Example payload size - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 123, - }, - message: LpMessage::EncryptedData(EncryptedDataPayload(payload.clone())), - trailer: [0; TRAILER_LEN], - }; - - // Serialize the packet (cleartext) - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - - // Parse the packet (cleartext) - let decoded = parse_lp_packet(&dst, None).unwrap(); - - // Verify the packet fields - assert_eq!(decoded.header.protocol_version, 1); - assert_eq!(decoded.header.receiver_idx, 42); - assert_eq!(decoded.header.counter, 123); - - // Verify message type and data - match decoded.message { - LpMessage::EncryptedData(decoded_payload) => { - assert_eq!(decoded_payload, EncryptedDataPayload(payload)); - } - _ => panic!("Expected EncryptedData message"), - } - assert_eq!(decoded.trailer, [0; TRAILER_LEN]); - } - - // === Incomplete Data Tests === - - #[test] - fn test_parse_incomplete_header() { - // Create a buffer with incomplete header - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Only 4 bytes, not enough for LpHeader::SIZE - - // Attempt to parse - expect error - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - LpError::InsufficientBufferSize - )); - } - - #[test] - fn test_parse_incomplete_message_type() { - // Create a buffer with complete header but incomplete message type - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&[0]); // Only 1 byte of message type (need 2) - - // Buffer length = 16 + 1 = 17. Min size = 16 + 2 + 16 = 34. - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - LpError::InsufficientBufferSize - )); - } - - #[test] - fn test_parse_incomplete_message_data() { - // Create a buffer simulating Handshake but missing trailer and maybe partial payload - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type - buf.extend_from_slice(&[42; 40]); // 40 bytes of payload data - - // Buffer length = 16 + 2 + 40 = 58. Min size = 16 + 2 + 16 = 34. - // Payload size calculated as 58 - 34 = 24. - // Trailer expected at index 16 + 2 + 24 = 42. - // Trailer read attempts src[42..58]. - // This *should* parse successfully based on the logic, but the trailer is garbage. - // Let's rethink: parse_lp_packet assumes the *entire slice* is the packet. - // If the slice doesn't end exactly where the trailer should, it's an error. - // In this case, total length is 58. OuterHdr(12) + InnerPrefix(4) + Type(2) + Trailer(16) = 34. Payload = 58-34=24. - // Trailer starts at 16+2+24 = 42. Ends at 42+16=58. It fits exactly. - // This test *still* doesn't test incompleteness correctly for the datagram parser. - - // Let's test a buffer that's *too short* even for header+type+trailer+min_payload - // Note: Buffer order doesn't matter for this test since we fail on minimum size check - let mut buf_too_short = BytesMut::new(); - buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx (outer header) - buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // counter (outer header) - buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // version + reserved (inner prefix) - buf_too_short.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // msg type - // No payload, no trailer. Length = 12+4+2=18. Min size = 34. - let result_too_short = parse_lp_packet(&buf_too_short, None); - assert!(result_too_short.is_err()); - assert!(matches!( - result_too_short.unwrap_err(), - LpError::InsufficientBufferSize - )); - - // Test a buffer missing PART of the trailer - let mut buf_partial_trailer = BytesMut::new(); - buf_partial_trailer.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf_partial_trailer.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf_partial_trailer.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf_partial_trailer.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type - let payload = vec![42u8; 20]; // Assume 20 byte payload - buf_partial_trailer.extend_from_slice(&payload); - buf_partial_trailer.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer - - // Total length = 16 + 2 + 20 + 15 = 53. Min size = 34. This passes. - // Payload size = 53 - 34 = 19. <--- THIS IS WRONG. The parser assumes the length dictates payload. - // Let's fix the parser logic slightly. - - // The point is, parse_lp_packet expects a COMPLETE datagram. Providing less bytes - // than LpHeader + Type + Trailer should fail. Providing *more* is also an issue unless - // the length calculation works out perfectly. The most direct test is just < min_size. - // Renaming test to reflect this. - } - - #[test] - fn test_parse_buffer_smaller_than_minimum() { - // Test a buffer that's smaller than the smallest possible packet (LpHeader+Type+Trailer) - let mut buf_too_short = BytesMut::new(); - buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf_too_short.extend_from_slice(&MessageType::Busy.to_u32().to_le_bytes()); // Type - buf_too_short.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer - // Length = 16 + 2 + 15 = 33. Min Size = 34. - let result_too_short = parse_lp_packet(&buf_too_short, None); - assert!( - result_too_short.is_err(), - "Expected error for buffer size 33, min 34" - ); - assert!(matches!( - result_too_short.unwrap_err(), - LpError::InsufficientBufferSize - )); - } - - #[test] - fn test_parse_invalid_message_type() { - // Create a buffer with invalid message type - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type - // Need payload and trailer to meet min_size requirement - let payload_size = 10; // Arbitrary - buf.extend_from_slice(&vec![0u8; payload_size]); // Some data - buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer - - // Attempt to parse - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - match result { - Err(LpError::InvalidMessageType(231)) => {} // Expected error - Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e), - Ok(_) => panic!("Expected error, but got Ok"), - } - } - - #[test] - fn test_parse_incorrect_payload_size_for_busy() { - // Create a Busy packet but *with* a payload (which is invalid) - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&MessageType::Busy.to_u32().to_le_bytes()); // Busy type - buf.extend_from_slice(&[42; 1]); // <<< Invalid 1-byte payload for Busy - buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer - - // Total size = 16 + 2 + 1 + 16 = 35. Min size = 34. - // Calculated payload size = 35 - 34 = 1. - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - LpError::InvalidPayloadSize { - expected: 0, - actual: 1 - } - )); - } - - // Test multiple packets simulation isn't relevant for datagram parsing - // #[test] - // fn test_multiple_packets_in_buffer() { ... } - - // === ClientHello Serialization Tests === - - #[test] - fn test_serialize_parse_client_hello() { - use crate::message::ClientHelloData; - - let mut rng = thread_rng(); - let valid_ed25519 = ed25519::KeyPair::new(&mut rng); - let mut dst = BytesMut::new(); - - // Create ClientHelloData - let client_key = x25519::PublicKey::from_byte_array(&[42u8; 32]); - let client_ed25519_key = *valid_ed25519.public_key(); - let salt = [99u8; 32]; - let hello_data = ClientHelloData { - receiver_index: 12345, - client_lp_public_key: client_key, - client_ed25519_public_key: client_ed25519_key, - salt, - }; - - // Create a ClientHello message packet - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 123, - }, - message: LpMessage::ClientHello(hello_data), - trailer: [0; TRAILER_LEN], - }; - - // Serialize the packet - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - - // Parse the packet - let decoded = parse_lp_packet(&dst, None).unwrap(); - - // Verify the packet fields - assert_eq!(decoded.header.protocol_version, 1); - assert_eq!(decoded.header.receiver_idx, 42); - assert_eq!(decoded.header.counter, 123); - - // Verify message type and data - match decoded.message { - LpMessage::ClientHello(decoded_data) => { - assert_eq!(decoded_data.client_lp_public_key, client_key); - assert_eq!(decoded_data.salt, salt); - } - _ => panic!("Expected ClientHello message"), - } - assert_eq!(decoded.trailer, [0; TRAILER_LEN]); - } - - #[test] - fn test_serialize_parse_client_hello_with_fresh_salt() { - use crate::message::ClientHelloData; - - let mut dst = BytesMut::new(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - // Create ClientHelloData with fresh salt - let mut rng = thread_rng(); - let valid_ed25519 = ed25519::KeyPair::new(&mut rng); - - let client_key = x25519::PublicKey::from_byte_array(&[7u8; 32]); - let client_ed25519_key = *valid_ed25519.public_key(); - let hello_data = - ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - - // Create a ClientHello message packet - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 100, - counter: 200, - }, - message: LpMessage::ClientHello(hello_data), - trailer: [55; TRAILER_LEN], - }; - - // Serialize the packet - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - - // Parse the packet - let decoded = parse_lp_packet(&dst, None).unwrap(); - - // Verify message type and data - match decoded.message { - LpMessage::ClientHello(decoded_data) => { - assert_eq!(decoded_data.client_lp_public_key, client_key); - assert_eq!(decoded_data.salt, hello_data.salt); - - // Verify timestamp can be extracted - let timestamp = decoded_data.extract_timestamp(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - // Timestamp should be within 2 seconds of now - assert!((timestamp as i64 - now as i64).abs() <= 2); - } - _ => panic!("Expected ClientHello message"), - } - } - - #[test] - fn test_parse_client_hello_malformed_data() { - // Create a buffer with ClientHello message type but invalid bincode data - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type - - // Add data that does not equal to ClientHelloData::LEN - buf.extend_from_slice(&[0xFF; 50]); // invalid data - buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer - - // Attempt to parse - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - match result { - Err(LpError::DeserializationError(_)) => {} // Expected error - Err(e) => panic!("Expected DeserializationError, got {:?}", e), - Ok(_) => panic!("Expected error, but got Ok"), - } - } - - #[test] - fn test_parse_client_hello_incomplete_bincode() { - // Create a buffer with ClientHello but truncated bincode data - let mut buf = BytesMut::new(); - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index - buf.extend_from_slice(&123u64.to_le_bytes()); // Counter - buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type - - // Add incomplete bincode data (only partial ClientHelloData) - buf.extend_from_slice(&[0; 20]); // Too few bytes for full ClientHelloData - buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer - - // Attempt to parse - let result = parse_lp_packet(&buf, None); - assert!(result.is_err()); - match result { - Err(LpError::DeserializationError(_)) => {} // Expected error - Err(e) => panic!("Expected DeserializationError, got {:?}", e), - Ok(_) => panic!("Expected error, but got Ok"), - } - } - - #[test] - fn test_forward_packet_encode_decode_roundtrip_v4() { - let mut dst = BytesMut::new(); - - let forward_data = crate::message::ForwardPacketData { - target_gateway_identity: [77u8; 32], - target_lp_address: "1.2.3.4:41264".parse().unwrap(), - inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd], - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 999, - counter: 555, - }, - message: LpMessage::ForwardPacket(forward_data), - trailer: [0xff; TRAILER_LEN], - }; - - // Serialize - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - - // Parse back - let decoded = parse_lp_packet(&dst, None).unwrap(); - - // Verify LP protocol handling works correctly - assert_eq!(decoded.header.receiver_idx, 999); - assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket)); - - if let LpMessage::ForwardPacket(data) = decoded.message { - assert_eq!(data.target_gateway_identity, [77u8; 32]); - assert_eq!(data.target_lp_address, "1.2.3.4:41264".parse().unwrap()); - assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]); - } else { - panic!("Expected ForwardPacket message"); - } - } - - #[test] - fn test_forward_packet_encode_decode_roundtrip_v6() { - let mut dst = BytesMut::new(); - - let forward_data = crate::message::ForwardPacketData { - target_gateway_identity: [77u8; 32], - target_lp_address: "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap(), - inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd], - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 999, - counter: 555, - }, - message: LpMessage::ForwardPacket(forward_data), - trailer: [0xff; TRAILER_LEN], - }; - - // Serialize - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - - // Parse back - let decoded = parse_lp_packet(&dst, None).unwrap(); - - // Verify LP protocol handling works correctly - assert_eq!(decoded.header.receiver_idx, 999); - assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket)); - - if let LpMessage::ForwardPacket(data) = decoded.message { - assert_eq!(data.target_gateway_identity, [77u8; 32]); assert_eq!( - data.target_lp_address, - "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap() + written_init1, read_resp1, + "should work for message {msg_size}" + ); + let message1_content = &plaintext[..written_resp1]; + assert_eq!( + message1_content, &message1, + "should work for message {msg_size}" ); - assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]); - } else { - panic!("Expected ForwardPacket message"); - } - } - // === Outer AEAD Tests === + // reverse the communication + let message2 = vec![43u8; msg_size]; - #[test] - fn test_aead_roundtrip_with_key() { - // Test that encrypt/decrypt roundtrip works with an AEAD key - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); + let mut ciphertext2 = vec![0u8; msg_size + 64]; + let written_resp2 = resp_transport + .write_message(&message2, &mut ciphertext2) + .unwrap(); - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 999, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; + let resp_ciphertext_overhead = written_resp2 - msg_size; + let ciphertext_content2 = &ciphertext2[..written_resp2]; - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); + let mut plaintext2 = vec![0u8; msg_size + 64]; + let (read_init2, written_init2) = init_transport + .read_message(ciphertext_content2, &mut plaintext2) + .unwrap(); + let init_plaintext_overhead = ciphertext_content2.len() - written_init2; - // Parse back with the same key - let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap(); + assert_eq!( + written_resp2, read_init2, + "should work for message {msg_size}" + ); + let message2_content = &plaintext2[..written_init2]; + assert_eq!( + message2_content, &message2, + "should work for message {msg_size}" + ); - assert_eq!(decoded.header.protocol_version, 1); - assert_eq!(decoded.header.receiver_idx, 12345); - assert_eq!(decoded.header.counter, 999); - assert!(matches!(decoded.message, LpMessage::Busy)); - } + // check consistent overheads + // enc/enc + assert_eq!(init_ciphertext_overhead, resp_ciphertext_overhead); - #[test] - fn test_aead_ciphertext_differs_from_plaintext() { - // Verify that encrypted payload differs from plaintext - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); + // dec/dec + assert_eq!(resp_plaintext_overhead, init_plaintext_overhead); - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 999, - }, - message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload(vec![ - 0xAA, 0xBB, 0xCC, 0xDD, - ])), - trailer: [0; TRAILER_LEN], - }; - - let mut cleartext = BytesMut::new(); - serialize_lp_packet(&packet, &mut cleartext, None).unwrap(); - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); - - // Outer header (receiver_idx + counter) should be the same - always cleartext - assert_eq!(&cleartext[..OUTER_HDR], &encrypted[..OUTER_HDR]); - - // Inner payload (proto + reserved + msg_type + content) should differ (encrypted) - let payload_start = OUTER_HDR; - let payload_end_cleartext = cleartext.len() - TRAILER_LEN; - let payload_end_encrypted = encrypted.len() - TRAILER_LEN; - - assert_ne!( - &cleartext[payload_start..payload_end_cleartext], - &encrypted[payload_start..payload_end_encrypted], - "Encrypted payload should differ from plaintext" - ); - - // Trailer should differ (zeros vs AEAD tag) - assert_ne!( - &cleartext[payload_end_cleartext..], - &encrypted[payload_end_encrypted..], - "Encrypted trailer should be a tag, not zeros" - ); - } - - #[test] - fn test_aead_tampered_tag_fails() { - // Verify that tampering with the tag causes decryption failure - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 999, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); - - // Tamper with the tag (last byte) - let last_idx = encrypted.len() - 1; - encrypted[last_idx] ^= 0xFF; - - // Parsing should fail with AeadTagMismatch - let result = parse_lp_packet(&encrypted, Some(&outer_key)); - assert!(matches!(result, Err(LpError::AeadTagMismatch))); - } - - #[test] - fn test_aead_tampered_header_fails() { - // Verify that tampering with the header (AAD) causes decryption failure - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 999, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); - - // Tamper with the outer header AAD (flip a bit in counter at byte 4) - // New format: [receiver_idx(0-3), counter(4-11)], so byte 4 is counter's LSB - encrypted[4] ^= 0x01; - - // Parsing should fail with AeadTagMismatch - let result = parse_lp_packet(&encrypted, Some(&outer_key)); - assert!(matches!(result, Err(LpError::AeadTagMismatch))); - } - - #[test] - fn test_aead_different_counters_produce_different_ciphertext() { - // Verify that different counters (nonces) produce different ciphertexts - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); - - let packet1 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 1, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; - - let packet2 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 2, // Different counter - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted1 = BytesMut::new(); - serialize_lp_packet(&packet1, &mut encrypted1, Some(&outer_key)).unwrap(); - - let mut encrypted2 = BytesMut::new(); - serialize_lp_packet(&packet2, &mut encrypted2, Some(&outer_key)).unwrap(); - - // The encrypted inner payloads should differ even though the message is the same - // (because nonce is different). Inner payload starts after outer header. - let payload_start = OUTER_HDR; - assert_ne!( - &encrypted1[payload_start..], - &encrypted2[payload_start..], - "Different counters should produce different ciphertexts" - ); - } - - #[test] - fn test_aead_wrong_key_fails() { - // Verify that decryption with wrong key fails - let psk1 = [42u8; 32]; - let psk2 = [43u8; 32]; // Different PSK - let outer_key1 = OuterAeadKey::from_psk(&psk1); - let outer_key2 = OuterAeadKey::from_psk(&psk2); - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 12345, - counter: 999, - }, - message: LpMessage::Busy, - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key1)).unwrap(); - - // Parsing with wrong key should fail - let result = parse_lp_packet(&encrypted, Some(&outer_key2)); - assert!(matches!(result, Err(LpError::AeadTagMismatch))); - } - - #[test] - fn test_aead_encrypted_data_message_roundtrip() { - // Test AEAD with EncryptedData message type (larger payload) - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); - - let payload_data = vec![0xDE; 100]; - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 54321, - counter: 12345678, - }, - message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload( - payload_data.clone(), - )), - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); - - let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap(); - - match decoded.message { - LpMessage::EncryptedData(data) => { - assert_eq!(data.0, payload_data); - } - _ => panic!("Expected EncryptedData message"), + // enc/dec + assert_eq!(init_ciphertext_overhead, resp_plaintext_overhead); } } #[test] - fn test_aead_handshake_message_roundtrip() { - // Test AEAD with Handshake message type - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); + fn basic_encryption() { + let (mut init_transport, mut resp_transport) = mock_transport(); - let handshake_data = vec![0x01, 0x02, 0x03, 0x04, 0x05]; - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 99999, - counter: 2, - }, - message: LpMessage::Handshake(HandshakeData(handshake_data.clone())), - trailer: [0; TRAILER_LEN], - }; + // happy path + let msg = b"foomp".to_vec(); + let ciphertext = encrypt_data(&msg, &mut init_transport).unwrap(); + let plaintext = decrypt_data(&ciphertext, &mut resp_transport).unwrap(); + assert_eq!(msg, plaintext); - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); + // incomplete ciphertext + let msg2 = b"foomp".to_vec(); + let ciphertext2 = encrypt_data(&msg2, &mut init_transport).unwrap(); + let len = ciphertext2.len(); + let dec_err = decrypt_data(&ciphertext2[..len - 1], &mut resp_transport).unwrap_err(); + assert!(matches!(dec_err, LpError::PSQSessionFailure { .. })); - let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap(); - - match decoded.message { - LpMessage::Handshake(data) => { - assert_eq!(data.0, handshake_data); - } - _ => panic!("Expected Handshake message"), - } - } - - // === Subsession Message Tests === - - #[test] - fn test_serialize_parse_subsession_request() { - let mut dst = BytesMut::new(); - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 100, - }, - message: LpMessage::SubsessionRequest, - trailer: [0; TRAILER_LEN], - }; - - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - let decoded = parse_lp_packet(&dst, None).unwrap(); - - assert_eq!(decoded.header.receiver_idx, 42); - assert_eq!(decoded.header.counter, 100); - assert!(matches!(decoded.message, LpMessage::SubsessionRequest)); + // too small buffer + let msg3 = b"foomp".to_vec(); + let ciphertext3 = encrypt_data(&msg3, &mut resp_transport).unwrap(); + let dec_err = decrypt_data(&ciphertext3[..10], &mut init_transport).unwrap_err(); + assert!(matches!(dec_err, LpError::InsufficientBufferSize)); } #[test] - fn test_serialize_parse_subsession_kk1() { - use crate::message::SubsessionKK1Data; + fn basic_packet_encryption() { + let (mut init_transport, mut resp_transport) = mock_transport(); - let mut dst = BytesMut::new(); + // happy path + let packet = LpPacket::new(LpHeader::new(123, 0, 1, MessageType::Busy), LpMessage::Busy); - let kk1_data = SubsessionKK1Data { - payload: vec![0xAA; 50], // 50 bytes KK payload - }; + let ciphertext = encrypt_lp_packet(packet.clone(), &mut init_transport).unwrap(); + assert_eq!(packet.header().outer, ciphertext.outer_header()); - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 123, - counter: 456, - }, - message: LpMessage::SubsessionKK1(kk1_data.clone()), - trailer: [0; TRAILER_LEN], - }; + let plaintext = decrypt_lp_packet(ciphertext, &mut resp_transport).unwrap(); + assert_eq!(packet, plaintext); - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - let decoded = parse_lp_packet(&dst, None).unwrap(); + // incomplete ciphertext + let packet = LpPacket::new(LpHeader::new(123, 1, 1, MessageType::Busy), LpMessage::Busy); + let ciphertext2 = encrypt_lp_packet(packet, &mut init_transport).unwrap(); + let l = ciphertext2.ciphertext().len(); + let malformed_content = ciphertext2.ciphertext()[..l - 1].to_vec(); + let malformed = EncryptedLpPacket::new(ciphertext2.outer_header(), malformed_content); + let dec_err = decrypt_lp_packet(malformed, &mut resp_transport).unwrap_err(); + assert!(matches!(dec_err, LpError::PSQSessionFailure { .. })); - assert_eq!(decoded.header.receiver_idx, 123); - match decoded.message { - LpMessage::SubsessionKK1(data) => { - assert_eq!(data.payload, kk1_data.payload); - } - _ => panic!("Expected SubsessionKK1 message"), - } - } - - #[test] - fn test_serialize_parse_subsession_kk2() { - use crate::message::SubsessionKK2Data; - - let mut dst = BytesMut::new(); - - let kk2_data = SubsessionKK2Data { - payload: vec![0x11; 60], // 60 bytes KK response payload - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 789, - counter: 1000, - }, - message: LpMessage::SubsessionKK2(kk2_data.clone()), - trailer: [0; TRAILER_LEN], - }; - - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - let decoded = parse_lp_packet(&dst, None).unwrap(); - - assert_eq!(decoded.header.receiver_idx, 789); - match decoded.message { - LpMessage::SubsessionKK2(data) => { - assert_eq!(data.payload, kk2_data.payload); - } - _ => panic!("Expected SubsessionKK2 message"), - } - } - - #[test] - fn test_serialize_parse_subsession_ready() { - use crate::message::SubsessionReadyData; - - let mut dst = BytesMut::new(); - - let ready_data = SubsessionReadyData { - receiver_index: 99999, - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 200, - }, - message: LpMessage::SubsessionReady(ready_data.clone()), - trailer: [0; TRAILER_LEN], - }; - - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - let decoded = parse_lp_packet(&dst, None).unwrap(); - - assert_eq!(decoded.header.receiver_idx, 42); - match decoded.message { - LpMessage::SubsessionReady(data) => { - assert_eq!(data.receiver_index, 99999); - } - _ => panic!("Expected SubsessionReady message"), - } - } - - #[test] - fn test_subsession_request_with_payload_fails() { - // SubsessionRequest should have no payload - let mut buf = BytesMut::new(); - buf.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx - buf.extend_from_slice(&123u64.to_le_bytes()); // counter - buf.extend_from_slice(&[1, 0, 0, 0]); // version + reserved - buf.extend_from_slice(&MessageType::SubsessionRequest.to_u32().to_le_bytes()); - buf.extend_from_slice(&[0xFF]); // Invalid payload for SubsessionRequest - buf.extend_from_slice(&[0; TRAILER_LEN]); - - let result = parse_lp_packet(&buf, None); - assert!(matches!( - result, - Err(LpError::InvalidPayloadSize { - expected: 0, - actual: 1 - }) - )); - } - - #[test] - fn test_aead_subsession_roundtrip() { - use crate::message::SubsessionKK1Data; - - let psk = [42u8; 32]; - let outer_key = OuterAeadKey::from_psk(&psk); - - let kk1_data = SubsessionKK1Data { - payload: vec![0xDE; 48], // 48 bytes KK payload - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 54321, - counter: 999, - }, - message: LpMessage::SubsessionKK1(kk1_data.clone()), - trailer: [0; TRAILER_LEN], - }; - - let mut encrypted = BytesMut::new(); - serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap(); - - let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap(); - - match decoded.message { - LpMessage::SubsessionKK1(data) => { - assert_eq!(data.payload, kk1_data.payload); - } - _ => panic!("Expected SubsessionKK1 message"), - } - } - - #[test] - fn test_serialize_parse_error() { - use crate::message::ErrorPacketData; - - let mut dst = BytesMut::new(); - - let error_data = ErrorPacketData { - message: "this is an error".to_string(), - }; - - let packet = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 200, - }, - message: LpMessage::Error(error_data.clone()), - trailer: [0; TRAILER_LEN], - }; - - serialize_lp_packet(&packet, &mut dst, None).unwrap(); - let decoded = parse_lp_packet(&dst, None).unwrap(); - - assert_eq!(decoded.header.receiver_idx, 42); - match decoded.message { - LpMessage::Error(data) => { - assert_eq!(data.message, "this is an error"); - } - _ => panic!("Expected Error message"), - } + // too small buffer + let packet = LpPacket::new(LpHeader::new(123, 1, 1, MessageType::Busy), LpMessage::Busy); + let ciphertext3 = encrypt_lp_packet(packet, &mut resp_transport).unwrap(); + let malformed = EncryptedLpPacket::new(ciphertext3.outer_header(), vec![]); + let dec_err = decrypt_lp_packet(malformed, &mut init_transport).unwrap_err(); + assert!(matches!(dec_err, LpError::InsufficientBufferSize)); } } diff --git a/common/nym-lp/src/config.rs b/common/nym-lp/src/config.rs index 4b22ad331d..bc6a0ab987 100644 --- a/common/nym-lp/src/config.rs +++ b/common/nym-lp/src/config.rs @@ -18,7 +18,7 @@ pub const DEFAULT_PSK_TTL_SECS: u64 = 3600; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LpConfig { /// KEM algorithm for PSQ key encapsulation. - /// X25519 = classical (testing), MlKem768 = PQ, XWing = hybrid. + /// Supported KEMs: MlKem768, McEliece #[serde(with = "kem_serde")] pub kem_algorithm: KEM, @@ -32,7 +32,7 @@ pub struct LpConfig { impl Default for LpConfig { fn default() -> Self { Self { - kem_algorithm: KEM::X25519, + kem_algorithm: KEM::MlKem768, psk_ttl_secs: DEFAULT_PSK_TTL_SECS, enable_kkt: true, } @@ -55,10 +55,10 @@ mod kem_serde { S: Serializer, { match kem { - KEM::X25519 => "X25519", KEM::MlKem768 => "MlKem768", - KEM::XWing => "XWing", KEM::McEliece => "McEliece", + KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")), + KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")), } .serialize(serializer) } @@ -69,10 +69,10 @@ mod kem_serde { { let s = String::deserialize(deserializer)?; match s.as_str() { - "X25519" => Ok(KEM::X25519), "MlKem768" => Ok(KEM::MlKem768), - "XWing" => Ok(KEM::XWing), "McEliece" => Ok(KEM::McEliece), + "X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")), + "XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")), _ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))), } } diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index cf3b5e5085..1033887a3a 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -1,11 +1,16 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::message::MessageType; -use crate::{noise_protocol::NoiseError, replay::ReplayError}; -use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; -use nym_kkt::ciphersuite::{HashFunction, KEM}; +use crate::packet::MalformedLpPacketError; +use crate::peer_config::LpReceiverIndex; +use crate::replay::ReplayError; +use crate::transport::LpTransportError; +use libcrux_psq::handshake::HandshakeError; +use libcrux_psq::handshake::builders::BuilderError; +use libcrux_psq::session::SessionError; +// use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; use nym_kkt::error::KKTError; +use nym_kkt_ciphersuite::{HashFunction, KEM}; use thiserror::Error; #[derive(Error, Debug)] @@ -13,33 +18,18 @@ pub enum LpError { #[error("IO Error: {0}")] IoError(#[from] std::io::Error), - #[error("Snow Error: {0}")] - SnowKeyError(#[from] snow::Error), - - #[error("Snow Pattern Error: {0}")] - SnowPatternError(String), - - #[error("Noise Protocol Error: {0}")] - NoiseError(#[from] NoiseError), - #[error("Replay detected: {0}")] Replay(#[from] ReplayError), - #[error("Invalid packet format: {0}")] - InvalidPacketFormat(String), - - #[error("Invalid message type: {0}")] - InvalidMessageType(u32), - - #[error("Payload too large: {0}")] - PayloadTooLarge(usize), - #[error("Insufficient buffer size provided")] InsufficientBufferSize, #[error("Attempted operation on closed session")] SessionClosed, + #[error("There already exists an LP session with receiver index {0}")] + DuplicateSessionId(LpReceiverIndex), + #[error("Internal error: {0}")] Internal(String), @@ -52,15 +42,12 @@ pub enum LpError { #[error("Deserialization error: {0}")] DeserializationError(String), - #[error("KKT protocol error: {0}")] - KKTError(String), - #[error(transparent)] InvalidBase58String(#[from] bs58::decode::Error), /// Session ID from incoming packet does not match any known session. #[error("Received packet with unknown session ID: {0}")] - UnknownSessionId(u32), + UnknownSessionId(LpReceiverIndex), /// Invalid state transition attempt in the state machine. #[error("Invalid input '{input}' for current state '{state}'")] @@ -75,27 +62,14 @@ pub enum LpError { LpSessionProcessing, /// State machine not found. - #[error("State machine not found for lp_id: {lp_id}")] - StateMachineNotFound { lp_id: u32 }, + #[error("State machine not found for lp_id: {0}")] + StateMachineNotFound(LpReceiverIndex), - /// Ed25519 to X25519 conversion error. - #[error("Ed25519 key conversion error: {0}")] - Ed25519RecoveryError(#[from] Ed25519RecoveryError), - - /// Outer AEAD authentication tag verification failed. - #[error("AEAD authentication tag verification failed")] - AeadTagMismatch, - - /// Received an LP packet with an incompatible, future, version - #[error("incompatible LP packet version. got: {got}, highest supported: {highest_supported}")] - IncompatibleFuturePacketVersion { got: u8, highest_supported: u8 }, - - /// Received an LP packet with an incompatible, legacy, version - #[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")] - IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 }, - - #[error("attempted to create an LP responder without providing a valid KEM key")] - ResponderWithMissingKEMKey, + // /// Ed25519 to X25519 conversion error. + // #[error("Ed25519 key conversion error: {0}")] + // Ed25519RecoveryError(#[from] Ed25519RecoveryError), + #[error("attempted to create an LP responder without providing a valid KEM keys")] + ResponderWithMissingKEMKeys, #[error( "there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function" @@ -113,16 +87,56 @@ pub enum LpError { #[from] source: KKTError, }, + + #[error(transparent)] + MalformedPacket(#[from] MalformedLpPacketError), + + #[error("version {version} is not supported")] + UnsupportedVersion { version: u8 }, + + #[error("failed to build PSQ responder: {inner:?}")] + PSQResponderBuilderFailure { inner: BuilderError }, + + #[error("failed to build PSQ initiator: {inner:?}")] + PSQInitiatorBuilderFailure { inner: BuilderError }, + + #[error("failed to complete the PSQ handshake: {inner:?}")] + PSQHandshakeFailure { inner: HandshakeError }, + + #[error("failed to run the PSQ session: {inner:?}")] + PSQSessionFailure { inner: SessionError }, + + #[error("failed to derive a transport channel: {inner:?}")] + TransportDerivationFailure { inner: SessionError }, + + #[error("the initiator authenticator is not available after ingesting PSQ msg1")] + MissingInitiatorAuthenticator, + + #[error("transport failure: {0}")] + TransportFailure(#[from] LpTransportError), + + #[error("the current session is not in transport state")] + NotInTransport, } impl LpError { pub fn kkt_psq_handshake(msg: impl Into) -> Self { Self::KKTPSQHandshake(msg.into()) } +} - pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError { - Self::KKTPSQHandshake(format!( - "received unexpected response, got: {got:?}, expected: {expected:?}" - )) +impl From for LpError { + fn from(handshake_error: HandshakeError) -> Self { + Self::PSQHandshakeFailure { + inner: handshake_error, + } + } +} + +impl From for LpError { + fn from(session_error: SessionError) -> Self { + Self::PSQSessionFailure { + inner: session_error, + } } } diff --git a/common/nym-lp/src/kkt_orchestrator.rs b/common/nym-lp/src/kkt_orchestrator.rs deleted file mode 100644 index 98e3cdff71..0000000000 --- a/common/nym-lp/src/kkt_orchestrator.rs +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! KKT (Key Encapsulation Transport) orchestration for nym-lp sessions. -//! -//! This module provides functions to perform KKT key exchange before establishing -//! an nym-lp session. The KKT protocol allows secure distribution of post-quantum -//! KEM public keys, which are then used with PSQ to derive a strong pre-shared key -//! for the Noise protocol. -//! -//! # Protocol Flow -//! -//! 1. **Client (Initiator)**: -//! - Calls `create_request()` to generate a KKT request -//! - Sends `LpMessage::KKTRequest` to gateway -//! - Receives `LpMessage::KKTResponse` from gateway -//! - Calls `process_response()` to validate and extract gateway's KEM key -//! -//! 2. **Gateway (Responder)**: -//! - Receives `LpMessage::KKTRequest` from client -//! - Calls `handle_request()` to validate request and generate response -//! - Sends `LpMessage::KKTResponse` to client -//! -//! # Example -//! -//! ```ignore -//! use nym_lp::kkt_orchestrator::{create_request, process_response, handle_request}; -//! use nym_lp::message::{KKTRequestData, KKTResponseData}; -//! use nym_kkt::ciphersuite::{Ciphersuite, KEM, HashFunction, SignatureScheme, EncapsulationKey}; -//! -//! // Setup ciphersuite -//! let ciphersuite = Ciphersuite::resolve_ciphersuite( -//! KEM::X25519, -//! HashFunction::Blake3, -//! SignatureScheme::Ed25519, -//! None, -//! ).unwrap(); -//! -//! // Client: Create request -//! let (session_secret, client_context, request_data) = create_request( -//! ciphersuite, -//! &client_signing_key, -//! &responder_dh_public_key -//! ).unwrap(); -//! -//! // Gateway: Handle request -//! let response_data = handle_request( -//! &request_data, -//! Some(&client_verification_key), -//! &gateway_signing_key, -//! &gateway_dh_private_key, -//! &gateway_kem_public_key, -//! ).unwrap(); -//! -//! // Client: Process response -//! let gateway_kem_key = process_response( -//! client_context, -//! &session_secret, -//! &gateway_verification_key, -//! &expected_key_hash, -//! &response_data, -//! ).unwrap(); -//! ``` - -use crate::LpError; -use crate::message::{KKTRequestData, KKTResponseData}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_kkt::ciphersuite::{Ciphersuite, EncapsulationKey}; -use nym_kkt::context::KKTContext; -use nym_kkt::encryption::KKTSessionSecret; -use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response}; - -/// Creates a KKT request to obtain the responder's KEM public key. -/// -/// This is called by the **client (initiator)** to begin the KKT exchange. -/// The returned context must be used when processing the response. -/// -/// # Arguments -/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms) -/// * `signing_key` - Client's Ed25519 signing key for authentication -/// * `responder_dh_public_key` - Gateway's x25519 public key (from directory) -/// -/// # Returns -/// * `KKTSessionSecret` - Session secret key to encrypt/decrypt KKT messages for this session -/// * `KKTContext` - Context to use when validating the response -/// * `KKTRequestData` - Serialized KKT request frame to send to gateway -/// -/// # Errors -/// Returns `LpError::KKTError` if KKT request generation fails. -pub fn create_request( - ciphersuite: Ciphersuite, - signing_key: &ed25519::PrivateKey, - responder_dh_public_key: &x25519::PublicKey, -) -> Result<(KKTSessionSecret, KKTContext, KKTRequestData), LpError> { - // Note: Uses rand 0.9's thread_rng() to match nym-kkt's rand version - let mut rng = rand09::rng(); - let (session_secret, context, request_bytes) = - request_kem_key(&mut rng, ciphersuite, signing_key, responder_dh_public_key) - .map_err(|e| LpError::KKTError(e.to_string()))?; - - Ok((session_secret, context, KKTRequestData(request_bytes))) -} - -/// Processes a KKT response and extracts the responder's KEM public key. -/// -/// This is called by the **client (initiator)** after receiving a KKT response -/// from the gateway. It verifies the signature and validates the key hash. -/// -/// # Arguments -/// * `context` - Context from the initial `create_request()` call -/// * `session_secret` - The KKT session secret key from the initial `create_request()` call -/// * `responder_vk` - Responder's Ed25519 verification key (from directory) -/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory) -/// * `response_data` - Serialized KKT response frame from responder -/// -/// # Returns -/// * `EncapsulationKey` - Authenticated KEM public key of the responder -/// -/// # Errors -/// Returns `LpError::KKTError` if: -/// - Response deserialization fails -/// - Signature verification fails -/// - Key hash doesn't match expected value -pub fn process_response<'a>( - mut context: KKTContext, - session_secret: &KKTSessionSecret, - responder_vk: &ed25519::PublicKey, - expected_key_hash: &[u8], - response_data: &KKTResponseData, -) -> Result, LpError> { - validate_kem_response( - &mut context, - session_secret, - responder_vk, - expected_key_hash, - &response_data.0, - ) - .map_err(|e| LpError::KKTError(e.to_string())) -} - -/// Handles a KKT request and generates a signed response with the responder's KEM key. -/// -/// This is called by the **gateway (responder)** when receiving a KKT request -/// from a client. It validates the request signature (if authenticated) and -/// responds with the gateway's KEM public key, signed for authenticity. -/// -/// # Arguments -/// * `request_data` - Serialized KKT request frame from initiator -/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous) -/// * `responder_signing_key` - Gateway's Ed25519 signing key -/// * `responder_dh_private_key` - Gateway's x25519 private key -/// * `responder_kem_key` - Gateway's KEM public key to send -/// -/// # Returns -/// * `KKTResponseData` - Signed response frame containing the KEM public key -/// -/// # Errors -/// Returns `LpError::KKTError` if: -/// - Request deserialization fails -/// - Signature verification fails (if authenticated) -/// - Response generation fails -pub fn handle_request<'a>( - request_data: &KKTRequestData, - initiator_vk: Option<&ed25519::PublicKey>, - responder_signing_key: &ed25519::PrivateKey, - responder_dh_private_key: &x25519::PrivateKey, - responder_kem_key: &EncapsulationKey<'a>, -) -> Result { - let mut rng = rand09::rng(); - // Handle the request and generate response - let response_bytes = handle_kem_request( - &mut rng, - &request_data.0, - initiator_vk, - responder_signing_key, - responder_dh_private_key, - responder_kem_key, - ) - .map_err(|e| LpError::KKTError(e.to_string()))?; - - Ok(KKTResponseData(response_bytes)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::peer::mock_peers; - use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme}; - use nym_kkt::key_utils::{ - generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519, - hash_encapsulation_key, - }; - use rand09::RngCore; - - #[test] - fn test_kkt_roundtrip_authenticated() { - let mut rng = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - let responder_x25519 = generate_keypair_x25519(&mut rng); - - // Generate responder's KEM keypair (X25519 for testing) - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - // Create ciphersuite - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Hash the KEM key (simulating directory storage) - let key_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &responder_kem_key.encode(), - ); - - // Client: Create request - let (session_secret, context, request_data) = create_request( - ciphersuite, - initiator_ed25519_keypair.private_key(), - responder_x25519.public_key(), - ) - .unwrap(); - - // Gateway: Handle request - let response_data = handle_request( - &request_data, - Some(initiator_ed25519_keypair.public_key()), - responder_ed25519_keypair.private_key(), - responder_x25519.private_key(), - &responder_kem_key, - ) - .unwrap(); - - // Client: Process response - let obtained_key = process_response( - context, - &session_secret, - responder_ed25519_keypair.public_key(), - &key_hash, - &response_data, - ) - .unwrap(); - - // Verify we got the correct KEM key - assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - } - - // #[test] - // fn test_kkt_roundtrip_anonymous() { - // let mut rng = rand09::rng(); - - // // Only responder has keys (anonymous initiator) - // // Generate Ed25519 keypairs for both parties - - // let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - // let responder_x25519 = generate_keypair_x25519(&mut rng); - - // let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - // let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - // let ciphersuite = Ciphersuite::resolve_ciphersuite( - // KEM::X25519, - // HashFunction::Blake3, - // SignatureScheme::Ed25519, - // None, - // ) - // .unwrap(); - - // let key_hash = hash_encapsulation_key( - // &ciphersuite.hash_function(), - // ciphersuite.hash_len(), - // &responder_kem_key.encode(), - // ); - - // // Anonymous initiator - use anonymous_initiator_process directly - // use nym_kkt::kkt::anonymous_initiator_process; - // let (mut context, request_frame) = - // anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); - // let request_data = KKTRequestData(request_frame.to_bytes()); - - // // Gateway: Handle anonymous request - // let response_data = handle_request( - // &request_data, - // None, - // responder_ed25519_keypair.private_key(), - // &responder_x25519_sk, - // &responder_kem_key, - // ) - // .unwrap(); - - // // Initiator: Validate response - // let obtained_key = initiator_ingest_response( - // &mut context, - // responder_ed25519_keypair.public_key(), - // &key_hash, - // &response_data.0, - // ) - // .unwrap(); - - // assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - // } - - #[test] - fn test_invalid_signature_rejected() { - let mut rng = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - let responder_x25519 = generate_keypair_x25519(&mut rng); - - // Different keypair for wrong signature - let mut wrong_secret = [0u8; 32]; - rng.fill_bytes(&mut wrong_secret); - let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let (_session_secret, _context, request_data) = create_request( - ciphersuite, - initiator_ed25519_keypair.private_key(), - responder_x25519.public_key(), - ) - .unwrap(); - - // Gateway handles request but we provide WRONG verification key - let result = handle_request( - &request_data, - Some(wrong_keypair.public_key()), // Wrong key! - responder_ed25519_keypair.private_key(), - responder_x25519.private_key(), - &responder_kem_key, - ); - - // Should fail signature verification - assert!(result.is_err()); - if let Err(LpError::KKTError(_)) = result { - // Expected - } else { - panic!("Expected KKTError"); - } - } - - #[test] - fn test_hash_mismatch_rejected() { - let (init, resp) = mock_peers(); - let responder_kem_key = resp.encapsulate_kem_key().unwrap(); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Use WRONG hash - let wrong_hash = [0u8; 32]; - - let (session_secret, context, request_data) = create_request( - ciphersuite, - init.ed25519.private_key(), - resp.x25519.public_key(), - ) - .unwrap(); - - let response_data = handle_request( - &request_data, - Some(init.ed25519.public_key()), - resp.ed25519.private_key(), - resp.x25519.private_key(), - &responder_kem_key, - ) - .unwrap(); - - // Client validates with WRONG hash - let result = process_response( - context, - &session_secret, - resp.ed25519.public_key(), - &wrong_hash, // Wrong! - &response_data, - ); - - // Should fail hash validation - assert!(result.is_err()); - if let Err(LpError::KKTError(_)) = result { - // Expected - } else { - panic!("Expected KKTError"); - } - } - - #[test] - fn test_malformed_request_rejected() { - let mut rng = rand09::rng(); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - let responder_x25519 = generate_keypair_x25519(&mut rng); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - // Create malformed request data (invalid bytes) - let malformed_request = KKTRequestData(vec![0xFF; 100]); - - let result = handle_request( - &malformed_request, - None, - responder_ed25519_keypair.private_key(), - responder_x25519.private_key(), - &responder_kem_key, - ); - - // Should fail to parse - assert!(result.is_err()); - if let Err(LpError::KKTError(_)) = result { - // Expected - } else { - panic!("Expected KKTError"); - } - } - - #[test] - fn test_malformed_response_rejected() { - let mut rng = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - let responder_x25519 = generate_keypair_x25519(&mut rng); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let (session_secret, context, _request_data) = create_request( - ciphersuite, - initiator_ed25519_keypair.private_key(), - responder_x25519.public_key(), - ) - .unwrap(); - - // Create malformed response data - let malformed_response = KKTResponseData(vec![0xFF; 100]); - let key_hash = [0u8; 32]; - - let result = process_response( - context, - &session_secret, - responder_ed25519_keypair.public_key(), - &key_hash, - &malformed_response, - ); - - // Should fail to parse - assert!(result.is_err()); - if let Err(LpError::KKTError(_)) = result { - // Expected - } else { - panic!("Expected KKTError"); - } - } -} diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index be5bb991af..7f2c80b404 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -2,32 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 pub mod codec; -pub mod config; pub mod error; -// pub mod kkt_orchestrator; -pub mod message; -pub mod noise_protocol; pub mod packet; pub mod peer; -pub mod psk; -mod psq; +pub mod peer_config; +pub mod psq; pub mod replay; pub mod session; mod session_integration; pub mod session_manager; pub mod state_machine; +pub mod transport; -pub use config::LpConfig; pub use error::LpError; -pub use message::{ClientHelloData, LpMessage}; -pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader}; +pub use nym_kkt_ciphersuite::{ + Ciphersuite, HashFunction, HashLength, KEM, KEMKeyDigests, SignatureScheme, +}; + +#[cfg(any(feature = "mock", test))] pub use replay::{ReceivingKeyCounterValidator, ReplayError}; pub use session::LpSession; pub use session_manager::SessionManager; pub use state_machine::LpStateMachine; -pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256"; -pub const NOISE_PSK_INDEX: u8 = 3; +#[cfg(any(feature = "mock", test))] +use nym_test_utils::helpers::u64_seeded_rng_09; + +#[cfg(any(feature = "mock", test))] +use crate::psq::{PSQ_MSG2_SIZE, initiator, psq_msg1_size, responder}; + +#[cfg(any(feature = "mock", test))] +use crate::session::PersistentSessionBinding; + +#[cfg(any(feature = "mock", test))] +use libcrux_psq::{Channel, IntoSession}; #[cfg(any(feature = "mock", test))] pub struct SessionsMock { @@ -37,118 +45,103 @@ pub struct SessionsMock { #[cfg(any(feature = "mock", test))] impl SessionsMock { - pub fn mock_post_handshake(session_id: u32) -> SessionsMock { + pub fn mock_seeded_post_handshake(seed: u64, kem: KEM) -> SessionsMock { use crate::peer::mock_peers; - use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; + use crate::peer_config::LpReceiverIndex; + use rand09::Rng; let (init, resp) = mock_peers(); let resp_remote = resp.as_remote(); - let init_remote = init.as_remote(); - let salt = [42u8; 32]; - let session_id_bytes = session_id.to_le_bytes(); + + let mut init_rng = u64_seeded_rng_09(seed); + let resp_rng = u64_seeded_rng_09(seed + 1); + + let receiver_index: LpReceiverIndex = init_rng.random(); + + let kem_keys = resp.kem_keypairs.as_ref().unwrap(); // skip KKT by just deriving the kem key locally - let kem_keys = resp.kem_psq.as_ref().unwrap(); + let encapsulation_key = kem_keys.encapsulation_key(kem).unwrap(); + let enc_key = encapsulation_key.clone(); - let libcrux_private_key = libcrux_kem::PrivateKey::decode( - libcrux_kem::Algorithm::X25519, - kem_keys.private_key().as_bytes(), - ) - .unwrap(); - let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key); + let initiator_ciphersuite = + initiator::build_psq_ciphersuite(&init, &resp_remote, &enc_key).unwrap(); + let mut initiator = + initiator::build_psq_principal(init_rng, 1, initiator_ciphersuite).unwrap(); - let libcrux_public_key = libcrux_kem::PublicKey::decode( - libcrux_kem::Algorithm::X25519, - kem_keys.public_key().as_bytes(), - ) - .unwrap(); - let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key); + let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem).unwrap(); + let mut responder = + responder::build_psq_principal(resp_rng, 1, responder_ciphersuite).unwrap(); - // INIT -> RESP: PSQ MSG1 - let psq_initiator = crate::psk::psq_initiator_create_message( - init.x25519.private_key(), - &resp_remote.x25519_public, - &encapsulation_key, - init.ed25519.private_key(), - init.ed25519.public_key(), - &salt, - &session_id_bytes, - ) - .unwrap(); + // run PSQ + let mut payload_buf_responder = vec![0u8; 4096]; + let mut payload_buf_initiator = vec![0u8; 4096]; - let psk = psq_initiator.psk; - let psq_payload = psq_initiator.payload; - let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk); + // Send first message + let mut buf = vec![0u8; psq_msg1_size(kem)]; + let len_i = initiator.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_i, buf.len()); - let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params()) - .local_private_key(init.x25519().private_key().as_bytes()) - .remote_public_key(resp_remote.x25519_public.as_bytes()) - .psk(crate::NOISE_PSK_INDEX, &psk) - .build_initiator() + // Read first message + let (_, _) = responder + .read_message(&buf, &mut payload_buf_responder) .unwrap(); - let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init); - let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap(); - let psq_responder = crate::psk::psq_responder_process_message( - resp.x25519.private_key(), - &init_remote.x25519_public, - (&decapsulation_key, &encapsulation_key), - &init_remote.ed25519_public, - &psq_payload, - &salt, - &session_id_bytes, - ) - .unwrap(); + // Get the authenticator out here, so we can deserialize the session later. + let Some(initiator_authenticator) = responder.initiator_authenticator() else { + panic!("No initiator authenticator found") + }; - let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params()) - .local_private_key(resp.x25519().private_key().as_bytes()) - .remote_public_key(init_remote.x25519_public.as_bytes()) - .psk(crate::NOISE_PSK_INDEX, &psk) - .build_responder() + // Respond + let mut buf = [0u8; PSQ_MSG2_SIZE]; + let len_r = responder.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_r, buf.len()); + + // Finalize on registration initiator + let (_, _) = initiator + .read_message(&buf, &mut payload_buf_initiator) .unwrap(); - let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp); - noise_protocol_resp.read_message(&noise_msg1).unwrap(); - let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap(); - noise_protocol_init.read_message(&noise_msg2).unwrap(); - let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap(); + assert!(initiator.is_handshake_finished()); + assert!(responder.is_handshake_finished()); - assert!(noise_protocol_init.is_handshake_finished()); - - noise_protocol_resp.read_message(&noise_msg3).unwrap(); - assert!(noise_protocol_resp.is_handshake_finished()); + let binding = PersistentSessionBinding { + initiator_authenticator, + responder_ecdh_pk: resp_remote.x25519_public, + responder_pq_pk: Some(encapsulation_key), + }; SessionsMock { initiator: LpSession::new( - session_id, + initiator.into_session().unwrap(), + binding.clone(), + receiver_index, 1, - outer_aead_key.clone(), - init, - resp_remote, - crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret), - noise_protocol_init, - ), + ) + .unwrap(), responder: LpSession::new( - session_id, + responder.into_session().unwrap(), + binding, + receiver_index, 1, - outer_aead_key, - resp, - init_remote, - crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret), - noise_protocol_resp, - ), + ) + .unwrap(), } } + pub fn mock_post_handshake(kem: KEM) -> SessionsMock { + Self::mock_seeded_post_handshake(1, kem) + } + // we just need a dummy 'valid' session for simpler tests pub fn mock_initiator() -> LpSession { - Self::mock_post_handshake(1234).initiator + Self::mock_post_handshake(KEM::default()).initiator } } #[cfg(any(feature = "mock", test))] pub fn sessions_for_tests() -> (LpSession, LpSession) { - let sessions = SessionsMock::mock_post_handshake(69); + let sessions = SessionsMock::mock_post_handshake(KEM::default()); (sessions.initiator, sessions.responder) } @@ -156,224 +149,3 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) { pub fn mock_session_for_test() -> LpSession { SessionsMock::mock_initiator() } - -#[cfg(test)] -mod tests { - use crate::message::LpMessage; - use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; - use crate::session_manager::SessionManager; - use crate::{LpError, SessionsMock, mock_session_for_test}; - use bytes::BytesMut; - - // Import the new standalone functions - use crate::codec::{parse_lp_packet, serialize_lp_packet}; - - #[test] - fn test_replay_protection_integration() { - // Create session - let mut session = mock_session_for_test(); - - // === Packet 1 (Counter 0 - Should succeed) === - let packet1 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, // Matches session's sending_index assumption for this test - counter: 0, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize packet - let mut buf1 = BytesMut::new(); - serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); - - // Parse packet - let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); - - // Perform replay check (should pass) - session - .receiving_counter_quick_check(parsed_packet1.header.counter) - .expect("Initial packet failed replay check"); - - // Mark received (simulating successful processing) - session - .receiving_counter_mark(parsed_packet1.header.counter) - .expect("Failed to mark initial packet received"); - - // === Packet 2 (Counter 0 - Replay, should fail check) === - let packet2 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 0, // Same counter as before (replay) - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize packet - let mut buf2 = BytesMut::new(); - serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); - - // Parse packet - let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); - - // Perform replay check (should fail) - let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter); - assert!(replay_result.is_err()); - match replay_result.unwrap_err() { - LpError::Replay(e) => { - assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); - } - e => panic!("Expected replay error, got {:?}", e), - } - // Do not mark received as it failed validation - - // === Packet 3 (Counter 1 - Should succeed) === - let packet3 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 1, // Incremented counter - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize packet - let mut buf3 = BytesMut::new(); - serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); - - // Parse packet - let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); - - // Perform replay check (should pass) - session - .receiving_counter_quick_check(parsed_packet3.header.counter) - .expect("Packet 3 failed replay check"); - - // Mark received - session - .receiving_counter_mark(parsed_packet3.header.counter) - .expect("Failed to mark packet 3 received"); - - // Verify validator state directly on the session - let state = session.current_packet_cnt(); - assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2) - assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3) - } - - #[test] - fn test_session_manager_integration() { - // Create session manager - let mut local_manager = SessionManager::new(); - let mut remote_manager = SessionManager::new(); - - // Use fixed receiver_index for deterministic test - let receiver_index: u32 = 54321; - - let sessions = SessionsMock::mock_post_handshake(receiver_index); - let local_session = sessions.initiator; - let remote_session = sessions.responder; - - // Create a session via manager - let _ = local_manager.create_session_state_machine(local_session); - let _ = remote_manager.create_session_state_machine(remote_session); - - // === Packet 1 (Counter 0 - Should succeed) === - let packet1 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 0, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize - let mut buf1 = BytesMut::new(); - serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); - - // Parse - let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); - - // Process via SessionManager method (which should handle checks + marking) - // NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet` - // that encapsulates parse -> check -> process_noise -> mark. - // For now, we simulate the steps using the retrieved session. - - // Perform replay check - local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter) - .expect("Packet 1 check failed"); - // Mark received - local_manager - .receiving_counter_mark(receiver_index, parsed_packet1.header.counter) - .expect("Packet 1 mark failed"); - - // === Packet 2 (Counter 1 - Should succeed on same session) === - let packet2 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 1, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize - let mut buf2 = BytesMut::new(); - serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); - - // Parse - let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); - - // Perform replay check - local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter) - .expect("Packet 2 check failed"); - // Mark received - local_manager - .receiving_counter_mark(receiver_index, parsed_packet2.header.counter) - .expect("Packet 2 mark failed"); - - // === Packet 3 (Counter 0 - Replay, should fail check) === - let packet3 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 0, // Replay of first packet - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize - let mut buf3 = BytesMut::new(); - serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); - - // Parse - let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); - - // Perform replay check (should fail) - let replay_result = local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter); - assert!(replay_result.is_err()); - match replay_result.unwrap_err() { - LpError::Replay(e) => { - assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); - } - e => panic!("Expected replay error for packet 3, got {:?}", e), - } - // Do not mark received - } -} diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs deleted file mode 100644 index fbd692175b..0000000000 --- a/common/nym-lp/src/message.rs +++ /dev/null @@ -1,892 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::packet::LpHeader; -use crate::peer::LpRemotePeer; -use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket}; -use bytes::{BufMut, BytesMut}; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use rand::RngCore; -use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display}; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -/// Data structure for the ClientHello message -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] -pub struct ClientHelloData { - /// Client-proposed receiver index for session identification (4 bytes) - /// Auto-generated randomly by the client - pub receiver_index: u32, - /// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key - pub client_lp_public_key: x25519::PublicKey, - /// Client's Ed25519 public key (32 bytes) - for PSQ authentication - pub client_ed25519_public_key: ed25519::PublicKey, - /// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce) - pub salt: [u8; 32], -} - -impl ClientHelloData { - // 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt - pub const LEN: usize = 100; - - pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket { - LpPacket::new( - LpHeader::new( - BOOTSTRAP_RECEIVER_IDX, // session_id not yet established - 0, // counter starts at 0 - protocol_version, - ), - LpMessage::ClientHello(self), - ) - } - - fn len(&self) -> usize { - Self::LEN - } - - fn generate_receiver_index() -> u32 { - loop { - let candidate = rand::random(); - if candidate != BOOTSTRAP_RECEIVER_IDX { - return candidate; - } - } - } - - /// Generates a new ClientHelloData with fresh salt. - /// - /// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce - /// - /// # Arguments - /// * `client_lp_public_key` - Client's x25519 public key (derived from Ed25519) - /// * `client_ed25519_public_key` - Client's Ed25519 public key (for PSQ authentication) - pub fn new_with_fresh_salt( - client_lp_public_key: x25519::PublicKey, - client_ed25519_public_key: ed25519::PublicKey, - timestamp: u64, - ) -> Self { - // Generate salt: timestamp + nonce - let mut salt = [0u8; 32]; - - // First 8 bytes: current timestamp as u64 little-endian - salt[..8].copy_from_slice(×tamp.to_le_bytes()); - - // Last 24 bytes: random nonce - rand::thread_rng().fill_bytes(&mut salt[8..]); - - Self { - receiver_index: Self::generate_receiver_index(), // Auto-generate random receiver index - client_lp_public_key, - client_ed25519_public_key, - salt, - } - } - - /// Extracts the timestamp from the salt. - /// - /// # Returns - /// Unix timestamp in seconds - pub fn extract_timestamp(&self) -> u64 { - let mut timestamp_bytes = [0u8; 8]; - timestamp_bytes.copy_from_slice(&self.salt[..8]); - u64::from_le_bytes(timestamp_bytes) - } - - pub fn encode(&self, dst: &mut BytesMut) { - dst.put_u32_le(self.receiver_index); - dst.put_slice(self.client_lp_public_key.as_bytes()); - dst.put_slice(self.client_ed25519_public_key.as_bytes()); - dst.put_slice(&self.salt); - } - - pub fn decode(b: &[u8]) -> Result { - if b.len() != Self::LEN { - return Err(LpError::DeserializationError(format!( - "Expected {} bytes to deserialise ClientHelloData. got {}", - Self::LEN, - b.len() - ))); - } - - // SAFETY: we checked for valid byte lengths - #[allow(clippy::unwrap_used)] - let client_lp_public_key_bytes = b[4..36].try_into().unwrap(); - let client_ed25519_public_key_bytes = b[36..68].try_into().unwrap(); - - Ok(ClientHelloData { - receiver_index: u32::from_le_bytes([b[0], b[1], b[2], b[3]]), - client_lp_public_key: x25519::PublicKey::from_byte_array(client_lp_public_key_bytes), - client_ed25519_public_key: ed25519::PublicKey::from_byte_array( - client_ed25519_public_key_bytes, - )?, - salt: b[68..].try_into().unwrap(), - }) - } - - /// Attempt to construct remote peer information based on the data provided in this packet. - pub fn to_remote_peer(&self) -> LpRemotePeer { - LpRemotePeer::new(self.client_ed25519_public_key, self.client_lp_public_key) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] -#[repr(u32)] -pub enum MessageType { - Busy = 0x0000, - Handshake = 0x0001, - EncryptedData = 0x0002, - ClientHello = 0x0003, - KKTRequest = 0x0004, - KKTResponse = 0x0005, - ForwardPacket = 0x0006, - /// Receiver index collision - client should retry with new index - Collision = 0x0007, - /// Acknowledgment - gateway confirms receipt of message - Ack = 0x0008, - /// Subsession request - client initiates subsession creation - SubsessionRequest = 0x0009, - /// Subsession KK1 - first message of Noise KK handshake - SubsessionKK1 = 0x000A, - /// Subsession KK2 - second message of Noise KK handshake - SubsessionKK2 = 0x000B, - /// Subsession ready - subsession established confirmation - SubsessionReady = 0x000C, - /// Subsession abort - race winner tells loser to become responder - SubsessionAbort = 0x000D, - /// General error - Error = 0x00FF, -} - -impl MessageType { - pub(crate) fn from_u32(value: u32) -> Option { - MessageType::try_from(value).ok() - } - - pub fn to_u32(&self) -> u32 { - u32::from(*self) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HandshakeData(pub Vec); - -impl HandshakeData { - pub(crate) fn new(bytes: Vec) -> Self { - Self(bytes) - } - fn len(&self) -> usize { - self.0.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.0); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(HandshakeData(bytes.to_vec())) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EncryptedDataPayload(pub Vec); - -impl EncryptedDataPayload { - #[allow(dead_code)] - pub(crate) fn new(bytes: Vec) -> Self { - Self(bytes) - } - - fn len(&self) -> usize { - self.0.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.0); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(EncryptedDataPayload(bytes.to_vec())) - } -} - -/// KKT request frame data (serialized KKTFrame bytes) -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct KKTRequestData(pub Vec); - -impl KKTRequestData { - pub(crate) fn new(bytes: Vec) -> Self { - Self(bytes) - } - - fn len(&self) -> usize { - self.0.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.0); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(KKTRequestData(bytes.to_vec())) - } -} - -/// KKT response frame data (serialized KKTFrame bytes) -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct KKTResponseData(pub Vec); - -impl KKTResponseData { - pub(crate) fn new(bytes: Vec) -> Self { - Self(bytes) - } - - fn len(&self) -> usize { - self.0.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.0); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(KKTResponseData(bytes.to_vec())) - } -} - -/// General human-readable error message -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ErrorPacketData { - pub message: String, -} - -impl ErrorPacketData { - pub(crate) fn new(message: impl Into) -> Self { - ErrorPacketData { - message: message.into(), - } - } - - fn len(&self) -> usize { - // length-encoding + message - 4 + self.message.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_u32_le(self.message.len() as u32); - dst.put_slice(self.message.as_bytes()); - } - - fn decode(bytes: &[u8]) -> Result { - if bytes.len() < 4 { - return Err(LpError::DeserializationError(format!( - "Too few bytes to deserialise ErrorPacketData. got {}", - bytes.len() - ))); - } - - let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; - if bytes[4..].len() != message_len { - return Err(LpError::DeserializationError(format!( - "Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}", - bytes.len(), - 4 + message_len - ))); - } - - let message = String::from_utf8_lossy(&bytes[4..]).to_string(); - - Ok(ErrorPacketData { message }) - } -} - -/// Packet forwarding request with embedded inner LP packet -#[derive(Debug, Clone)] -pub struct ForwardPacketData { - /// Target gateway's Ed25519 identity (32 bytes) - pub target_gateway_identity: [u8; 32], - - /// Target gateway's LP address (IP:port string) - pub target_lp_address: SocketAddr, - - /// Complete inner LP packet bytes (serialized LpPacket) - /// This is the CLIENT→EXIT gateway packet, encrypted for exit - pub inner_packet_bytes: Vec, -} - -impl ForwardPacketData { - pub fn new( - target_gateway_identity: ed25519::PublicKey, - target_lp_address: SocketAddr, - inner_packet_bytes: Vec, - ) -> Self { - ForwardPacketData { - target_gateway_identity: target_gateway_identity.to_bytes(), - target_lp_address, - inner_packet_bytes, - } - } - - fn len(&self) -> usize { - // 32 bytes target gateway identity - // + - // 1 byte length of target lp address type - // + - // {4,16} target_lp_address IPv{4,6} - // + - // 2 bytes target_lp_address port - // + - // 4 bytes of length of inner packet bytes - // + - // inner_packet_bytes.len() - match self.target_lp_address { - SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(), - SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(), - } - } - - fn encode(&self, dst: &mut BytesMut) { - let (is_ipv6, ip_bytes) = match &self.target_lp_address { - SocketAddr::V4(address) => (false, address.ip().octets().to_vec()), - SocketAddr::V6(address) => (true, address.ip().octets().to_vec()), - }; - - dst.put_slice(&self.target_gateway_identity); - dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4 - dst.put_slice(&ip_bytes); // IP bytes - dst.put_u16_le(self.target_lp_address.port()); // Port - dst.put_u32_le(self.inner_packet_bytes.len() as u32); - dst.put_slice(&self.inner_packet_bytes); - } - - pub fn to_bytes(&self) -> Vec { - let mut buf = BytesMut::new(); - self.encode(&mut buf); - buf.into() - } - - pub fn decode(bytes: &[u8]) -> Result { - // smallest possible packet with ipv4 and empty data - if bytes.len() < 43 { - // 32 + 1 + 4 + 2 + 4 + 0 - return Err(LpError::DeserializationError(format!( - "Too few bytes to deserialise ForwardPacketData. got {}", - bytes.len() - ))); - } - // SAFETY: we ensured we have sufficient data - #[allow(clippy::unwrap_used)] - let target_gateway_identity = bytes[0..32].try_into().unwrap(); - let target_lp_address_is_ipv6 = bytes[32] != 0; - - let (target_lp_address, next_index) = if target_lp_address_is_ipv6 { - // IPv6, first check we have actually enough bytes - // smallest possible packet with ipv6 and empty data - if bytes.len() < 55 { - // 32 + 1 + 16 + 2 + 4 + 0 - return Err(LpError::DeserializationError(format!( - "Too few bytes to deserialise ipv6 ForwardPacketData. got {}", - bytes.len() - ))); - } - // SAFETY: we ensured we have sufficient data, and the length is correct for casting - #[allow(clippy::unwrap_used)] - let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap())); - let port = u16::from_le_bytes([bytes[49], bytes[50]]); - (SocketAddr::new(ipv6, port), 51) - } else { - // IPv4. Length check done at the start - // SAFETY: we ensured we have sufficient data, and the length is correct for casting - #[allow(clippy::unwrap_used)] - let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap())); - let port = u16::from_le_bytes([bytes[37], bytes[38]]); - (SocketAddr::new(ipv4, port), 39) - }; - - let inner_packet_bytes_len = u32::from_le_bytes([ - bytes[next_index], - bytes[next_index + 1], - bytes[next_index + 2], - bytes[next_index + 3], - ]); - if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize { - return Err(LpError::DeserializationError(format!( - "Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}", - bytes[next_index + 4..].len() - ))); - } - let inner_packet_bytes = bytes[next_index + 4..].to_vec(); - - Ok(ForwardPacketData { - target_gateway_identity, - target_lp_address, - inner_packet_bytes, - }) - } -} - -/// Subsession KK1 message - first message of Noise KK handshake -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubsessionKK1Data { - /// Noise KK first message payload (ephemeral key + encrypted static) - pub payload: Vec, -} - -impl SubsessionKK1Data { - fn len(&self) -> usize { - self.payload.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.payload); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(SubsessionKK1Data { - payload: bytes.to_vec(), - }) - } -} - -/// Subsession KK2 message - second message of Noise KK handshake -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubsessionKK2Data { - /// Noise KK second message payload (ephemeral key + encrypted response) - pub payload: Vec, -} - -impl SubsessionKK2Data { - fn len(&self) -> usize { - self.payload.len() - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_slice(&self.payload); - } - - fn decode(bytes: &[u8]) -> Result { - Ok(SubsessionKK2Data { - payload: bytes.to_vec(), - }) - } -} - -/// Subsession ready confirmation with new session index -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubsessionReadyData { - /// New subsession's receiver index for routing - pub receiver_index: u32, -} - -impl SubsessionReadyData { - pub const LEN: usize = 4; - - fn len(&self) -> usize { - Self::LEN - } - - fn encode(&self, dst: &mut BytesMut) { - dst.put_u32_le(self.receiver_index); - } - - fn decode(bytes: &[u8]) -> Result { - if bytes.len() != 4 { - return Err(LpError::DeserializationError(format!( - "Expected 4 bytes to deserialise SubsessionReadyData. got {}", - bytes.len() - ))); - } - Ok(SubsessionReadyData { - receiver_index: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - }) - } -} - -#[derive(Debug, Clone)] -pub enum LpMessage { - Busy, - Handshake(HandshakeData), - EncryptedData(EncryptedDataPayload), - ClientHello(ClientHelloData), - KKTRequest(KKTRequestData), - KKTResponse(KKTResponseData), - ForwardPacket(ForwardPacketData), - /// Receiver index collision - client should retry with new receiver_index - Collision, - /// Acknowledgment - gateway confirms receipt of message - Ack, - /// Subsession request - client initiates subsession creation (empty, signal only) - SubsessionRequest, - /// Subsession KK1 - first message of Noise KK handshake - SubsessionKK1(SubsessionKK1Data), - /// Subsession KK2 - second message of Noise KK handshake - SubsessionKK2(SubsessionKK2Data), - /// Subsession ready - subsession established confirmation - SubsessionReady(SubsessionReadyData), - /// Subsession abort - race winner tells loser to become responder (empty, signal only) - SubsessionAbort, - /// An error has occurred - Error(ErrorPacketData), -} - -impl From for LpMessage { - fn from(value: HandshakeData) -> Self { - LpMessage::Handshake(value) - } -} - -impl From for LpMessage { - fn from(value: EncryptedDataPayload) -> Self { - LpMessage::EncryptedData(value) - } -} - -impl From for LpMessage { - fn from(value: ClientHelloData) -> Self { - LpMessage::ClientHello(value) - } -} - -impl From for LpMessage { - fn from(value: KKTRequestData) -> Self { - LpMessage::KKTRequest(value) - } -} - -impl From for LpMessage { - fn from(value: KKTResponseData) -> Self { - LpMessage::KKTResponse(value) - } -} - -impl From for LpMessage { - fn from(value: ForwardPacketData) -> Self { - LpMessage::ForwardPacket(value) - } -} - -impl From for LpMessage { - fn from(value: SubsessionKK1Data) -> Self { - LpMessage::SubsessionKK1(value) - } -} - -impl From for LpMessage { - fn from(value: SubsessionKK2Data) -> Self { - LpMessage::SubsessionKK2(value) - } -} - -impl From for LpMessage { - fn from(value: SubsessionReadyData) -> Self { - LpMessage::SubsessionReady(value) - } -} - -impl Display for LpMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LpMessage::Busy => write!(f, "Busy"), - LpMessage::Handshake(_) => write!(f, "Handshake"), - LpMessage::EncryptedData(_) => write!(f, "EncryptedData"), - LpMessage::ClientHello(_) => write!(f, "ClientHello"), - LpMessage::KKTRequest(_) => write!(f, "KKTRequest"), - LpMessage::KKTResponse(_) => write!(f, "KKTResponse"), - LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"), - LpMessage::Collision => write!(f, "Collision"), - LpMessage::Ack => write!(f, "Ack"), - LpMessage::SubsessionRequest => write!(f, "SubsessionRequest"), - LpMessage::SubsessionKK1(_) => write!(f, "SubsessionKK1"), - LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"), - LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"), - LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"), - LpMessage::Error(_) => write!(f, "Error"), - } - } -} - -impl LpMessage { - pub fn payload(&self) -> &[u8] { - match self { - LpMessage::Busy => &[], - LpMessage::Handshake(payload) => payload.0.as_slice(), - LpMessage::EncryptedData(payload) => payload.0.as_slice(), - LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content - LpMessage::KKTRequest(payload) => payload.0.as_slice(), - LpMessage::KKTResponse(payload) => payload.0.as_slice(), - LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content - LpMessage::Collision => &[], - LpMessage::Ack => &[], - LpMessage::SubsessionRequest => &[], - LpMessage::SubsessionKK1(_) => &[], // Structured data, serialized in encode_content - LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content - LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content - LpMessage::SubsessionAbort => &[], - LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?) - } - } - - pub fn is_empty(&self) -> bool { - match self { - LpMessage::Busy => true, - LpMessage::Handshake(payload) => payload.0.is_empty(), - LpMessage::EncryptedData(payload) => payload.0.is_empty(), - LpMessage::ClientHello(_) => false, // Always has data - LpMessage::KKTRequest(payload) => payload.0.is_empty(), - LpMessage::KKTResponse(payload) => payload.0.is_empty(), - LpMessage::ForwardPacket(_) => false, // Always has data - LpMessage::Collision => true, - LpMessage::Ack => true, - LpMessage::SubsessionRequest => true, // Empty signal - LpMessage::SubsessionKK1(_) => false, // Always has payload - LpMessage::SubsessionKK2(_) => false, // Always has payload - LpMessage::SubsessionReady(_) => false, // Always has receiver_index - LpMessage::SubsessionAbort => true, // Empty signal - LpMessage::Error(_) => false, - } - } - - pub fn len(&self) -> usize { - match self { - LpMessage::Busy => 0, - LpMessage::Handshake(payload) => payload.len(), - LpMessage::EncryptedData(payload) => payload.len(), - LpMessage::ClientHello(payload) => payload.len(), - LpMessage::KKTRequest(payload) => payload.len(), - LpMessage::KKTResponse(payload) => payload.len(), - LpMessage::ForwardPacket(payload) => payload.len(), - LpMessage::Collision => 0, - LpMessage::Ack => 0, - LpMessage::SubsessionRequest => 0, - LpMessage::SubsessionKK1(payload) => payload.len(), - LpMessage::SubsessionKK2(payload) => payload.len(), - LpMessage::SubsessionReady(payload) => payload.len(), - LpMessage::SubsessionAbort => 0, - LpMessage::Error(payload) => payload.len(), - } - } - - pub fn typ(&self) -> MessageType { - match self { - LpMessage::Busy => MessageType::Busy, - LpMessage::Handshake(_) => MessageType::Handshake, - LpMessage::EncryptedData(_) => MessageType::EncryptedData, - LpMessage::ClientHello(_) => MessageType::ClientHello, - LpMessage::KKTRequest(_) => MessageType::KKTRequest, - LpMessage::KKTResponse(_) => MessageType::KKTResponse, - LpMessage::ForwardPacket(_) => MessageType::ForwardPacket, - LpMessage::Collision => MessageType::Collision, - LpMessage::Ack => MessageType::Ack, - LpMessage::SubsessionRequest => MessageType::SubsessionRequest, - LpMessage::SubsessionKK1(_) => MessageType::SubsessionKK1, - LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2, - LpMessage::SubsessionReady(_) => MessageType::SubsessionReady, - LpMessage::SubsessionAbort => MessageType::SubsessionAbort, - LpMessage::Error(_) => MessageType::Error, - } - } - - pub fn encode_content(&self, dst: &mut BytesMut) { - match self { - LpMessage::Busy => { /* No content */ } - LpMessage::Handshake(payload) => payload.encode(dst), - LpMessage::EncryptedData(payload) => payload.encode(dst), - LpMessage::ClientHello(data) => data.encode(dst), - LpMessage::KKTRequest(payload) => payload.encode(dst), - LpMessage::KKTResponse(payload) => payload.encode(dst), - LpMessage::ForwardPacket(data) => data.encode(dst), - LpMessage::Collision => { /* No content */ } - LpMessage::Ack => { /* No content */ } - LpMessage::SubsessionRequest => { /* No content - signal only */ } - LpMessage::SubsessionKK1(data) => data.encode(dst), - LpMessage::SubsessionKK2(data) => data.encode(dst), - LpMessage::SubsessionReady(data) => data.encode(dst), - LpMessage::SubsessionAbort => { /* No content - signal only */ } - LpMessage::Error(data) => data.encode(dst), - } - } - - /// Parse message from its type and content bytes. - /// - /// Used when decrypting outer-encrypted packets where the message type - /// was encrypted along with the content. - pub fn decode_content(content: &[u8], message_type: MessageType) -> Result { - match message_type { - MessageType::Busy => { - content.ensure_empty()?; - Ok(LpMessage::Busy) - } - MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData::decode(content)?)), - MessageType::EncryptedData => Ok(LpMessage::EncryptedData( - EncryptedDataPayload::decode(content)?, - )), - MessageType::ClientHello => { - Ok(LpMessage::ClientHello(ClientHelloData::decode(content)?)) - } - MessageType::KKTRequest => Ok(LpMessage::KKTRequest(KKTRequestData::decode(content)?)), - MessageType::KKTResponse => { - Ok(LpMessage::KKTResponse(KKTResponseData::decode(content)?)) - } - MessageType::ForwardPacket => Ok(LpMessage::ForwardPacket(ForwardPacketData::decode( - content, - )?)), - MessageType::Collision => { - content.ensure_empty()?; - Ok(LpMessage::Collision) - } - MessageType::Ack => { - content.ensure_empty()?; - Ok(LpMessage::Ack) - } - MessageType::SubsessionRequest => { - content.ensure_empty()?; - Ok(LpMessage::SubsessionRequest) - } - MessageType::SubsessionKK1 => Ok(LpMessage::SubsessionKK1(SubsessionKK1Data::decode( - content, - )?)), - MessageType::SubsessionKK2 => Ok(LpMessage::SubsessionKK2(SubsessionKK2Data::decode( - content, - )?)), - MessageType::SubsessionReady => Ok(LpMessage::SubsessionReady( - SubsessionReadyData::decode(content)?, - )), - MessageType::SubsessionAbort => { - content.ensure_empty()?; - Ok(LpMessage::SubsessionAbort) - } - MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)), - } - } -} - -/// Helper trait for improving readability to return error if bytes content is not empty -trait EnsureEmptyContent { - fn ensure_empty(&self) -> Result<(), LpError>; -} - -impl EnsureEmptyContent for &[u8] { - fn ensure_empty(&self) -> Result<(), LpError> { - if !self.is_empty() { - return Err(LpError::InvalidPayloadSize { - expected: 0, - actual: self.len(), - }); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::time::{SystemTime, UNIX_EPOCH}; - - use super::*; - use crate::LpPacket; - use crate::packet::{LpHeader, TRAILER_LEN}; - - #[test] - fn encoding() { - let message = LpMessage::EncryptedData(EncryptedDataPayload(vec![11u8; 124])); - - let resp_header = LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 0, - counter: 0, - }; - - let packet = LpPacket { - header: resp_header, - message, - trailer: [80; TRAILER_LEN], - }; - - // Just print packet for debug, will be captured in test output - println!("{packet:?}"); - - // Verify message type - assert!(matches!(packet.message.typ(), MessageType::EncryptedData)); - - // Verify correct data in message - match &packet.message { - LpMessage::EncryptedData(data) => { - assert_eq!(*data, EncryptedDataPayload(vec![11u8; 124])); - } - _ => panic!("Wrong message type"), - } - } - - #[test] - fn test_client_hello_salt_generation() { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - let mut rng = rand::thread_rng(); - let ed25519 = ed25519::KeyPair::new(&mut rng); - let x25519 = ed25519.to_x25519(); - - let client_key = *x25519.public_key(); - let client_ed25519_key = *ed25519.public_key(); - let hello1 = - ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - let hello2 = - ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - - // Different salts should be generated - assert_ne!(hello1.salt, hello2.salt); - - // But timestamps should be very close (within 1 second) - let ts1 = hello1.extract_timestamp(); - let ts2 = hello2.extract_timestamp(); - assert!((ts1 as i64 - ts2 as i64).abs() <= 1); - } - - #[test] - fn test_client_hello_timestamp_extraction() { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - let mut rng = rand::thread_rng(); - let ed25519 = ed25519::KeyPair::new(&mut rng); - let x25519 = ed25519.to_x25519(); - - let client_key = *x25519.public_key(); - let client_ed25519_key = *ed25519.public_key(); - let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - - let timestamp = hello.extract_timestamp(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - - // Timestamp should be within 1 second of now - assert!((timestamp as i64 - now as i64).abs() <= 1); - } - - #[test] - fn test_client_hello_salt_format() { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - let mut rng = rand::thread_rng(); - let ed25519 = ed25519::KeyPair::new(&mut rng); - let x25519 = ed25519.to_x25519(); - - let client_key = *x25519.public_key(); - let client_ed25519_key = *ed25519.public_key(); - let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - - // First 8 bytes should be non-zero timestamp - let timestamp_bytes = &hello.salt[..8]; - assert_ne!(timestamp_bytes, &[0u8; 8]); - - // Salt should be 32 bytes total - assert_eq!(hello.salt.len(), 32); - } -} diff --git a/common/nym-lp/src/noise_protocol.rs b/common/nym-lp/src/noise_protocol.rs deleted file mode 100644 index c56f7cd9b9..0000000000 --- a/common/nym-lp/src/noise_protocol.rs +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Sans-IO Noise protocol state machine, adapted from noise-psq. - -use snow::{TransportState, params::NoiseParams}; -use thiserror::Error; - -// --- Error Definition --- - -/// Errors related to the Noise protocol state machine. -#[derive(Error, Debug)] -pub enum NoiseError { - #[error("encountered a Noise decryption error")] - DecryptionError, - - #[error("encountered a Noise Protocol error - {0}")] - ProtocolError(snow::Error), - - #[error("operation is invalid in the current protocol state")] - IncorrectStateError, - - #[error("attempted transport mode operation without real PSK injection")] - PskNotInjected, - - #[error("Other Noise-related error: {0}")] - Other(String), - - #[error("session is read-only after demotion")] - SessionReadOnly, -} - -impl From for NoiseError { - fn from(err: snow::Error) -> Self { - match err { - snow::Error::Decrypt => NoiseError::DecryptionError, - err => NoiseError::ProtocolError(err), - } - } -} - -// --- Protocol State and Structs --- - -/// Represents the possible states of the Noise protocol machine. -#[derive(Debug)] -pub enum NoiseProtocolState { - /// The protocol is currently performing the handshake. - /// Contains the Snow handshake state. - Handshaking(Box), - - /// The handshake is complete, and the protocol is in transport mode. - /// Contains the Snow transport state. - Transport(TransportState), - - /// The protocol has encountered an unrecoverable error. - /// Stores the error description. - Failed(String), -} - -/// The core sans-io Noise protocol state machine. -#[derive(Debug)] -pub struct NoiseProtocol { - state: NoiseProtocolState, - // We might need buffers for incoming/outgoing data later if we add internal buffering - // read_buffer: Vec, - // write_buffer: Vec, -} - -/// Represents the outcome of processing received bytes via `read_message`. -#[derive(Debug, PartialEq)] -pub enum ReadResult { - /// A handshake or transport message was successfully processed, but yielded no application data - /// and did not complete the handshake. - NoOp, - /// A complete application data message was decrypted. - DecryptedData(Vec), - /// The handshake successfully completed during this read operation. - HandshakeComplete, - // NOTE: NeedMoreBytes variant removed as read_message expects full frames. -} - -// --- Implementation --- - -impl NoiseProtocol { - pub fn params() -> NoiseParams { - // SAFETY: the hardcoded pattern must be valid - // and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery - #[allow(clippy::unwrap_used)] - crate::NOISE_PATTERN.parse().unwrap() - } - - /// Creates a new `NoiseProtocol` instance in the Handshaking state. - /// - /// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`). - pub fn new(initial_state: snow::HandshakeState) -> Self { - NoiseProtocol { - state: NoiseProtocolState::Handshaking(Box::new(initial_state)), - } - } - - fn prepare_handshake_state<'a>( - local_private_key: &'a [u8], - remote_public_key: &'a [u8], - psk: &'a [u8], - ) -> snow::Builder<'a> { - let psk_index = crate::NOISE_PSK_INDEX; - let noise_params = NoiseProtocol::params(); - - snow::Builder::new(noise_params) - .local_private_key(local_private_key) - .remote_public_key(remote_public_key) - .psk(psk_index, psk) - } - - /// Builds a new `NoiseProtocol` initiator instance with the provided local private key, - /// remote public key and psk - pub fn build_new_initiator( - local_private_key: &[u8], - remote_public_key: &[u8], - psk: &[u8], - ) -> Result { - let handshake_state = - Self::prepare_handshake_state(local_private_key, remote_public_key, psk) - .build_initiator()?; - Ok(Self::new(handshake_state)) - } - - /// Builds a new `NoiseProtocol` responder instance with the provided local private key, - /// remote public key and psk - pub fn build_new_responder( - local_private_key: &[u8], - remote_public_key: &[u8], - psk: &[u8], - ) -> Result { - let handshake_state = - Self::prepare_handshake_state(local_private_key, remote_public_key, psk) - .build_responder()?; - Ok(Self::new(handshake_state)) - } - - /// Processes a single, complete incoming Noise message frame. - /// - /// Assumes the caller handles buffering and framing to provide one full message. - /// Returns the result of processing the message. - pub fn read_message(&mut self, input: &[u8]) -> Result { - // Allocate a buffer large enough for the maximum possible Noise message size. - // TODO: Consider reusing a buffer for efficiency. - let mut buffer = vec![0u8; 65535]; // Max Noise message size - - match &mut self.state { - NoiseProtocolState::Handshaking(handshake_state) => { - match handshake_state.read_message(input, &mut buffer) { - Ok(_) => { - if handshake_state.is_handshake_finished() { - // Transition to Transport state. - let current_state = std::mem::replace( - &mut self.state, - // Temporary placeholder needed for mem::replace - NoiseProtocolState::Failed( - NoiseError::IncorrectStateError.to_string(), - ), - ); - if let NoiseProtocolState::Handshaking(state_to_convert) = current_state - { - match state_to_convert.into_transport_mode() { - Ok(transport_state) => { - self.state = NoiseProtocolState::Transport(transport_state); - Ok(ReadResult::HandshakeComplete) - } - Err(e) => { - let err = NoiseError::from(e); - self.state = NoiseProtocolState::Failed(err.to_string()); - Err(err) - } - } - } else { - // Should be unreachable - let err = NoiseError::IncorrectStateError; - self.state = NoiseProtocolState::Failed(err.to_string()); - Err(err) - } - } else { - // Handshake continues - Ok(ReadResult::NoOp) - } - } - Err(e) => { - let err = NoiseError::from(e); - self.state = NoiseProtocolState::Failed(err.to_string()); - Err(err) - } - } - } - NoiseProtocolState::Transport(transport_state) => { - match transport_state.read_message(input, &mut buffer) { - Ok(len) => Ok(ReadResult::DecryptedData(buffer[..len].to_vec())), - Err(e) => { - let err = NoiseError::from(e); - self.state = NoiseProtocolState::Failed(err.to_string()); - Err(err) - } - } - } - NoiseProtocolState::Failed(_) => Err(NoiseError::IncorrectStateError), - } - } - - /// Checks if there are pending handshake messages to send. - /// - /// If in Handshaking state and it's our turn, generates the message. - /// Transitions state to Transport if the handshake completes after this message. - /// Returns `None` if not in Handshaking state or not our turn. - pub fn get_bytes_to_send(&mut self) -> Option, NoiseError>> { - match &mut self.state { - NoiseProtocolState::Handshaking(handshake_state) => { - if handshake_state.is_my_turn() { - let mut buffer = vec![0u8; 65535]; - match handshake_state.write_message(&[], &mut buffer) { - // Empty payload for handshake msg - Ok(len) => { - if handshake_state.is_handshake_finished() { - // Transition to Transport state. - let current_state = std::mem::replace( - &mut self.state, - NoiseProtocolState::Failed( - NoiseError::IncorrectStateError.to_string(), - ), - ); - if let NoiseProtocolState::Handshaking(state_to_convert) = - current_state - { - match state_to_convert.into_transport_mode() { - Ok(transport_state) => { - self.state = - NoiseProtocolState::Transport(transport_state); - Some(Ok(buffer[..len].to_vec())) // Return final handshake msg - } - Err(e) => { - let err = NoiseError::from(e); - self.state = - NoiseProtocolState::Failed(err.to_string()); - Some(Err(err)) - } - } - } else { - // Should be unreachable - let err = NoiseError::IncorrectStateError; - self.state = NoiseProtocolState::Failed(err.to_string()); - Some(Err(err)) - } - } else { - // Handshake continues - Some(Ok(buffer[..len].to_vec())) - } - } - Err(e) => { - let err = NoiseError::from(e); - self.state = NoiseProtocolState::Failed(err.to_string()); - Some(Err(err)) - } - } - } else { - // Not our turn - None - } - } - NoiseProtocolState::Transport(_) | NoiseProtocolState::Failed(_) => { - // No handshake messages to send in these states - None - } - } - } - - /// Encrypts an application data payload for sending during the Transport phase. - /// - /// Returns the ciphertext (payload + 16-byte tag). - /// Errors if not in Transport state or encryption fails. - pub fn write_message(&mut self, payload: &[u8]) -> Result, NoiseError> { - match &mut self.state { - NoiseProtocolState::Transport(transport_state) => { - let mut buffer = vec![0u8; payload.len() + 16]; // Payload + tag - match transport_state.write_message(payload, &mut buffer) { - Ok(len) => Ok(buffer[..len].to_vec()), - Err(e) => { - let err = NoiseError::from(e); - self.state = NoiseProtocolState::Failed(err.to_string()); - Err(err) - } - } - } - NoiseProtocolState::Handshaking(_) | NoiseProtocolState::Failed(_) => { - Err(NoiseError::IncorrectStateError) - } - } - } - - /// Returns true if the protocol is in the transport phase (handshake complete). - pub fn is_transport(&self) -> bool { - matches!(self.state, NoiseProtocolState::Transport(_)) - } - - /// Returns true if the protocol has failed. - pub fn is_failed(&self) -> bool { - matches!(self.state, NoiseProtocolState::Failed(_)) - } - - /// Check if the handshake has finished and the protocol is in transport mode. - pub fn is_handshake_finished(&self) -> bool { - matches!(self.state, NoiseProtocolState::Transport(_)) - } - - /// Inject a PSK into the Noise HandshakeState. - /// - /// This allows dynamic PSK injection after HandshakeState construction, - /// which is required for PSQ (Post-Quantum Secure PSK) integration where - /// the PSK is derived during the handshake process. - /// - /// # Arguments - /// * `index` - PSK index (typically 3 for XKpsk3 pattern) - /// * `psk` - The pre-shared key bytes to inject - /// - /// # Errors - /// Returns an error if: - /// - Not in handshake state - /// - The underlying snow library rejects the PSK - pub fn set_psk(&mut self, index: u8, psk: &[u8]) -> Result<(), NoiseError> { - match &mut self.state { - NoiseProtocolState::Handshaking(handshake_state) => { - handshake_state - .set_psk(index as usize, psk) - .map_err(NoiseError::ProtocolError)?; - Ok(()) - } - _ => Err(NoiseError::IncorrectStateError), - } - } -} diff --git a/common/nym-lp/src/packet.rs b/common/nym-lp/src/packet.rs deleted file mode 100644 index db2644b5fa..0000000000 --- a/common/nym-lp/src/packet.rs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::LpError; -use crate::message::{LpMessage, MessageType}; -use crate::replay::ReceivingKeyCounterValidator; -use bytes::{BufMut, BytesMut}; -use nym_lp_common::format_debug_bytes; -use parking_lot::Mutex; -use std::fmt::Write; -use std::fmt::{Debug, Formatter}; -use std::sync::Arc; -use tracing::warn; - -#[allow(dead_code)] -pub(crate) const UDP_HEADER_LEN: usize = 8; -#[allow(dead_code)] -pub(crate) const IP_HEADER_LEN: usize = 40; // v4 - 20, v6 - 40 -#[allow(dead_code)] -pub(crate) const MTU: usize = 1500; -#[allow(dead_code)] -pub(crate) const UDP_OVERHEAD: usize = UDP_HEADER_LEN + IP_HEADER_LEN; - -#[allow(dead_code)] -pub const TRAILER_LEN: usize = 16; -#[allow(dead_code)] -pub(crate) const UDP_PAYLOAD_SIZE: usize = MTU - UDP_OVERHEAD - TRAILER_LEN; - -pub mod version { - /// The current version of the Lewes Protocol that is put into each new constructed header. - pub const CURRENT: u8 = 1; -} - -#[derive(Clone)] -pub struct LpPacket { - pub(crate) header: LpHeader, - pub(crate) message: LpMessage, - pub(crate) trailer: [u8; TRAILER_LEN], -} - -impl Debug for LpPacket { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", format_debug_bytes(&self.debug_bytes())?) - } -} - -impl LpPacket { - pub fn new(header: LpHeader, message: LpMessage) -> Self { - Self { - header, - message, - trailer: [0; TRAILER_LEN], - } - } - - pub fn typ(&self) -> MessageType { - self.message.typ() - } - - /// Compute a hash of the message payload - /// - /// This can be used for message integrity verification or deduplication - pub fn hash_payload(&self) -> [u8; 32] { - use sha2::{Digest, Sha256}; - - let mut hasher = Sha256::new(); - let mut buffer = BytesMut::new(); - - // Include message type and content in the hash - buffer.put_slice(&(self.message.typ() as u16).to_le_bytes()); - self.message.encode_content(&mut buffer); - - hasher.update(&buffer); - hasher.finalize().into() - } - - pub fn hash_payload_hex(&self) -> String { - let hash = self.hash_payload(); - hash.iter() - .fold(String::with_capacity(hash.len() * 2), |mut acc, byte| { - let _ = write!(acc, "{:02x}", byte); - acc - }) - } - - pub fn message(&self) -> &LpMessage { - &self.message - } - - pub fn header(&self) -> &LpHeader { - &self.header - } - - pub(crate) fn debug_bytes(&self) -> Vec { - let mut bytes = BytesMut::new(); - self.encode(&mut bytes); - bytes.freeze().to_vec() - } - - pub(crate) fn encode(&self, dst: &mut BytesMut) { - self.header.encode(dst); - - dst.put_slice(&(self.message.typ() as u16).to_le_bytes()); - self.message.encode_content(dst); - - dst.put_slice(&self.trailer) - } - - /// Validate packet counter against a replay protection validator - /// - /// This performs a quick check to see if the packet counter is valid before - /// any expensive processing is done. - pub fn validate_counter( - &self, - validator: &Arc>, - ) -> Result<(), LpError> { - let guard = validator.lock(); - guard.will_accept_branchless(self.header.counter)?; - Ok(()) - } - - /// Mark packet as received in the replay protection validator - /// - /// This should be called after a packet has been successfully processed. - pub fn mark_received( - &self, - validator: &Arc>, - ) -> Result<(), LpError> { - let mut guard = validator.lock(); - guard.mark_did_receive_branchless(self.header.counter)?; - Ok(()) - } -} - -/// Session ID used for ClientHello bootstrap packets before session is established. -/// -/// When a client first connects, it sends a ClientHello packet with receiver_idx=0 -/// because neither side can compute the deterministic session ID yet (requires -/// both parties' X25519 keys). After ClientHello is processed, both sides derive -/// the same session ID from their keys, and all subsequent packets use that ID. -pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0; - -/// Outer header (12 bytes) - always cleartext, used for routing. -/// -/// This is the first 12 bytes of every LP packet, containing only the fields -/// needed for session lookup (receiver_idx) and replay protection (counter). -/// For encrypted packets, this is the AAD (additional authenticated data). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OuterHeader { - pub receiver_idx: u32, - pub counter: u64, -} - -impl OuterHeader { - pub const SIZE: usize = 12; // receiver_idx(4) + counter(8) - - pub fn new(receiver_idx: u32, counter: u64) -> Self { - Self { - receiver_idx, - counter, - } - } - - pub fn parse(src: &[u8]) -> Result { - if src.len() < Self::SIZE { - return Err(LpError::InsufficientBufferSize); - } - Ok(Self { - receiver_idx: u32::from_le_bytes(src[0..4].try_into().unwrap()), - counter: u64::from_le_bytes(src[4..12].try_into().unwrap()), - }) - } - - pub fn encode(&self) -> [u8; Self::SIZE] { - let mut buf = [0u8; Self::SIZE]; - buf[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes()); - buf[4..12].copy_from_slice(&self.counter.to_le_bytes()); - buf - } - - /// Encode directly into a BytesMut buffer - pub fn encode_into(&self, dst: &mut BytesMut) { - dst.put_slice(&self.receiver_idx.to_le_bytes()); - dst.put_slice(&self.counter.to_le_bytes()); - } -} - -/// Internal LP header representation containing all logical header fields. -/// -/// **Note**: This struct represents the LOGICAL header, not the wire format. -/// On the wire, packets use the unified format where: -/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext) -/// - Inner content (version + reserved + payload) follows (cleartext or encrypted) -/// -/// The `LpHeader::encode()` method outputs the old logical format for debug purposes only. -/// Use `serialize_lp_packet()` in codec.rs for actual wire serialization. -#[derive(Debug, Clone)] -pub struct LpHeader { - pub protocol_version: u8, - pub reserved: [u8; 3], - pub receiver_idx: u32, - pub counter: u64, -} - -impl LpHeader { - pub const SIZE: usize = 16; -} - -impl LpHeader { - pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self { - Self { - protocol_version, - reserved: [0u8; 3], - receiver_idx, - counter, - } - } - - pub fn encode(&self, dst: &mut BytesMut) { - // protocol version - dst.put_u8(self.protocol_version); - - // reserved - dst.put_slice(&self.reserved); - - // sender index - dst.put_slice(&self.receiver_idx.to_le_bytes()); - - // counter - dst.put_slice(&self.counter.to_le_bytes()); - } - - pub fn parse(src: &[u8]) -> Result { - if src.len() < Self::SIZE { - return Err(LpError::InsufficientBufferSize); - } - - let protocol_version = src[0]; - - // Ensure we are using compatible protocol - // right now only support a single version - if protocol_version > version::CURRENT { - return Err(LpError::IncompatibleFuturePacketVersion { - got: protocol_version, - highest_supported: version::CURRENT, - }); - } - - if protocol_version < version::CURRENT { - return Err(LpError::IncompatibleLegacyPacketVersion { - got: protocol_version, - lowest_supported: version::CURRENT, - }); - } - - // skip reserved bytes, but log if they're different from the expected zeroes - let reserved = [src[1], src[2], src[3]]; - if reserved != [0u8; 3] { - warn!("received non-zero reserved bytes. got: {reserved:?}"); - } - - let mut receiver_idx_bytes = [0u8; 4]; - receiver_idx_bytes.copy_from_slice(&src[4..8]); - let receiver_idx = u32::from_le_bytes(receiver_idx_bytes); - - let mut counter_bytes = [0u8; 8]; - counter_bytes.copy_from_slice(&src[8..16]); - let counter = u64::from_le_bytes(counter_bytes); - - Ok(LpHeader { - protocol_version, - reserved, - receiver_idx, - counter, - }) - } - - /// Get the counter value from the header - pub fn counter(&self) -> u64 { - self.counter - } - - /// Get the sender index from the header - pub fn receiver_idx(&self) -> u32 { - self.receiver_idx - } -} - -// subsequent data: MessageType || Data diff --git a/common/nym-lp/src/packet/error.rs b/common/nym-lp/src/packet/error.rs new file mode 100644 index 0000000000..15578f4b4f --- /dev/null +++ b/common/nym-lp/src/packet/error.rs @@ -0,0 +1,33 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MalformedLpPacketError { + #[error("failed to deserialise received data: {0}")] + DeserialisationFailure(String), + + #[error("provided insufficient data to fully deserialise the struct")] + InsufficientData, + + #[error("{0} is not a valid MessageType")] + InvalidMessageType(u32), + + #[error("invalid payload size: expected {expected}, got {actual}")] + InvalidPayloadSize { expected: usize, actual: usize }, + + /// Received an LP packet with an incompatible, future, version + #[error("incompatible LP packet version. got: {got}, highest supported: {highest_supported}")] + IncompatibleFuturePacketVersion { got: u8, highest_supported: u8 }, + + /// Received an LP packet with an incompatible, legacy, version + #[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")] + IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 }, +} + +impl MalformedLpPacketError { + pub fn invalid_message_type(message_type: u32) -> Self { + MalformedLpPacketError::InvalidMessageType(message_type) + } +} diff --git a/common/nym-lp/src/packet/header.rs b/common/nym-lp/src/packet/header.rs new file mode 100644 index 0000000000..fb4653efb8 --- /dev/null +++ b/common/nym-lp/src/packet/header.rs @@ -0,0 +1,165 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::packet::message::MessageType; +use crate::packet::version; +use crate::{packet::error::MalformedLpPacketError, peer_config::LpReceiverIndex}; +use bytes::{BufMut, BytesMut}; +// use nym_lp::peer_config::LpReceiverIndex; +use tracing::warn; + +/// Outer header (12 bytes) - always cleartext, used for routing. +/// +/// This is the first 12 bytes of every LP packet, containing only the fields +/// needed for session lookup (receiver_idx) and replay protection (counter). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OuterHeader { + pub receiver_idx: LpReceiverIndex, + pub counter: u64, +} + +impl OuterHeader { + pub const SIZE: usize = 12; // receiver_idx(4) + counter(8) + + pub fn new(receiver_idx: LpReceiverIndex, counter: u64) -> Self { + Self { + receiver_idx, + counter, + } + } + + pub fn parse(src: &[u8]) -> Result { + if src.len() < Self::SIZE { + return Err(MalformedLpPacketError::InsufficientData); + } + #[allow(clippy::unwrap_used)] + Ok(Self { + receiver_idx: LpReceiverIndex::from_le_bytes(src[0..4].try_into().unwrap()), + counter: u64::from_le_bytes(src[4..12].try_into().unwrap()), + }) + } + + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + let mut bytes = [0u8; Self::SIZE]; + bytes[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes()); + bytes[4..12].copy_from_slice(&self.counter.to_le_bytes()); + bytes + } + + /// Encode directly into a BytesMut buffer + pub fn encode(&self, dst: &mut BytesMut) { + dst.put_slice(&self.receiver_idx.to_le_bytes()); + dst.put_slice(&self.counter.to_le_bytes()); + } +} + +/// InnerHeader header (8 bytes) - encrypted, used for message parsing +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InnerHeader { + pub protocol_version: u8, + pub reserved: [u8; 3], + pub message_type: MessageType, +} + +impl InnerHeader { + pub const SIZE: usize = 8; // protocol_version(1) + reserved(3) + message_type(4) + + pub fn encode(&self, dst: &mut BytesMut) { + // protocol version + dst.put_u8(self.protocol_version); + + // reserved + dst.put_slice(&self.reserved); + + // message type + dst.put_slice(&(self.message_type as u32).to_le_bytes()); + } + + pub fn parse(src: &[u8]) -> Result { + if src.len() < Self::SIZE { + return Err(MalformedLpPacketError::InsufficientData); + } + + let protocol_version = src[0]; + + // Ensure we are using compatible protocol + // right now only support a single version + if protocol_version > version::CURRENT { + return Err(MalformedLpPacketError::IncompatibleFuturePacketVersion { + got: protocol_version, + highest_supported: version::CURRENT, + }); + } + + if protocol_version < version::CURRENT { + return Err(MalformedLpPacketError::IncompatibleLegacyPacketVersion { + got: protocol_version, + lowest_supported: version::CURRENT, + }); + } + + // skip reserved bytes, but log if they're different from the expected zeroes + let reserved = [src[1], src[2], src[3]]; + if reserved != [0u8; 3] { + warn!("received non-zero reserved bytes. got: {reserved:?}"); + } + + let msg_type_raw = u32::from_le_bytes([src[4], src[5], src[6], src[7]]); + let message_type = MessageType::from_u32(msg_type_raw) + .ok_or_else(|| MalformedLpPacketError::invalid_message_type(msg_type_raw))?; + + Ok(InnerHeader { + protocol_version, + reserved, + message_type, + }) + } +} + +/// Internal LP header representation containing all logical header fields. +/// +/// **Note**: This struct represents the LOGICAL header, not the wire format. +/// On the wire, packets use the unified format where: +/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext) +/// - Inner content (version + reserved + payload) follows (cleartext or encrypted) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LpHeader { + pub outer: OuterHeader, + pub inner: InnerHeader, +} + +impl LpHeader { + pub fn new( + receiver_idx: LpReceiverIndex, + counter: u64, + protocol_version: u8, + message_type: MessageType, + ) -> Self { + Self { + outer: OuterHeader { + receiver_idx, + counter, + }, + inner: InnerHeader { + protocol_version, + reserved: [0u8; 3], + message_type, + }, + } + } + + pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) { + self.outer.encode(dst); + self.inner.encode(dst); + } + + /// Get the counter value from the header + pub fn counter(&self) -> u64 { + self.outer.counter + } + + /// Get the sender index from the header + pub fn receiver_idx(&self) -> LpReceiverIndex { + self.outer.receiver_idx + } +} diff --git a/common/nym-lp/src/packet/message.rs b/common/nym-lp/src/packet/message.rs new file mode 100644 index 0000000000..84574d1b93 --- /dev/null +++ b/common/nym-lp/src/packet/message.rs @@ -0,0 +1,501 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::packet::error::MalformedLpPacketError; +use bytes::{BufMut, BytesMut}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use std::fmt; +use std::fmt::Display; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u32)] +pub enum MessageType { + /// The party is busy + Busy = 0x0000, + + /// Encrypted payload + EncryptedData = 0x0001, + + /// Receiver should forward this message via telescoping + ForwardPacket = 0x0002, + + /// Receiver index collision - client should retry with new index + Collision = 0x0003, + + /// Acknowledgment - gateway confirms receipt of message + Ack = 0x0004, + + /// General error + Error = 0x00FF, +} + +impl MessageType { + pub(crate) fn from_u32(value: u32) -> Option { + MessageType::try_from(value).ok() + } + + pub fn to_u32(&self) -> u32 { + u32::from(*self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationData(pub Vec); + +impl ApplicationData { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn encode(&self, dst: &mut BytesMut) { + dst.put_slice(&self.0); + } + + fn decode(bytes: &[u8]) -> Result { + Ok(ApplicationData(bytes.to_vec())) + } +} + +/// General human-readable error message +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErrorPacketData { + pub message: String, +} + +impl ErrorPacketData { + pub fn new(message: impl Into) -> Self { + ErrorPacketData { + message: message.into(), + } + } + + fn len(&self) -> usize { + // length-encoding + message + 4 + self.message.len() + } + + fn encode(&self, dst: &mut BytesMut) { + dst.put_u32_le(self.message.len() as u32); + dst.put_slice(self.message.as_bytes()); + } + + fn decode(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + return Err(MalformedLpPacketError::DeserialisationFailure(format!( + "Too few bytes to deserialise ErrorPacketData. got {}", + bytes.len() + ))); + } + + let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; + if bytes[4..].len() != message_len { + return Err(MalformedLpPacketError::DeserialisationFailure(format!( + "Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}", + bytes.len(), + 4 + message_len + ))); + } + + let message = String::from_utf8_lossy(&bytes[4..]).to_string(); + + Ok(ErrorPacketData { message }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExpectedResponseSize { + /// We've sent a handshake message and expect response of predefined size + Handshake(u32), + + /// We've sent a transport message and the response is length-prefixed + Transport, +} + +impl ExpectedResponseSize { + pub fn to_bytes(&self) -> [u8; 4] { + // there are no empty handshake messages, so we use 0 bytes to indicate Transport variant + match self { + ExpectedResponseSize::Handshake(size) => size.to_le_bytes(), + ExpectedResponseSize::Transport => [0u8; 4], + } + } + + pub fn from_bytes(b: [u8; 4]) -> Self { + let size = u32::from_le_bytes(b); + if size == 0 { + ExpectedResponseSize::Transport + } else { + ExpectedResponseSize::Handshake(size) + } + } +} + +/// Packet forwarding request with embedded inner LP packet +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForwardPacketData { + /// Target gateway's LP address (IP:port string) + pub target_lp_address: SocketAddr, + + /// Indication of the expected size of the response + /// to allow the proxy to read correct data from the stream + pub expected_response_size: ExpectedResponseSize, + + /// Complete inner LP packet bytes (serialized LpPacket) + /// This is the CLIENT→EXIT gateway packet, encrypted for exit + pub inner_packet_bytes: Vec, +} + +impl ForwardPacketData { + pub fn new( + target_lp_address: SocketAddr, + expected_response_size: ExpectedResponseSize, + inner_packet_bytes: Vec, + ) -> Self { + ForwardPacketData { + target_lp_address, + expected_response_size, + inner_packet_bytes, + } + } + + fn len(&self) -> usize { + // 1 byte length of target lp address type + // + + // {4,16} target_lp_address IPv{4,6} + // + + // 2 bytes target_lp_address port + // + + // 4 bytes for expected response size + // + + // 4 bytes of length of inner packet bytes + // + + // inner_packet_bytes.len() + match self.target_lp_address { + SocketAddr::V4(_) => 1 + 4 + 2 + 4 + 4 + self.inner_packet_bytes.len(), + SocketAddr::V6(_) => 1 + 16 + 2 + 4 + 4 + self.inner_packet_bytes.len(), + } + } + + // 0 || [4B ipv4] || [2B port] || [4B res size] || [4B plen] || payload + // 1 || [16B ipv6] || [2B port] || [4B res size] || [4B plen] || payload + fn encode(&self, dst: &mut BytesMut) { + let (is_ipv6, ip_bytes) = match &self.target_lp_address { + SocketAddr::V4(address) => (false, address.ip().octets().to_vec()), + SocketAddr::V6(address) => (true, address.ip().octets().to_vec()), + }; + + dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4 + dst.put_slice(&ip_bytes); // IP bytes + dst.put_u16_le(self.target_lp_address.port()); // Port + dst.put_slice(&self.expected_response_size.to_bytes()); + dst.put_u32_le(self.inner_packet_bytes.len() as u32); + dst.put_slice(&self.inner_packet_bytes); + } + + pub fn to_bytes(&self) -> Vec { + let mut buf = BytesMut::new(); + self.encode(&mut buf); + buf.into() + } + + pub fn decode(b: &[u8]) -> Result { + // smallest possible packet with ipv4 and empty data + if b.len() < 15 { + // 1 + 4 + 2 + 4 + 4 + 0 + return Err(MalformedLpPacketError::DeserialisationFailure(format!( + "Too few bytes to deserialise ForwardPacketData. got {}", + b.len() + ))); + } + + let target_lp_address_is_ipv6 = b[0] != 0; + + let (target_lp_address, i) = if target_lp_address_is_ipv6 { + // IPv6, first check we have actually enough bytes + // smallest possible packet with ipv6 and empty data + if b.len() < 27 { + // 1 + 16 + 2 + 4 + 4+ 0 + return Err(MalformedLpPacketError::DeserialisationFailure(format!( + "Too few bytes to deserialise ipv6 ForwardPacketData. got {}", + b.len() + ))); + } + // Ipv6Addr::from_octets is not available until 1.91 so we have to use + // the slightly less obvious u128 conversion + // SAFETY: we ensured we have sufficient data, and the length is correct for casting + #[allow(clippy::unwrap_used)] + let ipv6 = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes( + b[1..17].try_into().unwrap(), + ))); + let port = u16::from_le_bytes([b[17], b[18]]); + (SocketAddr::new(ipv6, port), 19) + } else { + // IPv4. Length check done at the start + + // Ipv4Addr::from_octets is not available until 1.91 + let ipv4 = IpAddr::V4(Ipv4Addr::new(b[1], b[2], b[3], b[4])); + let port = u16::from_le_bytes([b[5], b[6]]); + (SocketAddr::new(ipv4, port), 7) + }; + + let expected_response_size_bytes = [b[i], b[i + 1], b[i + 2], b[i + 3]]; + let inner_packet_bytes_len = u32::from_le_bytes([b[i + 4], b[i + 5], b[i + 6], b[i + 7]]); + + if b[i + 8..].len() != inner_packet_bytes_len as usize { + return Err(MalformedLpPacketError::DeserialisationFailure(format!( + "Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}", + b[i + 8..].len() + ))); + } + let inner_packet_bytes = b[i + 8..].to_vec(); + + Ok(ForwardPacketData { + target_lp_address, + expected_response_size: ExpectedResponseSize::from_bytes(expected_response_size_bytes), + inner_packet_bytes, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LpMessage { + /// The party is busy + Busy, + + /// Application payload is being sent + ApplicationData(ApplicationData), + + /// Receiver should forward this message via telescoping + ForwardPacket(ForwardPacketData), + + /// Receiver index collision - client should retry with new receiver_index + Collision, + + /// Acknowledgment - gateway confirms receipt of message + Ack, + + /// An error has occurred + Error(ErrorPacketData), +} + +impl From for LpMessage { + fn from(value: ApplicationData) -> Self { + LpMessage::ApplicationData(value) + } +} + +impl From for LpMessage { + fn from(value: ForwardPacketData) -> Self { + LpMessage::ForwardPacket(value) + } +} + +impl Display for LpMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LpMessage::Busy => write!(f, "Busy"), + LpMessage::ApplicationData(_) => write!(f, "EncryptedData"), + LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"), + LpMessage::Collision => write!(f, "Collision"), + LpMessage::Ack => write!(f, "Ack"), + LpMessage::Error(_) => write!(f, "Error"), + } + } +} + +impl LpMessage { + #[deprecated(note = "is it actually needed?")] + pub fn payload(&self) -> &[u8] { + match self { + LpMessage::Busy => &[], + LpMessage::ApplicationData(payload) => payload.0.as_slice(), + LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content + LpMessage::Collision => &[], + LpMessage::Ack => &[], + LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?) + } + } + + #[deprecated(note = "is it actually needed?")] + pub fn is_empty(&self) -> bool { + match self { + LpMessage::Busy => true, + LpMessage::ApplicationData(payload) => payload.0.is_empty(), + LpMessage::ForwardPacket(_) => false, // Always has data + LpMessage::Collision => true, + LpMessage::Ack => true, + LpMessage::Error(_) => false, + } + } + + pub fn len(&self) -> usize { + match self { + LpMessage::Busy => 0, + LpMessage::ApplicationData(payload) => payload.len(), + LpMessage::ForwardPacket(payload) => payload.len(), + LpMessage::Collision => 0, + LpMessage::Ack => 0, + LpMessage::Error(payload) => payload.len(), + } + } + + pub fn typ(&self) -> MessageType { + match self { + LpMessage::Busy => MessageType::Busy, + LpMessage::ApplicationData(_) => MessageType::EncryptedData, + LpMessage::ForwardPacket(_) => MessageType::ForwardPacket, + LpMessage::Collision => MessageType::Collision, + LpMessage::Ack => MessageType::Ack, + LpMessage::Error(_) => MessageType::Error, + } + } + + pub fn encode_content(&self, dst: &mut BytesMut) { + match self { + LpMessage::Busy => { /* No content */ } + LpMessage::ApplicationData(payload) => payload.encode(dst), + LpMessage::ForwardPacket(data) => data.encode(dst), + LpMessage::Collision => { /* No content */ } + LpMessage::Ack => { /* No content */ } + LpMessage::Error(data) => data.encode(dst), + } + } + + /// Parse message from its type and content bytes. + /// + /// Used when decrypting outer-encrypted packets where the message type + /// was encrypted along with the content. + pub fn decode_content( + content: &[u8], + message_type: MessageType, + ) -> Result { + match message_type { + MessageType::Busy => { + content.ensure_empty()?; + Ok(LpMessage::Busy) + } + MessageType::EncryptedData => Ok(LpMessage::ApplicationData(ApplicationData::decode( + content, + )?)), + MessageType::ForwardPacket => Ok(LpMessage::ForwardPacket(ForwardPacketData::decode( + content, + )?)), + MessageType::Collision => { + content.ensure_empty()?; + Ok(LpMessage::Collision) + } + MessageType::Ack => { + content.ensure_empty()?; + Ok(LpMessage::Ack) + } + MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)), + } + } +} + +/// Helper trait for improving readability to return error if bytes content is not empty +trait EnsureEmptyContent { + fn ensure_empty(&self) -> Result<(), MalformedLpPacketError>; +} + +impl EnsureEmptyContent for &[u8] { + fn ensure_empty(&self) -> Result<(), MalformedLpPacketError> { + if !self.is_empty() { + return Err(MalformedLpPacketError::InvalidPayloadSize { + expected: 0, + actual: self.len(), + }); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::{InnerHeader, LpHeader, LpPacket, OuterHeader}; + + #[test] + fn encoding() { + let message = LpMessage::ApplicationData(ApplicationData(vec![11u8; 124])); + + let resp_header = LpHeader { + outer: OuterHeader { + receiver_idx: 456, + counter: 123, + }, + inner: InnerHeader { + protocol_version: 1, + reserved: [0u8; 3], + message_type: MessageType::EncryptedData, + }, + }; + + let packet = LpPacket { + header: resp_header, + message, + }; + + // Just print packet for debug, will be captured in test output + println!("{packet:?}"); + + // Verify message type + assert!(matches!(packet.message.typ(), MessageType::EncryptedData)); + + // Verify correct data in message + match &packet.message { + LpMessage::ApplicationData(data) => { + assert_eq!(*data, ApplicationData(vec![11u8; 124])); + } + _ => panic!("Wrong message type"), + } + } + + #[test] + fn forward_message_encoding() { + let msg1 = ForwardPacketData { + target_lp_address: "1.2.3.4:5678".parse().unwrap(), + expected_response_size: ExpectedResponseSize::Transport, + inner_packet_bytes: vec![], + }; + + let msg2 = ForwardPacketData { + target_lp_address: "1.2.3.4:5678".parse().unwrap(), + expected_response_size: ExpectedResponseSize::Handshake(250), + inner_packet_bytes: vec![42u8; 64], + }; + + let msg3 = ForwardPacketData { + target_lp_address: "[2001:db8::1]:8080".parse().unwrap(), + expected_response_size: ExpectedResponseSize::Transport, + inner_packet_bytes: vec![], + }; + + let msg4 = ForwardPacketData { + target_lp_address: "[2001:db8::1]:8080".parse().unwrap(), + expected_response_size: ExpectedResponseSize::Handshake(250), + inner_packet_bytes: vec![42u8; 64], + }; + + let b = msg1.to_bytes(); + let msg1_r = ForwardPacketData::decode(&b).unwrap(); + assert_eq!(msg1_r, msg1); + + let b = msg2.to_bytes(); + let msg2_r = ForwardPacketData::decode(&b).unwrap(); + assert_eq!(msg2_r, msg2); + + let b = msg3.to_bytes(); + let msg3_r = ForwardPacketData::decode(&b).unwrap(); + assert_eq!(msg3_r, msg3); + + let b = msg4.to_bytes(); + let msg4_r = ForwardPacketData::decode(&b).unwrap(); + assert_eq!(msg4_r, msg4); + } +} diff --git a/common/nym-lp/src/packet/mod.rs b/common/nym-lp/src/packet/mod.rs new file mode 100644 index 0000000000..016ae24b69 --- /dev/null +++ b/common/nym-lp/src/packet/mod.rs @@ -0,0 +1,126 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::packet::utils::format_debug_bytes; +use bytes::{BufMut, BytesMut}; +use std::fmt::{Debug, Formatter}; + +pub use error::MalformedLpPacketError; +pub use header::{InnerHeader, LpHeader, OuterHeader}; +pub use message::{ApplicationData, ForwardPacketData, LpMessage, MessageType}; + +pub mod error; +pub mod header; +pub mod message; +pub mod replay; +pub mod utils; + +pub mod version { + /// The current version of the Lewes Protocol that is put into each new constructed header. + pub const CURRENT: u8 = 1; +} + +#[allow(dead_code)] +pub(crate) const UDP_HEADER_LEN: usize = 8; +#[allow(dead_code)] +pub(crate) const IP_HEADER_LEN: usize = 40; // v4 - 20, v6 - 40 +#[allow(dead_code)] +pub(crate) const MTU: usize = 1500; +#[allow(dead_code)] +pub(crate) const UDP_OVERHEAD: usize = UDP_HEADER_LEN + IP_HEADER_LEN; +#[allow(dead_code)] +pub(crate) const UDP_PAYLOAD_SIZE: usize = MTU - UDP_OVERHEAD; + +#[derive(Clone)] +pub struct EncryptedLpPacket { + // The outer header that's sent in plaintext + pub(crate) outer_header: OuterHeader, + + // The ciphertext containing the inner header and the payload + pub(crate) ciphertext: Vec, +} + +impl std::fmt::Debug for EncryptedLpPacket { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format_debug_bytes(&self.debug_bytes())?) + } +} + +impl EncryptedLpPacket { + pub fn new(outer_header: OuterHeader, ciphertext: Vec) -> EncryptedLpPacket { + EncryptedLpPacket { + outer_header, + ciphertext, + } + } + + pub fn encoded_length(&self) -> usize { + OuterHeader::SIZE + self.ciphertext.len() + } + + pub(crate) fn debug_bytes(&self) -> Vec { + let mut bytes = BytesMut::new(); + self.encode(&mut bytes); + bytes.freeze().to_vec() + } + + pub fn encode(&self, dst: &mut BytesMut) { + self.outer_header.encode(dst); + dst.put_slice(&self.ciphertext) + } + + pub fn ciphertext(&self) -> &[u8] { + &self.ciphertext + } + + pub fn outer_header(&self) -> OuterHeader { + self.outer_header + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct LpPacket { + pub(crate) header: LpHeader, + pub(crate) message: LpMessage, +} + +impl Debug for LpPacket { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format_debug_bytes(&self.debug_bytes())?) + } +} + +impl LpPacket { + pub fn new(header: LpHeader, message: LpMessage) -> Self { + Self { header, message } + } + + pub fn typ(&self) -> MessageType { + self.message.typ() + } + + pub fn message(&self) -> &LpMessage { + &self.message + } + + pub fn into_message(self) -> LpMessage { + self.message + } + + pub fn header(&self) -> &LpHeader { + &self.header + } + + pub(crate) fn debug_bytes(&self) -> Vec { + let mut bytes = BytesMut::new(); + self.dbg_encode(&mut bytes); + bytes.freeze().to_vec() + } + + pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) { + self.header.dbg_encode(dst); + + dst.put_slice(&(self.message.typ() as u16).to_le_bytes()); + self.message.encode_content(dst); + } +} diff --git a/common/nym-lp/src/packet/replay.rs b/common/nym-lp/src/packet/replay.rs new file mode 100644 index 0000000000..5cbdc3c0d2 --- /dev/null +++ b/common/nym-lp/src/packet/replay.rs @@ -0,0 +1,33 @@ +use crate::{LpError, packet::LpPacket, replay::ReceivingKeyCounterValidator}; + +pub trait LpPacketReplayExt { + /// Validate packet counter against a replay protection validator + /// + /// This performs a quick check to see if the packet counter is valid before + /// any expensive processing is done. + fn validate_counter(&self, validator: &ReceivingKeyCounterValidator) -> Result<(), LpError>; + + /// Mark packet as received in the replay protection validator + /// + /// This should be called after a packet has been successfully processed. + fn mark_received(&self, validator: &mut ReceivingKeyCounterValidator) -> Result<(), LpError>; +} + +impl LpPacketReplayExt for LpPacket { + /// Validate packet counter against a replay protection validator + /// + /// This performs a quick check to see if the packet counter is valid before + /// any expensive processing is done. + fn validate_counter(&self, validator: &ReceivingKeyCounterValidator) -> Result<(), LpError> { + validator.will_accept_branchless(self.header().outer.counter)?; + Ok(()) + } + + /// Mark packet as received in the replay protection validator + /// + /// This should be called after a packet has been successfully processed. + fn mark_received(&self, validator: &mut ReceivingKeyCounterValidator) -> Result<(), LpError> { + validator.mark_did_receive_branchless(self.header().outer.counter)?; + Ok(()) + } +} diff --git a/common/nym-lp-common/src/lib.rs b/common/nym-lp/src/packet/utils.rs similarity index 84% rename from common/nym-lp-common/src/lib.rs rename to common/nym-lp/src/packet/utils.rs index 98da6cebd3..4abe90c213 100644 --- a/common/nym-lp-common/src/lib.rs +++ b/common/nym-lp/src/packet/utils.rs @@ -1,8 +1,4 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt; -use std::fmt::Write; +use std::fmt::{self, Write}; pub fn format_debug_bytes(bytes: &[u8]) -> Result { let mut out = String::new(); diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs index 085ea8be5b..d68d1caf9f 100644 --- a/common/nym-lp/src/peer.rs +++ b/common/nym-lp/src/peer.rs @@ -1,102 +1,77 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ClientHelloData, LpError}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests}; -use std::collections::HashMap; +use crate::LpError; +use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests}; +use std::collections::BTreeMap; +use std::fmt::Debug; use std::sync::Arc; +pub use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey}; +pub use nym_kkt::key_utils::{ + generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, +}; +pub use nym_kkt::keys::KEMKeys; + /// Representation of a local Lewes Protocol peer /// encapsulating all the known information and keys. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LpLocalPeer { - /// Local Ed25519 keys for PSQ authentication - pub(crate) ed25519: Arc, + pub(crate) ciphersuite: Ciphersuite, /// Local x25519 keys (Noise static key) - pub(crate) x25519: Arc, + pub(crate) x25519: Arc, - /// Local KEM key used for PSQ - pub(crate) kem_psq: Option>, + /// Local KEM keys used for PSQ + pub(crate) kem_keypairs: Option, } impl LpLocalPeer { - pub fn new(ed25519: Arc, x25519: Arc) -> Self { + pub fn new(ciphersuite: Ciphersuite, x25519: Arc) -> Self { LpLocalPeer { - ed25519, + ciphersuite, x25519, - kem_psq: None, + kem_keypairs: Default::default(), } } - pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData { - ClientHelloData::new_with_fresh_salt( - *self.x25519().public_key(), - *self.ed25519().public_key(), - timestamp, - ) - } - #[must_use] - pub fn with_kem_psq_key(mut self, key: Arc) -> Self { - self.kem_psq = Some(key); + pub fn with_kem_keys(mut self, kem_keys: KEMKeys) -> Self { + self.kem_keypairs = Some(kem_keys); self } - pub fn ed25519(&self) -> &Arc { - &self.ed25519 - } - - pub fn x25519(&self) -> &Arc { + pub fn x25519(&self) -> &Arc { &self.x25519 } - /// Returns the reference to the KEM Public key of the peer (if available). - pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> { - self.kem_psq - .as_ref() - .map(|kp| kp.public_key()) - .ok_or(LpError::ResponderWithMissingKEMKey) - } - /// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests #[doc(hidden)] pub fn as_remote(&self) -> LpRemotePeer { - let expected_kem_key_digests = match &self.kem_psq { - None => HashMap::new(), - Some(kem_keys) => { - let mut digests = HashMap::new(); - digests.insert( - KEM::X25519, - nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()), - ); - digests - } - }; - - let mut expected_signing_key_digests = HashMap::new(); - expected_signing_key_digests.insert( - SignatureScheme::Ed25519, - nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()), - ); + let expected_kem_key_digests = self + .kem_keypairs + .as_ref() + .map(|k| k.encapsulation_keys_digests()) + .unwrap_or_default(); LpRemotePeer { - ed25519_public: *self.ed25519.public_key(), - x25519_public: *self.x25519.public_key(), + x25519_public: self.x25519.pk, expected_kem_key_digests, - expected_signing_key_digests, } } - // this is only exposed in tests as ideally we should be storing the proper types to begin with - #[cfg(test)] - pub fn encapsulate_kem_key(&self) -> Option> { - let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes(); - let libcrux_pk = - libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?; + pub fn ciphersuite(&self) -> Ciphersuite { + self.ciphersuite + } +} - Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk)) +impl Debug for LpLocalPeer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LpLocalPeer") + .field("ciphersuite", &self.ciphersuite) + .field("x25519", &self.x25519.pk) + .field("kem_keypairs", &self.kem_keypairs) + .finish() } } @@ -104,45 +79,31 @@ impl LpLocalPeer { /// encapsulating all the known information and keys. #[derive(Debug, Clone)] pub struct LpRemotePeer { - /// Remote Ed25519 public key for PSQ authentication - pub(crate) ed25519_public: ed25519::PublicKey, - /// Remote X25519 public key (Noise static key) - pub(crate) x25519_public: x25519::PublicKey, + pub(crate) x25519_public: DHPublicKey, /// Expected digests of the remote's KEM key - pub(crate) expected_kem_key_digests: HashMap, - - /// Expected digests of the remote's signing key - pub(crate) expected_signing_key_digests: HashMap, + pub(crate) expected_kem_key_digests: BTreeMap, } impl LpRemotePeer { - pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self { + pub fn new(x25519_public: DHPublicKey) -> Self { LpRemotePeer { - ed25519_public, x25519_public, expected_kem_key_digests: Default::default(), - expected_signing_key_digests: Default::default(), } } - pub fn ed25519(&self) -> ed25519::PublicKey { - self.ed25519_public - } - - pub fn x25519(&self) -> x25519::PublicKey { - self.x25519_public + pub fn x25519(&self) -> &DHPublicKey { + &self.x25519_public } #[must_use] pub fn with_key_digests( mut self, - expected_kem_key_digests: HashMap, - expected_signing_key_digests: HashMap, + expected_kem_key_digests: BTreeMap, ) -> Self { self.expected_kem_key_digests = expected_kem_key_digests; - self.expected_signing_key_digests = expected_signing_key_digests; self } @@ -168,30 +129,40 @@ impl LpRemotePeer { } } +impl From for LpRemotePeer { + fn from(value: DHPublicKey) -> Self { + LpRemotePeer { + x25519_public: value, + expected_kem_key_digests: Default::default(), + } + } +} + #[cfg(any(feature = "mock", test))] pub fn mock_peer() -> LpLocalPeer { // use deterministic rng - let mut rng = nym_test_utils::helpers::deterministic_rng(); + let mut rng = nym_test_utils::helpers::deterministic_rng_09(); random_peer(&mut rng) } #[cfg(any(feature = "mock", test))] -pub fn random_peer(rng: &mut R) -> LpLocalPeer { - let ed25519 = Arc::new(ed25519::KeyPair::new(rng)); - let x25519 = Arc::new(ed25519.to_x25519()); - let kem_psq = Some(x25519.clone()); +pub fn random_peer(rng: &mut R) -> LpLocalPeer { + let x25519 = Arc::new(nym_kkt::key_utils::generate_lp_keypair_x25519(rng)); LpLocalPeer { - ed25519, + ciphersuite: Ciphersuite::default(), + x25519, - kem_psq, + kem_keypairs: Some(KEMKeys::new( + nym_kkt::key_utils::generate_keypair_mceliece(rng), + nym_kkt::key_utils::generate_keypair_mlkem(rng), + )), } } #[cfg(any(feature = "mock", test))] pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) { - // use deterministic rng - let mut rng = nym_test_utils::helpers::deterministic_rng(); + let mut rng = nym_test_utils::helpers::deterministic_rng_09(); (random_peer(&mut rng), random_peer(&mut rng)) } diff --git a/common/nym-lp/src/peer_config.rs b/common/nym-lp/src/peer_config.rs new file mode 100644 index 0000000000..19c4e8e608 --- /dev/null +++ b/common/nym-lp/src/peer_config.rs @@ -0,0 +1,482 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::LpError; +use libcrux_psq::handshake::types::Authenticator; + +use nym_crypto::hkdf::blake3::derive_key_blake3_multi_input; +use nym_kkt::keys::EncapsulationKey; +use rand09::{self, CryptoRng, Rng}; +use tls_codec::Serialize; +use zeroize::Zeroize; + +pub type LpReceiverIndex = u32; + +pub const MAX_HOPS: u8 = 16; +pub const LP_PEER_CONFIG_SIZE: usize = 20; + +const SEED_LEN: usize = 16; +const CONFIG_LEN: usize = 1; +const FILLER_LEN: usize = LP_PEER_CONFIG_SIZE - SEED_LEN - CONFIG_LEN; + +const RECEIVER_INDEX_DERIVATION_CONTEXT: &str = "LP_PEER_CONFIG_RECEIVER_INDEX_DERIVATION_V1"; + +// 20 bytes +#[derive(PartialEq)] +pub struct LpPeerConfig { + // The first 4 fields will be packed in one u8 + // with 1 bit left at the end + + // Determine the hop id. + // Should be 0 if node_initiator is true + // Should be > 1 if is_exit is true + hop_id: u8, + + // Determine if the recipient should be an exit node + is_exit: bool, + + // Determine if we are establishing a node<>node connection + // Should be false if is_exit is true + node_initiator: bool, + + // Enable censorship resistance countermeasures + censorship_resistance: bool, + + // If we add more config params later, we can use this + filler: [u8; FILLER_LEN], + + seed: [u8; SEED_LEN], +} + +impl LpPeerConfig { + /// Creates a new client to entry config. + /// Sets `hop_id` to 0. + /// Input: censorship_resistance flag to enable censorship resistance features. + pub fn new_client_to_entry(rng: &mut R, censorship_resistance: bool) -> Self + where + R: Rng + CryptoRng, + { + Self::build( + 0, + false, + false, + censorship_resistance, + rng.random(), + rng.random(), + ) + } + /// Creates a new client to exit config. + /// Inputs: + /// hop_id: this value must be in the range (1..=15). This function returns an error if this is not the case. + /// censorship_resistance flag to enable censorship resistance features. + pub fn new_client_to_exit( + rng: &mut R, + hop_id: u8, + censorship_resistance: bool, + ) -> Result + where + R: Rng + CryptoRng, + { + Self::new(rng, hop_id, true, false, censorship_resistance) + } + /// Creates a new client to an intermediate node config. + /// Inputs: + /// hop_id: this value must be in the range (1..=14). This function returns an error if this is not the case. + /// censorship_resistance flag to enable censorship resistance features. + pub fn new_client_to_intermediate( + rng: &mut R, + hop_id: u8, + censorship_resistance: bool, + ) -> Result + where + R: Rng + CryptoRng, + { + if hop_id == 0 || hop_id == 15 { + Err(LpError::Internal(format!( + "An intermediate hop cannot be the first or last hop. Requested hop id {hop_id}" + ))) + } else { + Self::new(rng, hop_id, false, false, censorship_resistance) + } + } + + /// Creates a new node to node config. + /// Censorship resistance features are disabled by default between nodes. + pub fn new_node_to_node(rng: &mut R) -> Result + where + R: Rng + CryptoRng, + { + // no need for censorship resistance between nodes (for now) + // hop_id between nodes is 0 + Self::new(rng, 0, false, true, false) + } + + pub fn new( + rng: &mut R, + hop_id: u8, + is_exit: bool, + node_initiator: bool, + censorship_resistance: bool, + ) -> Result + where + R: Rng + CryptoRng, + { + Self::build_checked( + hop_id, + is_exit, + node_initiator, + censorship_resistance, + rng.random(), + rng.random(), + ) + } + fn build( + hop_id: u8, + is_exit: bool, + node_initiator: bool, + censorship_resistance: bool, + seed: [u8; SEED_LEN], + filler: [u8; FILLER_LEN], + ) -> Self { + Self { + hop_id, + is_exit, + node_initiator, + censorship_resistance, + filler, + seed, + } + } + fn build_checked( + hop_id: u8, + is_exit: bool, + node_initiator: bool, + censorship_resistance: bool, + seed: [u8; SEED_LEN], + filler: [u8; FILLER_LEN], + ) -> Result { + if node_initiator && is_exit { + Err(LpError::Internal( + "A node cannot establish an exit node for itself.".into(), + )) + } else if node_initiator && hop_id != 0 { + Err(LpError::Internal( + "Hop id in node to node connections must be zero.".into(), + )) + } else if !node_initiator && hop_id >= MAX_HOPS { + Err(LpError::Internal(format!( + "Requested hop index ({}) is greater than the allowed maximum {}.", + hop_id, + MAX_HOPS - 1 + ))) + } else if !node_initiator && is_exit && hop_id == 0 { + Err(LpError::Internal( + "Hop id for exit node cannot be zero.".into(), + )) + } else if !node_initiator && !is_exit && hop_id == 15 { + Err(LpError::Internal( + "The hop with id 15 must be an exit node.".into(), + )) + } else { + Ok(Self::build( + hop_id, + is_exit, + node_initiator, + censorship_resistance, + seed, + filler, + )) + } + } + + pub fn hop_id(&self) -> u8 { + self.hop_id + } + + pub fn seed(&self) -> &[u8; SEED_LEN] { + &self.seed + } + + pub fn serialize(&self) -> [u8; LP_PEER_CONFIG_SIZE] { + let mut output_bytes: [u8; LP_PEER_CONFIG_SIZE] = [0u8; LP_PEER_CONFIG_SIZE]; + output_bytes[0..4].copy_from_slice(self.pack_config().as_slice()); + output_bytes[4..].copy_from_slice(&self.seed); + output_bytes + } + pub fn deserialize(bytes: &[u8]) -> Result { + if bytes.len() != LP_PEER_CONFIG_SIZE { + Err(LpError::DeserializationError(format!( + "Invalid Lp Config Length ({}), expected ({})", + bytes.len(), + LP_PEER_CONFIG_SIZE + ))) + } else { + let (hop_id, is_exit, node_initiator, censorship_resistance) = + Self::unpack_first_byte(bytes[0]); + + let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN]; + filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]); + + let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN]; + seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]); + + Self::build_checked( + hop_id, + is_exit, + node_initiator, + censorship_resistance, + seed, + filler, + ) + } + } + + fn pack_config(&self) -> [u8; 4] { + [ + self.pack_first_byte(), + self.filler[0], + self.filler[1], + self.filler[2], + ] + } + + fn pack_first_byte(&self) -> u8 { + let mut byte = self.hop_id; + + // Set the 5th bit to determine if the node is an exit node + if self.is_exit { + byte |= 0b0001_0000; + } + // Set the 6th bit to determine if we're establishing a node to node connection + if self.node_initiator { + byte |= 0b0010_0000; + } + // Set the 7th bit to determine if we should use censorship resistance measures + if self.censorship_resistance { + byte |= 0b0100_0000; + } + + // There will be 1 free bit at the end + + byte + } + + fn unpack_first_byte(byte: u8) -> (u8, bool, bool, bool) { + // extract 4 bits + let hop_id = byte & 0b0000_1111; + + // extract 5th bit + let is_exit = (byte & 0b0001_0000) >> 4 == 1; + // extract 6th bit + let node_initiator = (byte & 0b0010_0000) >> 5 == 1; + // extract 7th bit + let censorship_resistance = (byte & 0b0100_0000) >> 6 == 1; + + // If we need to use the last bit, we can add something here + (hop_id, is_exit, node_initiator, censorship_resistance) + } + + pub fn is_client_entry(&self) -> bool { + self.hop_id == 0 && !self.is_exit && !self.node_initiator + } + + pub fn is_client_intermediate_node(&self) -> bool { + self.hop_id > 0 && !self.is_exit && !self.node_initiator + } + + pub fn is_client_exit(&self) -> bool { + self.hop_id > 0 && self.is_exit && !self.node_initiator + } + + pub fn is_node_to_node(&self) -> bool { + self.hop_id == 0 && !self.is_exit && self.node_initiator + } + + // This returns a LpReceiverIndex made out of the first 4 bytes from + // KDF(RECEIVER_INDEX_DERIVATION_CONTEXT, initiator_pub_key || responder_kem_key, seed) + pub fn derive_receiver_index( + &self, + initiator_public_key: &Authenticator, + responder_kem_pk: &EncapsulationKey, + ) -> Result { + let initiator_public_key = initiator_public_key.tls_serialize_detached().map_err(|_| { + LpError::Internal( + "Failed to serialize initiator public key when computing receiver index".into(), + ) + })?; + let mut h = derive_key_blake3_multi_input( + RECEIVER_INDEX_DERIVATION_CONTEXT, + &[initiator_public_key.as_slice(), responder_kem_pk.as_bytes()], + self.seed(), + ); + let index = LpReceiverIndex::from_le_bytes([h[0], h[1], h[2], h[3]]); + h.zeroize(); + Ok(index) + } +} + +#[cfg(test)] +mod test { + use crate::peer_config::LpPeerConfig; + + #[test] + fn test_pack_config() { + let mut rng = rand09::rng(); + + // Node to node, no censorship resistance + { + let expected_conf = 0b0010_0000; + let conf = LpPeerConfig::new(&mut rng, 0, false, true, false).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!( + conf_bytes[0], + LpPeerConfig::new_node_to_node(&mut rng) + .unwrap() + .serialize()[0] + ); + assert!(conf.is_node_to_node()); + } + + // Node to node, with censorship resistance + { + let expected_conf = 0b0110_0000; + let conf = LpPeerConfig::new(&mut rng, 0, false, true, true).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert!(conf.is_node_to_node()); + } + + // Client to Entry, no censorship resistance + { + let expected_conf = 0b0000_0000; + let conf = LpPeerConfig::new(&mut rng, 0, false, false, false).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + let conf_alt_first_byte = + LpPeerConfig::new_client_to_entry(&mut rng, false).serialize()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!(conf_bytes[0], conf_alt_first_byte); + assert!(conf.is_client_entry()) + } + + // Client to Entry, with censorship resistance + { + let expected_conf = 0b0100_0000; + let conf = LpPeerConfig::new(&mut rng, 0, false, false, true).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + let conf_alt_first_byte = + LpPeerConfig::new_client_to_entry(&mut rng, true).serialize()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!(conf_bytes[0], conf_alt_first_byte); + assert!(conf.is_client_entry()); + } + + // Client to Exit(exit hop = 1), with censorship resistance + { + let expected_conf = 0b0101_0001; + let conf = LpPeerConfig::new(&mut rng, 1, true, false, true).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + let conf_alt_first_byte = LpPeerConfig::new_client_to_exit(&mut rng, 1, true) + .unwrap() + .serialize()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!(conf_bytes[0], conf_alt_first_byte); + assert!(conf.is_client_exit()); + } + + // Client to Exit(exit hop = 2), without censorship resistance + { + let expected_conf = 0b0001_0010; + let conf = LpPeerConfig::new(&mut rng, 2, true, false, false).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + let conf_alt_first_byte = LpPeerConfig::new_client_to_exit(&mut rng, 2, false) + .unwrap() + .serialize()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!(conf_bytes[0], conf_alt_first_byte); + assert!(conf.is_client_exit()); + } + // Client to Intermediate (hop_id = 14), without censorship resistance + { + let expected_conf = 0b0000_1110; + let conf = LpPeerConfig::new(&mut rng, 14, false, false, false).unwrap(); + let conf_bytes = conf.serialize(); + let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes) + .unwrap() + .pack_config()[0]; + let conf_alt_first_byte = LpPeerConfig::new_client_to_intermediate(&mut rng, 14, false) + .unwrap() + .serialize()[0]; + + assert_eq!(expected_conf, conf_bytes[0]); + assert_eq!(expected_conf, deserialized_conf_first_byte); + assert_eq!(conf_bytes[0], conf_alt_first_byte); + assert!(conf.is_client_intermediate_node()); + } + } + + #[test] + fn test_failures() { + let mut rng = rand09::rng(); + // Hop with id 15 must be an exit node + assert!(LpPeerConfig::new(&mut rng, 15, false, false, false).is_err()); + + // intermediate hop cannot be the first hop + assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 0, false).is_err()); + // intermediate hop cannot be the last hop + assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 15, false).is_err()); + + // Hop with id 0 must be an entry node + assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 0, false).is_err()); + assert!(LpPeerConfig::new_client_to_exit(&mut rng, 0, false).is_err()); + assert!(LpPeerConfig::new(&mut rng, 0, true, false, false).is_err()); + + // cannot be node to node with hop_id > 0 + assert!(LpPeerConfig::new(&mut rng, 1, false, true, false).is_err()); + + // cannot be node to node and exit at the same time + assert!(LpPeerConfig::new(&mut rng, 0, true, true, false).is_err()); + + // cannot have hop_id greater than 15 + // this is a valid config + assert!(LpPeerConfig::new(&mut rng, 0, false, false, false).is_ok()); + // this is a valid config + assert!(LpPeerConfig::new(&mut rng, 14, false, false, false).is_ok()); + // this is a valid config + assert!(LpPeerConfig::new(&mut rng, 15, true, false, false).is_ok()); + // these are not valid configs + assert!(LpPeerConfig::new(&mut rng, 16, false, false, false).is_err()); + assert!(LpPeerConfig::new(&mut rng, 16, true, false, false).is_err()); + assert!(LpPeerConfig::new(&mut rng, 240, false, false, false).is_err()); + } +} diff --git a/common/nym-lp/src/psk.rs b/common/nym-lp/src/psk.rs deleted file mode 100644 index 45c352f490..0000000000 --- a/common/nym-lp/src/psk.rs +++ /dev/null @@ -1,792 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! PSK (Pre-Shared Key) derivation for LP sessions using Blake3 KDF. -//! -//! This module implements identity-bound PSK derivation where both client and gateway -//! derive the same PSK from their LP keypairs. -//! -//! PSQ is embedded in Noise (not separate protocol) because: -//! 1. Single round-trip: PSQ ciphertext piggybacks on Noise handshake messages -//! 2. PSK binding: Noise XKpsk3 pattern authenticates both ECDH and PSQ-derived PSK -//! 3. Simpler state machine: No separate PSQ negotiation phase needed -//! 4. Atomic security: Session establishment either succeeds fully or fails completely -//! -//! Two approaches are supported: -//! - **Legacy ECDH-only** (`derive_psk`) - Simple but no post-quantum security -//! - **PSQ-enhanced** (`derive_psk_with_psq_*`) - Combines ECDH with post-quantum KEM -//! -//! ## Error Handling Strategy -//! -//! **PSQ failures always abort the handshake cleanly with no retry or fallback.** -//! -//! ### Rationale -//! -//! PSQ errors indicate: -//! - **Authentication failures** (CredError) - Potential attack or misconfiguration -//! - **Timing failures** (TimestampElapsed) - Replay attacks or clock skew -//! - **Crypto failures** (CryptoError) - Library bugs or hardware faults -//! - **Serialization failures** (Serialization) - Protocol violations or corruption -//! -//! None of these are transient errors that benefit from retry. Falling back to -//! ECDH-only PSK would silently degrade post-quantum security. -//! -//! ### Error Recovery Behavior -//! -//! On any PSQ error: -//! 1. Function returns `Err(LpError)` immediately -//! 2. Session state remains unchanged (dummy PSK, clean Noise state) -//! 3. Handshake aborts - caller must start fresh connection -//! 4. Error is logged with diagnostic context -//! -//! ### State Guarantees on Error -//! -//! - **`psq_state`**: Remains in `NotStarted` (initiator) or `ResponderWaiting` (responder) -//! - **Noise `HandshakeState`**: PSK slot 3 = dummy `[0u8; 32]` (not modified on error) -//! - **No partial data**: All allocations are stack-local to failed function -//! - **No cleanup needed**: No state was mutated - -use crate::LpError; -use libcrux_psq::v1::cred::{Authenticator, Ed25519}; -use libcrux_psq::v1::impls::X25519 as PsqX25519; -use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder}; -use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; -use std::time::Duration; -use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait}; - -/// Context string for Blake3 KDF domain separation (PSQ-enhanced). -const PSK_PSQ_CONTEXT: &str = "nym-lp-psk-psq-v1"; - -/// Session context for PSQ protocol. -const PSQ_SESSION_CONTEXT: &[u8] = b"nym-lp-psq-session"; - -/// Context string for subsession PSK derivation. -const SUBSESSION_PSK_CONTEXT: &str = "lp-subsession-psk-v1"; - -/// Result from PSQ initiator message creation. -/// -/// Contains all outputs needed for session establishment: -/// - `psk`: Final derived PSK for Noise handshake (ECDH || K_pq || salt → Blake3) -/// - `payload`: Serialized PSQ message to send to responder -/// - `pq_shared_secret`: Raw K_pq from KEM encapsulation (for subsession derivation) -#[derive(Debug)] -pub struct PsqInitiatorResult { - /// Final PSK for Noise XKpsk3 handshake - pub psk: [u8; 32], - /// Serialized PSQ payload to embed in handshake message - pub payload: Vec, - /// Raw PQ shared secret (K_pq) before KDF combination. - /// Used for deriving subsession PSKs to preserve PQ protection. - pub pq_shared_secret: [u8; 32], -} - -/// Result from PSQ responder message processing. -/// -/// Contains all outputs needed for session establishment: -/// - `psk`: Final derived PSK for Noise handshake (matches initiator's) -/// - `psk_handle`: Encrypted PSK handle (ctxt_B) to send back to initiator -/// - `pq_shared_secret`: Raw K_pq from KEM decapsulation (for subsession derivation) -#[derive(Debug)] -pub struct PsqResponderResult { - /// Final PSK for Noise XKpsk3 handshake - pub psk: [u8; 32], - /// Encrypted PSK handle (ctxt_B) from PSQ responder message - pub psk_handle: Vec, - /// Raw PQ shared secret (K_pq) before KDF combination. - /// Used for deriving subsession PSKs to preserve PQ protection. - pub pq_shared_secret: [u8; 32], -} - -/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Initiator side. -/// -/// This function combines classical ECDH with post-quantum KEM to provide forward secrecy -/// and HNDL (Harvest-Now, Decrypt-Later) resistance. -/// -/// # Formula -/// ```text -/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public) -/// (psq_psk, ct) = PSQ_Encapsulate(remote_kem_public, session_context) -/// psk = Blake3_derive_key( -/// context="nym-lp-psk-psq-v1", -/// input=ecdh_secret || psq_psk || salt -/// ) -/// ``` -/// -/// # Arguments -/// * `local_x25519_private` - Initiator's X25519 private key (for Noise) -/// * `remote_x25519_public` - Responder's X25519 public key (for Noise) -/// * `remote_kem_public` - Responder's KEM public key (obtained via KKT) -/// * `salt` - 32-byte salt for session binding -/// -/// # Returns -/// * `Ok((psk, ciphertext))` - PSK and ciphertext to send to responder -/// * `Err(LpError)` - If PSQ encapsulation fails -/// -/// # Example -/// ```ignore -/// // Client side (after KKT exchange) -/// let (psk, ciphertext) = derive_psk_with_psq_initiator( -/// client_x25519_private, -/// gateway_x25519_public, -/// &gateway_kem_key, // from KKT -/// &salt -/// )?; -/// // Send ciphertext to gateway -/// ``` -pub fn derive_psk_with_psq_initiator( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, - remote_kem_public: &EncapsulationKey, - salt: &[u8; 32], -) -> Result<([u8; 32], Vec), LpError> { - // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); - - // Step 2: PSQ encapsulation for post-quantum security - // KEM algorithm migration path: - // - X25519: Current default for testing/compatibility (no HNDL resistance) - // - MlKem768: Future production default (NIST PQ Level 3, HNDL resistant) - // - XWing: Maximum security option (hybrid X25519 + ML-KEM) - // Migration: Update LpConfig.kem_algorithm, no protocol changes needed. - // KKT protocol adapts automatically to different KEM key sizes. - let kem_pk = match remote_kem_public { - EncapsulationKey::X25519(pk) => pk, - _ => { - return Err(LpError::KKTError( - "Only X25519 KEM is currently supported for PSQ".to_string(), - )); - } - }; - - let mut rng = rand09::rng(); - let (psq_psk, ciphertext) = - PsqX25519::encapsulate_psq(kem_pk, PSQ_SESSION_CONTEXT, &mut rng) - .map_err(|e| LpError::Internal(format!("PSQ encapsulation failed: {:?}", e)))?; - - // Step 3: Combine ECDH + PSQ via Blake3 KDF - let mut combined = Vec::with_capacity(64 + psq_psk.len()); - combined.extend_from_slice(&ecdh_secret); - combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & - combined.extend_from_slice(salt); - - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); - - // Serialize ciphertext using TLS encoding for transport - let ct_bytes = ciphertext - .tls_serialize_detached() - .map_err(|e| LpError::Internal(format!("Ciphertext serialization failed: {:?}", e)))?; - - Ok((final_psk, ct_bytes)) -} - -/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Responder side. -/// -/// This function decapsulates the ciphertext from the initiator and combines it with -/// ECDH to derive the same PSK. -/// -/// # Formula -/// ```text -/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public) -/// psq_psk = PSQ_Decapsulate(local_kem_keypair, ciphertext, session_context) -/// psk = Blake3_derive_key( -/// context="nym-lp-psk-psq-v1", -/// input=ecdh_secret || psq_psk || salt -/// ) -/// ``` -/// -/// # Arguments -/// * `local_x25519_private` - Responder's X25519 private key (for Noise) -/// * `remote_x25519_public` - Initiator's X25519 public key (for Noise) -/// * `local_kem_keypair` - Responder's KEM keypair (decapsulation key, public key) -/// * `ciphertext` - PSQ ciphertext from initiator -/// * `salt` - 32-byte salt for session binding -/// -/// # Returns -/// * `Ok(psk)` - Derived PSK -/// * `Err(LpError)` - If PSQ decapsulation fails -/// -/// # Example -/// ```ignore -/// // Gateway side (after receiving ciphertext) -/// let psk = derive_psk_with_psq_responder( -/// gateway_x25519_private, -/// client_x25519_public, -/// (&gateway_kem_sk, &gateway_kem_pk), -/// &ciphertext, // from client -/// &salt -/// )?; -/// ``` -pub fn derive_psk_with_psq_responder( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, - local_kem_keypair: (&DecapsulationKey, &EncapsulationKey), - ciphertext: &[u8], - salt: &[u8; 32], -) -> Result<[u8; 32], LpError> { - // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); - - // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey - let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) { - (DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk), - _ => { - return Err(LpError::KKTError( - "Only X25519 KEM is currently supported for PSQ".to_string(), - )); - } - }; - - // Step 3: Deserialize ciphertext using TLS decoding - let ct = PsqCiphertext::::tls_deserialize(&mut &ciphertext[..]) - .map_err(|e| LpError::Internal(format!("Ciphertext deserialization failed: {:?}", e)))?; - - // Step 4: PSQ decapsulation for post-quantum security - let psq_psk = PsqX25519::decapsulate_psq(kem_sk, kem_pk, &ct, PSQ_SESSION_CONTEXT) - .map_err(|e| LpError::Internal(format!("PSQ decapsulation failed: {:?}", e)))?; - - // Step 5: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator) - let mut combined = Vec::with_capacity(64 + psq_psk.len()); - combined.extend_from_slice(&ecdh_secret); - combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & - combined.extend_from_slice(salt); - - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); - - Ok(final_psk) -} - -/// PSQ protocol wrapper for initiator (client) side. -/// -/// Creates a PSQ initiator message with Ed25519 authentication, following the protocol: -/// 1. Encapsulate PSK using responder's KEM key -/// 2. Derive PSK and AEAD keys from K_pq -/// 3. Sign the encapsulation with Ed25519 -/// 4. AEAD encrypt (timestamp || signature || public_key) -/// -/// Returns (PSK, serialized_payload) where payload includes enc_pq and encrypted auth data. -/// -/// # Arguments -/// * `local_x25519_private` - Client's X25519 private key (for hybrid ECDH) -/// * `remote_x25519_public` - Gateway's X25519 public key (for hybrid ECDH) -/// * `remote_kem_public` - Gateway's PQ KEM public key (from KKT) -/// * `client_ed25519_sk` - Client's Ed25519 signing key -/// * `client_ed25519_pk` - Client's Ed25519 public key (credential) -/// * `salt` - Session salt -/// * `session_context` - Context bytes for PSQ (e.g., b"nym-lp-psq-session") -/// -/// # Returns -/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret -pub fn psq_initiator_create_message( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, - remote_kem_public: &EncapsulationKey, - client_ed25519_sk: &ed25519::PrivateKey, - client_ed25519_pk: &ed25519::PublicKey, - salt: &[u8; 32], - session_context: &[u8], -) -> Result { - // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); - - // Step 2: PSQ v1 with Ed25519 authentication - // Extract X25519 KEM key from EncapsulationKey - let kem_pk = match remote_kem_public { - EncapsulationKey::X25519(pk) => pk, - _ => { - return Err(LpError::KKTError( - "Only X25519 KEM is currently supported for PSQ".to_string(), - )); - } - }; - - // Convert nym Ed25519 keys to libcrux format - type Ed25519VerificationKey = ::VerificationKey; - let ed25519_sk_bytes = client_ed25519_sk.to_bytes(); - let ed25519_pk_bytes = client_ed25519_pk.to_bytes(); - let ed25519_verification_key = Ed25519VerificationKey::from_bytes(ed25519_pk_bytes); - - // Use PSQ v1 API with Ed25519 authentication - let mut rng = rand09::rng(); - let (state, initiator_msg) = Initiator::send_initial_message::( - session_context, - Duration::from_secs(3600), // 1 hour expiry - kem_pk, - &ed25519_sk_bytes, - &ed25519_verification_key, - &mut rng, - ) - .map_err(|e| { - tracing::error!( - "PSQ initiator failed - KEM encapsulation or signing error: {:?}", - e - ); - LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e)) - })?; - - // Extract PSQ shared secret (unregistered PSK) - this is K_pq - let psq_psk = state.unregistered_psk(); - - // pq_shared_secret is the raw K_pq from KEM encapsulation. - // Store it for subsession derivation before it's combined with ECDH. - let pq_shared_secret: [u8; 32] = *psq_psk; - - // Step 3: Combine ECDH + PSQ via Blake3 KDF - let mut combined = Vec::with_capacity(64 + psq_psk.len()); - combined.extend_from_slice(&ecdh_secret); - combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32] - combined.extend_from_slice(salt); - - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); - - // Serialize InitiatorMsg with TLS encoding for transport - let msg_bytes = initiator_msg - .tls_serialize_detached() - .map_err(|e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e)))?; - - Ok(PsqInitiatorResult { - psk: final_psk, - payload: msg_bytes, - pq_shared_secret, - }) -} - -/// PSQ protocol wrapper for responder (gateway) side. -/// -/// Processes a PSQ initiator message, verifies authentication, and derives PSK. -/// Follows the protocol: -/// 1. Decapsulate to get K_pq -/// 2. Derive AEAD keys and verify encrypted auth data -/// 3. Verify Ed25519 signature -/// 4. Check timestamp validity -/// 5. Derive PSK -/// -/// # Arguments -/// * `local_x25519_private` - Gateway's X25519 private key (for hybrid ECDH) -/// * `remote_x25519_public` - Client's X25519 public key (for hybrid ECDH) -/// * `local_kem_keypair` - Gateway's PQ KEM keypair -/// * `initiator_ed25519_pk` - Client's Ed25519 public key (for signature verification) -/// * `psq_payload` - Serialized PSQ payload from initiator -/// * `salt` - Session salt (must match initiator's) -/// * `session_context` - Context bytes for PSQ -/// -/// # Returns -/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret -pub fn psq_responder_process_message( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, - local_kem_keypair: (&DecapsulationKey, &EncapsulationKey), - initiator_ed25519_pk: &ed25519::PublicKey, - psq_payload: &[u8], - salt: &[u8; 32], - session_context: &[u8], -) -> Result { - // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); - - // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey - let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) { - (DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk), - _ => { - return Err(LpError::KKTError( - "Only X25519 KEM is currently supported for PSQ".to_string(), - )); - } - }; - - // Step 3: Deserialize InitiatorMsg using TLS decoding - let initiator_msg = InitiatorMsg::::tls_deserialize(&mut &psq_payload[..]) - .map_err(|e| LpError::Internal(format!("InitiatorMsg deserialization failed: {:?}", e)))?; - - // Step 4: Convert nym Ed25519 public key to libcrux VerificationKey format - type Ed25519VerificationKey = ::VerificationKey; - let initiator_ed25519_pk_bytes = initiator_ed25519_pk.to_bytes(); - let initiator_verification_key = Ed25519VerificationKey::from_bytes(initiator_ed25519_pk_bytes); - - // Step 5: PSQ v1 responder processing with Ed25519 verification - let (registered_psk, responder_msg) = Responder::send::( - b"nym-lp-handle", // PSK storage handle - Duration::from_secs(3600), // 1 hour expiry (must match initiator) - session_context, // Must match initiator's session_context - kem_pk, // Responder's public key - kem_sk, // Responder's secret key - &initiator_verification_key, // Initiator's Ed25519 public key for verification - &initiator_msg, // InitiatorMsg to verify and process - ) - .map_err(|e| { - use libcrux_psq::v1::Error as PsqError; - match e { - PsqError::CredError => { - tracing::warn!( - "PSQ responder auth failure - invalid Ed25519 signature (potential attack)" - ); - } - PsqError::TimestampElapsed | PsqError::RegistrationError => { - tracing::warn!( - "PSQ responder timing failure - TTL expired (potential replay attack)" - ); - } - _ => { - tracing::error!("PSQ responder failed - {:?}", e); - } - } - LpError::Internal(format!("PSQ v1 responder send failed: {:?}", e)) - })?; - - // Extract the PSQ PSK from the registered PSK - this is K_pq - let psq_psk = registered_psk.psk; - - // pq_shared_secret is the raw K_pq from KEM decapsulation. - // Store it for subsession derivation before it's combined with ECDH. - let pq_shared_secret: [u8; 32] = psq_psk; - - // Step 6: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator) - let mut combined = Vec::with_capacity(64 + psq_psk.len()); - combined.extend_from_slice(&ecdh_secret); - combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & - combined.extend_from_slice(salt); - - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); - - // Step 7: Serialize ResponderMsg (contains ctxt_B - encrypted PSK handle) - use tls_codec::Serialize; - let responder_msg_bytes = responder_msg - .tls_serialize_detached() - .map_err(|e| LpError::Internal(format!("ResponderMsg serialization failed: {:?}", e)))?; - - Ok(PsqResponderResult { - psk: final_psk, - psk_handle: responder_msg_bytes, - pq_shared_secret, - }) -} - -/// Derive subsession PSK from parent's PQ shared secret. -/// -/// Uses Blake3 KDF with domain separation to derive unique PSK for each subsession. -/// This preserves PQ protection: subsession keys inherit quantum resistance from -/// parent's KEM shared secret (K_pq). -/// -/// # Security Model -/// -/// Subsessions use Noise KKpsk0 pattern where: -/// - Both parties already know each other's static X25519 keys (from parent session) -/// - PSK provides PQ protection by deriving from parent's K_pq -/// - Each subsession gets unique PSK via index parameter (prevents key reuse) -/// -/// # Arguments -/// * `pq_shared_secret` - Parent session's K_pq (32 bytes from KEM) -/// * `subsession_index` - Monotonic index for this subsession (prevents reuse) -/// -/// # Returns -/// 32-byte PSK for Noise KKpsk0 handshake -pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] { - nym_crypto::kdf::derive_key_blake3( - SUBSESSION_PSK_CONTEXT, - pq_shared_secret, - &subsession_index.to_le_bytes(), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::thread_rng; - - fn generate_x25519_keypair() -> x25519::KeyPair { - x25519::KeyPair::new(&mut thread_rng()) - } - - #[test] - fn test_psk_derivation_is_symmetric() { - let keypair_1 = generate_x25519_keypair(); - let keypair_2 = generate_x25519_keypair(); - let salt = [2u8; 32]; - - let mut rng = &mut rand09::rng(); - let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - let dec_key = DecapsulationKey::X25519(_kem_sk); - - // Client derives PSK - let (client_psk, ciphertext) = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - // Gateway derives PSK from their perspective - let gateway_psk = derive_psk_with_psq_responder( - keypair_2.private_key(), - keypair_1.public_key(), - (&dec_key, &enc_key), - &ciphertext, - &salt, - ) - .unwrap(); - - assert_eq!( - client_psk, gateway_psk, - "Both sides should derive identical PSK" - ); - } - - #[test] - fn test_different_salts_produce_different_psks() { - let keypair_1 = generate_x25519_keypair(); - let keypair_2 = generate_x25519_keypair(); - - let salt1 = [1u8; 32]; - let salt2 = [2u8; 32]; - let mut rng = &mut rand09::rng(); - let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - - let psk1 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt1, - ) - .unwrap(); - let psk2 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt2, - ) - .unwrap(); - - assert_ne!(psk1, psk2, "Different salts should produce different PSKs"); - } - - #[test] - fn test_different_keys_produce_different_psks() { - let keypair_1 = generate_x25519_keypair(); - let keypair_2 = generate_x25519_keypair(); - let keypair_3 = generate_x25519_keypair(); - let salt = [3u8; 32]; - - let mut rng = &mut rand09::rng(); - let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - - let psk1 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - let psk2 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_3.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - assert_ne!( - psk1, psk2, - "Different remote keys should produce different PSKs" - ); - } - - // PSQ-enhanced PSK tests - use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey, KEM}; - use nym_kkt::key_utils::generate_keypair_libcrux; - - #[test] - fn test_psq_derivation_deterministic() { - let mut rng = rand09::rng(); - - // Generate X25519 keypairs for Noise - let client_keypair = generate_x25519_keypair(); - let gateway_keypair = generate_x25519_keypair(); - - // Generate KEM keypair for PSQ - let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - let dec_key = DecapsulationKey::X25519(kem_sk); - - let salt = [1u8; 32]; - - // Derive PSK twice with same inputs (initiator side) - let (_psk1, ct1) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - let (_psk2, _ct2) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - // PSKs will be different due to randomness in PSQ, but ciphertexts too - // This test verifies the function is deterministic given the SAME ciphertext - let psk_responder1 = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), - (&dec_key, &enc_key), - &ct1, - &salt, - ) - .unwrap(); - - let psk_responder2 = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), - (&dec_key, &enc_key), - &ct1, // Same ciphertext - &salt, - ) - .unwrap(); - - assert_eq!( - psk_responder1, psk_responder2, - "Same ciphertext should produce same PSK" - ); - } - - #[test] - fn test_psq_derivation_symmetric() { - let mut rng = rand09::rng(); - - // Generate X25519 keypairs for Noise - let client_keypair = generate_x25519_keypair(); - let gateway_keypair = generate_x25519_keypair(); - - // Generate KEM keypair for PSQ - let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - let dec_key = DecapsulationKey::X25519(kem_sk); - - let salt = [2u8; 32]; - - // Client derives PSK (initiator) - let (client_psk, ciphertext) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - // Gateway derives PSK from ciphertext (responder) - let gateway_psk = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), - (&dec_key, &enc_key), - &ciphertext, - &salt, - ) - .unwrap(); - - assert_eq!( - client_psk, gateway_psk, - "Both sides should derive identical PSK via PSQ" - ); - } - - #[test] - fn test_different_kem_keys_different_psk() { - let mut rng = rand09::rng(); - - let client_keypair = generate_x25519_keypair(); - let gateway_keypair = generate_x25519_keypair(); - - // Two different KEM keypairs - let (_, kem_pk1) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let (_, kem_pk2) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - - let enc_key1 = EncapsulationKey::X25519(kem_pk1); - let enc_key2 = EncapsulationKey::X25519(kem_pk2); - - let salt = [3u8; 32]; - - let (psk1, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key1, - &salt, - ) - .unwrap(); - - let (psk2, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key2, - &salt, - ) - .unwrap(); - - assert_ne!( - psk1, psk2, - "Different KEM keys should produce different PSKs" - ); - } - - #[test] - fn test_psq_psk_output_length() { - let mut rng = rand09::rng(); - - let client_keypair = generate_x25519_keypair(); - let gateway_keypair = generate_x25519_keypair(); - - let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - - let salt = [4u8; 32]; - - let (psk, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - - assert_eq!(psk.len(), 32, "PSQ PSK should be exactly 32 bytes"); - } - - #[test] - fn test_psq_different_salts_different_psks() { - let mut rng = rand09::rng(); - - let client_keypair = generate_x25519_keypair(); - let gateway_keypair = generate_x25519_keypair(); - - let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let enc_key = EncapsulationKey::X25519(kem_pk); - - let salt1 = [1u8; 32]; - let salt2 = [2u8; 32]; - - let (psk1, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt1, - ) - .unwrap(); - - let (psk2, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), - &enc_key, - &salt2, - ) - .unwrap(); - - assert_ne!(psk1, psk2, "Different salts should produce different PSKs"); - } -} diff --git a/common/nym-lp/src/psq/handshake_message.rs b/common/nym-lp/src/psq/handshake_message.rs new file mode 100644 index 0000000000..2877049b46 --- /dev/null +++ b/common/nym-lp/src/psq/handshake_message.rs @@ -0,0 +1,145 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size}; +use crate::transport::{LpTransportError, traits::HandshakeMessage}; +use nym_kkt::context::KKTMode; +use nym_kkt_ciphersuite::KEM; +use std::ops::Deref; + +pub struct KKTRequest(nym_kkt::message::KKTRequest); + +impl From for KKTRequest { + fn from(request: nym_kkt::message::KKTRequest) -> Self { + KKTRequest(request) + } +} + +impl From for nym_kkt::message::KKTRequest { + fn from(request: KKTRequest) -> Self { + request.0 + } +} + +impl HandshakeMessage for KKTRequest { + fn into_bytes(self) -> Vec { + self.0.into_bytes() + } + + fn try_from_bytes(bytes: Vec) -> Result { + Ok(KKTRequest( + nym_kkt::message::KKTRequest::try_from_bytes(&bytes) + .map_err(|err| LpTransportError::MalformedPacket(err.to_string()))?, + )) + } + + fn expected_size(mode: KKTMode, expected_kem: KEM, payload_size: usize) -> usize { + nym_kkt::message::KKTRequest::size_excluding_payload(mode, expected_kem) + payload_size + } + + fn response_size(&self, expected_kem: KEM) -> Option { + Some(nym_kkt::message::KKTResponse::size_excluding_payload( + expected_kem, + )) + } +} + +pub struct KKTResponse(nym_kkt::message::KKTResponse); + +impl From for KKTResponse { + fn from(request: nym_kkt::message::KKTResponse) -> Self { + KKTResponse(request) + } +} + +impl From for nym_kkt::message::KKTResponse { + fn from(request: KKTResponse) -> Self { + request.0 + } +} + +impl HandshakeMessage for KKTResponse { + fn into_bytes(self) -> Vec { + self.0.into_bytes() + } + + fn try_from_bytes(bytes: Vec) -> Result { + Ok(KKTResponse(nym_kkt::message::KKTResponse::from_bytes( + bytes, + ))) + } + + fn expected_size(_: KKTMode, expected_kem: KEM, payload_size: usize) -> usize { + nym_kkt::message::KKTResponse::size_excluding_payload(expected_kem) + payload_size + } + + fn response_size(&self, expected_kem: KEM) -> Option { + Some(psq_msg1_size(expected_kem)) + } +} + +pub struct PSQMsg1(Vec); + +impl Deref for PSQMsg1 { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PSQMsg1 { + pub fn new(bytes: Vec) -> Self { + PSQMsg1(bytes) + } +} + +impl HandshakeMessage for PSQMsg1 { + fn into_bytes(self) -> Vec { + self.0 + } + + fn try_from_bytes(bytes: Vec) -> Result { + Ok(PSQMsg1(bytes)) + } + + fn expected_size(_: KKTMode, expected_kem: KEM, payload_size: usize) -> usize { + psq_msg1_size(expected_kem) + payload_size + } + + fn response_size(&self, _: KEM) -> Option { + Some(PSQ_MSG2_SIZE) + } +} + +pub struct PSQMsg2(Vec); + +impl Deref for PSQMsg2 { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PSQMsg2 { + pub fn new(bytes: Vec) -> Self { + PSQMsg2(bytes) + } +} + +impl HandshakeMessage for PSQMsg2 { + fn into_bytes(self) -> Vec { + self.0 + } + + fn try_from_bytes(bytes: Vec) -> Result { + Ok(PSQMsg2(bytes)) + } + + fn expected_size(_: KKTMode, _: KEM, payload_size: usize) -> usize { + PSQ_MSG2_SIZE + payload_size + } + + fn response_size(&self, _: KEM) -> Option { + None + } +} diff --git a/common/nym-lp/src/psq/helpers.rs b/common/nym-lp/src/psq/helpers.rs index 7c488187a6..ab4edfa7cb 100644 --- a/common/nym-lp/src/psq/helpers.rs +++ b/common/nym-lp/src/psq/helpers.rs @@ -1,52 +1,12 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; -use crate::{LpError, LpPacket}; -use bytes::BytesMut; -use nym_lp_transport::traits::LpTransport; +use libcrux_psq::handshake::ciphersuites::CiphersuiteName; +use nym_kkt_ciphersuite::KEM; -#[cfg(test)] -use mock_instant::thread_local::{SystemTime, UNIX_EPOCH}; -#[cfg(not(test))] -use std::time::{SystemTime, UNIX_EPOCH}; - -pub(crate) fn current_timestamp() -> Result { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| LpError::Internal("System time before UNIX epoch".into())) - .map(|d| d.as_secs()) -} - -// only used in internal code (and tests) -#[allow(async_fn_in_trait)] -pub trait LpTransportHandshakeExt: LpTransport { - // the outer key is temporary until the algorithm is changed with psqv2 - async fn receive_packet( - &mut self, - outer_key: Option<&OuterAeadKey>, - ) -> Result - where - Self: Unpin, - { - let raw = self.receive_raw_packet().await?; - parse_lp_packet(&raw, outer_key) - } - - async fn send_packet( - &mut self, - packet: LpPacket, - outer_key: Option<&OuterAeadKey>, - ) -> Result<(), LpError> - where - Self: Unpin, - { - let mut packet_buf = BytesMut::new(); - - serialize_lp_packet(&packet, &mut packet_buf, outer_key)?; - self.send_serialised_packet(&packet_buf).await?; - Ok(()) +pub(crate) fn kem_to_ciphersuite(kem: KEM) -> CiphersuiteName { + match kem { + KEM::MlKem768 => CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256, + KEM::McEliece => CiphersuiteName::X25519_CLASSICMCELIECE_X25519_AESGCM128_HKDFSHA256, } } - -impl LpTransportHandshakeExt for T where T: LpTransport {} diff --git a/common/nym-lp/src/psq/initiator.rs b/common/nym-lp/src/psq/initiator.rs index 61804124a9..ef450158f0 100644 --- a/common/nym-lp/src/psq/initiator.rs +++ b/common/nym-lp/src/psq/initiator.rs @@ -1,391 +1,340 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::codec::OuterAeadKey; -use crate::message::{HandshakeData, KKTRequestData, MessageType}; -use crate::noise_protocol::NoiseProtocol; -use crate::peer::LpRemotePeer; -use crate::psk::psq_initiator_create_message; -use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp}; -use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState}; -use crate::session::PqSharedSecret; -use crate::{ClientHelloData, LpError, LpMessage, LpSession}; -use nym_kkt::KKT_RESPONSE_AAD; -use nym_kkt::ciphersuite::EncapsulationKey; -use nym_kkt::context::KKTContext; -use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame}; -use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response}; -use nym_lp_transport::traits::LpTransport; -use rand09::rng; +use crate::peer::{LpLocalPeer, LpRemotePeer}; +use crate::peer_config::LpPeerConfig; +use crate::psq::handshake_message::{PSQMsg1, PSQMsg2}; +use crate::psq::helpers::kem_to_ciphersuite; +use crate::psq::{ + AAD_INITIATOR_INNER_V1, AAD_INITIATOR_OUTER_V1, InitiatorData, PSQ_MSG2_SIZE, + PSQHandshakeState, SESSION_CONTEXT_V1, handshake_message, psq_msg1_size, +}; +use crate::session::PersistentSessionBinding; +use crate::transport::traits::LpHandshakeChannel; +use crate::{LpError, LpSession}; +use libcrux_psq::handshake::RegistrationInitiator; +use libcrux_psq::handshake::builders::{ + CiphersuiteBuilder, InitiatorCiphersuite, PrincipalBuilder, +}; +use libcrux_psq::handshake::types::Authenticator; +use libcrux_psq::{Channel, IntoSession}; +use nym_kkt::initiator::KKTInitiator; +use nym_kkt::keys::EncapsulationKey; +use nym_kkt::message::{KKTRequest, KKTResponse}; +use rand09::SeedableRng; use tracing::debug; -impl<'a, S> PSQHandshakeState<'a, S> +pub struct PSQHandshakeStateInitiator<'a, S> { + pub(super) inner_state: PSQHandshakeState<'a, S>, + pub(super) initiator_data: InitiatorData, +} + +pub(crate) fn build_psq_principal( + rng: R, + version: u8, + ciphersuite: InitiatorCiphersuite, +) -> Result, LpError> where - S: LpTransport + Unpin, + R: rand09::CryptoRng, { - /// Generate and send client hello to the responder - pub(crate) async fn send_client_hello(&mut self) -> Result { - let protocol = self.protocol_version()?; + let (ctx, inner_aad, outer_aad) = match version { + 1 => ( + SESSION_CONTEXT_V1, + AAD_INITIATOR_INNER_V1, + AAD_INITIATOR_OUTER_V1, + ), + other => return Err(LpError::UnsupportedVersion { version: other }), + }; - // 1. Generate and send ClientHelloData with fresh salt and both public keys - let timestamp = current_timestamp()?; + PrincipalBuilder::new(rng) + .outer_aad(outer_aad) + .inner_aad(inner_aad) + .context(ctx) + .build_registration_initiator(ciphersuite) + .map_err(|inner| LpError::PSQInitiatorBuilderFailure { inner }) +} - let client_hello_data = self.local_peer.build_client_hello_data(timestamp); - self.connection - .send_packet(client_hello_data.into_lp_packet(protocol), None) - .await?; - Ok(client_hello_data) - } - - /// Attempt to receive an ack to sent client hello. returns a boolean indicating - /// whether the request has been successful or whether there has been a collision in receiver - /// index requiring a retry - pub(crate) async fn receive_client_hello_ack(&mut self) -> Result { - match self.receive_non_error(None).await?.message { - LpMessage::Ack => Ok(true), - LpMessage::Collision => Ok(false), - other => { - // TODO: retry on collision - Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::Ack, - )) - } - } +pub(crate) fn build_psq_ciphersuite<'a>( + init: &'a LpLocalPeer, + responder: &'a LpRemotePeer, + kem_key: &'a EncapsulationKey, +) -> Result, LpError> { + let psq_ciphersuite = kem_to_ciphersuite(kem_key.kem()); + + let builder = CiphersuiteBuilder::new(psq_ciphersuite) + .longterm_x25519_keys(init.x25519()) + .peer_longterm_x25519_pk(responder.x25519()); + + match kem_key { + EncapsulationKey::McEliece(kem_key) => builder.peer_longterm_cmc_pk(kem_key), + EncapsulationKey::MlKem768(kem_key) => builder.peer_longterm_mlkem_pk(kem_key), } + .build_initiator_ciphersuite() + .map_err(|inner| LpError::PSQInitiatorBuilderFailure { inner }) +} +impl<'a, S> PSQHandshakeStateInitiator<'a, S> +where + S: LpHandshakeChannel + Unpin, +{ /// Attempt to send KKT request to begin the handshake - pub(crate) async fn send_kkt_request( - &mut self, - session_id: u32, - remote_peer: &LpRemotePeer, - ) -> Result<(KKTContext, KKTSessionSecret), LpError> { - let protocol = self.protocol_version()?; + async fn send_kkt_request(&mut self, request: KKTRequest) -> Result<(), LpError> { + let kem = self.inner_state.local_peer.ciphersuite.kem(); - let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?; - let (session_secret, encrypted_frame) = - encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?; - let lp_message = KKTRequestData::new(encrypted_frame).into(); - let lp_packet = self.next_packet(session_id, protocol, lp_message); - self.connection.send_packet(lp_packet, None).await?; - Ok((kkt_context, session_secret)) - } - - /// Attempt to receive a KKT response to the previously sent request and extract (and validate) - /// the received encapsulation key - pub(crate) async fn receive_kkt_response( - &mut self, - (kkt_context, session_secret): (KKTContext, KKTSessionSecret), - remote_peer: &LpRemotePeer, - ) -> Result, LpError> { - let kkt_response = match self.receive_non_error(None).await?.message { - LpMessage::KKTResponse(response) => response, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::KKTResponse, - )); - } - }; - debug!("received KKT response"); - let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?; - - let (response_frame, remote_context) = - decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?; - let encapsulation_key = initiator_ingest_response( - &kkt_context, - &response_frame, - &remote_context, - &remote_peer.ed25519_public, - &expected_kem_key_digest, - )?; - Ok(encapsulation_key) - } - - /// Attempt to prepare and send initial PSQ msg1 - pub(crate) async fn send_psq_initiator_message( - &mut self, - remote_peer: &LpRemotePeer, - encapsulation_key: &EncapsulationKey<'_>, - salt: &[u8; 32], - session_id_bytes: &[u8; 4], - ) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> { - let protocol = self.protocol_version()?; - let session_id = u32::from_le_bytes(*session_id_bytes); - - let psq_initiator = psq_initiator_create_message( - self.local_peer.x25519.private_key(), - &remote_peer.x25519_public, - encapsulation_key, - self.local_peer.ed25519.private_key(), - self.local_peer.ed25519.public_key(), - salt, - session_id_bytes, - )?; - let psk = psq_initiator.psk; - let psq_payload = psq_initiator.payload; - - // TEMP \/ - let outer_aead_key = OuterAeadKey::from_psk(&psk); - // TEMP /\ - - // prepare noise state and msg1 - let mut noise_protocol = NoiseProtocol::build_new_initiator( - self.local_peer.x25519().private_key().as_bytes(), - remote_peer.x25519_public.as_bytes(), - &psk, - )?; - - // prepare noise msg1 - let noise_msg1 = noise_protocol - .get_bytes_to_send() - .ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??; - let psq_len = psq_payload.len() as u16; - let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len()); - combined.extend_from_slice(&psq_len.to_le_bytes()); - combined.extend_from_slice(&psq_payload); - combined.extend_from_slice(&noise_msg1); - - let lp_message = HandshakeData::new(combined).into(); - let lp_packet = self.next_packet(session_id, protocol, lp_message); - - self.connection.send_packet(lp_packet, None).await?; - Ok(( - outer_aead_key, - noise_protocol, - PqSharedSecret::new(psq_initiator.pq_shared_secret), - )) - } - - /// Attempt to receive and validate received PSQ msg2 - pub(crate) async fn receive_psq_responder_message( - &mut self, - outer_aead_key: &OuterAeadKey, - noise_protocol: &mut NoiseProtocol, - ) -> Result<(), LpError> { - let psq_msg2 = match self + self.inner_state .connection - .receive_packet(Some(outer_aead_key)) - .await? - .message - { - LpMessage::Handshake(response) => response.0, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::Handshake, - )); - } - }; - - // Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg] - if psq_msg2.len() < 2 { - return Err(LpError::kkt_psq_handshake("too short msg2 received")); - } - let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize; - if psq_msg2.len() < 2 + handle_len { - return Err(LpError::kkt_psq_handshake("too short msg2 received")); - } - // Extract and "store" the PSK handle - let _psq_handle_bytes = &psq_msg2[2..2 + handle_len]; - let noise_payload = &psq_msg2[2 + handle_len..]; - - // *sigh* ignore the message - let _noise_msg2 = noise_protocol.read_message(noise_payload)?; + .send_handshake_message::(request.into(), kem) + .await?; Ok(()) } - /// Attempt to prepare and send final PSQ msg3 - pub(crate) async fn send_final_psq_message( - &mut self, - session_id: u32, - outer_aead_key: &OuterAeadKey, - noise_protocol: &mut NoiseProtocol, - ) -> Result<(), LpError> { - let protocol = self.protocol_version()?; + /// Attempt to receive a KKT response to the previously sent request + async fn receive_kkt_response(&mut self) -> Result { + // no response payload + let packet_len = + KKTResponse::size_excluding_payload(self.inner_state.local_peer.ciphersuite.kem()); - let noise_msg3 = noise_protocol - .get_bytes_to_send() - .ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??; - - let lp_message = HandshakeData::new(noise_msg3).into(); - let lp_packet = self.next_packet(session_id, protocol, lp_message); - self.connection - .send_packet(lp_packet, Some(outer_aead_key)) + let resp = self + .inner_state + .connection + .receive_handshake_message::(packet_len) .await?; - if !noise_protocol.is_handshake_finished() { - return Err(LpError::kkt_psq_handshake( - "noise handshake not finished after msg3", - )); - } - - Ok(()) + Ok(resp.into()) } - /// Receive final ACK that indicates finalisation of the handshake - pub(crate) async fn receive_final_ack( - &mut self, - outer_aead_key: &OuterAeadKey, - ) -> Result<(), LpError> { - match self - .connection - .receive_packet(Some(outer_aead_key)) - .await? - .message - { - LpMessage::Ack => Ok(()), - other => Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::Ack, - )), - } - } - - async fn complete_as_initiator_inner( - &mut self, - ) -> Result + pub async fn complete_handshake(self) -> Result where - S: LpTransport + Unpin, + S: LpHandshakeChannel + Unpin, { - // 0. retrieve the expected kem key hash. if we don't know it, - // there's no point in even trying to start the handshake - let Some(remote_peer) = self.remote_peer.take() else { - return Err(IntermediateHandshakeFailure::plain( - LpError::kkt_psq_handshake("initiator can't proceed without remote information"), - )); - }; + let mut rng = rand09::rngs::StdRng::from_os_rng(); + self.complete_handshake_with_rng(&mut rng).await + } - // 1. Generate and send ClientHelloData with fresh salt and both public keys - // and keep retrying until we manage to establish a receiver index without collisions - let mut attempt = 0; - let client_hello_data = loop { - attempt += 1; + pub async fn complete_handshake_with_rng(mut self, rng: &mut R) -> Result + where + S: LpHandshakeChannel + Unpin, + R: rand09::CryptoRng, + { + let ciphersuite = self.inner_state.local_peer.ciphersuite(); + let kem = ciphersuite.kem(); - debug!("sending client hello"); - let client_hello = self - .send_client_hello() - .await - .map_err(IntermediateHandshakeFailure::plain)?; - if self - .receive_client_hello_ack() - .await - .map_err(IntermediateHandshakeFailure::plain)? - { - debug!("received client hello ACK"); - break client_hello; - } - debug!("received client hello collision"); + let lp_peer_config = LpPeerConfig::new_client_to_entry(rng, false); - // TODO: make it configurable - if attempt > 3 { - return Err(IntermediateHandshakeFailure::plain( - LpError::kkt_psq_handshake( - "failed to establish receiver index without collision", - ), - )); - } - }; - let session_id = client_hello_data.receiver_index; - let session_id_bytes = session_id.to_le_bytes(); - let salt = client_hello_data.salt; + // 1. retrieve the expected kem key hash. if we don't know it, + let dir_hash = self + .initiator_data + .remote_peer + .expected_kem_key_hash(ciphersuite)?; + + // 2. prepare and send KKT request + let (mut initiator, kkt_request) = KKTInitiator::generate_one_way_request( + rng, + ciphersuite, + self.initiator_data.remote_peer.x25519(), + &dir_hash, + self.initiator_data.protocol_version, + Some(Vec::from(lp_peer_config.serialize())), + )?; + // derive the receiver index from the request + // let receiver_index = kkt_request - // 3. prepare and send KKT request debug!("sending KKT request"); - let kkt_data = self - .send_kkt_request(session_id, &remote_peer) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + self.send_kkt_request(kkt_request).await?; - // 4. receive and process KKT response - let encapsulation_key = self - .receive_kkt_response(kkt_data, &remote_peer) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + // 3. receive and process KKT response + let raw_response = self.receive_kkt_response().await?; debug!("received KKT response"); - // 5. prepare and send PSQ msg1 - debug!("sending PSQ msg1"); - let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self - .send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + // the responder does not send a payload + let response = initiator.process_response(raw_response, 0)?; - // 6. receive and process PSQ msg2 - debug!("received PSQ msg2"); - if let Err(source) = self - .receive_psq_responder_message(&outer_aead_key, &mut noise_protocol) - .await - { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); + // 4. generate and send PSQ request + let protocol = self.initiator_data.protocol_version; + let conn = self.inner_state.connection; + + // note: the clone is cheap due to internal Arcs + let encapsulation_key = response.encapsulation_key.clone(); + + // build the PSQ initiator + let initiator_ciphersuite = build_psq_ciphersuite( + &self.inner_state.local_peer, + &self.initiator_data.remote_peer, + &response.encapsulation_key, + )?; + + let mut psq_initiator = build_psq_principal(rng, protocol, initiator_ciphersuite)?; + + // PSQ msg 1 send + let mut buf = vec![0u8; psq_msg1_size(kem)]; + // annoyingly `RegistrationInitiator` has to write into unresizable `&mut [u8]`... + let n = psq_initiator.write_message(&[], &mut buf)?; + debug!("sending PSQ handshake msg"); + if n != buf.len() { + return Err(LpError::Internal( + "unexpected changes in PSQ msg1 size".to_string(), + )); + } + let msg = PSQMsg1::new(buf); + conn.send_handshake_message(msg, kem).await?; + + // 5. receive and process PSQ response + let psq_msg: PSQMsg2 = conn.receive_handshake_message(PSQ_MSG2_SIZE).await?; + debug!("received PSQ handshake msg"); + psq_initiator.read_message(&psq_msg, &mut [])?; + + if !psq_initiator.is_handshake_finished() { + return Err(LpError::kkt_psq_handshake( + "handshake not finished after receiving psq response", + )); } - // 7. prepare and send PSQ msg3 - debug!("sending PSQ msg3"); - if let Err(source) = self - .send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol) - .await - { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); - } + let initiator_authenticator = Authenticator::Dh(self.inner_state.local_peer.x25519().pk); - // 8. receive final ACK and finalise - debug!("received final ACK"); - if let Err(source) = self.receive_final_ack(&outer_aead_key).await { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); - } + let receiver_index = + lp_peer_config.derive_receiver_index(&initiator_authenticator, &encapsulation_key)?; - #[allow(clippy::expect_used)] - Ok(LpSession::new( - session_id, - self.protocol_version() - .expect("protocol version is known at this point"), - outer_aead_key, - self.local_peer.clone(), - remote_peer, - pq_shared_secret, - noise_protocol, - )) - } + let binding = PersistentSessionBinding { + initiator_authenticator, + responder_ecdh_pk: self.initiator_data.remote_peer.x25519_public, + responder_pq_pk: Some(encapsulation_key), + }; - // TODO: missing: receive counter check - pub async fn complete_as_initiator(mut self) -> Result - where - S: LpTransport + Unpin, - { - match self.complete_as_initiator_inner().await { - Ok(res) => Ok(res), - Err(err) => Err(self.try_send_error_packet(err).await), - } + let psq_session = psq_initiator.into_session()?; + LpSession::new(psq_session, binding, receiver_index, protocol) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::{decrypt_data, encrypt_data}; + use crate::peer::mock_peers; + use crate::peer_config::LP_PEER_CONFIG_SIZE; + use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size, responder}; + use nym_kkt::context::KKTMode; + use nym_kkt::responder::KKTResponder; + use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, IntoEnumIterator, KEM, SignatureScheme}; + use nym_test_utils::helpers::{DeterministicRng09Send, u64_seeded_rng_09}; + use nym_test_utils::mocks::async_read_write::MockIOStream; + use nym_test_utils::traits::{Leak, Timeboxed}; + + #[tokio::test] + async fn initiator_test_plain() -> anyhow::Result<()> { + for kem in KEM::iter() { + let conn_init = MockIOStream::default(); + let conn_resp = conn_init.try_get_remote_handle(); + + // leak the connections (JUST FOR THE PURPOSE OF THIS TEST!) + // so they'd get 'static lifetime + let conn_init = conn_init.leak(); + let conn_resp = conn_resp.leak(); + + let (mut init, mut resp) = mock_peers(); + let resp_remote = resp.as_remote(); + + let ciphersuite = Ciphersuite::default().with_kem(kem); + init.ciphersuite = ciphersuite; + resp.ciphersuite = ciphersuite; + let initiator_data = InitiatorData::new(1, resp_remote); + + let handshake_init = + PSQHandshakeState::new(conn_init, init).as_initiator(initiator_data); + + let mut init_rng = DeterministicRng09Send::new(u64_seeded_rng_09(1)); + + let init_fut = tokio::spawn(async move { + handshake_init + .complete_handshake_with_rng(&mut init_rng) + .timeboxed() + .await + }); + + // responder: + let supported_sigs = [SignatureScheme::Ed25519]; + let supported_hash = [ + HashFunction::Blake3, + HashFunction::Shake256, + HashFunction::Shake128, + HashFunction::SHA256, + ]; + let resp_keys = resp.kem_keypairs.as_ref().unwrap(); + let responder_x25519_keypair = resp.x25519(); + + let kkt_responder = KKTResponder::new( + responder_x25519_keypair, + resp_keys, + &supported_hash, + &supported_sigs, + &[1], + )?; + + // 1. read KKT request + let raw_kkt_req: handshake_message::KKTRequest = conn_resp + .receive_handshake_message( + KKTRequest::size_excluding_payload(KKTMode::OneWay, kem) + LP_PEER_CONFIG_SIZE, + ) + .timeboxed() + .await??; + let req = raw_kkt_req.into(); + + // 2. process + let processed_req = kkt_responder.process_request(req, LP_PEER_CONFIG_SIZE)?; + conn_resp + .send_handshake_message::( + processed_req.response.into(), + kem, + ) + .timeboxed() + .await??; + + // 3. read PSQ req + let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem)?; + let mut responder = + responder::build_psq_principal(rand09::rng(), 1, responder_ciphersuite)?; + let response_len = psq_msg1_size(kem); + + let msg: PSQMsg1 = conn_resp + .receive_handshake_message(response_len) + .timeboxed() + .await??; + responder.read_message(&msg, &mut []).unwrap(); + + // 4 send PSQ response + let mut buf = vec![0u8; PSQ_MSG2_SIZE]; + let n = responder.write_message(&[], &mut buf).unwrap(); + assert_eq!(n, buf.len()); + let msg = PSQMsg2::new(buf); + conn_resp + .send_handshake_message(msg, kem) + .timeboxed() + .await??; + + assert!(responder.is_handshake_finished()); + + let mut session_init = init_fut.await???; + + let mut r_transport = responder.into_session().unwrap(); + + // test serialization, deserialization + let channel_i = session_init.active_transport(); + let mut channel_r = r_transport.transport_channel().unwrap(); + + assert_eq!(channel_i.identifier(), channel_r.identifier()); + + let app_data_i = b"Derived session hey".as_slice(); + let app_data_r = b"Derived session ho".as_slice(); + + let ct_i = encrypt_data(app_data_i, channel_i)?; + let pt_r = decrypt_data(&ct_i, &mut channel_r)?; + + assert_eq!(app_data_i, pt_r); + + let ct_r = encrypt_data(app_data_r, &mut channel_r)?; + let pt_i = decrypt_data(&ct_r, channel_i)?; + + assert_eq!(app_data_r, pt_i); + } + Ok(()) } } diff --git a/common/nym-lp/src/psq/mod.rs b/common/nym-lp/src/psq/mod.rs index 06be6f26ec..b00ba087f0 100644 --- a/common/nym-lp/src/psq/mod.rs +++ b/common/nym-lp/src/psq/mod.rs @@ -1,172 +1,108 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::codec::OuterAeadKey; -use crate::message::ErrorPacketData; -use crate::packet::LpHeader; +use crate::packet::version; use crate::peer::{LpLocalPeer, LpRemotePeer}; -use crate::psq::helpers::LpTransportHandshakeExt; -use crate::{LpError, LpMessage, LpPacket}; -use nym_kkt::ciphersuite::Ciphersuite; -use nym_lp_transport::traits::LpTransport; -use tracing::debug; +use crate::transport::traits::LpHandshakeChannel; +use nym_kkt_ciphersuite::{HashFunction, IntoEnumIterator, KEM, SignatureScheme}; +pub(crate) mod handshake_message; mod helpers; -mod initiator; -mod responder; +pub mod initiator; +pub mod responder; -pub(crate) struct IntermediateHandshakeFailure { - /// Session id established during exchange if we managed to derive it - session_id: Option, +pub use initiator::PSQHandshakeStateInitiator; +pub use responder::PSQHandshakeStateResponder; - /// Protocol version established during the exchange - protocol_version: Option, +pub(crate) const AAD_INITIATOR_OUTER_V1: &[u8] = b"NYM-PQ-AAD-INIT-OUTER-V1"; +pub(crate) const AAD_INITIATOR_INNER_V1: &[u8] = b"NYM-PQ-AAD-INIT-INNER-V1"; +pub(crate) const AAD_RESPONDER_V1: &[u8] = b"NYM-PQ-AAD-RESP-V1"; +pub(crate) const SESSION_CONTEXT_V1: &[u8] = b"NYM-PQ-SESSION-CONTEXT-V1"; - /// Outer aead key established during exchange if we managed to derive it - outer_aead_key: Option, - - /// The error source - source: LpError, -} - -impl IntermediateHandshakeFailure { - fn plain(source: LpError) -> IntermediateHandshakeFailure { - IntermediateHandshakeFailure { - session_id: None, - protocol_version: None, - outer_aead_key: None, - source, - } +/// Size of the first (initiator) PSQ message including all serialisation overheads if no additional payload has been attached +pub(crate) fn psq_msg1_size(kem: KEM) -> usize { + match kem { + KEM::MlKem768 => 1247, + KEM::McEliece => 315, } } +/// Size of the second (responder) PSQ message including all serialisation overheads if no additional payload has been attached +pub(crate) const PSQ_MSG2_SIZE: usize = 70; + pub struct PSQHandshakeState<'a, S> { /// The underlying connection established for the handshake connection: &'a mut S, - /// Protocol version used for the exchange. - /// either known implicitly through the directory (initiator) - /// or established through client hello (responder) - protocol_version: Option, - - /// Ciphersuite selected for the KKT/PSQ exchange - ciphersuite: Ciphersuite, - /// Representation of a local Lewes Protocol peer /// encapsulating all the known information and keys. local_peer: LpLocalPeer, +} + +#[derive(Debug)] +pub struct InitiatorData { + /// Protocol version used for the exchange known implicitly through the directory + pub protocol_version: u8, /// Representation of a remote Lewes Protocol peer /// encapsulating all the known information and keys. - remote_peer: Option, + pub remote_peer: LpRemotePeer, +} - /// Counter for outgoing packets - sending_counter: u64, +impl InitiatorData { + pub fn new(protocol_version: u8, remote_peer: LpRemotePeer) -> Self { + InitiatorData { + protocol_version, + remote_peer, + } + } +} + +#[derive(Debug, Clone)] +pub struct ResponderData { + /// List of supported Hash Functions by this Responder + pub supported_hash_functions: Vec, + + /// List of supported Signature Schemes by this Responder + pub supported_signature_schemes: Vec, + + /// List of supported outer (LP) protocol version by this Responder + pub supported_outer_protocol_versions: Vec, +} + +impl Default for ResponderData { + fn default() -> Self { + // by default all schemes are supported + ResponderData { + supported_hash_functions: HashFunction::iter().collect(), + supported_signature_schemes: SignatureScheme::iter().collect(), + supported_outer_protocol_versions: vec![version::CURRENT], + } + } } impl<'a, S> PSQHandshakeState<'a, S> where - S: LpTransport + Unpin, + S: LpHandshakeChannel + Unpin, { - pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self { + pub fn new(connection: &'a mut S, local_peer: LpLocalPeer) -> Self { PSQHandshakeState { connection, - protocol_version: None, - ciphersuite, local_peer, - remote_peer: None, - sending_counter: 0, } } - #[must_use] - pub fn with_protocol_version(mut self, protocol_version: u8) -> Self { - self.protocol_version = Some(protocol_version); - self - } - - #[must_use] - pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self { - self.remote_peer = Some(remote_peer); - self - } - - fn protocol_version(&self) -> Result { - self.protocol_version - .ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version")) - } - - /// Generates the next counter value for outgoing packets. - pub fn next_counter(&mut self) -> u64 { - let counter = self.sending_counter; - self.sending_counter += 1; - counter - } - - pub fn next_packet( - &mut self, - session_id: u32, - protocol_version: u8, - message: LpMessage, - ) -> LpPacket { - let counter = self.next_counter(); - let header = LpHeader::new(session_id, counter, protocol_version); - LpPacket::new(header, message) - } - - pub(crate) async fn try_send_error_packet( - &mut self, - err: IntermediateHandshakeFailure, - ) -> LpError { - // if session_id is not known, we can't send the packet back (with the current design) - let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else { - return err.source; - }; - if let Err(err) = self - .send_error_packet( - session_id, - protocol, - err.source.to_string(), - err.outer_aead_key.as_ref(), - ) - .await - { - debug!("failed to send back error response: {err}") + pub fn as_initiator(self, initiator_data: InitiatorData) -> PSQHandshakeStateInitiator<'a, S> { + PSQHandshakeStateInitiator { + initiator_data, + inner_state: self, } - err.source } - /// Attempt to send an error packet - pub(crate) async fn send_error_packet( - &mut self, - session_id: u32, - protocol_version: u8, - msg: impl Into, - outer_aead_key: Option<&OuterAeadKey>, - ) -> Result<(), LpError> { - let packet = self.next_packet( - session_id, - protocol_version, - LpMessage::Error(ErrorPacketData::new(msg)), - ); - self.connection.send_packet(packet, outer_aead_key).await?; - Ok(()) - } - - /// Attempt to receive a packet from connection, explicitly checking for an error response - /// and returning corresponding message if received - pub(crate) async fn receive_non_error( - &mut self, - outer_aead_key: Option<&OuterAeadKey>, - ) -> Result { - let packet = self.connection.receive_packet(outer_aead_key).await?; - - match &packet.message { - LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!( - "remote error: {}", - error_packet.message - ))), - _ => Ok(packet), + pub fn as_responder(self, responder_data: ResponderData) -> PSQHandshakeStateResponder<'a, S> { + PSQHandshakeStateResponder { + responder_data, + inner_state: self, } } } @@ -174,167 +110,263 @@ where #[cfg(test)] mod tests { use super::*; + use crate::codec::{decrypt_data, encrypt_data}; use crate::peer::mock_peers; - use crate::psq::helpers::LpTransportHandshakeExt; - use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE; - use mock_instant::thread_local::MockClock; - use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme}; + use crate::peer_config::{LP_PEER_CONFIG_SIZE, LpPeerConfig}; + use libcrux_psq::handshake::types::Authenticator; + use libcrux_psq::session::{Session, SessionBinding}; + use libcrux_psq::{Channel, IntoSession}; + use nym_kkt::initiator::KKTInitiator; + use nym_kkt::responder::KKTResponder; + use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}; + use nym_test_utils::helpers::{ + DeterministicRng09Send, deterministic_rng_09, u64_seeded_rng_09, + }; use nym_test_utils::mocks::async_read_write::MockIOStream; use nym_test_utils::traits::{Leak, TimeboxedSpawnable}; - use std::time::Duration; use tokio::join; - #[allow(dead_code)] - async fn extract_error(conn: &mut MockIOStream) -> String { - let packet = conn.receive_packet(None).await.unwrap(); - match packet.message { - LpMessage::Error(error) => error.message, - _ => panic!("non error packet"), - } - } - #[tokio::test] async fn e2e_psq_handshake() -> anyhow::Result<()> { - let conn_init = MockIOStream::default(); - let conn_resp = conn_init.try_get_remote_handle(); + for kem in KEM::iter() { + let conn_init = MockIOStream::default(); + let conn_resp = conn_init.try_get_remote_handle(); - // leak the connections (JUST FOR THE PURPOSE OF THIS TEST!) - // so they'd get 'static lifetime - let conn_init = conn_init.leak(); - let conn_resp = conn_resp.leak(); + // leak the connections (JUST FOR THE PURPOSE OF THIS TEST!) + // so they'd get 'static lifetime + let conn_init = conn_init.leak(); + let conn_resp = conn_resp.leak(); + let ciphersuite = Ciphersuite::default().with_kem(kem); - let ciphersuite = Ciphersuite::new( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - HashLength::Default, - ); + let (mut init, mut resp) = mock_peers(); + init.ciphersuite = ciphersuite; + resp.ciphersuite = ciphersuite; + let resp_remote = resp.as_remote(); - let (init, resp) = mock_peers(); - let resp_remote = resp.as_remote(); + let handshake_init = PSQHandshakeState::new(conn_init, init) + .as_initiator(InitiatorData::new(1, resp_remote)); + let handshake_resp = + PSQHandshakeState::new(conn_resp, resp).as_responder(ResponderData::default()); - let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init) - .with_protocol_version(1) - .with_remote_peer(resp_remote); - let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp); + let init_rng = DeterministicRng09Send::new(u64_seeded_rng_09(1)); + let resp_rng = DeterministicRng09Send::new(u64_seeded_rng_09(2)); - let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed(); - let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed(); + // similarly leak the rngs to get the static lifetimes + let init_rng = init_rng.leak(); + let resp_rng = resp_rng.leak(); - let (session_init, session_resp) = join!(init_fut, resp_fut); + let init_fut = handshake_init + .complete_handshake_with_rng(init_rng) + .spawn_timeboxed(); + let resp_fut = handshake_resp + .complete_handshake_with_rng(resp_rng) + .spawn_timeboxed(); - let session_init = session_init???; - let session_resp = session_resp???; + let (session_init, session_resp) = join!(init_fut, resp_fut); - assert_eq!(session_init.id(), session_resp.id()); - assert_eq!( - session_init.outer_aead_key().as_bytes(), - session_resp.outer_aead_key().as_bytes() - ); - assert_eq!( - session_init.pq_shared_secret().as_bytes(), - session_resp.pq_shared_secret().as_bytes() - ); + let mut session_init = session_init???; + let mut session_resp = session_resp???; + + assert_eq!(session_init.receiver_index(), session_resp.receiver_index()); + + assert_eq!( + session_init.session_identifier(), + session_resp.session_identifier() + ); + + // test serialization, deserialization + let channel_i = session_init.active_transport(); + let channel_r = session_resp.active_transport(); + + assert_eq!(channel_i.identifier(), channel_r.identifier()); + + let app_data_i = b"Derived session hey".as_slice(); + let app_data_r = b"Derived session ho".as_slice(); + + let ct_i = encrypt_data(app_data_i, channel_i)?; + let pt_r = decrypt_data(&ct_i, channel_r)?; + + assert_eq!(app_data_i, pt_r); + + let ct_r = encrypt_data(app_data_r, channel_r)?; + let pt_i = decrypt_data(&ct_r, channel_i)?; + + assert_eq!(app_data_r, pt_i); + } Ok(()) } - #[tokio::test] - async fn preparing_client_hello_initiator() -> anyhow::Result<()> { - let mut conn_init = MockIOStream::default(); - let mut conn_resp = conn_init.try_get_remote_handle(); + // plain test without any wrappers + #[test] + fn e2e_test_plain() { + let mut rng = deterministic_rng_09(); - let ciphersuite = Ciphersuite::new( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - HashLength::Default, - ); - let (init, resp) = mock_peers(); - let resp_remote = resp.as_remote(); + for kem in KEM::iter() { + // SETUP START: + let protocol_version = 1; + let (mut init, resp) = mock_peers(); + init.ciphersuite = Ciphersuite::default().with_kem(kem); + let resp_remote = resp.as_remote(); + let dir_hash = resp_remote.expected_kem_key_hash(init.ciphersuite).unwrap(); - // as initiator - let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init) - .with_protocol_version(1) - .with_remote_peer(resp_remote); + let resp_keys = resp.kem_keypairs.as_ref().unwrap(); + let responder_x25519_keypair = resp.x25519(); - // you can generate and send (valid) client hello as initiator - let client_hello = handshake_init.send_client_hello().await?; - let LpMessage::ClientHello(received_client_hello) = - conn_resp.receive_packet(None).await?.message - else { - panic!("wrong message type"); - }; - assert_eq!(client_hello, received_client_hello); - Ok(()) - } + let supported_sigs = [SignatureScheme::Ed25519]; + let supported_hash = [ + HashFunction::Blake3, + HashFunction::Shake256, + HashFunction::Shake128, + HashFunction::SHA256, + ]; + let kkt_responder = KKTResponder::new( + responder_x25519_keypair, + resp_keys, + &supported_hash, + &supported_sigs, + &[protocol_version], + ) + .unwrap(); - // essentially make sure you can't accidentally trigger the handshake as the responder - #[tokio::test] - async fn preparing_client_hello_responder() -> anyhow::Result<()> { - let conn_init = MockIOStream::default(); - let mut conn_resp = conn_init.try_get_remote_handle(); + // SETUP END - let ciphersuite = Ciphersuite::new( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - HashLength::Default, - ); - let (_, resp) = mock_peers(); + let lp_peer_config = LpPeerConfig::new_client_to_entry(&mut rng, false); - // as initiator - let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp); + // OneWay - MlKem + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + init.ciphersuite, + &responder_x25519_keypair.pk, + &dir_hash, + protocol_version, + Some(Vec::from(lp_peer_config.serialize())), + ) + .unwrap(); - // you can generate and send (valid) client hello as initiator - let sending_res = handshake_resp.send_client_hello().await; - assert!(sending_res.is_err()); - Ok(()) - } + let processed_req = kkt_responder + .process_request(request, LP_PEER_CONFIG_SIZE) + .unwrap(); - #[tokio::test] - async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> { - let current_time = Duration::from_secs(10000); - MockClock::set_system_time(current_time); + let response = initiator + .process_response(processed_req.response, 0) + .unwrap(); + let encapsulation_key = response.encapsulation_key; - let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1); - let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1); + let mut payload_buf_responder = vec![0u8; 4096]; + let mut payload_buf_initiator = vec![0u8; 4096]; - let ciphersuite = Ciphersuite::new( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - HashLength::Default, - ); + let initiator_ciphersuite = + initiator::build_psq_ciphersuite(&init, &resp_remote, &encapsulation_key).unwrap(); + let mut initiator = initiator::build_psq_principal( + rand09::rng(), + protocol_version, + initiator_ciphersuite, + ) + .unwrap(); - // TOO OLD - let mut conn_init = MockIOStream::default(); - let mut conn_resp = conn_init.try_get_remote_handle(); - let (init, resp) = mock_peers(); + let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem).unwrap(); + let mut responder = responder::build_psq_principal( + rand09::rng(), + protocol_version, + responder_ciphersuite, + ) + .unwrap(); - let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp); - let client_hello_too_old = init.build_client_hello_data(too_old.as_secs()); + // Send first message + let mut buf = vec![0u8; psq_msg1_size(kem)]; + let len_i = initiator.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_i, buf.len()); - conn_init - .send_packet(client_hello_too_old.into_lp_packet(1), None) - .await?; - let err = handshake_resp.receive_client_hello().await.unwrap_err(); - assert!(err.to_string().contains("too old")); + // Read first message + let (_, _) = responder + .read_message(&buf, &mut payload_buf_responder) + .unwrap(); - // TOO RECENT - let mut conn_init = MockIOStream::default(); - let mut conn_resp = conn_init.try_get_remote_handle(); - let (init, resp) = mock_peers(); + // Get the authenticator out here, so we can deserialize the session later. + let Some(initiator_authenticator) = responder.initiator_authenticator() else { + panic!("No initiator authenticator found") + }; - let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp); - let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs()); + // Respond + let mut buf = [0u8; PSQ_MSG2_SIZE]; + let len_r = responder.write_message(&[], &mut buf).unwrap(); + assert_eq!(len_r, buf.len()); - conn_init - .send_packet(client_hello_too_recent.into_lp_packet(1), None) - .await?; - let err = handshake_resp.receive_client_hello().await.unwrap_err(); + // Finalize on registration initiator + let (len_i_deserialized, _) = initiator + .read_message(&buf, &mut payload_buf_initiator) + .unwrap(); - assert!(err.to_string().contains("too future")); - Ok(()) + // We read the same amount of data. + assert_eq!(len_r, len_i_deserialized); + + // Ready for transport mode + assert!(initiator.is_handshake_finished()); + assert!(responder.is_handshake_finished()); + + let i_transport = initiator.into_session().unwrap(); + let r_transport = responder.into_session().unwrap(); + + // test serialization, deserialization + let mut session_storage = vec![0u8; 4096]; + i_transport + .serialize( + &mut session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(init.x25519().pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()), + }, + ) + .unwrap(); + let mut i_transport = Session::deserialize( + &session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(init.x25519().pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()), + }, + ) + .unwrap(); + + r_transport + .serialize( + &mut session_storage, + SessionBinding { + initiator_authenticator: &initiator_authenticator, + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()), + }, + ) + .unwrap(); + let mut r_transport = Session::deserialize( + &session_storage, + SessionBinding { + initiator_authenticator: &initiator_authenticator, + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()), + }, + ) + .unwrap(); + + let mut channel_i = i_transport.transport_channel().unwrap(); + let mut channel_r = r_transport.transport_channel().unwrap(); + + assert_eq!(channel_i.identifier(), channel_r.identifier()); + + let app_data_i = b"Derived session hey".as_slice(); + let app_data_r = b"Derived session ho".as_slice(); + + let ct_i = encrypt_data(app_data_i, &mut channel_i).unwrap(); + let pt_r = decrypt_data(&ct_i, &mut channel_r).unwrap(); + + assert_eq!(app_data_i, pt_r); + + let ct_r = encrypt_data(app_data_r, &mut channel_r).unwrap(); + let pt_i = decrypt_data(&ct_r, &mut channel_i).unwrap(); + + assert_eq!(app_data_r, pt_i); + } } } diff --git a/common/nym-lp/src/psq/responder.rs b/common/nym-lp/src/psq/responder.rs index 8882e1269f..1b85202602 100644 --- a/common/nym-lp/src/psq/responder.rs +++ b/common/nym-lp/src/psq/responder.rs @@ -1,461 +1,351 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::codec::OuterAeadKey; -use crate::message::{HandshakeData, KKTResponseData, MessageType}; -use crate::noise_protocol::NoiseProtocol; -use crate::peer::LpRemotePeer; -use crate::psk::psq_responder_process_message; -use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp}; -use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState}; -use crate::session::PqSharedSecret; -use crate::{ClientHelloData, LpError, LpMessage, LpSession}; -use nym_kkt::KKT_RESPONSE_AAD; -use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; -use nym_kkt::context::KKTContext; -use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame}; -use nym_kkt::frame::KKTSessionId; -use nym_kkt::session::{responder_ingest_message, responder_process}; -use nym_lp_transport::traits::LpTransport; -use rand09::rng; -use std::time::Duration; +use crate::peer::LpLocalPeer; +use crate::peer_config::{LP_PEER_CONFIG_SIZE, LpPeerConfig}; +use crate::psq::handshake_message::{PSQMsg1, PSQMsg2}; +use crate::psq::helpers::kem_to_ciphersuite; +use crate::psq::{ + AAD_RESPONDER_V1, PSQ_MSG2_SIZE, PSQHandshakeState, ResponderData, SESSION_CONTEXT_V1, + handshake_message, psq_msg1_size, +}; +use crate::session::PersistentSessionBinding; +use crate::transport::traits::{HandshakeMessage, LpHandshakeChannel}; +use crate::{LpError, LpSession}; +use libcrux_psq::handshake::Responder; +use libcrux_psq::handshake::builders::{ + CiphersuiteBuilder, PrincipalBuilder, ResponderCiphersuite, +}; +use libcrux_psq::{Channel, IntoSession}; +use nym_kkt::context::KKTMode; +use nym_kkt::message::{KKTRequest, KKTResponse, ProcessedKKTRequest}; +use nym_kkt::responder::KKTResponder; +use nym_kkt_ciphersuite::KEM; +use rand09::SeedableRng; use tracing::debug; -pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30); - -// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder -fn validate_client_hello_timestamp( - client_timestamp: u64, - tolerance: Duration, -) -> Result<(), LpError> { - let now = current_timestamp()?; - - let age = now.abs_diff(client_timestamp); - if age > tolerance.as_secs() { - let direction = if now >= client_timestamp { - "old" - } else { - "future" - }; - - return Err(LpError::kkt_psq_handshake(format!( - "ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)", - tolerance.as_secs() - ))); - } - - Ok(()) +pub struct PSQHandshakeStateResponder<'a, S> { + pub(super) inner_state: PSQHandshakeState<'a, S>, + pub(super) responder_data: ResponderData, } -impl<'a, S> PSQHandshakeState<'a, S> +pub(crate) fn build_psq_principal( + rng: R, + version: u8, + ciphersuite: ResponderCiphersuite, +) -> Result, LpError> where - S: LpTransport + Unpin, + R: rand09::CryptoRng, { - pub(crate) fn encapsulated_kem_keys( - &self, - ) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> { - let kem_keys = self + let (ctx, aad) = match version { + 1 => (SESSION_CONTEXT_V1, AAD_RESPONDER_V1), + other => return Err(LpError::UnsupportedVersion { version: other }), + }; + + PrincipalBuilder::new(rng) + .context(ctx) + .outer_aad(aad) + .recent_keys_upper_bound(30) + .build_responder(ciphersuite) + .map_err(|inner| LpError::PSQResponderBuilderFailure { inner }) +} + +pub(crate) fn build_psq_ciphersuite( + peer: &LpLocalPeer, + kem: KEM, +) -> Result, LpError> { + let Some(kem_keys) = peer.kem_keypairs.as_ref() else { + return Err(LpError::ResponderWithMissingKEMKeys); + }; + + let psq_ciphersuite = kem_to_ciphersuite(kem); + let builder = CiphersuiteBuilder::new(psq_ciphersuite).longterm_x25519_keys(peer.x25519()); + + match kem { + KEM::MlKem768 => builder + .longterm_mlkem_encapsulation_key(kem_keys.ml_kem768_encapsulation_key()) + .longterm_mlkem_decapsulation_key(kem_keys.ml_kem768_decapsulation_key()), + KEM::McEliece => builder + .longterm_cmc_encapsulation_key(kem_keys.mc_eliece_encapsulation_key()) + .longterm_cmc_decapsulation_key(kem_keys.mc_eliece_decapsulation_key()), + } + .build_responder_ciphersuite() + .map_err(|inner| LpError::PSQResponderBuilderFailure { inner }) +} + +impl<'a, S> PSQHandshakeStateResponder<'a, S> +where + S: LpHandshakeChannel + Unpin, +{ + /// Attempt to receive a KKT request from a one-way client + async fn receive_one_way_kkt_request(&mut self) -> Result { + let packet_len = KKTRequest::size_excluding_payload( + KKTMode::OneWay, + self.inner_state.local_peer.ciphersuite.kem(), + ) + LP_PEER_CONFIG_SIZE; + + let req = self + .inner_state + .connection + .receive_handshake_message::(packet_len) + .await?; + + Ok(req.into()) + } + + /// Attempt to process the received KKT request + fn process_kkt_request(&self, kkt_request: KKTRequest) -> Result { + let kem_keys = &self + .inner_state .local_peer - .kem_psq + .kem_keypairs .as_ref() - .ok_or(LpError::ResponderWithMissingKEMKey)?; + .ok_or(LpError::ResponderWithMissingKEMKeys)?; - let libcrux_private_key = libcrux_kem::PrivateKey::decode( - libcrux_kem::Algorithm::X25519, - kem_keys.private_key().as_bytes(), - ) - .map_err(|e| { - LpError::KKTError(format!( - "Failed to convert X25519 private key to libcrux PrivateKey: {e:?}", - )) - })?; - let dec_key = DecapsulationKey::X25519(libcrux_private_key); - - let libcrux_public_key = libcrux_kem::PublicKey::decode( - libcrux_kem::Algorithm::X25519, - kem_keys.public_key().as_bytes(), - ) - .map_err(|e| { - LpError::KKTError(format!( - "Failed to convert X25519 public key to libcrux PublicKey: {e:?}", - )) - })?; - let enc_key = EncapsulationKey::X25519(libcrux_public_key); - Ok((dec_key, enc_key)) - } - - /// Attempt to receive and validate ClientHello - pub(crate) async fn receive_client_hello( - &mut self, - ) -> Result<(ClientHelloData, LpRemotePeer), LpError> { - let client_hello_packet = self.receive_non_error(None).await?; - let client_hello = match client_hello_packet.message { - LpMessage::ClientHello(client_hello) => client_hello, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::ClientHello, - )); - } - }; - - validate_client_hello_timestamp( - client_hello.extract_timestamp(), - DEFAULT_TIMESTAMP_TOLERANCE, - )?; - - // TODO: somehow check for collision - - // set version and remote peer information - self.protocol_version = Some(client_hello_packet.header.protocol_version); - let remote_peer = LpRemotePeer::new( - client_hello.client_ed25519_public_key, - client_hello.client_lp_public_key, - ); - - Ok((client_hello, remote_peer)) - } - - /// Send client hello ACK - pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> { - let protocol = self.protocol_version()?; - - let ack = self.next_packet(session_id, protocol, LpMessage::Ack); - self.connection.send_packet(ack, None).await?; - Ok(()) - } - - /// Attempt to receive and process a KKT request - pub(crate) async fn receive_kkt_request( - &mut self, - ) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> { - let kkt_request = match self.receive_non_error(None).await?.message { - LpMessage::KKTRequest(request) => request.0, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::KKTRequest, - )); - } - }; - - let (session_secret, request_frame, remote_context) = - decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?; - let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?; - - Ok((context, session_secret, request_frame.session_id())) + let processed_req = KKTResponder::new( + &self.inner_state.local_peer.x25519, + kem_keys, + &self.responder_data.supported_hash_functions, + &self.responder_data.supported_signature_schemes, + &self.responder_data.supported_outer_protocol_versions, + )? + .process_request(kkt_request, LP_PEER_CONFIG_SIZE)?; + Ok(processed_req) } /// Attempt to send KKT response to the previously received request - pub(crate) async fn send_kkt_response( - &mut self, - session_id: u32, - (kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId), - encapsulation_key: &EncapsulationKey<'_>, - ) -> Result<(), LpError> { - let protocol = self.protocol_version()?; - - let response_frame = responder_process( - &kkt_context, - kkt_session_id, - self.local_peer.ed25519().private_key(), - encapsulation_key, - )?; - let encrypted_frame = encrypt_kkt_frame( - &mut rng(), - &session_secret, - &response_frame, - KKT_RESPONSE_AAD, - )?; - let lp_message = KKTResponseData::new(encrypted_frame).into(); - let lp_packet = self.next_packet(session_id, protocol, lp_message); - - self.connection.send_packet(lp_packet, None).await?; + async fn send_kkt_response(&mut self, response: KKTResponse, kem: KEM) -> Result<(), LpError> { + self.inner_state + .connection + .send_handshake_message::(response.into(), kem) + .await?; Ok(()) } /// Attempt to receive and process a PSQ msg1 request - pub(crate) async fn receive_psq_initiator_message( - &mut self, - remote_peer: &LpRemotePeer, - local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>), - salt: &[u8; 32], - session_id_bytes: &[u8; 4], - ) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec), LpError> { - let psq_msg1 = match self.receive_non_error(None).await?.message { - LpMessage::Handshake(response) => response.0, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::Handshake, - )); - } - }; - - // Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg] - if psq_msg1.len() < 2 { - return Err(LpError::kkt_psq_handshake("too short msg1 received")); - } - let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize; - if psq_msg1.len() < 2 + handle_len { - return Err(LpError::kkt_psq_handshake("too short msg1 received")); - } - let psq_payload = &psq_msg1[2..2 + handle_len]; - let noise_payload = &psq_msg1[2 + handle_len..]; - - // Decapsulate PSK from PSQ payload using X25519 as DHKEM - let psq_responder = psq_responder_process_message( - self.local_peer.x25519.private_key(), - &remote_peer.x25519_public, - local_kem_keypair, - &remote_peer.ed25519_public, - psq_payload, - salt, - session_id_bytes, - )?; - - let psk = psq_responder.psk; - let psk_handle = psq_responder.psk_handle; - - // TEMP \/ - let outer_aead_key = OuterAeadKey::from_psk(&psk); - // TEMP /\ - - let mut noise_protocol = NoiseProtocol::build_new_responder( - self.local_peer.x25519().private_key().as_bytes(), - remote_peer.x25519_public.as_bytes(), - &psk, - )?; - noise_protocol.read_message(noise_payload)?; - - Ok(( - outer_aead_key, - noise_protocol, - PqSharedSecret::new(psq_responder.pq_shared_secret), - psk_handle, - )) - } - - /// Attempt to prepare and generate a responder PSQ msg2 - pub(crate) async fn send_psq_responder_message( - &mut self, - session_id: u32, - psk_handle: &[u8], - outer_aead_key: &OuterAeadKey, - noise_protocol: &mut NoiseProtocol, - ) -> Result<(), LpError> { - let protocol = self.protocol_version()?; - - let msg2 = noise_protocol - .get_bytes_to_send() - .ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??; - // Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg] - let handle_len = psk_handle.len() as u16; - let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len()); - combined.extend_from_slice(&handle_len.to_le_bytes()); - combined.extend_from_slice(psk_handle); - combined.extend_from_slice(&msg2); - - let lp_message = HandshakeData::new(combined).into(); - let lp_packet = self.next_packet(session_id, protocol, lp_message); - self.connection - .send_packet(lp_packet, Some(outer_aead_key)) - .await?; - Ok(()) - } - - /// Attempt to receive and process final PSQ msg3 - pub(crate) async fn receive_final_psq_message( - &mut self, - outer_aead_key: &OuterAeadKey, - noise_protocol: &mut NoiseProtocol, - ) -> Result<(), LpError> { - let psq_msg3 = match self + async fn receive_psq_initiator_message(&mut self, kem: KEM) -> Result, LpError> { + let packet_len = psq_msg1_size(kem); + let msg: PSQMsg1 = self + .inner_state .connection - .receive_packet(Some(outer_aead_key)) - .await? - .message - { - LpMessage::Handshake(response) => response.0, - other => { - return Err(LpError::unexpected_handshake_response( - other.typ(), - MessageType::Handshake, - )); - } - }; - - noise_protocol.read_message(&psq_msg3)?; - if !noise_protocol.is_handshake_finished() { - return Err(LpError::kkt_psq_handshake( - "noise handshake not finished after msg3", - )); - } - Ok(()) - } - - /// Send final ACK to indicate finalisation of the handshake - pub(crate) async fn send_final_ack( - &mut self, - session_id: u32, - outer_aead_key: &OuterAeadKey, - ) -> Result<(), LpError> { - let protocol = self.protocol_version()?; - - let ack = self.next_packet(session_id, protocol, LpMessage::Ack); - self.connection - .send_packet(ack, Some(outer_aead_key)) + .receive_handshake_message(packet_len) .await?; - Ok(()) + Ok(msg.into_bytes()) } - async fn complete_as_responder_inner( - &mut self, - ) -> Result + pub async fn complete_handshake(self) -> Result where - S: LpTransport + Unpin, + S: LpHandshakeChannel + Unpin, { - // 1. receive and validate ClientHello - let (client_hello_data, remote_peer) = - self.receive_client_hello() - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: None, - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; - debug!("received client hello"); + let mut rng = rand09::rngs::StdRng::from_os_rng(); + self.complete_handshake_with_rng(&mut rng).await + } - let session_id = client_hello_data.receiver_index; - let session_id_bytes = session_id.to_le_bytes(); - let salt = client_hello_data.salt; - - // 2. send ack - debug!("sending client hello ACK"); - self.send_client_hello_ack(session_id) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; - - // 3. receive and process KKT request - let kkt_data = - self.receive_kkt_request() - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + pub async fn complete_handshake_with_rng(mut self, rng: &mut R) -> Result + where + S: LpHandshakeChannel + Unpin, + R: rand09::CryptoRng, + { + // 1. receive and process KKTRequest + let kkt_request = self.receive_one_way_kkt_request().await?; debug!("received KKT request"); - // TEMP: 'derive' KEM keys - let (dec_key, enc_key) = - self.encapsulated_kem_keys() - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + let processed_req = self.process_kkt_request(kkt_request)?; + let kem = processed_req.requested_kem; - // 4. prepare and send KKT response + let lp_peer_config = LpPeerConfig::deserialize(&processed_req.request_payload)?; + + // 2. send back the KKTResponse debug!("sending KKT response"); - self.send_kkt_response(session_id, kkt_data, &enc_key) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + self.send_kkt_response(processed_req.response, kem).await?; - // 5. receive and process PSQ msg1 - debug!("received PSQ msg1"); - let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self - .receive_psq_initiator_message( - &remote_peer, - (&dec_key, &enc_key), - &salt, - &session_id_bytes, - ) - .await - .map_err(|source| IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: None, - source, - })?; + // 3. receive and process PSQ request + let raw_psq1 = self.receive_psq_initiator_message(kem).await?; + debug!("received PSQ handshake msg"); - // 6. prepare and send PSQ msg2 - debug!("sending PSQ msg2"); - if let Err(source) = self - .send_psq_responder_message( - session_id, - &psk_handle, - &outer_aead_key, - &mut noise_protocol, - ) - .await - { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); + // construct the responder and process the message + let responder_ciphersuite = build_psq_ciphersuite(&self.inner_state.local_peer, kem)?; + let version = processed_req.outer_protocol_version; + let mut psq_responder = build_psq_principal(rng, version, responder_ciphersuite)?; + psq_responder.read_message(&raw_psq1, &mut [])?; + + let initiator_authenticator = psq_responder + .initiator_authenticator() + .ok_or(LpError::MissingInitiatorAuthenticator)?; + + // 4. send PSQ response + let conn = self.inner_state.connection; + + let mut buf = vec![0u8; PSQ_MSG2_SIZE]; + psq_responder.write_message(&[], &mut buf)?; + debug!("sending PSQ handshake msg"); + conn.send_handshake_message(PSQMsg2::new(buf), kem).await?; + + if !psq_responder.is_handshake_finished() { + return Err(LpError::kkt_psq_handshake( + "handshake not finished after receiving psq response", + )); } - // 7. receive and process PSQ msg3 - debug!("received PSQ msg3"); - if let Err(source) = self - .receive_final_psq_message(&outer_aead_key, &mut noise_protocol) - .await - { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); - } + // SAFETY: we have completed the exchange so this key MUST HAVE been present + #[allow(clippy::unwrap_used)] + let kem_key = self + .inner_state + .local_peer + .kem_keypairs + .as_ref() + .unwrap() + .encapsulation_key(kem) + .unwrap(); - // 8. [optionally] send ACK to finalise - debug!("sending final ACK"); - if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await { - return Err(IntermediateHandshakeFailure { - session_id: Some(session_id), - protocol_version: self.protocol_version, - outer_aead_key: Some(outer_aead_key), - source, - }); - } + let receiver_index = + lp_peer_config.derive_receiver_index(&initiator_authenticator, &kem_key)?; - #[allow(clippy::expect_used)] - Ok(LpSession::new( - session_id, - self.protocol_version() - .expect("protocol version is known at this point"), - outer_aead_key, - self.local_peer.clone(), - remote_peer, - pq_shared_secret, - noise_protocol, - )) - } + let binding = PersistentSessionBinding { + initiator_authenticator, + responder_ecdh_pk: self.inner_state.local_peer.x25519().pk, + responder_pq_pk: Some(kem_key), + }; - pub async fn complete_as_responder(mut self) -> Result - where - S: LpTransport + Unpin, - { - match self.complete_as_responder_inner().await { - Ok(res) => Ok(res), - Err(err) => Err(self.try_send_error_packet(err).await), - } + let psq_session = psq_responder.into_session()?; + LpSession::new( + psq_session, + binding, + receiver_index, + processed_req.outer_protocol_version, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::{decrypt_data, encrypt_data}; + use crate::peer::mock_peers; + use crate::peer_config::LpPeerConfig; + use crate::psq::initiator; + use nym_kkt::initiator::KKTInitiator; + use nym_kkt_ciphersuite::{Ciphersuite, IntoEnumIterator}; + use nym_test_utils::helpers::{ + DeterministicRng09Send, deterministic_rng_09, u64_seeded_rng_09, + }; + use nym_test_utils::mocks::async_read_write::MockIOStream; + use nym_test_utils::traits::{Leak, Timeboxed}; + + #[tokio::test] + async fn responder_test_plain() -> anyhow::Result<()> { + for kem in KEM::iter() { + let conn_init = MockIOStream::default(); + let conn_resp = conn_init.try_get_remote_handle(); + + // SETUP START: + // leak the connections (JUST FOR THE PURPOSE OF THIS TEST!) + // so they'd get 'static lifetime + let conn_init = conn_init.leak(); + let conn_resp = conn_resp.leak(); + + let (mut init, mut resp) = mock_peers(); + let resp_remote = resp.as_remote(); + + let ciphersuite = Ciphersuite::default().with_kem(kem); + init.ciphersuite = ciphersuite; + resp.ciphersuite = ciphersuite; + + let responder_data = ResponderData::default(); + let handshake_resp = + PSQHandshakeState::new(conn_resp, resp).as_responder(responder_data); + + let mut resp_rng = DeterministicRng09Send::new(u64_seeded_rng_09(2)); + let resp_fut = tokio::spawn(async move { + handshake_resp + .complete_handshake_with_rng(&mut resp_rng) + .timeboxed() + .await + }); + + // initiator: + + let mut rng = deterministic_rng_09(); + let dir_hash = resp_remote.expected_kem_key_hash(init.ciphersuite)?; + + let lp_peer_config = LpPeerConfig::new_client_to_entry(&mut rng, false); + + // OneWay - MlKem + let (mut initiator, request) = KKTInitiator::generate_one_way_request( + &mut rng, + init.ciphersuite, + &resp_remote.x25519_public, + &dir_hash, + 1, + Some(Vec::from(lp_peer_config.serialize())), + )?; + + // 1. send kkt request + conn_init + .send_handshake_message::(request.into(), kem) + .timeboxed() + .await??; + + // 2. receive KKT response + let response_len = KKTResponse::size_excluding_payload(kem); + let resp: handshake_message::KKTResponse = conn_init + .receive_handshake_message(response_len) + .timeboxed() + .await??; + let kkt_response = resp.into(); + + let response = initiator.process_response(kkt_response, 0)?; + let encapsulation_key = response.encapsulation_key; + + let initiator_ciphersuite = + initiator::build_psq_ciphersuite(&init, &resp_remote, &encapsulation_key)?; + let mut initiator = + initiator::build_psq_principal(rand09::rng(), 1, initiator_ciphersuite)?; + + // 3. send PSQ msg1 + // Send first message + let mut buf = vec![0u8; psq_msg1_size(kem)]; + let n = initiator.write_message(&[], &mut buf).unwrap(); + assert_eq!(n, buf.len()); + let msg = PSQMsg1::new(buf); + conn_init + .send_handshake_message(msg, kem) + .timeboxed() + .await??; + + // 4. receive PSQ msg2 + let msg: PSQMsg2 = conn_init + .receive_handshake_message(PSQ_MSG2_SIZE) + .timeboxed() + .await??; + initiator.read_message(&msg, &mut []).unwrap(); + + assert!(initiator.is_handshake_finished()); + + let mut session_resp = resp_fut.await???; + + let mut i_transport = initiator.into_session().unwrap(); + + // test serialization, deserialization + let mut channel_i = i_transport.transport_channel().unwrap(); + let channel_r = session_resp.active_transport(); + + assert_eq!(channel_i.identifier(), channel_r.identifier()); + + let app_data_i = b"Derived session hey".as_slice(); + let app_data_r = b"Derived session ho".as_slice(); + + let ct_i = encrypt_data(app_data_i, &mut channel_i)?; + let pt_r = decrypt_data(&ct_i, channel_r)?; + + assert_eq!(app_data_i, pt_r); + + let ct_r = encrypt_data(app_data_r, channel_r)?; + let pt_i = decrypt_data(&ct_r, &mut channel_i)?; + + assert_eq!(app_data_r, pt_i); + } + + Ok(()) } } diff --git a/common/nym-lp/src/replay/validator.rs b/common/nym-lp/src/replay/validator.rs index 3e7a0cfa5d..d3edb7b84b 100644 --- a/common/nym-lp/src/replay/validator.rs +++ b/common/nym-lp/src/replay/validator.rs @@ -38,6 +38,16 @@ const N_WORDS: usize = 16; /// Total number of bits in the bitmap const N_BITS: usize = WORD_SIZE * N_WORDS; +/// Current packet count statistics +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct PacketCount { + /// the next expected counter value + pub next: u64, + + /// the total number of received packets + pub received: u64, +} + /// Validator for receiving key counters to prevent replay attacks. /// /// This structure maintains a bitmap of received packets and validates @@ -205,11 +215,14 @@ impl ReceivingKeyCounterValidator { /// Returns the current packet count statistics. /// - /// Returns a tuple of `(next, receive_cnt)` where: + /// Returns a struct consisting of `(next, receive_cnt)` where: /// - `next` is the next expected counter value /// - `receive_cnt` is the total number of received packets - pub fn current_packet_cnt(&self) -> (u64, u64) { - (self.next, self.receive_cnt) + pub fn current_packet_cnt(&self) -> PacketCount { + PacketCount { + next: self.next, + received: self.receive_cnt, + } } #[inline(always)] @@ -481,7 +494,10 @@ mod tests { let mut validator = ReceivingKeyCounterValidator::default(); // Initial state - let (next, count) = validator.current_packet_cnt(); + let PacketCount { + next, + received: count, + } = validator.current_packet_cnt(); assert_eq!(next, 0); assert_eq!(count, 0); @@ -490,21 +506,30 @@ mod tests { assert!(validator.mark_did_receive_branchless(1).is_ok()); assert!(validator.mark_did_receive_branchless(2).is_ok()); - let (next, count) = validator.current_packet_cnt(); + let PacketCount { + next, + received: count, + } = validator.current_packet_cnt(); assert_eq!(next, 3); assert_eq!(count, 3); // After an out of order packet assert!(validator.mark_did_receive_branchless(10).is_ok()); - let (next, count) = validator.current_packet_cnt(); + let PacketCount { + next, + received: count, + } = validator.current_packet_cnt(); assert_eq!(next, 11); assert_eq!(count, 4); // After a packet from the past (within window) assert!(validator.mark_did_receive_branchless(5).is_ok()); - let (next, count) = validator.current_packet_cnt(); + let PacketCount { + next, + received: count, + } = validator.current_packet_cnt(); assert_eq!(next, 11); // Next doesn't change assert_eq!(count, 5); // Count increases } @@ -553,7 +578,7 @@ mod tests { assert!(validator.mark_did_receive_branchless(first_jump).is_ok()); // Verify next counter is updated - let (next, _) = validator.current_packet_cnt(); + let PacketCount { next, .. } = validator.current_packet_cnt(); assert_eq!(next, first_jump + 1); // Second large jump, even further ahead @@ -561,7 +586,7 @@ mod tests { assert!(validator.mark_did_receive_branchless(second_jump).is_ok()); // Verify next counter is updated again - let (next, _) = validator.current_packet_cnt(); + let PacketCount { next, .. } = validator.current_packet_cnt(); assert_eq!(next, second_jump + 1); // Test packets within the new window @@ -726,10 +751,10 @@ mod tests { // Check final state of the validator let final_state = validator.lock().unwrap(); - let (_next, receive_cnt) = final_state.current_packet_cnt(); + let count = final_state.current_packet_cnt(); // Verify that the received count matches our successful operations - assert_eq!(receive_cnt, total_successes as u64); + assert_eq!(count.received, total_successes as u64); } #[test] diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 52a336cfca..c92d967dee 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -4,218 +4,182 @@ //! Session management for the Lewes Protocol. //! //! This module implements session management functionality, including replay protection -//! and Noise protocol state handling. -use crate::codec::OuterAeadKey; -use crate::message::EncryptedDataPayload; -// noiserm -use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult}; -use crate::packet::LpHeader; +use crate::codec::{decrypt_lp_packet, encrypt_lp_packet}; +use crate::packet::{ApplicationData, EncryptedLpPacket, LpHeader, LpMessage, LpPacket}; use crate::peer::{LpLocalPeer, LpRemotePeer}; -use crate::psk::derive_subsession_psk; -use crate::psq::PSQHandshakeState; -use crate::replay::ReceivingKeyCounterValidator; -use crate::{LpError, LpMessage, LpPacket}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; -use nym_lp_transport::traits::LpTransport; -use parking_lot::Mutex; -use snow::Builder; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use crate::peer_config::LpReceiverIndex; +use crate::psq::{ + InitiatorData, PSQHandshakeState, PSQHandshakeStateInitiator, PSQHandshakeStateResponder, + ResponderData, +}; +use crate::replay::validator::PacketCount; +use crate::transport::LpHandshakeChannel; +use crate::{LpError, replay::ReceivingKeyCounterValidator}; +use libcrux_psq::handshake::types::{Authenticator, DHPublicKey}; +use libcrux_psq::session::{Session, SessionBinding}; +use nym_kkt::keys::EncapsulationKey; +use std::fmt::{Debug, Formatter}; -/// PQ shared secret wrapper with automatic memory zeroization. -/// Ensures K_pq is cleared from memory when dropped. -#[derive(Clone, Zeroize, ZeroizeOnDrop)] -pub struct PqSharedSecret([u8; 32]); - -impl PqSharedSecret { - pub fn new(secret: [u8; 32]) -> Self { - Self(secret) - } - - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -impl std::fmt::Debug for PqSharedSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PqSharedSecret") - .field("secret", &"") - .finish() - } -} +pub type SessionId = [u8; 32]; /// A session in the Lewes Protocol, handling connection state with Noise. /// /// Sessions manage connection state, including LP replay protection. /// Each session has a unique receiving index and sending index for connection identification. -#[derive(Debug)] pub struct LpSession { - /// Id of the established session - session_id: u32, + /// The underlying established session + psq_session: Session, + + /// The public key material bound to the underlying session. Used for serialisation. + session_binding: PersistentSessionBinding, + + /// The current active transport channel + // In the future it might get split between UDP and TCP transports + active_transport: libcrux_psq::session::Transport, + + /// Look-up index established during the initial KKT exchange + receiver_index: LpReceiverIndex, /// Negotiated protocol version from handshake. - /// Set during handshake completion from the ClientHello/ServerHello packet header. - /// Used for future version negotiation and compatibility checks. - version: u8, - - /// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake). - outer_aead_key: OuterAeadKey, - - /// Representation of a local Lewes Protocol peer - /// encapsulating all the known information and keys. - local_peer: LpLocalPeer, - - /// Representation of a remote Lewes Protocol peer - /// encapsulating all the known information and keys. - remote_peer: LpRemotePeer, - - // TODO: ALL BELOW maybe not needed after all? - /// Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation. - /// Stored after PSQ handshake completes for subsession PSK derivation. - pq_shared_secret: PqSharedSecret, - - /// Noise protocol state machine - noise_state: NoiseProtocol, + protocol_version: u8, /// Counter for outgoing packets sending_counter: u64, /// Validator for incoming packet counters to prevent replay attacks receiving_counter: ReceivingKeyCounterValidator, +} - /// Monotonically increasing counter for subsession indices. - /// Each subsession gets a unique index to ensure unique PSK derivation. - /// Uses u64 to make overflow practically impossible (~585k years at 1M/sec). - subsession_counter: u64, +/// Wraps public key material that is bound to a session. +#[derive(Clone)] +pub struct PersistentSessionBinding { + /// The initiator's authenticator value, i.e. a long-term DH public value or signature verification key. + pub initiator_authenticator: Authenticator, - /// True if this session has been demoted to read-only mode. - /// Demoted sessions can still receive/decrypt but cannot send/encrypt. - read_only: bool, + /// The responder's long term DH public value. + pub responder_ecdh_pk: DHPublicKey, - /// ID of the successor session that replaced this one. - /// Set when demote() is called. - successor_session_id: Option, + /// The responder's long term PQ-KEM public key (if any). + pub responder_pq_pk: Option, +} + +impl Debug for PersistentSessionBinding { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PersistentSessionBinding") + .field("initiator_authenticator", &"") + .field("responder_ecdh_pk", &self.responder_ecdh_pk) + .field("responder_pq_pk", &self.responder_pq_pk) + .finish() + } +} + +impl<'a> From<&'a PersistentSessionBinding> for SessionBinding<'a> { + fn from(value: &'a PersistentSessionBinding) -> Self { + SessionBinding { + initiator_authenticator: &value.initiator_authenticator, + responder_ecdh_pk: &value.responder_ecdh_pk, + responder_pq_pk: value + .responder_pq_pk + .as_ref() + .map(|k| k.as_pq_encapsulation_key()), + } + } +} + +impl Debug for LpSession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LpSession") + .field("session_id", &self.psq_session.identifier()) + .field("session_binding", &self.session_binding) + .field("active_transport_id", &self.active_transport.identifier()) + .field("protocol_version", &self.protocol_version) + .field("sending_counter", &self.sending_counter) + .field("receiving_counter", &self.receiving_counter) + .finish() + } } impl LpSession { /// Creates a new session after completed KTT/PSQ exchange - /// - /// # Arguments - /// - /// * `session_id` - Session identifier - /// * `version` - Protocol version to attach in all `LpPacket`s - /// * `outer_aead_key` - Outer AEAD key for packet encryption - /// * `local_peer` - This side's LP peer's keys - /// * `remote_peer` - The remote's LP peer's keys - /// * `pq_shared_secret` - Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation. - /// * `noise_state` - Noise protocol state machine pub fn new( - session_id: u32, - version: u8, - outer_aead_key: OuterAeadKey, - local_peer: LpLocalPeer, - remote_peer: LpRemotePeer, - pq_shared_secret: PqSharedSecret, - noise_state: NoiseProtocol, - ) -> Self { - LpSession { - session_id, - version, - outer_aead_key, - local_peer, - remote_peer, - pq_shared_secret, - noise_state, + mut psq_session: Session, + session_binding: PersistentSessionBinding, + receiver_index: LpReceiverIndex, + protocol_version: u8, + ) -> Result { + // attempt to derive initial transport + let transport = psq_session + .transport_channel() + .map_err(|inner| LpError::TransportDerivationFailure { inner })?; + + Ok(LpSession { + psq_session, + session_binding, + active_transport: transport, + receiver_index, + protocol_version, sending_counter: 0, receiving_counter: Default::default(), - subsession_counter: 0, - read_only: false, - successor_session_id: None, - } - } - - /// Create an instance of `Ciphersuite` using hardcoded defaults. - /// This is a temporary workaround until values can be properly inferred - /// from reported version - pub fn default_ciphersuite() -> Ciphersuite { - Ciphersuite::new( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - HashLength::Default, - ) + }) } /// Helper function to create `PSQHandshakeState` for the handshake initiator - pub fn complete_as_initiator( + pub fn psq_handshake_initiator( connection: &'_ mut S, - ciphersuite: Ciphersuite, local_peer: LpLocalPeer, remote_peer: LpRemotePeer, remote_protocol_version: u8, - ) -> PSQHandshakeState<'_, S> + ) -> PSQHandshakeStateInitiator<'_, S> where - S: LpTransport + Unpin, + S: LpHandshakeChannel + Unpin, { - PSQHandshakeState::new(connection, ciphersuite, local_peer) - .with_protocol_version(remote_protocol_version) - .with_remote_peer(remote_peer) + PSQHandshakeState::new(connection, local_peer) + .as_initiator(InitiatorData::new(remote_protocol_version, remote_peer)) } /// Helper function to create `PSQHandshakeState` for the handshake responder pub fn psq_handshake_responder( connection: &'_ mut S, - ciphersuite: Ciphersuite, local_peer: LpLocalPeer, - ) -> PSQHandshakeState<'_, S> + ) -> PSQHandshakeStateResponder<'_, S> where - S: LpTransport + Unpin, + S: LpHandshakeChannel + Unpin, { - PSQHandshakeState::new(connection, ciphersuite, local_peer) + PSQHandshakeState::new(connection, local_peer).as_responder(ResponderData::default()) } - pub fn id(&self) -> u32 { - self.session_id + pub fn session_binding(&self) -> &PersistentSessionBinding { + &self.session_binding + } + + pub fn active_transport(&mut self) -> &mut libcrux_psq::session::Transport { + &mut self.active_transport + } + + pub fn session_identifier(&self) -> &[u8; 32] { + self.psq_session.identifier() + } + + pub fn receiver_index(&self) -> LpReceiverIndex { + self.receiver_index } /// Returns the negotiated protocol version from the handshake. /// /// Set during `LpSession` creation after sending / receiving `ClientHelloData` pub fn negotiated_version(&self) -> u8 { - self.version - } - - /// Returns the local X25519 public key. - /// - /// This is used for KKT protocol when the responder needs to send their - /// KEM public key in the KKT response. - pub fn local_x25519_public(&self) -> x25519::PublicKey { - *self.local_peer.x25519.public_key() - } - - /// Returns the remote ed25519 public key. - pub fn remote_ed25519_public(&self) -> ed25519::PublicKey { - self.remote_peer.ed25519_public - } - - /// Returns the remote X25519 public key. - /// - /// Used for tie-breaking in simultaneous subsession initiation. - /// Lower key loses and becomes responder. - pub fn remote_x25519_public(&self) -> &x25519::PublicKey { - &self.remote_peer.x25519_public - } - - /// Returns the outer AEAD key for packet encryption/decryption. - pub fn outer_aead_key(&self) -> &OuterAeadKey { - &self.outer_aead_key + self.protocol_version } pub fn next_packet(&mut self, message: LpMessage) -> Result { let counter = self.next_counter(); - let header = LpHeader::new(self.id(), counter, self.version); + let header = LpHeader::new( + self.receiver_index(), + counter, + self.protocol_version, + message.typ(), + ); let packet = LpPacket::new(header, message); Ok(packet) } @@ -274,508 +238,132 @@ impl LpSession { /// A tuple containing: /// * The next expected counter value for incoming packets /// * The total number of received packets - pub fn current_packet_cnt(&self) -> (u64, u64) { + pub fn current_packet_cnt(&self) -> PacketCount { self.receiving_counter.current_packet_cnt() } - /// Returns the PQ shared secret (K_pq). - /// - /// This is the raw KEM output from PSQ before Blake3 KDF combination. - /// Used for deriving subsession PSKs to maintain PQ protection. - pub fn pq_shared_secret(&self) -> &PqSharedSecret { - &self.pq_shared_secret - } - - /// Gets the next subsession index and increments the counter. - /// - /// Each subsession requires a unique index to ensure unique PSK derivation. - /// The index is monotonically increasing per session. - pub fn next_subsession_index(&mut self) -> u64 { - let next = self.subsession_counter; - self.subsession_counter += 1; - next - } - - /// Returns true if this session is in read-only mode. - /// - /// Read-only sessions have been demoted after a subsession was promoted. - /// They can still decrypt incoming messages but cannot encrypt outgoing ones. - pub fn is_read_only(&self) -> bool { - self.read_only - } - - /// Demotes this session to read-only mode after a subsession replaces it. - /// - /// After demotion: - /// - `encrypt_data()` will return `NoiseError::SessionReadOnly` - /// - `decrypt_data()` still works (to drain in-flight messages) - /// - Session should be cleaned up after TTL expires - /// - /// # Arguments - /// * `successor_idx` - The receiver index of the session that replaced this one - pub fn demote(&mut self, successor_idx: u32) { - self.successor_session_id = Some(successor_idx); - self.read_only = true; - } - - /// Returns the successor session ID if this session was demoted. - pub fn successor_session_id(&self) -> Option { - self.successor_session_id - } - - /// Encrypts application data payload using the established Noise transport session. + /// Encrypts a produced application using the established transport session + /// and produce an `EncryptedLpPacket` /// /// # Arguments /// - /// * `payload` - The application data to encrypt. + /// * `data` - plaintext data to encrypt /// /// # Returns /// - /// * `Ok(Vec)` containing the encrypted Noise message ciphertext. - /// * `Err(NoiseError)` if the session is not in transport mode or encryption fails. - pub fn encrypt_data(&mut self, payload: &[u8]) -> Result { - // Check if session is read-only (demoted) - if self.read_only { - return Err(NoiseError::SessionReadOnly); - } - - let payload = self.noise_state.write_message(payload)?; - Ok(LpMessage::EncryptedData(EncryptedDataPayload(payload))) + /// * `Ok(EncryptedLpPacket)` containing the encrypted message ciphertext. + /// * `Err(LpError)` if the session is not in transport mode or encryption fails. + pub(crate) fn encrypt_application_data( + &mut self, + data: Vec, + ) -> Result { + let packet = self.next_packet(LpMessage::ApplicationData(ApplicationData::new(data)))?; + encrypt_lp_packet(packet, &mut self.active_transport) } - /// Decrypts an incoming Noise message containing application data. + /// Decrypts an incoming LpPacket /// /// # Arguments /// - /// * `noise_ciphertext` - The encrypted Noise message received from the peer. + /// * `ciphertext` - The encrypted packet /// /// # Returns /// - /// * `Ok(Vec)` containing the decrypted application data payload. - /// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data. - pub fn decrypt_data(&mut self, noise_ciphertext: &LpMessage) -> Result, NoiseError> { - let payload = noise_ciphertext.payload(); - - match self.noise_state.read_message(payload)? { - ReadResult::DecryptedData(data) => Ok(data), - _ => Err(NoiseError::IncorrectStateError), - } - } - - /// Creates a new subsession using Noise KKpsk0 pattern. - /// - /// KKpsk0 reuses parent's static X25519 keys (both parties know each other from parent session). - /// PSK is derived from parent's PQ shared secret, preserving quantum resistance. - /// - /// # Arguments - /// * `subsession_index` - Unique index for this subsession (use `next_subsession_index()`) - /// * `is_initiator` - True if this side initiates the subsession handshake - /// - /// # Returns - /// `SubsessionHandshake` ready for KK1/KK2 message exchange - /// - /// # Errors - /// * Returns error if parent handshake not complete - /// * Returns error if PQ shared secret not available - pub fn create_subsession( - &self, - subsession_index: u64, - is_initiator: bool, - ) -> Result { - // Get PQ shared secret - let pq_secret = self.pq_shared_secret(); - - // Derive subsession PSK from parent's PQ shared secret - let subsession_psk = derive_subsession_psk(pq_secret.as_bytes(), subsession_index); - - // Build KKpsk0 handshake - // Pattern: Noise_KKpsk0_25519_ChaChaPoly_SHA256 - // Both parties already know each other's static keys from parent session - let pattern_name = "Noise_KKpsk0_25519_ChaChaPoly_SHA256"; - let params = pattern_name.parse()?; - - let local_key_bytes = self.local_peer.x25519.private_key().to_bytes(); - let remote_key_bytes = self.remote_x25519_public().to_bytes(); - - let builder = Builder::new(params) - .local_private_key(&local_key_bytes) - .remote_public_key(&remote_key_bytes) - .psk(0, &subsession_psk); // PSK at position 0 for KKpsk0 - - let handshake_state = if is_initiator { - builder.build_initiator().map_err(LpError::SnowKeyError)? - } else { - builder.build_responder().map_err(LpError::SnowKeyError)? - }; - - Ok(SubsessionHandshake { - index: subsession_index, - noise_state: Mutex::new(NoiseProtocol::new(handshake_state)), - is_initiator, - local_peer: self.local_peer.clone(), - remote_peer: self.remote_peer.clone(), - pq_shared_secret: self.pq_shared_secret.clone(), - subsession_psk, - negotiated_version: self.version, - }) - } -} - -/// Subsession created via Noise KKpsk0 handshake tunneled through parent session. -/// -/// Subsessions provide fresh session keys while inheriting PQ protection from parent's -/// ML-KEM shared secret. After handshake completes, the subsession can be promoted -/// to replace the parent session. -/// -/// # Lifecycle -/// 1. Parent calls `create_subsession()` to get `SubsessionHandshake` -/// 2. Initiator calls `prepare_message()` to get KK1 -/// 3. KK1 sent through parent session (encrypted tunnel) -/// 4. Responder calls `process_message(kk1)` to process KK1 -/// 5. Responder calls `prepare_message()` to get KK2 -/// 6. KK2 sent through parent session -/// 7. Initiator calls `process_message(kk2)` to complete handshake -/// 8. Both call `is_complete()` to verify -#[derive(Debug)] -pub struct SubsessionHandshake { - /// Subsession index (unique per parent session) - pub index: u64, - /// Noise KKpsk0 handshake state - noise_state: Mutex, - /// Is this side the initiator? - is_initiator: bool, - - // Key material inherited from parent session for into_session() conversion - /// Representation of a local Lewes Protocol peer - /// encapsulating all the known information and keys. - local_peer: LpLocalPeer, - - /// Representation of a remote Lewes Protocol peer - /// encapsulating all the known information and keys. - remote_peer: LpRemotePeer, - - /// PQ shared secret inherited from parent (for creating further subsessions) - pq_shared_secret: PqSharedSecret, - - /// Subsession PSK (for deriving outer AEAD key) - subsession_psk: [u8; 32], - - /// Negotiated protocol version from handshake. - negotiated_version: u8, -} - -impl SubsessionHandshake { - /// Prepares the next KK handshake message (KK1 or KK2 depending on role/state). - /// - /// # Returns - /// Noise handshake message bytes to send through parent session tunnel. - pub fn prepare_message(&self) -> Result, LpError> { - let mut noise_state = self.noise_state.lock(); - noise_state - .get_bytes_to_send() - .ok_or_else(|| LpError::Internal("Not our turn to send".into()))? - .map_err(LpError::NoiseError) - } - - /// Processes a received KK handshake message (KK1 or KK2). - /// - /// # Arguments - /// * `message` - Noise handshake message received through parent session tunnel. - /// - /// # Returns - /// Any payload embedded in the handshake message (usually empty for KK). - pub fn process_message(&self, message: &[u8]) -> Result, LpError> { - let mut noise_state = self.noise_state.lock(); - let result = noise_state - .read_message(message) - .map_err(LpError::NoiseError)?; - match result { - ReadResult::HandshakeComplete | ReadResult::NoOp => Ok(vec![]), - ReadResult::DecryptedData(data) => Ok(data), - } - } - - /// Checks if the handshake is complete (ready for transport mode). - pub fn is_complete(&self) -> bool { - self.noise_state.lock().is_handshake_finished() - } - - /// Returns whether this side is the initiator. - pub fn is_initiator(&self) -> bool { - self.is_initiator - } - - /// Returns the subsession index. - pub fn subsession_index(&self) -> u64 { - self.index - } - - /// Convert completed subsession handshake into a full LpSession. - /// - /// This consumes the SubsessionHandshake and creates a new LpSession - /// that can be used as a replacement for the parent session. - /// - /// # Arguments - /// * `receiver_index` - New receiver index for the promoted session - /// - /// # Errors - /// Returns error if handshake is not complete - pub fn into_session(self, receiver_index: u32) -> Result { - if !self.is_complete() { - return Err(LpError::Internal( - "Cannot convert incomplete subsession to session".to_string(), - )); - } - - // Extract the noise state (now in transport mode) - let noise_state = self.noise_state.into_inner(); - - // Derive outer AEAD key from the subsession PSK - let outer_key = OuterAeadKey::from_psk(&self.subsession_psk); - - Ok(LpSession { - // noiserm - session_id: receiver_index, - noise_state, - sending_counter: 0, - receiving_counter: ReceivingKeyCounterValidator::new(0), - local_peer: self.local_peer, - remote_peer: self.remote_peer, - outer_aead_key: outer_key, - pq_shared_secret: self.pq_shared_secret, - subsession_counter: 0, - read_only: false, - successor_session_id: None, - version: self.negotiated_version, - }) + /// * `Ok(LpPacket)` containing the decrypted application data payload. + /// * `Err(LpError)` if the session is not in transport mode, decryption fails, or the message is not data. + pub(crate) fn decrypt_packet( + &mut self, + packet: EncryptedLpPacket, + ) -> Result { + decrypt_lp_packet(packet, &mut self.active_transport) } } #[cfg(test)] mod tests { use super::*; - use crate::{SessionsMock, replay::ReplayError, sessions_for_tests}; - use rand::thread_rng; - - // Helper function to generate keypairs for tests - fn generate_x25519_keypair() -> x25519::KeyPair { - x25519::KeyPair::new(&mut thread_rng()) - } + use crate::{ReplayError, SessionsMock}; + use nym_kkt_ciphersuite::{IntoEnumIterator, KEM}; #[test] fn test_session_creation() { - let mut session = sessions_for_tests().0; + for kem in KEM::iter() { + let mut session = SessionsMock::mock_post_handshake(kem).responder; - // Initial counter should be zero - let counter = session.next_counter(); - assert_eq!(counter, 0); + // Initial counter should be zero + let counter = session.next_counter(); + assert_eq!(counter, 0); - // Counter should increment - let counter = session.next_counter(); - assert_eq!(counter, 1); + // Counter should increment + let counter = session.next_counter(); + assert_eq!(counter, 1); + } } - // NOTE: These tests are obsolete after removing optional KEM parameters. - // PSQ now always runs using X25519 keys internally converted to KEM format. - // The new tests at the end of this file (test_psq_*) cover PSQ integration. - /* - #[test] - fn test_session_creation_with_psq_state_initiator() { - // OLD API - REMOVED - } - - #[test] - fn test_session_creation_with_psq_state_responder() { - // OLD API - REMOVED - } - */ - #[test] fn test_replay_protection_sequential() { - let mut session = sessions_for_tests().1; + for kem in KEM::iter() { + let mut session = SessionsMock::mock_post_handshake(kem).responder; - // Sequential counters should be accepted - assert!(session.receiving_counter_quick_check(0).is_ok()); - assert!(session.receiving_counter_mark(0).is_ok()); + // Sequential counters should be accepted + assert!(session.receiving_counter_quick_check(0).is_ok()); + assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_quick_check(1).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); + assert!(session.receiving_counter_quick_check(1).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); - // Duplicates should be rejected - assert!(session.receiving_counter_quick_check(0).is_err()); - let err = session.receiving_counter_mark(0).unwrap_err(); - match err { - LpError::Replay(replay_error) => { - assert!(matches!(replay_error, ReplayError::DuplicateCounter)); + // Duplicates should be rejected + assert!(session.receiving_counter_quick_check(0).is_err()); + let err = session.receiving_counter_mark(0).unwrap_err(); + match err { + LpError::Replay(replay_error) => { + assert!(matches!(replay_error, ReplayError::DuplicateCounter)); + } + _ => panic!("Expected replay error"), } - _ => panic!("Expected replay error"), } } #[test] fn test_replay_protection_out_of_order() { - let mut session = sessions_for_tests().1; + for kem in KEM::iter() { + let mut session = SessionsMock::mock_post_handshake(kem).responder; - // Receive packets in order - assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); - assert!(session.receiving_counter_mark(2).is_ok()); + // Receive packets in order + assert!(session.receiving_counter_mark(0).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); + assert!(session.receiving_counter_mark(2).is_ok()); - // Skip ahead - assert!(session.receiving_counter_mark(10).is_ok()); + // Skip ahead + assert!(session.receiving_counter_mark(10).is_ok()); - // Can still receive out-of-order packets within window - assert!(session.receiving_counter_quick_check(5).is_ok()); - assert!(session.receiving_counter_mark(5).is_ok()); + // Can still receive out-of-order packets within window + assert!(session.receiving_counter_quick_check(5).is_ok()); + assert!(session.receiving_counter_mark(5).is_ok()); - // But duplicates are still rejected - assert!(session.receiving_counter_quick_check(5).is_err()); - assert!(session.receiving_counter_mark(5).is_err()); - } - - #[test] - fn test_packet_stats() { - let mut session = sessions_for_tests().1; - - // Initial stats - let (next, received) = session.current_packet_cnt(); - assert_eq!(next, 0); - assert_eq!(received, 0); - - // After receiving packets - assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); - - let (next, received) = session.current_packet_cnt(); - assert_eq!(next, 2); - assert_eq!(received, 2); - } - - /* - // These tests remain commented as they rely on the old mock crypto functions - #[test] - fn test_mock_crypto() { - let mut session = create_test_session(true); - let data = [1, 2, 3, 4, 5]; - let mut encrypted = [0; 5]; - let mut decrypted = [0; 5]; - - // Mock encrypt should copy the data - // let encrypted_len = session.encrypt_packet(&data, &mut encrypted).unwrap(); // Removed method - // assert_eq!(encrypted_len, 5); - // assert_eq!(encrypted, data); - - // Mock decrypt should copy the data - // let decrypted_len = session.decrypt_packet(&encrypted, &mut decrypted).unwrap(); // Removed method - // assert_eq!(decrypted_len, 5); - // assert_eq!(decrypted, data); - } - - #[test] - fn test_mock_crypto_buffer_too_small() { - let mut session = create_test_session(true); - let data = [1, 2, 3, 4, 5]; - let mut too_small = [0; 3]; - - // Should fail with buffer too small - // let result = session.encrypt_packet(&data, &mut too_small); // Removed method - // assert!(result.is_err()); - // match result.unwrap_err() { - // LpError::InsufficientBufferSize => {} // Error type might change - // _ => panic!("Expected InsufficientBufferSize error"), - // } - } - */ - - /// Test that X25519 keys are correctly converted to KEM format - #[test] - fn test_x25519_to_kem_conversion() { - use nym_kkt::ciphersuite::EncapsulationKey; - - let initiator_keys = generate_x25519_keypair(); - let responder_keys = generate_x25519_keypair(); - - // Verify we can convert X25519 public key to KEM format (as done in session.rs) - let x25519_public_bytes = responder_keys.public_key().as_bytes(); - let libcrux_public_key = - libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, x25519_public_bytes) - .expect("X25519 public key should convert to libcrux PublicKey"); - - let _kem_key = EncapsulationKey::X25519(libcrux_public_key); - - // Verify we can convert X25519 private key to KEM format - let x25519_private_bytes = initiator_keys.private_key().to_bytes(); - let _libcrux_private_key = - libcrux_kem::PrivateKey::decode(libcrux_kem::Algorithm::X25519, &x25519_private_bytes) - .expect("X25519 private key should convert to libcrux PrivateKey"); - - // Successful conversion is sufficient - actual encapsulation is tested in psk.rs - // (libcrux_kem::PrivateKey is an enum with no len() method, conversion success is enough) - } - - #[test] - fn test_demote_sets_read_only() { - let sessions = SessionsMock::mock_post_handshake(12345); - let mut session = sessions.initiator; - - // Initially not read-only - assert!(!session.is_read_only()); - assert!(session.successor_session_id().is_none()); - - // Demote the session - session.demote(99999); - - // Now read-only with successor - assert!(session.is_read_only()); - assert_eq!(session.successor_session_id(), Some(99999)); - } - - #[test] - fn test_encrypt_fails_after_demotion() { - let receiver_index = 12345; - let sessions = SessionsMock::mock_post_handshake(receiver_index); - let mut initiator_session = sessions.initiator; - - // Encryption works before demotion - let plaintext = b"Hello before demotion"; - assert!(initiator_session.encrypt_data(plaintext).is_ok()); - - // Demote the session - initiator_session.demote(99999); - - // Encryption fails after demotion - let result = initiator_session.encrypt_data(plaintext); - assert!(result.is_err()); - match result.unwrap_err() { - NoiseError::SessionReadOnly => { - // Expected - } - e => panic!("Expected SessionReadOnly error, got: {:?}", e), + // But duplicates are still rejected + assert!(session.receiving_counter_quick_check(5).is_err()); + assert!(session.receiving_counter_mark(5).is_err()); } } #[test] - fn test_decrypt_works_after_demotion() { - // --- Setup Handshake --- - let receiver_index = 12345; - let sessions = SessionsMock::mock_post_handshake(receiver_index); - let mut initiator_session = sessions.initiator; - let mut responder_session = sessions.responder; + fn test_packet_stats() { + for kem in KEM::iter() { + let mut session = SessionsMock::mock_post_handshake(kem).responder; - // Responder encrypts a message - let plaintext = b"Message to demoted initiator"; - let ciphertext = responder_session - .encrypt_data(plaintext) - .expect("Encryption failed"); + // Initial stats + let packet_count = session.current_packet_cnt(); + assert_eq!(packet_count.next, 0); + assert_eq!(packet_count.received, 0); - // Demote the initiator session - initiator_session.demote(99999); - assert!(initiator_session.is_read_only()); + // After receiving packets + assert!(session.receiving_counter_mark(0).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); - // Decryption still works on demoted session (drain in-flight) - let decrypted = initiator_session - .decrypt_data(&ciphertext) - .expect("Decryption should work on demoted session"); - assert_eq!(decrypted, plaintext); + let packet_count = session.current_packet_cnt(); + assert_eq!(packet_count.next, 2); + assert_eq!(packet_count.received, 2); + } } } diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index e72242a507..00486a1fb8 100644 --- a/common/nym-lp/src/session_integration/mod.rs +++ b/common/nym-lp/src/session_integration/mod.rs @@ -1,731 +1,392 @@ #[cfg(test)] mod tests { - use crate::codec::{parse_lp_packet, serialize_lp_packet}; - use crate::{ - LpError, SessionsMock, - message::LpMessage, - packet::{LpHeader, LpPacket, TRAILER_LEN}, - session_manager::SessionManager, - }; - use bytes::BytesMut; + use crate::packet::EncryptedLpPacket; + use crate::state_machine::{LpAction, LpData, LpInput, LpStateBare}; + use crate::{LpError, SessionManager, SessionsMock}; + use nym_kkt_ciphersuite::{IntoEnumIterator, KEM}; - // Function to create a test packet - similar to how it's done in codec.rs tests - fn create_test_packet( - protocol_version: u8, - receiver_idx: u32, - counter: u64, - message: LpMessage, - ) -> LpPacket { - // Create the header - let header = LpHeader { - protocol_version, - reserved: [0u8; 3], // reserved - receiver_idx, - counter, - }; + // helpers to make tests smaller + trait ActionExtract { + fn ciphertext(self) -> EncryptedLpPacket; - // Create the trailer (zeros for now, in a real implementation this might be a MAC) - let trailer = [0u8; TRAILER_LEN]; - - // Create and return the packet directly - LpPacket { - header, - message, - trailer, - } + fn data(self) -> LpData; } - /// Tests the complete session flow including: - /// - Creation of sessions through session manager - /// - Packet encoding/decoding with the session - /// - Replay protection across the session - /// - Multiple sessions with unique indices - /// - Session removal and cleanup - #[test] - fn test_full_session_flow() { - // 1. Initialize session manager - let mut session_manager_1 = SessionManager::new(); - let mut session_manager_2 = SessionManager::new(); + impl ActionExtract for LpAction { + fn ciphertext(self) -> EncryptedLpPacket { + if let LpAction::SendPacket(packet) = self { + packet + } else { + panic!("invalid action"); + } + } - let receiver_index = 12345; - let sessions = SessionsMock::mock_post_handshake(receiver_index); - - // 2. Create sessions using the pre-built Noise states - let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator); - let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder); - - // Verify session count - assert_eq!(session_manager_1.session_count(), 1); - assert_eq!(session_manager_2.session_count(), 1); - - // 3. Simulate Data Transfer (Post-Handshake) - println!("Starting data transfer simulation..."); - let plaintext_a_to_b = b"Hello from A!"; - - // A encrypts data - let ciphertext_a_to_b = session_manager_1 - .encrypt_data(peer_a_sm, plaintext_a_to_b) - .expect("A encrypt failed"); - - // A prepares packet - let counter_a = session_manager_1.next_counter(peer_a_sm).unwrap(); - let message_a_to_b = create_test_packet(1, receiver_index, counter_a, ciphertext_a_to_b); - let mut encoded_data_a_to_b = BytesMut::new(); - serialize_lp_packet(&message_a_to_b, &mut encoded_data_a_to_b, None) - .expect("A serialize data failed"); - - // B parses packet and checks replay - let decoded_packet_b = - parse_lp_packet(&encoded_data_a_to_b, None).expect("B parse data failed"); - assert_eq!(decoded_packet_b.header.counter, counter_a); - - // Check replay before decrypting - session_manager_2 - .receiving_counter_quick_check(peer_b_sm, decoded_packet_b.header.counter) - .expect("B data replay check failed (A->B)"); - - // B decrypts data - let decrypted_payload = session_manager_2 - .decrypt_data(peer_b_sm, &decoded_packet_b.message) - .expect("B decrypt failed"); - assert_eq!(decrypted_payload, plaintext_a_to_b); - // Mark counter only after successful decryption - session_manager_2 - .receiving_counter_mark(peer_b_sm, decoded_packet_b.header.counter) - .expect("B mark data counter failed"); - println!( - " A->B: Decrypted successfully: {:?}", - String::from_utf8_lossy(&decrypted_payload) - ); - - // B sends data to A - let plaintext_b_to_a = b"Hello from B!"; - let ciphertext_b_to_a = session_manager_2 - .encrypt_data(peer_b_sm, plaintext_b_to_a) - .expect("B encrypt failed"); - let counter_b = session_manager_2.next_counter(peer_b_sm).unwrap(); - let message_b_to_a = create_test_packet(1, receiver_index, counter_b, ciphertext_b_to_a); - let mut encoded_data_b_to_a = BytesMut::new(); - serialize_lp_packet(&message_b_to_a, &mut encoded_data_b_to_a, None) - .expect("B serialize data failed"); - - // A parses packet and checks replay - let decoded_packet_a = - parse_lp_packet(&encoded_data_b_to_a, None).expect("A parse data failed"); - assert_eq!(decoded_packet_a.header.counter, counter_b); - - // Check replay before decrypting - session_manager_1 - .receiving_counter_quick_check(peer_a_sm, decoded_packet_a.header.counter) - .expect("A data replay check failed (B->A)"); - - // A decrypts data - let decrypted_payload = session_manager_1 - .decrypt_data(peer_a_sm, &decoded_packet_a.message) - .expect("A decrypt failed"); - assert_eq!(decrypted_payload, plaintext_b_to_a); - // Mark counter only after successful decryption - session_manager_1 - .receiving_counter_mark(peer_a_sm, decoded_packet_a.header.counter) - .expect("A mark data counter failed"); - println!( - " B->A: Decrypted successfully: {:?}", - String::from_utf8_lossy(&decrypted_payload) - ); - - println!("Data transfer simulation completed."); - - // 4. Replay Protection Test (Data Packet) - println!("Testing data packet replay protection..."); - // Try to replay the last message from B to A - // Need to re-encode because decode consumes the buffer - let message_b_to_a_replay = create_test_packet( - 1, - receiver_index, - counter_b, - LpMessage::EncryptedData(crate::message::EncryptedDataPayload( - plaintext_b_to_a.to_vec(), - )), // Using plaintext here, but content doesn't matter for replay check - ); - let mut encoded_data_b_to_a_replay = BytesMut::new(); - serialize_lp_packet( - &message_b_to_a_replay, - &mut encoded_data_b_to_a_replay, - None, - ) - .expect("B serialize replay failed"); - - let parsed_replay_packet = - parse_lp_packet(&encoded_data_b_to_a_replay, None).expect("A parse replay failed"); - let replay_result = session_manager_1 - .receiving_counter_quick_check(peer_a_sm, parsed_replay_packet.header.counter); - assert!(replay_result.is_err(), "Data replay should be prevented"); - assert!( - matches!(replay_result.unwrap_err(), LpError::Replay(_)), - "Should be a replay protection error for data packet" - ); - println!("Data packet replay protection test passed."); - - // 5. Test out-of-order packet reception (send counter N+1 before counter N) - println!("Testing out-of-order data packet reception..."); - let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1 - let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2 - - // Prepare data for counter_a_skip (N+1) - let plaintext_skip = b"Out of order message"; - let ciphertext_skip = session_manager_1 - .encrypt_data(peer_a_sm, plaintext_skip) - .expect("A encrypt skip failed"); - - let message_a_to_b_skip = create_test_packet( - 1, // protocol version - receiver_index, - counter_a_skip, // Send N+1 first - ciphertext_skip, - ); - - // Encode the skip message - let mut encoded_skip = BytesMut::new(); - serialize_lp_packet(&message_a_to_b_skip, &mut encoded_skip, None) - .expect("Failed to serialize skip message"); - - // B parses skip message and checks replay - let decoded_packet_skip = - parse_lp_packet(&encoded_skip, None).expect("B parse skip failed"); - session_manager_2 - .receiving_counter_quick_check(peer_b_sm, decoded_packet_skip.header.counter) - .expect("B replay check skip failed"); - assert_eq!(decoded_packet_skip.header.counter, counter_a_skip); - - // B decrypts skip message - let decrypted_payload = session_manager_2 - .decrypt_data(peer_b_sm, &decoded_packet_skip.message) - .expect("B decrypt skip failed"); - assert_eq!(decrypted_payload, plaintext_skip); - // Mark counter N+1 - session_manager_2 - .receiving_counter_mark(peer_b_sm, decoded_packet_skip.header.counter) - .expect("B mark skip counter failed"); - println!( - " A->B (Counter {}): Decrypted successfully: {:?}", - counter_a_skip, - String::from_utf8_lossy(&decrypted_payload) - ); - - // 6. Now send the skipped counter N message (should still work) - println!("Testing delayed data packet reception..."); - // Prepare data for counter_a_next (N) - let plaintext_delayed = b"Delayed message"; - let ciphertext_delayed = session_manager_1 - .encrypt_data(peer_a_sm, plaintext_delayed) - .expect("A encrypt delayed failed"); - - let message_a_to_b_delayed = create_test_packet( - 1, // protocol version - receiver_index, - counter_a_next, // counter N (delayed packet) - ciphertext_delayed, - ); - - // Encode the delayed message - let mut encoded_delayed = BytesMut::new(); - serialize_lp_packet(&message_a_to_b_delayed, &mut encoded_delayed, None) - .expect("Failed to serialize delayed message"); - - // Make a copy for replay test later - let encoded_delayed_copy = encoded_delayed.clone(); - - // B parses delayed message and checks replay - let decoded_packet_delayed = - parse_lp_packet(&encoded_delayed, None).expect("B parse delayed failed"); - session_manager_2 - .receiving_counter_quick_check(peer_b_sm, decoded_packet_delayed.header.counter) - .expect("B replay check delayed failed"); - assert_eq!(decoded_packet_delayed.header.counter, counter_a_next); - - // B decrypts delayed message - let decrypted_payload = session_manager_2 - .decrypt_data(peer_b_sm, &decoded_packet_delayed.message) - .expect("B decrypt delayed failed"); - assert_eq!(decrypted_payload, plaintext_delayed); - // Mark counter N - session_manager_2 - .receiving_counter_mark(peer_b_sm, decoded_packet_delayed.header.counter) - .expect("B mark delayed counter failed"); - println!( - " A->B (Counter {}): Decrypted successfully: {:?}", - counter_a_next, - String::from_utf8_lossy(&decrypted_payload) - ); - - println!("Delayed data packet reception test passed."); - - // 7. Try to replay message with counter N (should fail) - println!("Testing replay of delayed packet..."); - let parsed_delayed_replay = - parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed"); - let result = session_manager_2 - .receiving_counter_quick_check(peer_b_sm, parsed_delayed_replay.header.counter); - assert!(result.is_err(), "Replay attack should be prevented"); - assert!( - matches!(result, Err(LpError::Replay(_))), - "Should be a replay protection error" - ); - - // 8. Session removal - assert!(session_manager_1.remove_state_machine(receiver_index)); - assert_eq!(session_manager_1.session_count(), 0); - - // Verify the session is gone - let session = session_manager_1.state_machine_exists(receiver_index); - assert!(!session, "Session should be removed"); - - // But the other session still exists - let session = session_manager_2.state_machine_exists(receiver_index); - assert!(session, "Session still exists in the other manager"); + fn data(self) -> LpData { + if let LpAction::DeliverData(data) = self { + data + } else { + panic!("invalid action"); + } + } } /// Tests simultaneous bidirectional communication between sessions #[test] fn test_bidirectional_communication() { - // 1. Initialize session manager - let mut session_manager_1 = SessionManager::new(); - let mut session_manager_2 = SessionManager::new(); + for kem in KEM::iter() { + // 1. Initialize session manager + let mut session_manager_1 = SessionManager::new(); + let mut session_manager_2 = SessionManager::new(); + let sessions = SessionsMock::mock_post_handshake(kem); - let receiver_index = 12345; - let sessions = SessionsMock::mock_post_handshake(receiver_index); + // 2. Create sessions using the pre-built Noise states + let peer_a_sm = session_manager_1 + .create_session_state_machine(sessions.initiator) + .unwrap(); + let peer_b_sm = session_manager_2 + .create_session_state_machine(sessions.responder) + .unwrap(); - // 2. Create sessions using the pre-built Noise states - let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator); - let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder); + // 3. Send multiple encrypted messages both ways + const NUM_MESSAGES: u64 = 5; + for i in 0..NUM_MESSAGES { + println!("Bidirectional test: Round {i}"); + // --- A sends to B --- + let plaintext_a = format!("A->B Message {i}").into_bytes(); + let ciphertext_a = session_manager_1 + .send_data(peer_a_sm, LpData::new_opaque(plaintext_a.clone())) + .unwrap() + .ciphertext(); - // Counters after handshake - let mut counter_a = 0; // Next counter for A to send - let mut counter_b = 0; // Next counter for B to send + // B parses and checks replay + let decrypted_payload = session_manager_2 + .receive_packet(peer_b_sm, ciphertext_a) + .unwrap() + .unwrap() + .data(); + assert_eq!(decrypted_payload.content, plaintext_a); - // 3. Send multiple encrypted messages both ways - const NUM_MESSAGES: u64 = 5; - for i in 0..NUM_MESSAGES { - println!("Bidirectional test: Round {}", i); - // --- A sends to B --- - let plaintext_a = format!("A->B Message {}", i).into_bytes(); - let ciphertext_a = session_manager_1 - .encrypt_data(peer_a_sm, &plaintext_a) - .expect("A encrypt failed"); - let current_counter_a = counter_a; - counter_a += 1; + // --- B sends to A --- + let plaintext_b = format!("B->A Message {i}").into_bytes(); + let ciphertext_b = session_manager_2 + .send_data(peer_b_sm, LpData::new_opaque(plaintext_b.clone())) + .unwrap() + .ciphertext(); - let message_a = create_test_packet(1, receiver_index, current_counter_a, ciphertext_a); - let mut encoded_a = BytesMut::new(); - serialize_lp_packet(&message_a, &mut encoded_a, None).expect("A serialize failed"); + // B parses and checks replay + let decrypted_payload = session_manager_1 + .receive_packet(peer_a_sm, ciphertext_b) + .unwrap() + .unwrap() + .data(); + assert_eq!(decrypted_payload.content, plaintext_b); + } - // B parses and checks replay - let decoded_packet_b = parse_lp_packet(&encoded_a, None).expect("B parse failed"); - session_manager_2 - .receiving_counter_quick_check(peer_b_sm, decoded_packet_b.header.counter) - .expect("B replay check failed (A->B)"); - assert_eq!(decoded_packet_b.header.counter, current_counter_a); - let decrypted_payload = session_manager_2 - .decrypt_data(peer_b_sm, &decoded_packet_b.message) - .expect("B decrypt failed"); - assert_eq!(decrypted_payload, plaintext_a); - session_manager_2 - .receiving_counter_mark(peer_b_sm, current_counter_a) - .expect("B mark counter failed"); + // 5. Verify counter stats + // Note: current_packet_cnt() returns (next_expected_receive_counter, total_received) + let count_a = session_manager_1.current_packet_cnt(peer_a_sm).unwrap(); + let count_b = session_manager_2.current_packet_cnt(peer_b_sm).unwrap(); - // --- B sends to A --- - let plaintext_b = format!("B->A Message {}", i).into_bytes(); - let ciphertext_b = session_manager_2 - .encrypt_data(peer_b_sm, &plaintext_b) - .expect("B encrypt failed"); - let current_counter_b = counter_b; - counter_b += 1; + // Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7. + // Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6. + assert_eq!( + count_a.received, NUM_MESSAGES, + "Peer A total received count mismatch" + ); // Received 5 data + assert_eq!( + count_a.next, NUM_MESSAGES, + "Peer A next expected receive counter mismatch" + ); // Expected counter for msg from B - let message_b = create_test_packet(1, receiver_index, current_counter_b, ciphertext_b); - let mut encoded_b = BytesMut::new(); - serialize_lp_packet(&message_b, &mut encoded_b, None).expect("B serialize failed"); + // Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6. + // Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7. + assert_eq!( + count_b.received, NUM_MESSAGES, + "Peer B total received count mismatch" + ); // Received 5 data + assert_eq!( + count_b.next, NUM_MESSAGES, + "Peer B next expected receive counter mismatch" + ); // Expected counter for msg from A - // A parses and checks replay - let decoded_packet_a = parse_lp_packet(&encoded_b, None).expect("A parse failed"); - session_manager_1 - .receiving_counter_quick_check(peer_a_sm, decoded_packet_a.header.counter) - .expect("A replay check failed (B->A)"); - assert_eq!(decoded_packet_a.header.counter, current_counter_b); - let decrypted_payload = session_manager_1 - .decrypt_data(peer_a_sm, &decoded_packet_a.message) - .expect("A decrypt failed"); - assert_eq!(decrypted_payload, plaintext_b); - session_manager_1 - .receiving_counter_mark(peer_a_sm, current_counter_b) - .expect("A mark counter failed"); + println!("Bidirectional test completed."); } - - // 5. Verify counter stats - // Note: current_packet_cnt() returns (next_expected_receive_counter, total_received) - let (next_recv_a, total_recv_a) = session_manager_1.current_packet_cnt(peer_a_sm).unwrap(); - let (next_recv_b, total_recv_b) = session_manager_2.current_packet_cnt(peer_b_sm).unwrap(); - - // Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7. - // Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6. - assert_eq!( - counter_a, NUM_MESSAGES, - "Peer A final send counter mismatch" - ); - assert_eq!( - total_recv_a, NUM_MESSAGES, - "Peer A total received count mismatch" - ); // Received 5 data - assert_eq!( - next_recv_a, NUM_MESSAGES, - "Peer A next expected receive counter mismatch" - ); // Expected counter for msg from B - - // Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6. - // Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7. - assert_eq!( - counter_b, NUM_MESSAGES, - "Peer B final send counter mismatch" - ); - assert_eq!( - total_recv_b, NUM_MESSAGES, - "Peer B total received count mismatch" - ); // Received 5 data - assert_eq!( - next_recv_b, NUM_MESSAGES, - "Peer B next expected receive counter mismatch" - ); // Expected counter for msg from A - - println!("Bidirectional test completed."); } /// Tests error handling in session flow #[test] fn test_session_error_handling() { - // 1. Initialize session manager - let mut session_manager = SessionManager::new(); + for kem in KEM::iter() { + // 1. Initialize session manager + let mut session_manager = SessionManager::new(); - let receiver_index = 123; - let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator; - let session2 = SessionsMock::mock_post_handshake(124).initiator; + let sessions = SessionsMock::mock_post_handshake(kem); + let session_id = sessions.initiator.receiver_index(); - // 2. Create a session (using real noise state) - let _session = session_manager.create_session_state_machine(session1); + let non_existent = 123; + // sanity check in case of the 1 in 2^256 + assert_ne!(session_id, non_existent); - // 3. Try to get a non-existent session - let result = session_manager.state_machine_exists(999); - assert!(!result, "Non-existent session should return None"); + let session1 = sessions.initiator; + let session2 = sessions.responder; - // 4. Try to remove a non-existent session - let result = session_manager.remove_state_machine(999); - assert!( - !result, - "Remove session should not remove a non-existent session" - ); + // 2. Create a session (using real noise state) + let _session = session_manager.create_session_state_machine(session1); - // 5. Create and immediately remove a session - let _temp_session = session_manager.create_session_state_machine(session2); + // 3. Try to get a non-existent session + let result = session_manager.state_machine_exists(non_existent); + assert!(!result, "Non-existent session should return None"); - assert!( - session_manager.remove_state_machine(124), - "Should remove the session" - ); + // 4. Try to remove a non-existent session + let result = session_manager.remove_state_machine(non_existent); + assert!( + !result, + "Remove session should not remove a non-existent session" + ); - // 6. Create a codec and test error cases - // let mut codec = LPCodec::new(session); + // 5. Create and immediately remove a session + let _temp_session = session_manager.create_session_state_machine(session2); - // 7. Create an invalid message type packet - let mut buf = BytesMut::new(); - - // Add header - buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved - buf.extend_from_slice(&receiver_index.to_le_bytes()); // Sender index - buf.extend_from_slice(&0u64.to_le_bytes()); // Counter - - // Add invalid message type - buf.extend_from_slice(&0xFFFFu16.to_le_bytes()); - - // Add some dummy data - buf.extend_from_slice(&[0u8; 80]); - - // Add trailer - buf.extend_from_slice(&[0u8; TRAILER_LEN]); - - // Try to parse the invalid message type - let result = parse_lp_packet(&buf, None); - assert!(result.is_err(), "Decoding invalid message type should fail"); - - // Add assertion for the specific error type - assert!(matches!( - result.unwrap_err(), - LpError::InvalidMessageType(0xFFFF) - )); - - // 8. Test partial packet decoding - let partial_packet = &buf[0..10]; // Too short to be a valid packet - let partial_bytes = BytesMut::from(partial_packet); - - let result = parse_lp_packet(&partial_bytes, None); - assert!(result.is_err(), "Parsing partial packet should fail"); - assert!(matches!( - result.unwrap_err(), - LpError::InsufficientBufferSize - )); + assert!( + session_manager.remove_state_machine(session_id), + "Should remove the session" + ); + } } - // Remove unused imports if SessionManager methods are no longer direct dependencies - // use crate::noise_protocol::{create_noise_state, create_noise_state_responder}; - use crate::state_machine::LpData; - use crate::state_machine::{LpAction, LpInput, LpStateBare}; - // Use Bytes for SendData input - - // Keep helper function for creating test packets if needed, - // but LpAction::SendPacket should provide the packets now. - // fn create_test_packet(...) -> LpPacket { ... } /// Tests the complete session flow using ONLY the process_input interface: /// - Creation of sessions through session manager - /// - Handshake driven by StartHandshake, ReceivePacket inputs /// - Data transfer driven by SendData, ReceivePacket inputs /// - Actions like SendPacket, DeliverData handled from output /// - Implicit replay protection via state machine logic /// - Closing driven by Close input #[test] - fn test_full_session_flow_with_process_input() { + fn test_full_session_flow() { // 1. Initialize session managers let mut session_manager_1 = SessionManager::new(); let mut session_manager_2 = SessionManager::new(); - let receiver_index = 12345; - let sessions = SessionsMock::mock_post_handshake(receiver_index); + for kem in KEM::iter() { + let sessions = SessionsMock::mock_post_handshake(kem); + let session_id = sessions.responder.receiver_index(); - // 2. Create sessions state machines - session_manager_1.create_session_state_machine(sessions.initiator); - session_manager_2.create_session_state_machine(sessions.responder); + // 2. Create sessions state machines + session_manager_1 + .create_session_state_machine(sessions.initiator) + .unwrap(); + session_manager_2 + .create_session_state_machine(sessions.responder) + .unwrap(); - assert_eq!(session_manager_1.session_count(), 1); - assert_eq!(session_manager_2.session_count(), 1); - assert!(session_manager_1.state_machine_exists(receiver_index)); - assert!(session_manager_2.state_machine_exists(receiver_index)); + assert_eq!(session_manager_1.session_count(), 1); + assert_eq!(session_manager_2.session_count(), 1); + assert!(session_manager_1.state_machine_exists(session_id)); + assert!(session_manager_2.state_machine_exists(session_id)); - // Verify initial states are Transport - assert_eq!( - session_manager_1.get_state(receiver_index).unwrap(), - LpStateBare::Transport - ); - assert_eq!( - session_manager_2.get_state(receiver_index).unwrap(), - LpStateBare::Transport - ); - - // --- 3. Simulate Data Transfer via process_input --- - println!("Starting data transfer simulation via process_input..."); - let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec()); - let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec()); - - // --- A sends to B --- - println!(" A sends to B"); - let action_a_send = session_manager_1 - .process_input(receiver_index, LpInput::SendData(plaintext_a_to_b.clone())) - .expect("A SendData should produce action") - .expect("A SendData failed"); - - let data_packet_a = if let LpAction::SendPacket(packet) = action_a_send { - packet - } else { - panic!("A SendData did not produce SendPacket"); - }; - - // Simulate network - let mut buf_data_a = BytesMut::new(); - serialize_lp_packet(&data_packet_a, &mut buf_data_a, None).unwrap(); - let parsed_data_a = parse_lp_packet(&buf_data_a, None).unwrap(); - - // B receives - println!(" B receives from A"); - let action_b_recv = session_manager_2 - .process_input(receiver_index, LpInput::ReceivePacket(parsed_data_a)) - .expect("B ReceivePacket (data) should produce action") - .expect("B ReceivePacket (data) failed"); - - if let LpAction::DeliverData(data) = action_b_recv { - assert_eq!(data, plaintext_a_to_b, "Decrypted data mismatch A->B"); - println!( - " B successfully decrypted: {:?}", - String::from_utf8_lossy(&data.content) + // Verify initial states are Transport + assert_eq!( + session_manager_1.get_state(session_id).unwrap(), + LpStateBare::Transport ); - } else { - panic!("B ReceivePacket did not produce DeliverData"); - } - - // --- B sends to A --- - println!(" B sends to A"); - let action_b_send = session_manager_2 - .process_input(receiver_index, LpInput::SendData(plaintext_b_to_a.clone())) - .expect("B SendData should produce action") - .expect("B SendData failed"); - - let data_packet_b = if let LpAction::SendPacket(packet) = action_b_send { - packet - } else { - panic!("B SendData did not produce SendPacket"); - }; - // Keep a copy for replay test - let data_packet_b_replay = data_packet_b.clone(); - - // Simulate network - let mut buf_data_b = BytesMut::new(); - serialize_lp_packet(&data_packet_b, &mut buf_data_b, None).unwrap(); - let parsed_data_b = parse_lp_packet(&buf_data_b, None).unwrap(); - - // A receives - println!(" A receives from B"); - let action_a_recv = session_manager_1 - .process_input(receiver_index, LpInput::ReceivePacket(parsed_data_b)) - .expect("A ReceivePacket (data) should produce action") - .expect("A ReceivePacket (data) failed"); - - if let LpAction::DeliverData(data) = action_a_recv { - assert_eq!(data, plaintext_b_to_a, "Decrypted data mismatch B->A"); - println!( - " A successfully decrypted: {:?}", - String::from_utf8_lossy(&data.content) + assert_eq!( + session_manager_2.get_state(session_id).unwrap(), + LpStateBare::Transport ); - } else { - panic!("A ReceivePacket did not produce DeliverData"); + + // --- 3. Simulate Data Transfer via process_input --- + println!("Starting data transfer simulation via process_input..."); + let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec()); + let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec()); + + // --- A sends to B --- + println!(" A sends to B"); + let action_a_send = session_manager_1 + .process_input(session_id, LpInput::SendData(plaintext_a_to_b.clone())) + .expect("A SendData should produce action") + .expect("A SendData failed"); + + let data_packet_a = action_a_send.ciphertext(); + + // B receives + println!(" B receives from A"); + let action_b_recv = session_manager_2 + .process_input(session_id, LpInput::ReceivePacket(data_packet_a)) + .expect("B ReceivePacket (data) should produce action") + .expect("B ReceivePacket (data) failed"); + + if let LpAction::DeliverData(data) = action_b_recv { + assert_eq!(data, plaintext_a_to_b, "Decrypted data mismatch A->B"); + println!( + " B successfully decrypted: {:?}", + String::from_utf8_lossy(&data.content) + ); + } else { + panic!("B ReceivePacket did not produce DeliverData"); + } + + // --- B sends to A --- + println!(" B sends to A"); + let action_b_send = session_manager_2 + .process_input(session_id, LpInput::SendData(plaintext_b_to_a.clone())) + .expect("B SendData should produce action") + .expect("B SendData failed"); + + let data_packet_b = action_b_send.ciphertext(); + + // Keep a copy for replay test + let data_packet_b_replay = data_packet_b.clone(); + + // A receives + println!(" A receives from B"); + let action_a_recv = session_manager_1 + .process_input(session_id, LpInput::ReceivePacket(data_packet_b)) + .expect("A ReceivePacket (data) should produce action") + .expect("A ReceivePacket (data) failed"); + + if let LpAction::DeliverData(data) = action_a_recv { + assert_eq!(data, plaintext_b_to_a, "Decrypted data mismatch B->A"); + println!( + " A successfully decrypted: {:?}", + String::from_utf8_lossy(&data.content) + ); + } else { + panic!("A ReceivePacket did not produce DeliverData"); + } + println!("Data transfer simulation completed."); + + // --- 4. Replay Protection Test --- + println!("Testing data packet replay protection via process_input..."); + let replay_result = session_manager_1 + .process_input(session_id, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet + + assert!(replay_result.is_err(), "Replay should produce Err(...)"); + let error = replay_result.err().unwrap(); + assert!( + matches!(error, LpError::Replay(_)), + "Expected Replay error, got {:?}", + error + ); + println!("Data packet replay protection test passed."); + + // --- 5. Out-of-Order Test --- + println!("Testing out-of-order reception via process_input..."); + + // A prepares N+1 then N + let data_n_plus_1 = LpData::new_opaque(b"Message N+1".to_vec()); + let data_n = LpData::new_opaque(b"Message N".to_vec()); + + let action_send_n1 = session_manager_1 + .process_input(session_id, LpInput::SendData(data_n_plus_1.clone())) + .unwrap() + .unwrap(); + let packet_n1 = match action_send_n1 { + LpAction::SendPacket(p) => p, + _ => panic!("Expected SendPacket"), + }; + + let action_send_n = session_manager_1 + .process_input(session_id, LpInput::SendData(data_n.clone())) + .unwrap() + .unwrap(); + let packet_n = match action_send_n { + LpAction::SendPacket(p) => p, + _ => panic!("Expected SendPacket"), + }; + let packet_n_replay = packet_n.clone(); // For replay test + + // B receives N+1 first + println!(" B receives N+1"); + let action_recv_n1 = session_manager_2 + .process_input(session_id, LpInput::ReceivePacket(packet_n1)) + .unwrap() + .unwrap(); + match action_recv_n1 { + LpAction::DeliverData(d) => assert_eq!(d, data_n_plus_1, "Data N+1 mismatch"), + _ => panic!("Expected DeliverData for N+1"), + } + + // B receives N second (should work) + println!(" B receives N"); + let action_recv_n = session_manager_2 + .process_input(session_id, LpInput::ReceivePacket(packet_n)) + .unwrap() + .unwrap(); + match action_recv_n { + LpAction::DeliverData(d) => assert_eq!(d, data_n, "Data N mismatch"), + _ => panic!("Expected DeliverData for N"), + } + + // B tries to replay N (should fail) + println!(" B tries to replay N"); + let replay_n_result = session_manager_2 + .process_input(session_id, LpInput::ReceivePacket(packet_n_replay)); + assert!(replay_n_result.is_err(), "Replay N should produce Err"); + assert!( + matches!(replay_n_result.err().unwrap(), LpError::Replay(_)), + "Expected Replay error for N" + ); + println!("Out-of-order test passed."); + + // --- 6. Close Test --- + println!("Testing close via process_input..."); + + // A closes + let action_a_close = session_manager_1 + .process_input(session_id, LpInput::Close) + .expect("A Close should produce action") + .expect("A Close failed"); + assert!(matches!(action_a_close, LpAction::ConnectionClosed)); + assert_eq!( + session_manager_1.get_state(session_id).unwrap(), + LpStateBare::Closed + ); + + // Further actions on A fail + let send_after_close_a = session_manager_1.process_input( + session_id, + LpInput::SendData(LpData::new_opaque(b"fail".to_vec())), + ); + assert!(send_after_close_a.is_err()); + assert!(matches!( + send_after_close_a.err().unwrap(), + LpError::LpSessionClosed + )); + + // B closes + let action_b_close = session_manager_2 + .process_input(session_id, LpInput::Close) + .expect("B Close should produce action") + .expect("B Close failed"); + assert!(matches!(action_b_close, LpAction::ConnectionClosed)); + assert_eq!( + session_manager_2.get_state(session_id).unwrap(), + LpStateBare::Closed + ); + + // Further actions on B fail + let send_after_close_b = session_manager_2.process_input( + session_id, + LpInput::SendData(LpData::new_opaque(b"fail".to_vec())), + ); + assert!(send_after_close_b.is_err()); + assert!(matches!( + send_after_close_b.err().unwrap(), + LpError::LpSessionClosed + )); + println!("Close test passed."); + + // --- 7. Session Removal --- + assert!(session_manager_1.remove_state_machine(session_id)); + assert_eq!(session_manager_1.session_count(), 0); + assert!(!session_manager_1.state_machine_exists(session_id)); + + // B's session manager still has it until removed + assert!(session_manager_2.state_machine_exists(session_id)); + assert!(session_manager_2.remove_state_machine(session_id)); + assert_eq!(session_manager_2.session_count(), 0); + assert!(!session_manager_2.state_machine_exists(session_id)); + println!("Session removal test passed."); } - println!("Data transfer simulation completed."); - - // --- 4. Replay Protection Test --- - println!("Testing data packet replay protection via process_input..."); - let replay_result = session_manager_1 - .process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet - - assert!(replay_result.is_err(), "Replay should produce Err(...)"); - let error = replay_result.err().unwrap(); - assert!( - matches!(error, LpError::Replay(_)), - "Expected Replay error, got {:?}", - error - ); - println!("Data packet replay protection test passed."); - - // --- 5. Out-of-Order Test --- - println!("Testing out-of-order reception via process_input..."); - - // A prepares N+1 then N - let data_n_plus_1 = LpData::new_opaque(b"Message N+1".to_vec()); - let data_n = LpData::new_opaque(b"Message N".to_vec()); - - let action_send_n1 = session_manager_1 - .process_input(receiver_index, LpInput::SendData(data_n_plus_1.clone())) - .unwrap() - .unwrap(); - let packet_n1 = match action_send_n1 { - LpAction::SendPacket(p) => p, - _ => panic!("Expected SendPacket"), - }; - - let action_send_n = session_manager_1 - .process_input(receiver_index, LpInput::SendData(data_n.clone())) - .unwrap() - .unwrap(); - let packet_n = match action_send_n { - LpAction::SendPacket(p) => p, - _ => panic!("Expected SendPacket"), - }; - let packet_n_replay = packet_n.clone(); // For replay test - - // B receives N+1 first - println!(" B receives N+1"); - let action_recv_n1 = session_manager_2 - .process_input(receiver_index, LpInput::ReceivePacket(packet_n1)) - .unwrap() - .unwrap(); - match action_recv_n1 { - LpAction::DeliverData(d) => assert_eq!(d, data_n_plus_1, "Data N+1 mismatch"), - _ => panic!("Expected DeliverData for N+1"), - } - - // B receives N second (should work) - println!(" B receives N"); - let action_recv_n = session_manager_2 - .process_input(receiver_index, LpInput::ReceivePacket(packet_n)) - .unwrap() - .unwrap(); - match action_recv_n { - LpAction::DeliverData(d) => assert_eq!(d, data_n, "Data N mismatch"), - _ => panic!("Expected DeliverData for N"), - } - - // B tries to replay N (should fail) - println!(" B tries to replay N"); - let replay_n_result = session_manager_2 - .process_input(receiver_index, LpInput::ReceivePacket(packet_n_replay)); - assert!(replay_n_result.is_err(), "Replay N should produce Err"); - assert!( - matches!(replay_n_result.err().unwrap(), LpError::Replay(_)), - "Expected Replay error for N" - ); - println!("Out-of-order test passed."); - - // --- 6. Close Test --- - println!("Testing close via process_input..."); - - // A closes - let action_a_close = session_manager_1 - .process_input(receiver_index, LpInput::Close) - .expect("A Close should produce action") - .expect("A Close failed"); - assert!(matches!(action_a_close, LpAction::ConnectionClosed)); - assert_eq!( - session_manager_1.get_state(receiver_index).unwrap(), - LpStateBare::Closed - ); - - // Further actions on A fail - let send_after_close_a = session_manager_1.process_input( - receiver_index, - LpInput::SendData(LpData::new_opaque(b"fail".to_vec())), - ); - assert!(send_after_close_a.is_err()); - assert!(matches!( - send_after_close_a.err().unwrap(), - LpError::LpSessionClosed - )); - - // B closes - let action_b_close = session_manager_2 - .process_input(receiver_index, LpInput::Close) - .expect("B Close should produce action") - .expect("B Close failed"); - assert!(matches!(action_b_close, LpAction::ConnectionClosed)); - assert_eq!( - session_manager_2.get_state(receiver_index).unwrap(), - LpStateBare::Closed - ); - - // Further actions on B fail - let send_after_close_b = session_manager_2.process_input( - receiver_index, - LpInput::SendData(LpData::new_opaque(b"fail".to_vec())), - ); - assert!(send_after_close_b.is_err()); - assert!(matches!( - send_after_close_b.err().unwrap(), - LpError::LpSessionClosed - )); - println!("Close test passed."); - - // --- 7. Session Removal --- - assert!(session_manager_1.remove_state_machine(receiver_index)); - assert_eq!(session_manager_1.session_count(), 0); - assert!(!session_manager_1.state_machine_exists(receiver_index)); - - // B's session manager still has it until removed - assert!(session_manager_2.state_machine_exists(receiver_index)); - assert!(session_manager_2.remove_state_machine(receiver_index)); - assert_eq!(session_manager_2.session_count(), 0); - assert!(!session_manager_2.state_machine_exists(receiver_index)); - println!("Session removal test passed."); } // ... other tests ... } diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index 9ddcb91d3f..0ad99b4c85 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -6,17 +6,20 @@ //! This module implements session lifecycle management functionality, handling //! creation, retrieval, and storage of sessions. -use crate::state_machine::{LpAction, LpInput, LpStateBare}; -use crate::{LpError, LpMessage, LpSession, LpStateMachine}; +use crate::packet::EncryptedLpPacket; +use crate::peer_config::LpReceiverIndex; +use crate::state_machine::{LpAction, LpData, LpInput, LpStateBare}; +use crate::{LpError, LpSession, LpStateMachine}; use std::collections::HashMap; +pub use crate::replay::validator::PacketCount; + /// Manages the lifecycle of Lewes Protocol sessions. /// -/// The SessionManager is responsible for creating, storing, and retrieving sessions, -/// ensuring proper thread-safety for concurrent access. +/// The SessionManager is responsible for creating, storing, and retrieving sessions pub struct SessionManager { /// Manages state machines directly, keyed by lp_id - state_machines: HashMap, + state_machines: HashMap, } impl Default for SessionManager { @@ -35,62 +38,43 @@ impl SessionManager { pub fn process_input( &mut self, - lp_id: u32, + lp_id: LpReceiverIndex, input: LpInput, ) -> Result, LpError> { self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())? } - pub fn closed(&self, lp_id: u32) -> Result { + pub fn send_data(&mut self, lp_id: LpReceiverIndex, data: LpData) -> Result { + self.process_input(lp_id, LpInput::SendData(data))? + .ok_or(LpError::NotInTransport) + } + + pub fn receive_packet( + &mut self, + lp_id: LpReceiverIndex, + packet: EncryptedLpPacket, + ) -> Result, LpError> { + self.process_input(lp_id, LpInput::ReceivePacket(packet)) + } + + pub fn closed(&self, lp_id: LpReceiverIndex) -> Result { Ok(self.get_state(lp_id)? == LpStateBare::Closed) } - pub fn transport(&self, lp_id: u32) -> Result { + pub fn transport(&self, lp_id: LpReceiverIndex) -> Result { Ok(self.get_state(lp_id)? == LpStateBare::Transport) } #[cfg(test)] - fn get_state_machine_id(&self, lp_id: u32) -> Result { - self.with_state_machine(lp_id, |sm| sm.id())? + fn get_state_machine_id(&self, lp_id: LpReceiverIndex) -> Result { + self.with_state_machine(lp_id, |sm| sm.receiver_index())? } - pub fn get_state(&self, lp_id: u32) -> Result { + pub fn get_state(&self, lp_id: LpReceiverIndex) -> Result { self.with_state_machine(lp_id, |sm| Ok(sm.bare_state()))? } - pub fn receiving_counter_quick_check(&self, lp_id: u32, counter: u64) -> Result<(), LpError> { - self.with_state_machine(lp_id, |sm| { - sm.session()?.receiving_counter_quick_check(counter) - })? - } - - pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> { - self.with_state_machine_mut(lp_id, |sm| { - sm.session_mut()?.receiving_counter_mark(counter) - })? - } - - pub fn next_counter(&mut self, lp_id: u32) -> Result { - self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))? - } - - pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result, LpError> { - self.with_state_machine_mut(lp_id, |sm| { - sm.session_mut()? - .decrypt_data(message) - .map_err(LpError::NoiseError) - })? - } - - pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result { - self.with_state_machine_mut(lp_id, |sm| { - sm.session_mut()? - .encrypt_data(message) - .map_err(LpError::NoiseError) - })? - } - - pub fn current_packet_cnt(&self, lp_id: u32) -> Result<(u64, u64), LpError> { + pub fn current_packet_cnt(&self, lp_id: LpReceiverIndex) -> Result { self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))? } @@ -98,43 +82,54 @@ impl SessionManager { self.state_machines.len() } - pub fn state_machine_exists(&self, lp_id: u32) -> bool { + pub fn state_machine_exists(&self, lp_id: LpReceiverIndex) -> bool { self.state_machines.contains_key(&lp_id) } - pub fn with_state_machine(&self, lp_id: u32, f: F) -> Result + pub fn with_state_machine(&self, lp_id: LpReceiverIndex, f: F) -> Result where F: FnOnce(&LpStateMachine) -> R, { if let Some(sm) = self.state_machines.get(&lp_id) { Ok(f(sm)) } else { - Err(LpError::StateMachineNotFound { lp_id }) + Err(LpError::StateMachineNotFound(lp_id)) } - // self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution } // For mutable access (like running process_input) - pub fn with_state_machine_mut(&mut self, lp_id: u32, f: F) -> Result + pub fn with_state_machine_mut( + &mut self, + lp_id: LpReceiverIndex, + f: F, + ) -> Result where F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref { if let Some(sm) = self.state_machines.get_mut(&lp_id) { Ok(f(sm)) } else { - Err(LpError::StateMachineNotFound { lp_id }) + Err(LpError::StateMachineNotFound(lp_id)) } } - pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 { - let receiver_index = lp_session.id(); + pub fn create_session_state_machine( + &mut self, + lp_session: LpSession, + ) -> Result { + let session_id = lp_session.receiver_index(); + + if self.state_machines.contains_key(&session_id) { + return Err(LpError::DuplicateSessionId(session_id)); + } + let sm = LpStateMachine::new(lp_session); - self.state_machines.insert(receiver_index, sm); - receiver_index + self.state_machines.insert(session_id, sm); + Ok(session_id) } /// Method to remove a state machine - pub fn remove_state_machine(&mut self, lp_id: u32) -> bool { + pub fn remove_state_machine(&mut self, lp_id: LpReceiverIndex) -> bool { let removed = self.state_machines.remove(&lp_id); removed.is_some() @@ -145,21 +140,21 @@ impl SessionManager { mod tests { use super::*; use crate::{SessionsMock, mock_session_for_test}; + use nym_kkt_ciphersuite::{IntoEnumIterator, KEM}; #[test] fn test_session_manager_get() { let mut manager = SessionManager::new(); - let local_session = mock_session_for_test(); - let id = local_session.id(); + let id = local_session.receiver_index(); - let sm_1_id = manager.create_session_state_machine(local_session); + let sm_1_id = manager.create_session_state_machine(local_session).unwrap(); assert_eq!(sm_1_id, id); let retrieved = manager.state_machine_exists(id); assert!(retrieved); - let not_found = manager.state_machine_exists(99); + let not_found = manager.state_machine_exists(123); assert!(!not_found); } @@ -167,8 +162,7 @@ mod tests { fn test_session_manager_remove() { let mut manager = SessionManager::new(); let local_session = mock_session_for_test(); - - let sm_1_id = manager.create_session_state_machine(local_session); + let sm_1_id = manager.create_session_state_machine(local_session).unwrap(); let removed = manager.remove_state_machine(sm_1_id); assert!(removed); @@ -180,24 +174,26 @@ mod tests { #[test] fn test_multiple_sessions() { - let mut manager = SessionManager::new(); - let session1 = SessionsMock::mock_post_handshake(123).initiator; - let session2 = SessionsMock::mock_post_handshake(124).initiator; - let session3 = SessionsMock::mock_post_handshake(125).initiator; + for kem in KEM::iter() { + let mut manager = SessionManager::new(); + let session1 = SessionsMock::mock_seeded_post_handshake(123, kem).initiator; + let session2 = SessionsMock::mock_seeded_post_handshake(124, kem).initiator; + let session3 = SessionsMock::mock_seeded_post_handshake(125, kem).initiator; - let sm_1 = manager.create_session_state_machine(session1); - let sm_2 = manager.create_session_state_machine(session2); - let sm_3 = manager.create_session_state_machine(session3); + let sm_1 = manager.create_session_state_machine(session1).unwrap(); + let sm_2 = manager.create_session_state_machine(session2).unwrap(); + let sm_3 = manager.create_session_state_machine(session3).unwrap(); - assert_eq!(manager.session_count(), 3); + assert_eq!(manager.session_count(), 3); - let retrieved1 = manager.get_state_machine_id(sm_1).unwrap(); - let retrieved2 = manager.get_state_machine_id(sm_2).unwrap(); - let retrieved3 = manager.get_state_machine_id(sm_3).unwrap(); + let retrieved1 = manager.get_state_machine_id(sm_1).unwrap(); + let retrieved2 = manager.get_state_machine_id(sm_2).unwrap(); + let retrieved3 = manager.get_state_machine_id(sm_3).unwrap(); - assert_eq!(retrieved1, sm_1); - assert_eq!(retrieved2, sm_2); - assert_eq!(retrieved3, sm_3); + assert_eq!(retrieved1, sm_1); + assert_eq!(retrieved2, sm_2); + assert_eq!(retrieved3, sm_3); + } } #[test] @@ -206,7 +202,7 @@ mod tests { let sesion = mock_session_for_test(); - let sm = manager.create_session_state_machine(sesion); + let sm = manager.create_session_state_machine(sesion).unwrap(); assert_eq!(manager.session_count(), 1); let retrieved = manager.get_state_machine_id(sm); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index 7892fbf017..733f48b694 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -2,48 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 //! Lewes Protocol State Machine for managing connection lifecycle. -//! -//! LP protocol flow (KKT → PSQ → Noise): -//! 1. KKTExchange: Client requests gateway's KEM public key (signed for MITM protection) -//! 2. Handshaking: Noise XKpsk3 with PSQ-derived PSK embedded in handshake messages -//! - PSQ ciphertext piggybacked on ClientHello (no extra round-trip) -//! - PSK = Blake3(ECDH || PSQ_secret || salt) provides hybrid classical+PQ security -//! 3. Transport: ChaCha20-Poly1305 authenticated encryption with derived keys -//! //! State machine ensures protocol steps execute in correct order. Invalid transitions //! return LpError, preventing protocol violations. -use crate::{ - LpError, - message::{LpMessage, SubsessionKK1Data, SubsessionKK2Data, SubsessionReadyData}, - noise_protocol::NoiseError, - packet::LpPacket, - session::{LpSession, SubsessionHandshake}, -}; +use crate::packet::{EncryptedLpPacket, LpMessage}; +use crate::peer_config::LpReceiverIndex; +use crate::session::SessionId; +use crate::{LpError, session::LpSession}; use bytes::{Buf, Bytes}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::mem; -use tracing::debug; + +#[derive(Debug)] +pub struct LpTransportState { + /// The underlying session in the transport state + session: Box, +} /// Represents the possible states of the Lewes Protocol connection. #[derive(Debug, Default)] pub enum LpState { /// Handshake complete, ready for data transport. - Transport { session: Box }, - - /// Performing subsession KK handshake while parent remains active. - /// Parent can still send/receive; subsession messages tunneled through parent. - SubsessionHandshaking { - session: Box, - subsession: Box, - }, - - /// Parent session demoted after subsession promoted. - /// Can only receive (drain in-flight), cannot send. - ReadOnlyTransport { session: Box }, + Transport(LpTransportState), /// An error occurred, or the connection was intentionally closed. Closed { reason: String }, + /// Processing an input event. #[default] Processing, @@ -52,8 +36,6 @@ pub enum LpState { #[derive(Debug, Clone, PartialEq, Eq)] pub enum LpStateBare { Transport, - SubsessionHandshaking, - ReadOnlyTransport, Closed, Processing, } @@ -62,8 +44,6 @@ impl From<&LpState> for LpStateBare { fn from(state: &LpState) -> Self { match state { LpState::Transport { .. } => LpStateBare::Transport, - LpState::SubsessionHandshaking { .. } => LpStateBare::SubsessionHandshaking, - LpState::ReadOnlyTransport { .. } => LpStateBare::ReadOnlyTransport, LpState::Closed { .. } => LpStateBare::Closed, LpState::Processing => LpStateBare::Processing, } @@ -74,40 +54,27 @@ impl From<&LpState> for LpStateBare { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum LpInput { - /// Received an LP Packet from the network. - ReceivePacket(LpPacket), + /// Received an encrypted LP Packet from the network. + ReceivePacket(EncryptedLpPacket), + /// Application wants to send data (only valid in Transport state). SendData(LpData), + /// Close the connection. Close, - /// Initiate a subsession handshake (only valid in Transport state). - /// Creates SubsessionHandshake and sends KK1 message. - InitiateSubsession, } /// Represents actions the state machine requests the environment to perform. #[derive(Debug)] pub enum LpAction { /// Send an LP Packet over the network. - SendPacket(LpPacket), + SendPacket(EncryptedLpPacket), + /// Deliver decrypted application data received from the peer. DeliverData(LpData), + /// Inform the environment that the connection is closed. ConnectionClosed, - /// Subsession KK handshake initiated by this side. - /// Contains the KK1 packet to send and the subsession index for tracking. - SubsessionInitiated { - packet: LpPacket, - subsession_index: u64, - }, - /// Subsession handshake complete, ready for promotion. - /// Contains the packet to send (Some for initiator with SubsessionReady, None for responder), - /// the completed SubsessionHandshake for into_session(), and the new receiver_index. - SubsessionComplete { - packet: Option, - subsession: Box, - new_receiver_index: u32, - }, } /// Represent application data being sent in Transport mode @@ -191,9 +158,7 @@ impl LpStateMachine { pub fn session_mut(&mut self) -> Result<&mut LpSession, LpError> { match &mut self.state { - LpState::Transport { session } - | LpState::SubsessionHandshaking { session, .. } - | LpState::ReadOnlyTransport { session } => Ok(session), + LpState::Transport(transport) => Ok(&mut transport.session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), LpState::Processing => Err(LpError::LpSessionProcessing), } @@ -201,9 +166,7 @@ impl LpStateMachine { pub fn session(&self) -> Result<&LpSession, LpError> { match &self.state { - LpState::Transport { session } - | LpState::SubsessionHandshaking { session, .. } - | LpState::ReadOnlyTransport { session } => Ok(session), + LpState::Transport(transport) => Ok(&transport.session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), LpState::Processing => Err(LpError::LpSessionProcessing), } @@ -214,50 +177,114 @@ impl LpStateMachine { /// ownership of the session to the caller. pub fn into_session(self) -> Result { match self.state { - LpState::Transport { session } - | LpState::SubsessionHandshaking { session, .. } - | LpState::ReadOnlyTransport { session } => Ok(*session), + LpState::Transport(transport) => Ok(*transport.session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), LpState::Processing => Err(LpError::LpSessionProcessing), } } - pub fn id(&self) -> Result { - Ok(self.session()?.id()) + pub fn session_identifier(&self) -> Result { + Ok(*self.session()?.session_identifier()) + } + + pub fn receiver_index(&self) -> Result { + Ok(self.session()?.receiver_index()) } /// Creates a new state machine in `Transport` state post-KKT/PSQ handshake pub fn new(session: LpSession) -> Self { LpStateMachine { - state: LpState::Transport { + state: LpState::Transport(LpTransportState { session: Box::new(session), - }, + }), } } - /// Creates a state machine in Transport state from a completed subsession handshake. - /// - /// This is used when a subsession (rekeying) completes and we need a new state machine - /// for the promoted session that can handle further subsession initiations (chained rekeying). - /// - /// # Arguments - /// - /// * `subsession` - The completed subsession handshake - /// * `receiver_index` - The new session's receiver index - /// - /// # Errors - /// - /// Returns error if the subsession handshake is not complete. - pub fn from_subsession( - subsession: SubsessionHandshake, - receiver_index: u32, - ) -> Result { - let session = subsession.into_session(receiver_index)?; - Ok(LpStateMachine { - state: LpState::Transport { - session: Box::new(session), - }, - }) + fn process_input_transport( + &mut self, + mut state: LpTransportState, + input: LpInput, + ) -> (LpState, Option>) { + let session = &mut state.session; + match input { + LpInput::ReceivePacket(packet) => { + // Check if packet lp_id matches our session + if packet.outer_header().receiver_idx != session.receiver_index() { + let result_action = Some(Err(LpError::UnknownSessionId( + packet.outer_header().receiver_idx, + ))); + return (LpState::Transport(state), result_action); + } + + let ctr = packet.outer_header().counter; + + // 1. Check replay protection + if let Err(e) = session.receiving_counter_quick_check(ctr) { + return (LpState::Transport(state), Some(Err(e))); + } + + // 2. decrypt the packet and attempt to deliver data + let packet = match session.decrypt_packet(packet) { + Ok(packet) => packet, + Err(e) => return (LpState::Transport(state), Some(Err(e))), + }; + + // 3. Mark counter as received + if let Err(e) = session.receiving_counter_mark(ctr) { + return (LpState::Transport(state), Some(Err(e))); + } + + // Check message type + match packet.into_message() { + // Normal encrypted data + LpMessage::ApplicationData(payload) => { + // Deliver data + match payload.0.try_into() { + Ok(data) => { + let result_action = Some(Ok(LpAction::DeliverData(data))); + (LpState::Transport(state), result_action) + } + Err(e) => { + let reason = e.to_string(); + (LpState::Closed { reason }, Some(Err(e))) + } + } + } + other => { + // Unexpected message type in Transport state + let err = LpError::InvalidStateTransition { + state: "Transport".to_string(), + input: format!("Unexpected message type: {other}"), + }; + (LpState::Transport(state), Some(Err(err))) + } + } + } + LpInput::SendData(data) => { + // Encrypt and send application data + let result_action = match self.prepare_data_packet(session, data) { + Ok(packet) => Some(Ok(LpAction::SendPacket(packet))), + Err(e) => { + // If prepare fails, should we close? Let's report error and stay Transport for now. + // Alternative: transition to Closed state. + Some(Err(e)) + } + }; + // Remain in transport state + (LpState::Transport(state), result_action) + } + + // --- Close Transition --- + LpInput::Close => { + // Transition to Closed state + ( + LpState::Closed { + reason: "Closed by user".to_string(), + }, + Some(Ok(LpAction::ConnectionClosed)), + ) + } + } } /// Processes an input event and returns a list of actions to perform. @@ -270,606 +297,10 @@ impl LpStateMachine { // 2. Match on the owned current_state. Each arm calculates and returns the NEXT state. let next_state = match (current_state, input) { // --- Transport State --- - (LpState::Transport { mut session }, LpInput::ReceivePacket(packet)) => { - // Check if packet lp_id matches our session - if packet.header.receiver_idx() != session.id() { - result_action = - Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - LpState::Transport { session } - } else { - // Check message type - handle subsession initiation from peer - match &packet.message { - // Peer initiated subsession - we become responder - LpMessage::SubsessionKK1(kk1_data) => { - // Create subsession as responder - let subsession_index = session.next_subsession_index(); - match session.create_subsession(subsession_index, false) { - Ok(subsession) => { - // Process KK1 - match subsession.process_message(&kk1_data.payload) { - Ok(_) => { - // Prepare KK2 response - match subsession.prepare_message() { - Ok(kk2_payload) => { - let kk2_msg = LpMessage::SubsessionKK2( - SubsessionKK2Data { - payload: kk2_payload, - }, - ); - match session.next_packet(kk2_msg) { - Ok(response_packet) => { - result_action = - Some(Ok(LpAction::SendPacket( - response_packet, - ))); - // Stay in SubsessionHandshaking, wait for SubsessionReady - LpState::SubsessionHandshaking { - session, - subsession: Box::new(subsession), - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - // Normal encrypted data - LpMessage::EncryptedData(_) => { - // 1. Check replay protection - if let Err(e) = - session.receiving_counter_quick_check(packet.header.counter) - { - result_action = Some(Err(e)); - LpState::Transport { session } - } else { - // 2. Decrypt data - match session.decrypt_data(&packet.message) { - Ok(plaintext) => { - // 3. Mark counter as received - if let Err(e) = - session.receiving_counter_mark(packet.header.counter) - { - result_action = Some(Err(e)); - LpState::Transport { session } - } else { - // 4. Deliver data - match plaintext.try_into() { - Ok(data) => { - result_action = - Some(Ok(LpAction::DeliverData(data))); - LpState::Transport { session } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e.into())); - LpState::Closed { reason } - } - } - } - } - // Stale abort in Transport state - race already resolved. - // This can happen if abort arrives after loser already returned to Transport - // via KK1 processing (loser detected local < remote and became responder). - // The winner's abort message arrived late. Silently ignore. - LpMessage::SubsessionAbort => { - debug!("Ignoring stale SubsessionAbort in Transport state"); - result_action = None; - LpState::Transport { session } - } - _ => { - // Unexpected message type in Transport state - let err = LpError::InvalidStateTransition { - state: "Transport".to_string(), - input: format!("Unexpected message type: {}", packet.message), - }; - result_action = Some(Err(err)); - LpState::Transport { session } - } - } - } - } - (LpState::Transport { mut session }, LpInput::SendData(data)) => { - // Encrypt and send application data - match self.prepare_data_packet(&mut session, data) { - Ok(packet) => result_action = Some(Ok(LpAction::SendPacket(packet))), - Err(e) => { - // If prepare fails, should we close? Let's report error and stay Transport for now. - // Alternative: transition to Closed state. - result_action = Some(Err(e.into())); - } - } - // Remain in transport state - LpState::Transport { session } - } - - // --- Transport + InitiateSubsession → SubsessionHandshaking --- - (LpState::Transport { mut session }, LpInput::InitiateSubsession) => { - // Get next subsession index - let subsession_index = session.next_subsession_index(); - - // Create subsession handshake (this side is initiator) - match session.create_subsession(subsession_index, true) { - Ok(subsession) => { - // Prepare KK1 message - match subsession.prepare_message() { - Ok(kk1_payload) => { - let kk1_msg = LpMessage::SubsessionKK1(SubsessionKK1Data { - payload: kk1_payload, - }); - match session.next_packet(kk1_msg) { - Ok(packet) => { - // Emit SubsessionInitiated with packet and index - result_action = Some(Ok(LpAction::SubsessionInitiated { - packet, - subsession_index, - })); - LpState::SubsessionHandshaking { - session, - subsession: Box::new(subsession), - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - - // --- SubsessionHandshaking State --- - ( - LpState::SubsessionHandshaking { - mut session, - subsession, - }, - LpInput::ReceivePacket(packet), - ) => { - // Check if packet receiver_idx matches our session - if packet.header.receiver_idx() != session.id() { - result_action = - Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - LpState::SubsessionHandshaking { - session, - subsession, - } - } else { - match &packet.message { - LpMessage::SubsessionKK1(kk1_data) if !subsession.is_initiator() => { - // Responder processes KK1, prepares KK2 - // Responder stays in SubsessionHandshaking after sending KK2, - // waiting for SubsessionReady from initiator before completing - match subsession.process_message(&kk1_data.payload) { - Ok(_) => { - match subsession.prepare_message() { - Ok(kk2_payload) => { - let kk2_msg = - LpMessage::SubsessionKK2(SubsessionKK2Data { - payload: kk2_payload, - }); - match session.next_packet(kk2_msg) { - Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket( - response_packet, - ))); - // Stay in SubsessionHandshaking, wait for SubsessionReady - LpState::SubsessionHandshaking { - session, - subsession, - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - LpMessage::SubsessionKK1(kk1_data) if subsession.is_initiator() => { - // Simultaneous initiation race detected. - // Both sides called InitiateSubsession and sent KK1 to each other. - // Use X25519 public key comparison as deterministic tie-breaker. - // Lower key loses and becomes responder. - let local_key = session.local_x25519_public(); - let remote_key = session.remote_x25519_public(); - - if local_key.as_bytes() < remote_key.as_bytes() { - // We LOSE - become responder - // Use the same index as our initiator subsession, which should - // match the winner's index if subsession counters are in sync. - // This works because both sides independently picked the same index when - // they initiated simultaneously (both counters were at the same value). - let subsession_index = subsession.index; - match session.create_subsession(subsession_index, false) { - Ok(new_subsession) => { - match new_subsession.process_message(&kk1_data.payload) { - Ok(_) => { - match new_subsession.prepare_message() { - Ok(kk2_payload) => { - let kk2_msg = LpMessage::SubsessionKK2( - SubsessionKK2Data { - payload: kk2_payload, - }, - ); - match session.next_packet(kk2_msg) { - Ok(response_packet) => { - result_action = - Some(Ok(LpAction::SendPacket( - response_packet, - ))); - // Replace old initiator subsession with new responder subsession - LpState::SubsessionHandshaking { - session, - subsession: Box::new( - new_subsession, - ), - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } else { - // We WIN - stay initiator, notify peer they lost - // Send SubsessionAbort to explicitly tell peer to become responder - let abort_msg = LpMessage::SubsessionAbort; - match session.next_packet(abort_msg) { - Ok(abort_packet) => { - result_action = - Some(Ok(LpAction::SendPacket(abort_packet))); - LpState::SubsessionHandshaking { - session, - subsession, - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - } - LpMessage::SubsessionKK2(kk2_data) if subsession.is_initiator() => { - // Initiator processes KK2, completes handshake - // Initiator emits SubsessionComplete with SubsessionReady packet - // and the subsession for caller to promote via into_session() - match subsession.process_message(&kk2_data.payload) { - Ok(_) if subsession.is_complete() => { - // Generate new receiver_index for subsession - let new_receiver_index: u32 = rand::random(); - session.demote(new_receiver_index); - - // Send SubsessionReady with new index - let ready_msg = - LpMessage::SubsessionReady(SubsessionReadyData { - receiver_index: new_receiver_index, - }); - match session.next_packet(ready_msg) { - Ok(ready_packet) => { - result_action = - Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - subsession, - new_receiver_index, - })); - LpState::ReadOnlyTransport { session } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Ok(_) => { - // Handshake not complete yet, shouldn't happen for KK - let err = LpError::Internal( - "Subsession handshake incomplete after KK2".to_string(), - ); - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - LpMessage::EncryptedData(_) => { - // Parent still processes normal traffic during subsession handshake - // Same as Transport state handling - if let Err(e) = - session.receiving_counter_quick_check(packet.header.counter) - { - result_action = Some(Err(e)); - LpState::SubsessionHandshaking { - session, - subsession, - } - } else { - match session.decrypt_data(&packet.message) { - Ok(plaintext) => { - if let Err(e) = - session.receiving_counter_mark(packet.header.counter) - { - result_action = Some(Err(e)); - LpState::SubsessionHandshaking { - session, - subsession, - } - } else { - match plaintext.try_into() { - Ok(data) => { - result_action = - Some(Ok(LpAction::DeliverData(data))); - LpState::SubsessionHandshaking { - session, - subsession, - } - } - Err(err) => { - result_action = Some(Err(err)); - LpState::SubsessionHandshaking { - session, - subsession, - } - } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e.into())); - LpState::Closed { reason } - } - } - } - } - LpMessage::SubsessionReady(ready_data) if !subsession.is_initiator() => { - // Responder receives SubsessionReady from initiator - // Responder completes handshake here, uses initiator's receiver_index - // The subsession handshake should already be complete (after KK2) - if subsession.is_complete() { - let new_receiver_index = ready_data.receiver_index; - session.demote(new_receiver_index); - result_action = Some(Ok(LpAction::SubsessionComplete { - packet: None, // Responder has no packet to send - subsession, - new_receiver_index, - })); - LpState::ReadOnlyTransport { session } - } else { - // Shouldn't happen - handshake should be complete after KK2 - let err = LpError::Internal( - "Received SubsessionReady but handshake not complete" - .to_string(), - ); - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - LpMessage::SubsessionAbort if subsession.is_initiator() => { - // We received abort from peer - we lost the simultaneous initiation race. - // Peer has higher X25519 key and is staying as initiator. - // Discard our initiator subsession and return to Transport to receive peer's KK1. - // Peer's KK1 should already be in flight or queued. - result_action = None; - LpState::Transport { session } - } - LpMessage::SubsessionAbort if !subsession.is_initiator() => { - // Race was already resolved via KK1 - this abort is stale. - // We already became responder when we received KK1 and detected local < remote. - // The winner's abort message arrived after we processed their KK1. - // Silently ignore it - we're in the correct state. - result_action = None; - LpState::SubsessionHandshaking { - session, - subsession, - } - } - _ => { - // Wrong message type for subsession handshake - let err = LpError::InvalidStateTransition { - state: "SubsessionHandshaking".to_string(), - input: format!("Unexpected message type: {:?}", packet.message), - }; - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - } - } - - // Parent can still send data during subsession handshake - ( - LpState::SubsessionHandshaking { - mut session, - subsession, - }, - LpInput::SendData(data), - ) => { - match self.prepare_data_packet(&mut session, data) { - Ok(packet) => result_action = Some(Ok(LpAction::SendPacket(packet))), - Err(e) => { - result_action = Some(Err(e.into())); - } - } - LpState::SubsessionHandshaking { - session, - subsession, - } - } - - // Reject other inputs during subsession handshake - ( - LpState::SubsessionHandshaking { - session, - subsession, - }, - LpInput::InitiateSubsession, - ) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "SubsessionHandshaking".to_string(), - input: "InitiateSubsession".to_string(), - })); - LpState::SubsessionHandshaking { - session, - subsession, - } - } - - // --- ReadOnlyTransport State --- - (LpState::ReadOnlyTransport { mut session }, LpInput::ReceivePacket(packet)) => { - // Can still receive and decrypt, but state stays ReadOnlyTransport - if packet.header.receiver_idx() != session.id() { - result_action = - Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - LpState::ReadOnlyTransport { session } - } else if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) - { - result_action = Some(Err(e)); - LpState::ReadOnlyTransport { session } - } else { - match session.decrypt_data(&packet.message) { - Ok(plaintext) => { - if let Err(e) = session.receiving_counter_mark(packet.header.counter) { - result_action = Some(Err(e)); - LpState::ReadOnlyTransport { session } - } else { - match plaintext.try_into() { - Ok(data) => { - result_action = Some(Ok(LpAction::DeliverData(data))); - LpState::ReadOnlyTransport { session } - } - Err(err) => { - result_action = Some(Err(err)); - LpState::ReadOnlyTransport { session } - } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e.into())); - LpState::Closed { reason } - } - } - } - } - - // Reject SendData in read-only mode - (LpState::ReadOnlyTransport { session }, LpInput::SendData(_)) => { - result_action = Some(Err(LpError::NoiseError(NoiseError::SessionReadOnly))); - LpState::ReadOnlyTransport { session } - } - - // Reject other inputs in read-only mode - (LpState::ReadOnlyTransport { session }, LpInput::InitiateSubsession) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "ReadOnlyTransport".to_string(), - input: "InitiateSubsession".to_string(), - })); - LpState::ReadOnlyTransport { session } - } - - // --- Close Transition (applies to ReadyToHandshake, KKTExchange, Handshaking, Transport, SubsessionHandshaking, ReadOnlyTransport) --- - ( - LpState::Transport { .. } - | LpState::SubsessionHandshaking { .. } - | LpState::ReadOnlyTransport { .. }, - LpInput::Close, - ) => { - result_action = Some(Ok(LpAction::ConnectionClosed)); - // Transition to Closed state - LpState::Closed { - reason: "Closed by user".to_string(), - } + (LpState::Transport(transport), input) => { + let (next_state, action) = self.process_input_transport(transport, input); + result_action = action; + next_state } // Ignore Close if already Closed (closed_state @ LpState::Closed { .. }, LpInput::Close) => { @@ -877,11 +308,6 @@ impl LpStateMachine { // Return the original closed state closed_state } - // Ignore StartHandshake if Closed - // (closed_state @ LpState::Closed { .. }, LpInput::StartHandshake) => { - // result_action = Some(Err(LpError::LpSessionClosed)); - // closed_state - // } // Ignore ReceivePacket if Closed (closed_state @ LpState::Closed { .. }, LpInput::ReceivePacket(_)) => { result_action = Some(Err(LpError::LpSessionClosed)); @@ -900,19 +326,6 @@ impl LpStateMachine { result_action = Some(Err(err)); LpState::Closed { reason } } - - // --- Default: Invalid input for current state (if any combinations missed) --- - // Consider if this should transition to Closed state. For now, just report error - // and transition to Closed as a safety measure. - (invalid_state, input) => { - let err = LpError::InvalidStateTransition { - state: format!("{:?}", invalid_state), // Use owned state for debug info - input: format!("{:?}", input), - }; - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } }; // 3. Put the calculated next state back into the machine. @@ -927,11 +340,8 @@ impl LpStateMachine { &self, session: &mut LpSession, data: LpData, - ) -> Result { - let encrypted_message = session.encrypt_data(Vec::::from(data).as_ref())?; - session - .next_packet(encrypted_message) - .map_err(|e| NoiseError::Other(e.to_string())) // Improve error conversion? + ) -> Result { + session.encrypt_application_data(data.to_vec()) } } @@ -939,238 +349,95 @@ impl LpStateMachine { mod tests { use super::*; use crate::SessionsMock; + use nym_kkt_ciphersuite::{IntoEnumIterator, KEM}; #[test] fn test_state_machine_init() { - let mock_sessions = SessionsMock::mock_post_handshake(123); + for kem in KEM::iter() { + let mock_sessions = SessionsMock::mock_post_handshake(kem); - let initiator_sm = LpStateMachine::new(mock_sessions.initiator); - assert!(matches!(initiator_sm.state, LpState::Transport { .. })); - let init_session = initiator_sm.session().unwrap(); + let initiator_sm = LpStateMachine::new(mock_sessions.initiator); + assert!(matches!(initiator_sm.state, LpState::Transport { .. })); + let init_session = initiator_sm.session().unwrap(); - let responder_sm = LpStateMachine::new(mock_sessions.responder); - assert!(matches!(responder_sm.state, LpState::Transport { .. })); - let resp_session = responder_sm.session().unwrap(); + let responder_sm = LpStateMachine::new(mock_sessions.responder); + assert!(matches!(responder_sm.state, LpState::Transport { .. })); + let resp_session = responder_sm.session().unwrap(); - // Check both state machines use the same receiver_index - assert_eq!(init_session.id(), resp_session.id()); + // Check both state machines use the same receiver_index + assert_eq!(init_session.receiver_index(), resp_session.receiver_index()); + } } #[test] fn test_state_machine_simplified_flow() { - let receiver_index: u32 = 123; - let mock_sessions = SessionsMock::mock_post_handshake(123); + for kem in KEM::iter() { + let mock_sessions = SessionsMock::mock_post_handshake(kem); + let receiver_index = mock_sessions.responder.receiver_index(); - // Create state machines (already in Transport) - let mut initiator = LpStateMachine::new(mock_sessions.initiator); - let mut responder = LpStateMachine::new(mock_sessions.responder); + // Create state machines (already in Transport) + let mut initiator = LpStateMachine::new(mock_sessions.initiator); + let mut responder = LpStateMachine::new(mock_sessions.responder); - assert_eq!(initiator.id().unwrap(), responder.id().unwrap()); - - // --- Transport Phase --- - println!("--- Step 1: Initiator sends data ---"); - let data_to_send_1 = LpData::new_opaque(b"hello responder".to_vec()); - let init_actions_4 = initiator.process_input(LpInput::SendData(data_to_send_1.clone())); - let data_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_4 { - packet.clone() - } else { - panic!("Initiator should send data packet"); - }; - assert_eq!(data_packet_1.header.receiver_idx(), receiver_index); - - println!("--- Step 2: Responder receives data ---"); - let resp_actions_5 = responder.process_input(LpInput::ReceivePacket(data_packet_1)); - let resp_data_1 = if let Some(Ok(LpAction::DeliverData(data))) = resp_actions_5 { - data - } else { - panic!("Responder should deliver data"); - }; - assert_eq!(resp_data_1, data_to_send_1); - - println!("--- Step 3: Responder sends data ---"); - let data_to_send_2 = LpData::new_opaque(b"hello initiator".to_vec()); - let resp_actions_6 = responder.process_input(LpInput::SendData(data_to_send_2.clone())); - let data_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_6 { - packet.clone() - } else { - panic!("Responder should send data packet"); - }; - assert_eq!(data_packet_2.header.receiver_idx(), receiver_index); - - println!("--- Step 4: Initiator receives data ---"); - let init_actions_5 = initiator.process_input(LpInput::ReceivePacket(data_packet_2)); - if let Some(Ok(LpAction::DeliverData(data))) = init_actions_5 { - assert_eq!(data, data_to_send_2); - } else { - panic!("Initiator should deliver data"); - } - - // --- Close --- - println!("--- Step 5: Initiator closes ---"); - let init_actions_6 = initiator.process_input(LpInput::Close); - assert!(matches!( - init_actions_6, - Some(Ok(LpAction::ConnectionClosed)) - )); - assert!(matches!(initiator.state, LpState::Closed { .. })); - - println!("--- Step 6: Responder closes ---"); - let resp_actions_7 = responder.process_input(LpInput::Close); - assert!(matches!( - resp_actions_7, - Some(Ok(LpAction::ConnectionClosed)) - )); - assert!(matches!(responder.state, LpState::Closed { .. })); - } - - /// Helper function to complete a full handshake between initiator and responder, - /// returning both in Transport state ready for subsession testing. - fn setup_transport_sessions() -> (LpStateMachine, LpStateMachine) { - let sessions = SessionsMock::mock_post_handshake(12345); - ( - LpStateMachine::new(sessions.initiator), - LpStateMachine::new(sessions.responder), - ) - } - - #[test] - fn test_simultaneous_subsession_initiation() { - // Test for simultaneous subsession initiation race condition. - // Both sides call InitiateSubsession at the same time, sending KK1 to each other. - // The tie-breaker uses X25519 public key comparison: lower key becomes responder. - - let (mut alice, mut bob) = setup_transport_sessions(); - - // Get X25519 public keys to determine expected winner - let alice_x25519 = alice.session().unwrap().local_x25519_public(); - let bob_x25519 = bob.session().unwrap().local_x25519_public(); - - // Determine who should win (higher key stays initiator) - let alice_wins = alice_x25519.as_bytes() > bob_x25519.as_bytes(); - - // --- Both sides initiate subsession simultaneously --- - // Alice initiates subsession - let alice_kk1_packet = if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = - alice.process_input(LpInput::InitiateSubsession) - { - packet - } else { - panic!("Alice should initiate subsession with KK1"); - }; - assert!(matches!(alice.state, LpState::SubsessionHandshaking { .. })); - - // Bob initiates subsession (simultaneously) - let bob_kk1_packet = if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = - bob.process_input(LpInput::InitiateSubsession) - { - packet - } else { - panic!("Bob should initiate subsession with KK1"); - }; - assert!(matches!(bob.state, LpState::SubsessionHandshaking { .. })); - - // --- Cross-delivery of KK1 packets (race resolution) --- - // Alice receives Bob's KK1 - let alice_response = alice.process_input(LpInput::ReceivePacket(bob_kk1_packet)); - - // Bob receives Alice's KK1 - let bob_response = bob.process_input(LpInput::ReceivePacket(alice_kk1_packet)); - - // --- Verify tie-breaker worked correctly --- - if alice_wins { - // Alice has higher key - she stays initiator, sends SubsessionAbort - assert!( - matches!(alice_response, Some(Ok(LpAction::SendPacket(_)))), - "Alice (winner) should send SubsessionAbort" - ); - assert!( - matches!(alice.state, LpState::SubsessionHandshaking { .. }), - "Alice should still be SubsessionHandshaking as initiator" + assert_eq!( + initiator.session_identifier().unwrap(), + responder.session_identifier().unwrap() ); - // Bob has lower key - he becomes responder, sends KK2 - let bob_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = bob_response { - p + // --- Transport Phase --- + println!("--- Step 1: Initiator sends data ---"); + let data_to_send_1 = LpData::new_opaque(b"hello responder".to_vec()); + let init_actions_4 = initiator.process_input(LpInput::SendData(data_to_send_1.clone())); + let data_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_4 { + packet.clone() } else { - panic!("Bob (loser) should send KK2 as new responder"); + panic!("Initiator should send data packet"); }; - assert!( - matches!(bob.state, LpState::SubsessionHandshaking { .. }), - "Bob should be SubsessionHandshaking as responder" - ); + assert_eq!(data_packet_1.outer_header().receiver_idx, receiver_index); - // Complete the handshake: Alice receives KK2 - let alice_completion = alice.process_input(LpInput::ReceivePacket(bob_kk2_packet)); - match alice_completion { - Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - .. - })) => { - assert!( - matches!(alice.state, LpState::ReadOnlyTransport { .. }), - "Alice should be ReadOnlyTransport after SubsessionComplete" - ); - - // Bob receives SubsessionReady - let bob_final = bob.process_input(LpInput::ReceivePacket(ready_packet)); - assert!( - matches!(bob_final, Some(Ok(LpAction::SubsessionComplete { .. }))), - "Bob should complete with SubsessionComplete" - ); - assert!( - matches!(bob.state, LpState::ReadOnlyTransport { .. }), - "Bob should be ReadOnlyTransport" - ); - } - other => panic!("Alice should complete subsession, got: {:?}", other), - } - } else { - // Bob has higher key - he stays initiator, sends SubsessionAbort - assert!( - matches!(bob_response, Some(Ok(LpAction::SendPacket(_)))), - "Bob (winner) should send SubsessionAbort" - ); - assert!( - matches!(bob.state, LpState::SubsessionHandshaking { .. }), - "Bob should still be SubsessionHandshaking as initiator" - ); - - // Alice has lower key - she becomes responder, sends KK2 - let alice_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = alice_response { - p + println!("--- Step 2: Responder receives data ---"); + let resp_actions_5 = responder.process_input(LpInput::ReceivePacket(data_packet_1)); + let resp_data_1 = if let Some(Ok(LpAction::DeliverData(data))) = resp_actions_5 { + data } else { - panic!("Alice (loser) should send KK2 as new responder"); + panic!("Responder should deliver data"); }; - assert!( - matches!(alice.state, LpState::SubsessionHandshaking { .. }), - "Alice should be SubsessionHandshaking as responder" - ); + assert_eq!(resp_data_1, data_to_send_1); - // Complete the handshake: Bob receives KK2 - let bob_completion = bob.process_input(LpInput::ReceivePacket(alice_kk2_packet)); - match bob_completion { - Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - .. - })) => { - assert!( - matches!(bob.state, LpState::ReadOnlyTransport { .. }), - "Bob should be ReadOnlyTransport after SubsessionComplete" - ); + println!("--- Step 3: Responder sends data ---"); + let data_to_send_2 = LpData::new_opaque(b"hello initiator".to_vec()); + let resp_actions_6 = responder.process_input(LpInput::SendData(data_to_send_2.clone())); + let data_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_6 { + packet.clone() + } else { + panic!("Responder should send data packet"); + }; + assert_eq!(data_packet_2.outer_header().receiver_idx, receiver_index); - // Alice receives SubsessionReady - let alice_final = alice.process_input(LpInput::ReceivePacket(ready_packet)); - assert!( - matches!(alice_final, Some(Ok(LpAction::SubsessionComplete { .. }))), - "Alice should complete with SubsessionComplete" - ); - assert!( - matches!(alice.state, LpState::ReadOnlyTransport { .. }), - "Alice should be ReadOnlyTransport" - ); - } - other => panic!("Bob should complete subsession, got: {:?}", other), + println!("--- Step 4: Initiator receives data ---"); + let init_actions_5 = initiator.process_input(LpInput::ReceivePacket(data_packet_2)); + if let Some(Ok(LpAction::DeliverData(data))) = init_actions_5 { + assert_eq!(data, data_to_send_2); + } else { + panic!("Initiator should deliver data"); } + + // --- Close --- + println!("--- Step 5: Initiator closes ---"); + let init_actions_6 = initiator.process_input(LpInput::Close); + assert!(matches!( + init_actions_6, + Some(Ok(LpAction::ConnectionClosed)) + )); + assert!(matches!(initiator.state, LpState::Closed { .. })); + + println!("--- Step 6: Responder closes ---"); + let resp_actions_7 = responder.process_input(LpInput::Close); + assert!(matches!( + resp_actions_7, + Some(Ok(LpAction::ConnectionClosed)) + )); + assert!(matches!(responder.state, LpState::Closed { .. })); } } } diff --git a/common/nym-lp/src/transport/error.rs b/common/nym-lp/src/transport/error.rs new file mode 100644 index 0000000000..ccfee827ce --- /dev/null +++ b/common/nym-lp/src/transport/error.rs @@ -0,0 +1,55 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LpTransportError { + #[error("the encoded packet is too long ({size} bytes)")] + PacketTooBig { size: usize }, + + #[error("the encoded packet is too small ({size} bytes) to encode valid data")] + PacketTooSmall { size: usize }, + + #[error("failed to establish connection with the remote host: {0}")] + ConnectionFailure(String), + + #[error("failed to configure the established connection: {0}")] + ConnectionConfigFailure(String), + + #[error("connection got closed before finishing the operation")] + ConnectionClosed, + + #[error("the received packet was malformed: {0}")] + MalformedPacket(String), + + #[error("failed to send bytes across the channel: {0}")] + TransportSendFailure(String), + + #[error("failed to receive bytes across the channel: {0}")] + TransportReceiveFailure(String), +} + +impl LpTransportError { + pub fn connection_failure(error: impl Into) -> Self { + LpTransportError::ConnectionFailure(error.into()) + } + + pub fn connection_config(error: impl Into) -> Self { + LpTransportError::ConnectionConfigFailure(error.into()) + } + + pub fn send_failure(error: std::io::Error) -> Self { + if error.kind() == std::io::ErrorKind::UnexpectedEof { + return LpTransportError::ConnectionClosed; + } + LpTransportError::TransportSendFailure(error.to_string()) + } + + pub fn receive_failure(error: std::io::Error) -> Self { + if error.kind() == std::io::ErrorKind::UnexpectedEof { + return LpTransportError::ConnectionClosed; + } + LpTransportError::TransportReceiveFailure(error.to_string()) + } +} diff --git a/common/nym-lp-transport/src/lib.rs b/common/nym-lp/src/transport/mod.rs similarity index 52% rename from common/nym-lp-transport/src/lib.rs rename to common/nym-lp/src/transport/mod.rs index 3f62a665ed..8770aad406 100644 --- a/common/nym-lp-transport/src/lib.rs +++ b/common/nym-lp/src/transport/mod.rs @@ -1,4 +1,9 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod error; pub mod traits; + +pub use error::LpTransportError; + +pub use traits::{LpHandshakeChannel, LpTransportChannel}; diff --git a/common/nym-lp/src/transport/traits.rs b/common/nym-lp/src/transport/traits.rs new file mode 100644 index 0000000000..f3c39af0af --- /dev/null +++ b/common/nym-lp/src/transport/traits.rs @@ -0,0 +1,302 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::packet::{EncryptedLpPacket, OuterHeader}; +use crate::transport::error::LpTransportError; +use nym_kkt::context::KKTMode; +use nym_kkt_ciphersuite::KEM; +use std::net::SocketAddr; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpStream; +use tracing::debug; + +#[cfg(any(feature = "mock", test))] +use nym_test_utils::mocks::async_read_write::MockIOStream; + +pub const MAX_TRANSPORT_PACKET_SIZE: usize = 65536; // 64KB max +pub const MAX_HANDSHAKE_PACKET_SIZE: usize = 524287; // 524'160 for mceliece key + a bit of overhead for safety + +/// Simple trait allowing sending bytes across. +/// It is not concerned with encryption. It is up to the caller. +// only used in internal code (and tests) +#[allow(async_fn_in_trait)] +pub trait LpHandshakeChannel: Sized { + /// Write all provided data and immediately flush the buffer + async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError>; + + /// Wrapper around `ReadExact` to return the `Vec` of `n` bytes directly + async fn read_n_bytes(&mut self, n: usize) -> Result, LpTransportError>; + + /// Send the provided handshake message on the connection + async fn send_handshake_message( + &mut self, + message: M, + _: KEM, + ) -> Result<(), LpTransportError> { + self.write_all_and_flush(&message.into_bytes()).await + } + + /// Attempt to receive a handshake message of the provided type from the stream + async fn receive_handshake_message( + &mut self, + expected_size: usize, + ) -> Result { + let bytes = self.read_n_bytes(expected_size).await?; + M::try_from_bytes(bytes) + } +} + +pub trait HandshakeMessage: Sized { + /// Convert this message into bytes + fn into_bytes(self) -> Vec; + + /// Attempt to recover this message from the byte stream + fn try_from_bytes(bytes: Vec) -> Result; + + /// Expected size of this message based on the provided parameters + fn expected_size(mode: KKTMode, expected_kem: KEM, payload_size: usize) -> usize; + + /// Expected size of the response from the remote party. + /// `None` if this is the final (PSQ msg2) message of the exchange + fn response_size(&self, expected_kem: KEM) -> Option; +} + +async fn write_all_and_flush_async_write( + writer: &mut W, + data: &[u8], +) -> Result<(), LpTransportError> +where + W: AsyncWrite + Unpin, +{ + writer + .write_all(data) + .await + .map_err(LpTransportError::send_failure)?; + writer.flush().await.map_err(LpTransportError::send_failure) +} + +async fn read_n_bytes_async_read(reader: &mut R, n: usize) -> Result, LpTransportError> +where + R: AsyncRead + Unpin, +{ + let mut buf = vec![0u8; n]; + if n > MAX_HANDSHAKE_PACKET_SIZE { + return Err(LpTransportError::PacketTooBig { size: n }); + } + reader + .read_exact(&mut buf) + .await + .map_err(LpTransportError::receive_failure)?; + Ok(buf) +} + +// only used in internal code (and tests) +#[allow(async_fn_in_trait)] +pub trait LpTransportChannel: Sized { + async fn connect(endpoint: SocketAddr) -> Result; + + fn set_no_delay(&mut self, nodelay: bool) -> Result<(), LpTransportError>; + + /// Sends a serialised and encrypted LP packet over the data stream with length-prefixed framing. + /// + /// Format: 4-byte big-endian u32 length + packet bytes + /// + /// # Arguments + /// * `packet` - The encrypted LP packet to send + /// + /// # Errors + /// Returns an error on network transmission fails. + async fn send_length_prefixed_transport_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result<(), LpTransportError>; + + /// Receives an LP packet from a TCP stream with length-prefixed framing without additional parsing + /// + /// Format: 4-byte big-endian u32 length + packet bytes + /// + /// # Errors + /// Returns an error on network transmission fails. + async fn receive_length_prefixed_transport_bytes( + &mut self, + ) -> Result, LpTransportError>; + + /// Receives an LP packet from a TCP stream with length-prefixed framing. + /// + /// Format: 4-byte big-endian u32 length + packet bytes + /// + /// # Errors + /// Returns an error on network transmission fails. + async fn receive_length_prefixed_transport_packet( + &mut self, + ) -> Result { + let mut bytes = self.receive_length_prefixed_transport_bytes().await?; + + if bytes.len() < OuterHeader::SIZE { + return Err(LpTransportError::PacketTooSmall { size: bytes.len() }); + } + + // split it into the outer header and ciphertext + let ciphertext = bytes.split_off(OuterHeader::SIZE); + + // SAFETY: we just checked we have at least OuterHeader::SIZE bytes + #[allow(clippy::unwrap_used)] + let outer_header = OuterHeader::parse(&bytes).unwrap(); + + tracing::trace!( + "Received LP packet ({} bytes + 4 byte length-prefix)", + bytes.len() + ); + Ok(EncryptedLpPacket::new(outer_header, ciphertext)) + } +} + +async fn send_serialised_packet_async_write( + writer: &mut W, + packet: &EncryptedLpPacket, +) -> Result<(), LpTransportError> +where + W: AsyncWrite + Unpin, +{ + // Send 4-byte length prefix (u32 big-endian) + let len = packet.encoded_length() as u32; + writer + .write_all(&len.to_le_bytes()) + .await + .inspect_err(|e| debug!("Failed to send packet length: {e}")) + .map_err(LpTransportError::send_failure)?; + + // TODO: benchmark whether it'd be faster to concatenate all slices slices and + // use a single `write_all` call + + // Send the outer header + writer + .write_all(&packet.outer_header().to_bytes()) + .await + .inspect_err(|e| debug!("Failed to send packet data: {e}")) + .map_err(LpTransportError::send_failure)?; + + // Send the actual packet data + writer + .write_all(packet.ciphertext()) + .await + .inspect_err(|e| debug!("Failed to send packet data: {e}")) + .map_err(LpTransportError::send_failure)?; + + // Flush to ensure data is sent immediately + writer + .flush() + .await + .inspect_err(|e| debug!("Failed to flush stream: {e}")) + .map_err(LpTransportError::send_failure)?; + + tracing::trace!( + "Sent LP packet ({} bytes + 4 byte length-prefix)", + packet.encoded_length() + ); + Ok(()) +} + +async fn receive_length_prefixed_bytes_async_read( + reader: &mut R, +) -> Result, LpTransportError> +where + R: AsyncRead + Unpin, +{ + // Read 4-byte length prefix (u32 big-endian) + let mut len_buf = [0u8; 4]; + reader + .read_exact(&mut len_buf) + .await + .inspect_err(|e| debug!("Failed to read packet length: {e}")) + .map_err(LpTransportError::receive_failure)?; + + let size = u32::from_le_bytes(len_buf) as usize; + + // Sanity check to prevent huge allocations + if size > MAX_TRANSPORT_PACKET_SIZE { + return Err(LpTransportError::PacketTooBig { size }); + } + + // Read the actual packet data + let mut packet_buf = vec![0u8; size]; + reader + .read_exact(&mut packet_buf) + .await + .inspect_err(|e| debug!("Failed to read packet data: {e}")) + .map_err(LpTransportError::receive_failure)?; + + Ok(packet_buf) +} + +impl LpTransportChannel for TcpStream { + async fn connect(endpoint: SocketAddr) -> Result { + TcpStream::connect(endpoint) + .await + .map_err(|err| LpTransportError::connection_failure(err.to_string())) + } + + fn set_no_delay(&mut self, nodelay: bool) -> Result<(), LpTransportError> { + // Set TCP_NODELAY for low latency + self.set_nodelay(nodelay) + .map_err(|err| LpTransportError::connection_config(err.to_string())) + } + + async fn send_length_prefixed_transport_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result<(), LpTransportError> { + send_serialised_packet_async_write(self, packet).await + } + + async fn receive_length_prefixed_transport_bytes( + &mut self, + ) -> Result, LpTransportError> { + receive_length_prefixed_bytes_async_read(self).await + } +} + +#[cfg(any(feature = "mock", test))] +impl LpTransportChannel for MockIOStream { + async fn connect(_endpoint: SocketAddr) -> Result { + Ok(MockIOStream::default()) + } + + fn set_no_delay(&mut self, _nodelay: bool) -> Result<(), LpTransportError> { + Ok(()) + } + + async fn send_length_prefixed_transport_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result<(), LpTransportError> { + send_serialised_packet_async_write(self, packet).await + } + + async fn receive_length_prefixed_transport_bytes( + &mut self, + ) -> Result, LpTransportError> { + receive_length_prefixed_bytes_async_read(self).await + } +} + +#[cfg(any(feature = "mock", test))] +impl LpHandshakeChannel for MockIOStream { + async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError> { + write_all_and_flush_async_write(self, data).await + } + + async fn read_n_bytes(&mut self, n: usize) -> Result, LpTransportError> { + read_n_bytes_async_read(self, n).await + } +} + +impl LpHandshakeChannel for TcpStream { + async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError> { + write_all_and_flush_async_write(self, data).await + } + + async fn read_n_bytes(&mut self, n: usize) -> Result, LpTransportError> { + read_n_bytes_async_read(self, n).await + } +} diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs index f297de143d..8e271ab1ea 100644 --- a/common/registration/src/lib.rs +++ b/common/registration/src/lib.rs @@ -5,13 +5,14 @@ use nym_authenticator_requests::AuthenticatorVersion; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_ip_packet_requests::IpPair; -use nym_kkt_ciphersuite::{KEM, KEMKeyDigests, SignatureScheme}; +use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; pub use lp_messages::*; +use nym_crypto::asymmetric::x25519::DHPublicKey; pub use serialisation::BincodeError; mod lp_messages; @@ -56,9 +57,11 @@ pub struct WireguardConfiguration { #[derive(Clone, Debug)] pub struct NymNodeLPInformation { pub address: SocketAddr, - pub expected_kem_key_hashes: HashMap, - pub expected_signing_key_hashes: HashMap, - pub x25519: x25519::PublicKey, + pub expected_kem_key_hashes: BTreeMap, + pub x25519: DHPublicKey, + + // to be inferred from node's version + pub ciphersuite: Ciphersuite, /// Supported protocol version of the remote gateway. /// Included in case we have to downgrade our version. diff --git a/common/store-cipher/Cargo.toml b/common/store-cipher/Cargo.toml index 124d5b1869..a2066a5247 100644 --- a/common/store-cipher/Cargo.toml +++ b/common/store-cipher/Cargo.toml @@ -21,7 +21,7 @@ thiserror = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } [target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies] -getrandom = { version = "0.2", features = ["js"] } +getrandom = { workspace = true, features = ["js"] } [features] default = [] diff --git a/common/test-utils/Cargo.toml b/common/test-utils/Cargo.toml index ab509eb8f9..ead0cff923 100644 --- a/common/test-utils/Cargo.toml +++ b/common/test-utils/Cargo.toml @@ -15,6 +15,7 @@ description = "Helpers, traits, and mock definitions for tests" anyhow = { workspace = true } futures = { workspace = true } rand_chacha = { workspace = true } +rand_chacha09 = { workspace = true } tokio = { workspace = true, features = ["sync", "time", "rt"] } tracing = { workspace = true } diff --git a/common/test-utils/src/helpers.rs b/common/test-utils/src/helpers.rs index e96494dfc0..17ef674c93 100644 --- a/common/test-utils/src/helpers.rs +++ b/common/test-utils/src/helpers.rs @@ -1,19 +1,29 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +// fine in test code +#![allow(clippy::unwrap_used)] + use crate::traits::Timeboxed; use nym_bin_common::logging::tracing_subscriber::EnvFilter; use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt; use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt; use nym_bin_common::logging::{default_tracing_fmt_layer, tracing_subscriber}; use rand_chacha::rand_core::SeedableRng; +use rand_chacha09::rand_core::SeedableRng as SeedableRng09; use std::future::Future; +use std::sync::{Arc, Mutex}; use tokio::task::JoinHandle; use tokio::time::error::Elapsed; +// 'current' rand crate pub use rand_chacha::ChaCha20Rng as DeterministicRng; pub use rand_chacha::rand_core::{CryptoRng, RngCore}; +// rand09 compat +pub use rand_chacha09::ChaChaRng as DeterministicRng09; +pub use rand_chacha09::rand_core::{CryptoRng as CryptoRng09, RngCore as RngCore09}; + pub fn leak(val: T) -> &'static mut T { Box::leak(Box::new(val)) } @@ -26,6 +36,35 @@ where tokio::spawn(async move { fut.timeboxed().await }) } +pub struct DeterministicRng09Send(Arc>); + +impl DeterministicRng09Send { + pub fn new(deterministic_rng09: DeterministicRng09) -> Self { + Self(Arc::new(Mutex::new(deterministic_rng09))) + } +} + +impl CryptoRng09 for DeterministicRng09Send {} + +// unwraps are perfectly fine in test code +impl RngCore09 for DeterministicRng09Send { + fn next_u32(&mut self) -> u32 { + self.0.lock().unwrap().next_u32() + } + + fn next_u64(&mut self) -> u64 { + self.0.lock().unwrap().next_u64() + } + + fn fill_bytes(&mut self, dst: &mut [u8]) { + self.0.lock().unwrap().fill_bytes(dst) + } +} + +pub fn deterministic_rng_09() -> DeterministicRng09 { + seeded_rng_09([42u8; 32]) +} + pub fn deterministic_rng() -> DeterministicRng { seeded_rng([42u8; 32]) } @@ -34,10 +73,18 @@ pub fn seeded_rng(seed: [u8; 32]) -> DeterministicRng { DeterministicRng::from_seed(seed) } +pub fn seeded_rng_09(seed: [u8; 32]) -> DeterministicRng09 { + DeterministicRng09::from_seed(seed) +} + pub fn u64_seeded_rng(seed: u64) -> DeterministicRng { DeterministicRng::seed_from_u64(seed) } +pub fn u64_seeded_rng_09(seed: u64) -> DeterministicRng09 { + DeterministicRng09::seed_from_u64(seed) +} + // test logger to use during debugging #[allow(clippy::unwrap_used)] pub fn setup_test_logger() { diff --git a/common/wasm/client-core/.cargo/config.toml b/common/wasm/client-core/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/common/wasm/client-core/.cargo/config.toml +++ b/common/wasm/client-core/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/common/wasm/storage/.cargo/config.toml b/common/wasm/storage/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/common/wasm/storage/.cargo/config.toml +++ b/common/wasm/storage/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/common/wasm/storage/Cargo.toml b/common/wasm/storage/Cargo.toml index 884a7b89b6..7c07592da2 100644 --- a/common/wasm/storage/Cargo.toml +++ b/common/wasm/storage/Cargo.toml @@ -13,8 +13,6 @@ documentation.workspace = true [dependencies] async-trait = { workspace = true } -getrandom = { workspace = true, features = ["js"] } -js-sys = { workspace = true } wasm-bindgen = { workspace = true } serde = { workspace = true, features = ["derive"] } serde-wasm-bindgen = { workspace = true } diff --git a/common/wasm/utils/.cargo/config.toml b/common/wasm/utils/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/common/wasm/utils/.cargo/config.toml +++ b/common/wasm/utils/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 5468e50112..84a73def30 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1137,9 +1137,9 @@ checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index ca508afdac..51e3c98c6c 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -78,13 +78,9 @@ nym-authenticator-requests = { workspace = true } nym-client-core = { workspace = true, features = ["cli"] } nym-id = { workspace = true } nym-service-provider-requests-common = { workspace = true } - -# LP dependencies -nym-lp = { path = "../common/nym-lp" } -nym-lp-transport = { path = "../common/nym-lp-transport" } -nym-kcp = { path = "../common/nym-kcp" } nym-registration-common = { path = "../common/registration" } -bytes = { workspace = true } + +nym-lp = { path = "../common/nym-lp" } defguard_wireguard_rs = { workspace = true } diff --git a/gateway/src/config.rs b/gateway/src/config.rs index f12189b1d0..8df528674b 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -15,8 +15,6 @@ pub struct Config { pub upgrade_mode_watcher: UpgradeModeWatcher, - pub lp: crate::node::lp_listener::LpConfig, - pub debug: Debug, } @@ -26,7 +24,6 @@ impl Config { network_requester: impl Into, ip_packet_router: impl Into, upgrade_mode_watcher: impl Into, - lp: impl Into, debug: impl Into, ) -> Self { Config { @@ -34,7 +31,6 @@ impl Config { network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), upgrade_mode_watcher: upgrade_mode_watcher.into(), - lp: lp.into(), debug: debug.into(), } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 1b9a39b6ae..c419246bcd 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -127,27 +127,12 @@ pub enum GatewayError { #[error("{0}")] CredentialVerificationError(#[from] nym_credential_verification::Error), - #[error("LP connection error: {0}")] - LpConnectionError(String), - - #[error("LP protocol error: {0}")] - LpProtocolError(String), - - #[error("LP handshake error: {0}")] - LpHandshakeError(String), - #[error("Service provider {service} is not running")] ServiceProviderNotRunning { service: String }, #[error("Internal error: {0}")] InternalError(String), - #[error("Failed to bind listener to {address}: {source}")] - ListenerBindFailure { - address: String, - source: Box, - }, - #[error("Failed to parse ip address: {source}")] IpAddrParseError { #[from] diff --git a/gateway/src/node/lp_listener/data_handler.rs b/gateway/src/node/lp_listener/data_handler.rs deleted file mode 100644 index cbeb46b058..0000000000 --- a/gateway/src/node/lp_listener/data_handler.rs +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -//! LP Data Handler - UDP listener for LP data plane (port 51264) -//! -//! This module handles the data plane for LP clients that have completed registration -//! via the control plane (TCP:41264). LP-wrapped Sphinx packets arrive here, get -//! decrypted, and are forwarded into the mixnet. -//! -//! # Packet Flow -//! -//! ```text -//! LP Client → UDP:51264 → LP Data Handler → Mixnet Entry -//! LP(Sphinx) decrypt LP forward Sphinx -//! ``` -//! -//! # Wire Format -//! -//! Each UDP packet is a complete LP packet: -//! - Header (8 bytes): receiver_idx (4) + counter (4) -//! - Payload: Outer AEAD encrypted Sphinx packet -//! -//! The receiver_idx is used to look up the session established during LP registration. - -use super::LpHandlerState; -use crate::error::GatewayError; -use nym_lp::state_machine::{LpAction, LpInput}; -use nym_metrics::inc; -use nym_sphinx::forwarding::packet::MixPacket; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::net::UdpSocket; -use tracing::*; - -/// Maximum UDP packet size we'll accept -/// Sphinx packets are typically ~2KB, LP overhead is ~50 bytes, so 4KB is plenty -const MAX_UDP_PACKET_SIZE: usize = 4096; - -/// LP Data Handler for UDP data plane -pub struct LpDataHandler { - /// UDP socket for receiving LP-wrapped Sphinx packets - socket: Arc, - - /// Shared state with TCP control plane - state: LpHandlerState, - - /// Shutdown token - shutdown: nym_task::ShutdownToken, -} - -impl LpDataHandler { - /// Create a new LP data handler - pub async fn new( - bind_addr: SocketAddr, - state: LpHandlerState, - shutdown: nym_task::ShutdownToken, - ) -> Result { - let socket = UdpSocket::bind(bind_addr).await.map_err(|e| { - error!("Failed to bind LP data socket to {bind_addr}: {e}"); - GatewayError::ListenerBindFailure { - address: bind_addr.to_string(), - source: Box::new(e), - } - })?; - - info!("LP data handler listening on UDP {bind_addr}"); - - Ok(Self { - socket: Arc::new(socket), - state, - shutdown, - }) - } - - /// Run the UDP packet receive loop - pub async fn run(self) -> Result<(), GatewayError> { - let mut buf = vec![0u8; MAX_UDP_PACKET_SIZE]; - - loop { - tokio::select! { - biased; - - _ = self.shutdown.cancelled() => { - info!("LP data handler: received shutdown signal"); - break; - } - - result = self.socket.recv_from(&mut buf) => { - match result { - Ok((len, src_addr)) => { - // Process packet in place (no spawn - UDP is fast) - if let Err(e) = self.handle_packet(&buf[..len], src_addr).await { - debug!("LP data packet error from {}: {}", src_addr, e); - inc!("lp_data_packet_errors"); - } - } - Err(e) => { - warn!("LP data socket recv error: {}", e); - inc!("lp_data_recv_errors"); - } - } - } - } - } - - info!("LP data handler shutdown complete"); - Ok(()) - } - - /// Handle a single UDP packet - /// - /// # Packet Processing Steps - /// 1. Parse LP header to get receiver_idx (for routing) - /// 2. Look up session state machine by receiver_idx - /// 3. Process packet through state machine (handles decryption + replay protection) - /// 4. Forward decrypted Sphinx packet to mixnet - /// - /// # Security - /// The state machine's `process_input()` method handles replay protection by: - /// - Checking packet counter against receiving window - /// - Marking counter as used after successful decryption - /// - /// This prevents replay attacks where captured packets are re-sent. - async fn handle_packet(&self, packet: &[u8], src_addr: SocketAddr) -> Result<(), GatewayError> { - inc!("lp_data_packets_received"); - - // Step 1: Parse LP header (always cleartext for routing) - let header = nym_lp::codec::parse_lp_header_only(packet).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to parse LP header: {}", e)) - })?; - - let receiver_idx = header.receiver_idx; - let counter = header.counter; - let len = packet.len(); - - trace!("LP data packet from {src_addr} (receiver_idx={receiver_idx}, counter={counter}, len={len})"); - - // Step 2: Look up session state machine by receiver_idx (mutable for state updates) - let mut state_entry = self - .state - .session_states - .get_mut(&receiver_idx) - .ok_or_else(|| { - inc!("lp_data_unknown_session"); - GatewayError::LpProtocolError(format!( - "Unknown session for receiver_idx {receiver_idx}" - )) - })?; - - // Update last activity timestamp - state_entry.value().touch(); - - // Step 3: Get outer AEAD key for packet parsing - let outer_key = state_entry - .value() - .state - .session() - .map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))? - .outer_aead_key(); - - // Parse full packet with outer AEAD decryption - let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(outer_key)).map_err(|e| { - inc!("lp_data_decrypt_errors"); - GatewayError::LpProtocolError(format!("Failed to decrypt LP packet: {}", e)) - })?; - - // Step 4: Process packet through state machine - // This handles: - // - Replay protection (counter check + mark) - // - Inner Noise decryption - // - Subsession handling if applicable - let state_machine = &mut state_entry.value_mut().state; - - let action = state_machine - .process_input(LpInput::ReceivePacket(lp_packet)) - .ok_or_else(|| { - GatewayError::LpProtocolError("State machine returned no action".to_string()) - })? - .map_err(|e| { - inc!("lp_data_state_machine_errors"); - GatewayError::LpProtocolError(format!("State machine error: {}", e)) - })?; - - // Release session lock before forwarding - drop(state_entry); - - // Step 5: Handle the action from state machine - match action { - LpAction::DeliverData(data) => { - // Decrypted application data - forward as Sphinx packet - self.forward_sphinx_packet(&data.content).await?; - inc!("lp_data_packets_forwarded"); - Ok(()) - } - LpAction::SendPacket(_response_packet) => { - // UDP is connectionless - we can't send responses back easily - // For subsession rekeying, the client should use TCP control plane - debug!( - "Ignoring SendPacket action on UDP (receiver_idx={receiver_idx}) - use TCP for rekeying", - ); - inc!("lp_data_ignored_send_actions"); - Ok(()) - } - other => { - warn!( - "Unexpected action on UDP data plane from {}: {:?}", - src_addr, other - ); - inc!("lp_data_unexpected_actions"); - Err(GatewayError::LpProtocolError(format!( - "Unexpected state machine action on UDP: {:?}", - other - ))) - } - } - } - - /// Parse Sphinx packet bytes and forward to mixnet - /// - /// The decrypted LP payload contains a serialized MixPacket that includes: - /// - Packet type (1 byte) - /// - Key rotation (1 byte) - /// - Next hop address (first mix node) - /// - Sphinx packet data - async fn forward_sphinx_packet(&self, sphinx_bytes: &[u8]) -> Result<(), GatewayError> { - // Parse as MixPacket v2 format (packet_type || key_rotation || next_hop || packet) - let mix_packet = MixPacket::try_from_v2_bytes(sphinx_bytes).map_err(|e| { - inc!("lp_data_sphinx_parse_errors"); - GatewayError::LpProtocolError(format!("Failed to parse MixPacket: {e}")) - })?; - - trace!( - "Forwarding Sphinx packet to mixnet (next_hop={}, type={:?})", - mix_packet.next_hop(), - mix_packet.packet_type() - ); - - // Forward to mixnet via the shared channel - if let Err(e) = self.state.outbound_mix_sender.forward_packet(mix_packet) { - error!("Failed to forward Sphinx packet to mixnet: {}", e); - inc!("lp_data_forward_errors"); - return Err(GatewayError::InternalError(format!( - "Mix packet forwarding failed: {e}", - ))); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Sphinx packets are typically around 2KB - // LP overhead is small (~50 bytes header + AEAD tag) - // 4KB should be plenty with room to spare - const _: () = { - assert!(MAX_UDP_PACKET_SIZE >= 2048 + 100); - }; -} diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs deleted file mode 100644 index c65efc8443..0000000000 --- a/gateway/src/node/lp_listener/handler.rs +++ /dev/null @@ -1,1427 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::{LpHandlerState, ReceiverIndex, TimestampedState}; -use crate::error::GatewayError; -use bytes::BytesMut; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_lp::codec::serialize_lp_packet; -use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput}; -use nym_lp::{ - message::ForwardPacketData, LpMessage, LpPacket, LpSession, LpStateMachine, OuterHeader, -}; -use nym_lp_transport::traits::LpTransport; -use nym_metrics::{add_histogram_obs, inc}; -use nym_registration_common::{LpRegistrationRequest, RegistrationStatus}; -use std::net::SocketAddr; -use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::time::timeout; -use tracing::*; - -// Histogram buckets for LP operation duration (legacy - used by unused forwarding methods) -const LP_DURATION_BUCKETS: &[f64] = &[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]; - -// Timeout for forward I/O operations (send + receive on exit stream) -// Must be long enough to cover exit gateway processing time -const FORWARD_IO_TIMEOUT_SECS: u64 = 30; - -// Histogram buckets for LP connection lifecycle duration -// LP connections can be very short (registration only: ~1s) or very long (dVPN sessions: hours/days) -// Covers full range from seconds to 24 hours -const LP_CONNECTION_DURATION_BUCKETS: &[f64] = &[ - 1.0, // 1 second - 5.0, // 5 seconds - 10.0, // 10 seconds - 30.0, // 30 seconds - 60.0, // 1 minute - 300.0, // 5 minutes - 600.0, // 10 minutes - 1800.0, // 30 minutes - 3600.0, // 1 hour - 7200.0, // 2 hours - 14400.0, // 4 hours - 28800.0, // 8 hours - 43200.0, // 12 hours - 86400.0, // 24 hours -]; - -/// Connection lifecycle statistics tracking -struct ConnectionStats { - /// When the connection started - start_time: std::time::Instant, - /// Total bytes received (including protocol framing) - bytes_received: u64, - /// Total bytes sent (including protocol framing) - bytes_sent: u64, -} - -impl ConnectionStats { - fn new() -> Self { - Self { - start_time: std::time::Instant::now(), - bytes_received: 0, - bytes_sent: 0, - } - } - - fn record_bytes_received(&mut self, bytes: usize) { - self.bytes_received += bytes as u64; - } - - fn record_bytes_sent(&mut self, bytes: usize) { - self.bytes_sent += bytes as u64; - } -} - -pub struct LpConnectionHandler { - stream: S, - remote_addr: SocketAddr, - state: LpHandlerState, - stats: ConnectionStats, - - /// Bound receiver_idx for this connection (set after first packet). - /// All subsequent packets on this connection must use this receiver_idx. - /// Set from ClientHello's proposed receiver_index, or from header for non-bootstrap packets. - bound_receiver_idx: Option, - - /// Persistent connection to exit gateway for forwarding. - /// Opened on first forward, reused for subsequent forwards, closed when client disconnects. - /// Tuple contains (stream, target_address) to verify subsequent forwards go to same exit. - exit_stream: Option<(S, SocketAddr)>, -} - -impl LpConnectionHandler -where - S: LpTransport + AsyncRead + AsyncWrite + Unpin, -{ - pub fn new(stream: S, remote_addr: SocketAddr, state: LpHandlerState) -> Self { - Self { - stream, - remote_addr, - state, - stats: ConnectionStats::new(), - bound_receiver_idx: None, - exit_stream: None, - } - } - - /// AIDEV-NOTE: Stream-oriented packet loop - /// This handler processes multiple packets on a single TCP connection. - /// Connection lifecycle: handshake + registration, then client closes. - /// First packet binds the connection to a receiver_idx (session-affine). - /// Binding is set by handle_client_hello() from payload's receiver_index, - /// or by validate_or_set_binding() for non-bootstrap first packets. - pub async fn handle(mut self) -> Result<(), GatewayError> { - debug!("Handling LP connection from {}", self.remote_addr); - - // Track total LP connections handled - inc!("lp_connections_total"); - - // ============================================================ - // STREAM-ORIENTED PROCESSING: Loop until connection closes - // State persists in LpHandlerState maps across packets - // ============================================================ - - // 1. complete KKT/PSQ handshake before doing anything else. - // bail if it takes too long - let timeout = self.state.lp_config.debug.handshake_ttl; - let local_peer = self.state.local_lp_peer.clone(); - let stream = &mut self.stream; - - // TODO: - let ciphersuite = LpSession::default_ciphersuite(); - let session = match tokio::time::timeout(timeout, async move { - LpSession::psq_handshake_responder(stream, ciphersuite, local_peer) - .complete_as_responder() - .await - }) - .await - { - Err(_timeout) => { - debug!( - "timed out attempting to complete KTT/PSQ handshake with {}", - self.remote_addr - ); - self.emit_lifecycle_metrics(false); - return Ok(()); - } - Ok(Err(handshake_failure)) => { - debug!( - "failed to complete KKT/PSQ handshake with {}: {handshake_failure}", - self.remote_addr - ); - self.emit_lifecycle_metrics(false); - return Ok(()); - } - Ok(Ok(session)) => session, - }; - let receiver_idx = session.id(); - - // 2. insert the state machine into the shared state - let state_machine = LpStateMachine::new(session); - self.state - .session_states - .insert(receiver_idx, TimestampedState::new(state_machine)); - self.bound_receiver_idx = Some(receiver_idx); - - // 3. handle any new incoming packet - loop { - // Step 1: Receive raw packet bytes and parse header only (for routing) - let (raw_bytes, header) = match self.receive_raw_packet().await { - Ok(result) => result, - Err(e) if Self::is_connection_closed(&e) => { - // Graceful EOF - client closed connection - trace!("Connection closed by {} (EOF)", self.remote_addr); - break; - } - Err(e) => { - inc!("lp_errors_receive_packet"); - self.emit_lifecycle_metrics(false); - return Err(e); - } - }; - - let receiver_idx = header.receiver_idx; - - // Step 2: Validate the binding - if let Err(e) = self.validate_binding(receiver_idx) { - self.emit_lifecycle_metrics(false); - return Err(e); - } - - // Step 3: Process the packet - if let Err(e) = self.process_packet(raw_bytes, receiver_idx).await { - self.emit_lifecycle_metrics(false); - return Err(e); - } - } - - self.emit_lifecycle_metrics(true); - Ok(()) - } - - fn bound_receiver_index(&self) -> Result { - self.bound_receiver_idx.ok_or_else(|| { - GatewayError::LpProtocolError( - "missing bound receiver index after KKT/PSQ handshake".into(), - ) - }) - } - - /// Check if an error indicates the connection was closed (EOF). - /// AIDEV-NOTE: Uses string matching on error messages. Tokio's read_exact - /// returns UnexpectedEof which gets formatted into the error message. - fn is_connection_closed(e: &GatewayError) -> bool { - match e { - GatewayError::LpConnectionError(msg) => { - msg.contains("unexpected end of file") - || msg.contains("connection reset") - || msg.contains("broken pipe") - } - _ => false, - } - } - - /// Validate that the receiver_idx matches the bound session. - /// - /// Binding rules: - /// - ClientHello (receiver_idx=0): binding deferred to handle_client_hello() which - /// extracts receiver_index from payload - /// - First non-bootstrap packet: sets binding from header's receiver_idx - /// - Subsequent packets: must match bound receiver_idx - fn validate_binding(&self, receiver_idx: u32) -> Result<(), GatewayError> { - let bound_receiver_idx = self.bound_receiver_index()?; - - if bound_receiver_idx != receiver_idx { - warn!( - "Receiver_idx mismatch from {}: expected {bound_receiver_idx}, got {receiver_idx}", - self.remote_addr - ); - inc!("lp_errors_receiver_idx_mismatch"); - return Err(GatewayError::LpProtocolError(format!( - "receiver_idx mismatch: connection bound to {bound_receiver_idx}, packet has {receiver_idx}", - ))); - } - - Ok(()) - } - - /// Process a single packet: lookup session, parse, route to handler. - /// Individual handlers do NOT emit lifecycle metrics - the main loop handles that. - async fn process_packet( - &mut self, - raw_bytes: Vec, - receiver_idx: u32, - ) -> Result<(), GatewayError> { - // Get outer_aead_key based on receiver_idx - // Header is always cleartext for routing. Payload is encrypted after PSK. - let Some(state_machine) = self.state.session_states.get(&receiver_idx) else { - // session might have gotten removed due to inactivity - return Err(GatewayError::LpConnectionError(format!( - "missing session state for {receiver_idx} - has it been removed due to inactivity?" - ))); - }; - - let outer_key = state_machine - .state - .session() - .map_err(|err| GatewayError::LpProtocolError(err.to_string()))? - .outer_aead_key(); - - // Parse full packet with outer AEAD key - let packet = nym_lp::codec::parse_lp_packet(&raw_bytes, Some(outer_key)).map_err(|e| { - inc!("lp_errors_parse_packet"); - GatewayError::LpProtocolError(format!("Failed to parse LP packet: {e}")) - })?; - - drop(state_machine); - - trace!( - "Received packet from {} (receiver_idx={}, counter={})", - self.remote_addr, - receiver_idx, - packet.header().counter, - ); - - self.handle_transport_packet(receiver_idx, packet).await - } - - /// Handle transport packet (receiver_idx!=0, session established) - /// - /// This handles packets on established sessions, which can be either: - /// 1. EncryptedData containing LpRegistrationRequest or ForwardPacketData - /// 2. SubsessionKK1 - Client initiates subsession/rekeying - /// 3. SubsessionReady - Client confirms subsession promotion - /// - /// We process all transport packets through the state machine to enable - /// subsession support. The state machine returns appropriate actions: - /// - DeliverData: decrypted application data to process - /// - SendPacket: subsession response (KK2) to send - /// - SubsessionComplete: subsession promoted, create new session - async fn handle_transport_packet( - &mut self, - receiver_idx: u32, - packet: LpPacket, - ) -> Result<(), GatewayError> { - let remote = self.remote_addr; - debug!("Processing transport packet from {remote} (receiver_idx={receiver_idx})",); - - // Get state machine and process packet - let mut state_entry = self - .state - .session_states - .get_mut(&receiver_idx) - .ok_or_else(|| { - GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) - })?; - - // Update last activity timestamp - state_entry.value().touch(); - - let state_machine = &mut state_entry.value_mut().state; - - // Process packet through state machine - let action = state_machine - .process_input(LpInput::ReceivePacket(packet)) - .ok_or_else(|| { - GatewayError::LpProtocolError("No action from state machine".to_string()) - })? - .map_err(|e| GatewayError::LpProtocolError(format!("State machine error: {e}")))?; - - let lp_session = state_machine.session().map_err(|e| { - GatewayError::LpProtocolError(format!("Session unavailable after processing: {e}")) - })?; - - // Get outer key before releasing borrow - let outer_key = lp_session.outer_aead_key(); - - match action { - LpAction::SendPacket(response_packet) => { - // Subsession KK2 response - gateway is responder - // This means we received SubsessionKK1 and are responding - debug!("Sending subsession KK2 response to {remote} (receiver_idx={receiver_idx})",); - inc!("lp_subsession_kk2_sent"); - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(&response_packet, &mut packet_buf, Some(outer_key)).map_err( - |e| GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e)), - )?; - - drop(state_entry); - self.send_serialised_packet(&packet_buf).await?; - Ok(()) - } - LpAction::DeliverData(data) => { - // Decrypted application data - process as registration/forwarding - drop(state_entry); - self.handle_decrypted_payload(receiver_idx, data).await - } - LpAction::SubsessionComplete { - packet: ready_packet, - subsession, - new_receiver_index, - } => { - // Subsession complete - promote to new session - - // Send SubsessionReady packet if present (for initiator - gateway is responder, so typically None) - if let Some(ready_packet) = ready_packet { - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(&ready_packet, &mut packet_buf, Some(outer_key)).map_err( - |e| { - GatewayError::LpProtocolError(format!( - "Failed to serialize packet: {e}", - )) - }, - )?; - drop(state_entry); - self.send_serialised_packet(&packet_buf).await?; - } else { - drop(state_entry); - } - self.handle_subsession_complete(receiver_idx, *subsession, new_receiver_index) - .await - } - other => { - warn!("Unexpected action in transport from {remote}: {other:?}",); - Err(GatewayError::LpProtocolError(format!( - "Unexpected action: {other:?}", - ))) - } - } - } - - /// Handle decrypted transport payload (registration or forwarding request) - async fn handle_decrypted_payload( - &mut self, - receiver_idx: u32, - decrypted_data: LpData, - ) -> Result<(), GatewayError> { - let remote = self.remote_addr; - - let bytes = decrypted_data.content; - match decrypted_data.kind { - LpDataKind::Registration => { - let request = LpRegistrationRequest::try_deserialise(&bytes).map_err(|err| { - GatewayError::LpProtocolError(format!("malformed LpRegistrationRequest: {err}")) - })?; - - debug!( - "LP registration request from {remote} (receiver_idx={receiver_idx}): mode={:?}", - request.mode()); - - self.handle_registration_request(receiver_idx, request) - .await - } - LpDataKind::Forward => { - let forward_data = ForwardPacketData::decode(&bytes).map_err(|err| { - GatewayError::LpProtocolError(format!("malformed ForwardPacketData: {err}")) - })?; - - self.handle_forwarding_request(receiver_idx, forward_data) - .await - } - LpDataKind::Opaque => { - // Neither registration nor forwarding - unknown payload type - warn!("Unknown transport payload type from {remote} (receiver_idx={receiver_idx}). dropping {} bytes", bytes.len()); - inc!("lp_errors_unknown_payload_type"); - Err(GatewayError::LpProtocolError( - "Unknown transport payload type (not registration or forwarding)".to_string(), - )) - } - } - } - - /// Handle subsession completion - promote subsession to new session - /// - /// When a subsession handshake completes (SubsessionReady received): - /// 1. Send SubsessionReady packet if present (for initiator - gateway is responder, so None) - /// 2. Create new state machine from completed subsession - /// 3. Store new session under new_receiver_index - /// 4. Old session stays in ReadOnlyTransport state until TTL cleanup - async fn handle_subsession_complete( - &mut self, - old_receiver_idx: u32, - subsession: nym_lp::session::SubsessionHandshake, - new_receiver_index: u32, - ) -> Result<(), GatewayError> { - use nym_lp::state_machine::LpStateMachine; - - info!( - "Subsession complete from {}: old_idx={}, new_idx={}", - self.remote_addr, old_receiver_idx, new_receiver_index - ); - - // Create new state machine from completed subsession - let new_state_machine = LpStateMachine::from_subsession(subsession, new_receiver_index) - .map_err(|e| { - GatewayError::LpProtocolError(format!( - "Failed to create session from subsession: {e}", - )) - })?; - - // Check for receiver_index collision before inserting - // new_receiver_index is client-generated (rand::random() in state machine). - // Collisions are statistically unlikely (1 in 4 billion) but could cause DoS if exploited. - if self.state.session_states.contains_key(&new_receiver_index) { - warn!( - "Subsession receiver_index collision: {} from {}", - new_receiver_index, self.remote_addr - ); - inc!("lp_subsession_receiver_index_collision"); - return Err(GatewayError::LpProtocolError( - "Subsession receiver index collision - client should retry".to_string(), - )); - } - - // Store new session under new_receiver_index - self.state.session_states.insert( - new_receiver_index, - super::TimestampedState::new(new_state_machine), - ); - - // Old session is now in ReadOnlyTransport state (handled by state machine) - // It will be cleaned up by TTL-based cleanup task - - inc!("lp_subsession_complete"); - Ok(()) - } - - /// Attempt to wrap and send specified response back to the client - async fn send_response_packet( - &mut self, - receiver_idx: u32, - serialised_response: Vec, - response_kind: LpDataKind, - ) -> Result<(), GatewayError> { - let mut session_entry = self - .state - .session_states - .get_mut(&receiver_idx) - .ok_or_else(|| { - GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) - })?; - - // Access session via state machine for subsession support - let session = session_entry - .value_mut() - .state - .session_mut() - .map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?; - - let wrapped_lp_data = LpData::new(response_kind, serialised_response); - let data_bytes = wrapped_lp_data.to_vec(); - - let encrypted_message = session.encrypt_data(&data_bytes).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to encrypt response: {e}")) - })?; - - let response_packet = session.next_packet(encrypted_message).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to create response packet: {e}")) - })?; - - let outer_key = session.outer_aead_key(); - - // make sure to drop the entry before the .await call - // Serialize the packet (encrypted if outer_key provided) - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(&response_packet, &mut packet_buf, Some(outer_key)).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e)) - })?; - drop(session_entry); - - // Send response (encrypted with outer AEAD) - self.send_serialised_packet(&packet_buf).await?; - Ok(()) - } - - /// Handle registration request on an established session - async fn handle_registration_request( - &mut self, - receiver_idx: ReceiverIndex, - request: LpRegistrationRequest, - ) -> Result<(), GatewayError> { - // Process registration (might modify state) - let response = self.state.process_registration(receiver_idx, request).await; - let response_bytes = response.serialise().map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to serialize response: {e}")) - })?; - - self.send_response_packet(receiver_idx, response_bytes, LpDataKind::Registration) - .await?; - - match response.status { - RegistrationStatus::Completed => { - info!("LP registration successful for {}", self.remote_addr); - } - RegistrationStatus::Failed => { - warn!( - "LP registration failed for {}: {:?}", - self.remote_addr, - response.error_message() - ); - } - RegistrationStatus::PendingMoreData => { - info!( - "we required more deta from {} to complete registration", - self.remote_addr - ); - } - } - - Ok(()) - } - - /// Handle forwarding request on an established session - /// - /// Entry gateway receives ForwardPacketData from client, forwards inner packet - /// to exit gateway, receives response, encrypts it, and sends back to client. - async fn handle_forwarding_request( - &mut self, - receiver_idx: u32, - forward_data: ForwardPacketData, - ) -> Result<(), GatewayError> { - // Forward the packet to the target gateway and retrieve its response - let response_bytes = self.handle_forward_packet(forward_data).await?; - - self.send_response_packet(receiver_idx, response_bytes, LpDataKind::Forward) - .await?; - - debug!( - "LP forwarding completed for {} (receiver_idx={})", - self.remote_addr, receiver_idx - ); - - Ok(()) - } - - /// Validates that a ClientHello timestamp is within the acceptable time window. - /// - /// # Arguments - /// * `client_timestamp` - Unix timestamp (seconds) from ClientHello salt - /// * `tolerance` - Maximum acceptable age - /// - /// # Returns - /// * `Ok(())` if timestamp is valid (within tolerance window) - /// * `Err(GatewayError)` if timestamp is too old or too far in the future - /// - /// # Security - /// This prevents replay attacks by rejecting stale ClientHello messages. - /// The tolerance window should be: - /// - Large enough for clock skew + network latency - /// - Small enough to limit replay attack window - fn validate_timestamp(client_timestamp: u64, tolerance: Duration) -> Result<(), GatewayError> { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - - let age = now.abs_diff(client_timestamp); - if age > tolerance.as_secs() { - let direction = if now >= client_timestamp { - "old" - } else { - "future" - }; - - // Track timestamp validation failures - inc!("lp_timestamp_validation_rejected"); - if now >= client_timestamp { - inc!("lp_errors_timestamp_too_old"); - } else { - inc!("lp_errors_timestamp_too_far_future"); - } - - return Err(GatewayError::LpProtocolError(format!( - "ClientHello timestamp is too {} (age: {}s, tolerance: {}s)", - direction, - age, - tolerance.as_secs() - ))); - } - - // Track successful timestamp validation - inc!("lp_timestamp_validation_accepted"); - Ok(()) - } - - /// Receive client's public key and salt via ClientHello message - /// - /// Note: This method is currently unused but retained for potential future use - /// in alternative handshake flows. The current implementation uses `handle_client_hello()` - /// which processes ClientHello as part of the single-packet model. - #[allow(dead_code)] - async fn receive_client_hello( - &mut self, - ) -> Result<(x25519::PublicKey, ed25519::PublicKey, [u8; 32]), GatewayError> { - // Receive first packet which should be ClientHello (no outer encryption) - let (raw_bytes, _header) = self.receive_raw_packet().await?; - let packet = nym_lp::codec::parse_lp_packet(&raw_bytes, None) - .map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)))?; - - // Verify it's a ClientHello message - match packet.message() { - LpMessage::ClientHello(hello_data) => { - // Extract and validate timestamp (nym-110: replay protection) - let timestamp = hello_data.extract_timestamp(); - Self::validate_timestamp( - timestamp, - self.state.lp_config.debug.timestamp_tolerance, - )?; - - tracing::debug!( - "ClientHello timestamp validated: {} (age: {}s, tolerance: {}s)", - timestamp, - { - use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - now.abs_diff(timestamp) - }, - self.state.lp_config.debug.timestamp_tolerance.as_secs() - ); - - // Retrieve X25519 PublicKey (for Noise protocol) - let client_pubkey = hello_data.client_lp_public_key; - - // Retrieve Ed25519 PublicKey (for PSQ authentication) - let client_ed25519_pubkey = hello_data.client_ed25519_public_key; - - // Extract salt for PSK derivation - let salt = hello_data.salt; - - Ok((client_pubkey, client_ed25519_pubkey, salt)) - } - other => Err(GatewayError::LpProtocolError(format!( - "Expected ClientHello, got {}", - other - ))), - } - } - - /// Returns reference to the established forwarding channel to the exit. - pub fn forwarding_channel(&self) -> &Option<(S, SocketAddr)> { - &self.exit_stream - } - - /// This method establishes connection to the target gateway in order to - /// forward received packets and retrieve any responses - // - // In the future it will also perform identity validation to make sure - // the target node is a valid gateway present in the network - // - // Do not manually call this function. It is only exposed for the purposes of integration tests - #[doc(hidden)] - pub async fn establish_exit_stream( - &mut self, - target_addr: SocketAddr, - ) -> Result<(), GatewayError> { - // Acquire semaphore permit to limit concurrent connection opens (FD exhaustion protection) - // Permit is scoped to this block - only protects the connect() call, not stream reuse - let _permit = match self.state.forward_semaphore.try_acquire() { - Ok(permit) => permit, - Err(_) => { - inc!("lp_forward_rejected"); - return Err(GatewayError::LpConnectionError( - "Gateway at forward capacity".into(), - )); - } - }; - - // Connect to target gateway with timeout - let stream = match timeout(Duration::from_secs(5), S::connect(target_addr)).await { - Ok(Ok(stream)) => stream, - Ok(Err(e)) => { - inc!("lp_forward_failed"); - return Err(GatewayError::LpConnectionError(format!( - "Failed to connect to target gateway: {e}", - ))); - } - Err(_) => { - inc!("lp_forward_failed"); - return Err(GatewayError::LpConnectionError( - "Target gateway connection timeout".to_string(), - )); - } - }; - - debug!("Opened persistent exit connection to {target_addr} for forwarding"); - self.exit_stream = Some((stream, target_addr)); - - Ok(()) - } - - /// Forward an LP packet to another gateway - /// - /// This method connects to the target gateway, forwards the inner packet bytes, - /// receives the response, and returns it. Used for telescoping (hiding client IP). - /// - /// # Arguments - /// * `forward_data` - ForwardPacketData containing target gateway info and inner packet - /// - /// # Returns - /// * `Ok(Vec)` - Raw response bytes from target gateway - /// * `Err(GatewayError)` - If forwarding fails - /// - /// AIDEV-NOTE: Persistent exit stream forwarding - /// Uses self.exit_stream to maintain a persistent connection to the exit gateway. - /// First forward opens the connection, subsequent forwards reuse it. - /// Connection errors clear exit_stream, causing reconnection on next forward. - /// - /// Semaphore rationale: The forward_semaphore limits concurrent connection OPENS - /// (FD exhaustion protection), not concurrent operations. Since: - /// 1. Each LpConnectionHandler owns its exit_stream exclusively - /// 2. The handler loop processes packets sequentially (no concurrent access) - /// 3. Only connection opens consume new FDs - /// - /// The semaphore is only acquired when opening a new connection, not for reuse. - async fn handle_forward_packet( - &mut self, - forward_data: ForwardPacketData, - ) -> Result, GatewayError> { - use std::time::Duration; - use tokio::time::timeout; - - inc!("lp_forward_total"); - let start = std::time::Instant::now(); - - // Parse target gateway address - let target_addr = forward_data.target_lp_address; - - // Check if we need to open a new connection - let need_new_connection = match &self.exit_stream { - Some((_, existing_addr)) if *existing_addr == target_addr => false, - Some((_, existing_addr)) => { - // Target mismatch - this shouldn't happen in normal operation - // (client should only forward to one exit gateway) - // Return error to prevent silent behavior changes that could mask bugs - inc!("lp_forward_failed"); - return Err(GatewayError::LpProtocolError(format!( - "Forward target mismatch: session bound to {existing_addr}, got request for {target_addr}" - ))); - } - None => true, - }; - - if need_new_connection { - self.establish_exit_stream(target_addr).await?; - } - - // Get mutable reference to the exit stream - #[allow(clippy::unwrap_used)] - let (target_stream, _) = self.exit_stream.as_mut().unwrap(); - - debug!( - "Forwarding packet to {} ({} bytes)", - target_addr, - forward_data.inner_packet_bytes.len() - ); - - // Wrap all I/O in timeout to prevent hanging on unresponsive exit gateway - let io_timeout = Duration::from_secs(FORWARD_IO_TIMEOUT_SECS); - let inner_bytes = &forward_data.inner_packet_bytes; - - let io_result: Result, GatewayError> = timeout(io_timeout, async { - // Forward inner packet bytes (4-byte length prefix + packet data) - let len = inner_bytes.len() as u32; - target_stream - .write_all(&len.to_be_bytes()) - .await - .map_err(|e| { - GatewayError::LpConnectionError(format!( - "Failed to send length to target: {}", - e - )) - })?; - - target_stream.write_all(inner_bytes).await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to send packet to target: {}", e)) - })?; - - target_stream.flush().await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to flush target stream: {}", e)) - })?; - - // Read response from target gateway (4-byte length prefix + packet data) - let mut len_buf = [0u8; 4]; - target_stream.read_exact(&mut len_buf).await.map_err(|e| { - GatewayError::LpConnectionError(format!( - "Failed to read response length from target: {}", - e - )) - })?; - - let response_len = u32::from_be_bytes(len_buf) as usize; - - // Sanity check - const MAX_PACKET_SIZE: usize = 65536; - if response_len > MAX_PACKET_SIZE { - return Err(GatewayError::LpProtocolError(format!( - "Response size {response_len} exceeds maximum {MAX_PACKET_SIZE}", - ))); - } - - let mut response_buf = vec![0u8; response_len]; - target_stream - .read_exact(&mut response_buf) - .await - .map_err(|e| { - GatewayError::LpConnectionError(format!( - "Failed to read response from target: {e}", - )) - })?; - - Ok(response_buf) - }) - .await - .unwrap_or_else(|_| { - Err(GatewayError::LpConnectionError( - "Forward I/O timeout".to_string(), - )) - }); - - // Handle result - clear exit_stream on any error - let response_buf = match io_result { - Ok(buf) => buf, - Err(e) => { - inc!("lp_forward_failed"); - self.exit_stream = None; - return Err(e); - } - }; - - // Record metrics - let duration = start.elapsed().as_secs_f64(); - add_histogram_obs!("lp_forward_duration_seconds", duration, LP_DURATION_BUCKETS); - - inc!("lp_forward_success"); - debug!( - "Forwarding successful to {} ({} bytes response, {:.3}s)", - target_addr, - response_buf.len(), - duration - ); - - Ok(response_buf) - } - - /// Receive raw packet bytes and parse outer header only (for routing before session lookup). - /// - /// Returns the raw packet bytes and parsed outer header (receiver_idx + counter). - /// The caller should look up the session to get outer_aead_key, then call - /// `parse_lp_packet()` with the key. - async fn receive_raw_packet(&mut self) -> Result<(Vec, OuterHeader), GatewayError> { - use nym_lp::codec::parse_lp_header_only; - - // Read 4-byte length prefix (u32 big-endian) - let mut len_buf = [0u8; 4]; - self.stream.read_exact(&mut len_buf).await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to read packet length: {}", e)) - })?; - - let packet_len = u32::from_be_bytes(len_buf) as usize; - - // Sanity check to prevent huge allocations - const MAX_PACKET_SIZE: usize = 65536; // 64KB max - if packet_len > MAX_PACKET_SIZE { - return Err(GatewayError::LpProtocolError(format!( - "Packet size {} exceeds maximum {}", - packet_len, MAX_PACKET_SIZE - ))); - } - - // Read the actual packet data - let mut packet_buf = vec![0u8; packet_len]; - self.stream.read_exact(&mut packet_buf).await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to read packet data: {}", e)) - })?; - - // Track bytes received (4 byte header + packet data) - self.stats.record_bytes_received(4 + packet_len); - - // Parse header only (for routing - header is always cleartext) - let header = parse_lp_header_only(&packet_buf).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to parse LP header: {}", e)) - })?; - - Ok((packet_buf, header)) - } - - /// Send a serialised LP packet over the stream with proper length-prefixed framing. - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> Result<(), GatewayError> { - self.stream - .send_serialised_packet(packet_data) - .await - .map_err(|err| { - GatewayError::LpConnectionError(format!("failed to send LP packet: {err}")) - })?; - - // Track bytes sent (4 byte header + packet data) - self.stats.record_bytes_sent(4 + packet_data.len()); - - Ok(()) - } - - // only used in tests - #[cfg(test)] - async fn send_lp_packet(&mut self, packet: LpPacket) -> Result<(), GatewayError> { - let receiver_idx = self.bound_receiver_index()?; - - let mut session_entry = self - .state - .session_states - .get_mut(&receiver_idx) - .ok_or_else(|| { - GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) - })?; - - // Access session via state machine for subsession support - let session = session_entry - .value_mut() - .state - .session_mut() - .map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?; - - let outer_key = session.outer_aead_key(); - - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(&packet, &mut packet_buf, Some(outer_key)).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to serialize packet: {e}",)) - })?; - - self.stream - .send_serialised_packet(&packet_buf) - .await - .map_err(|err| { - GatewayError::LpConnectionError(format!("failed to send LP packet: {err}")) - })?; - - // Track bytes sent (4 byte header + packet data) - self.stats.record_bytes_sent(4 + packet_buf.len()); - - Ok(()) - } - - /// Emit connection lifecycle metrics - fn emit_lifecycle_metrics(&self, graceful: bool) { - use nym_metrics::inc_by; - - // Track connection duration - let duration = self.stats.start_time.elapsed().as_secs_f64(); - add_histogram_obs!( - "lp_connection_duration_seconds", - duration, - LP_CONNECTION_DURATION_BUCKETS - ); - - // Track bytes transferred - inc_by!( - "lp_connection_bytes_received_total", - self.stats.bytes_received as i64 - ); - inc_by!( - "lp_connection_bytes_sent_total", - self.stats.bytes_sent as i64 - ); - - // Track completion type - if graceful { - inc!("lp_connections_completed_gracefully"); - } else { - inc!("lp_connections_completed_with_error"); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::node::lp_listener::{LpConfig, LpDebug}; - use crate::node::ActiveClientsStore; - use bytes::BytesMut; - use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey}; - use nym_lp::message::{EncryptedDataPayload, LpMessage}; - use nym_lp::packet::{LpHeader, LpPacket}; - use nym_lp::peer::LpLocalPeer; - use nym_lp::SessionsMock; - use std::sync::Arc; - use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; - // ==================== Test Helpers ==================== - - /// Create a minimal test state for handler tests - async fn create_minimal_test_state() -> LpHandlerState { - use nym_crypto::asymmetric::ed25519; - use rand::rngs::OsRng; - - // Create in-memory storage for testing - let storage = nym_gateway_storage::GatewayStorage::init(":memory:", 100) - .await - .expect("Failed to create test storage"); - - // Create mock ecash manager for testing - let ecash_verifier = - nym_credential_verification::ecash::MockEcashManager::new(Box::new(storage.clone())); - - let lp_config = LpConfig { - debug: LpDebug { - timestamp_tolerance: Duration::from_secs(30), - ..Default::default() - }, - ..Default::default() - }; - let forward_semaphore = Arc::new(tokio::sync::Semaphore::new( - lp_config.debug.max_concurrent_forwards, - )); - - // Create mix forwarding channel (unused in tests but required by struct) - let (mix_sender, _mix_receiver) = nym_mixnet_client::forwarder::mix_forwarding_channels(); - - let id_keys = Arc::new(ed25519::KeyPair::new(&mut OsRng)); - let x_keys = Arc::new(id_keys.to_x25519()); - - let lp_peer = LpLocalPeer::new(id_keys, x_keys.clone()).with_kem_psq_key(x_keys); - - LpHandlerState { - lp_config, - ecash_verifier: Arc::new(ecash_verifier) - as Arc, - storage, - local_lp_peer: lp_peer, - metrics: nym_node_metrics::NymNodeMetrics::default(), - active_clients_store: ActiveClientsStore::new(), - outbound_mix_sender: mix_sender, - session_states: Arc::new(dashmap::DashMap::new()), - forward_semaphore, - peer_registrator: None, - } - } - - fn add_dummy_lp_state(handler: &mut LpConnectionHandler, session: LpSession) { - let id = session.id(); - let state_machine = LpStateMachine::new(session); - handler.bound_receiver_idx = Some(id); - - handler - .state - .session_states - .insert(id, TimestampedState::new(state_machine)); - } - - /// Helper to write an LP packet to a stream with proper framing - async fn write_lp_packet_to_stream( - stream: &mut W, - packet: &LpPacket, - ) -> Result<(), std::io::Error> { - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(packet, &mut packet_buf, None) - .map_err(|e| std::io::Error::other(e.to_string()))?; - - // Write length prefix - let len = packet_buf.len() as u32; - stream.write_all(&len.to_be_bytes()).await?; - - // Write packet data - stream.write_all(&packet_buf).await?; - stream.flush().await?; - - Ok(()) - } - - /// Helper to read an LP packet from a stream with proper framing - async fn read_lp_packet_from_stream( - stream: &mut R, - outer_aead_key: &OuterAeadKey, - ) -> Result { - // Read length prefix - let mut len_buf = [0u8; 4]; - stream.read_exact(&mut len_buf).await?; - let packet_len = u32::from_be_bytes(len_buf) as usize; - - // Read packet data - let mut packet_buf = vec![0u8; packet_len]; - stream.read_exact(&mut packet_buf).await?; - - // Parse packet - parse_lp_packet(&packet_buf, Some(outer_aead_key)) - .map_err(|e| std::io::Error::other(e.to_string())) - } - - // ==================== Existing Tests ==================== - - #[test] - fn test_validate_timestamp_current() { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // Current timestamp should always pass - assert!( - LpConnectionHandler::::validate_timestamp(now, Duration::from_secs(30)) - .is_ok() - ); - } - - #[test] - fn test_validate_timestamp_within_tolerance() { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // 10 seconds old, tolerance 30s -> should pass - let old_timestamp = now - 10; - assert!(LpConnectionHandler::::validate_timestamp( - old_timestamp, - Duration::from_secs(30) - ) - .is_ok()); - - // 10 seconds in future, tolerance 30s -> should pass - let future_timestamp = now + 10; - assert!(LpConnectionHandler::::validate_timestamp( - future_timestamp, - Duration::from_secs(30) - ) - .is_ok()); - } - - #[test] - fn test_validate_timestamp_too_old() { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // 60 seconds old, tolerance 30s -> should fail - let old_timestamp = now - 60; - let result = LpConnectionHandler::::validate_timestamp( - old_timestamp, - Duration::from_secs(30), - ); - assert!(result.is_err()); - assert!(format!("{:?}", result).contains("too old")); - } - - #[test] - fn test_validate_timestamp_too_far_future() { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // 60 seconds in future, tolerance 30s -> should fail - let future_timestamp = now + 60; - let result = LpConnectionHandler::::validate_timestamp( - future_timestamp, - Duration::from_secs(30), - ); - assert!(result.is_err()); - assert!(format!("{:?}", result).contains("too future")); - } - - #[test] - fn test_validate_timestamp_boundary() { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - // Exactly at tolerance boundary -> should pass - let boundary_timestamp = now - 30; - assert!(LpConnectionHandler::::validate_timestamp( - boundary_timestamp, - Duration::from_secs(30) - ) - .is_ok()); - - // Just beyond boundary -> should fail - let beyond_timestamp = now - 31; - assert!(LpConnectionHandler::::validate_timestamp( - beyond_timestamp, - Duration::from_secs(30) - ) - .is_err()); - } - - // ==================== Packet I/O Tests ==================== - - #[tokio::test] - async fn test_receive_raw_packet_valid() { - use tokio::net::{TcpListener, TcpStream}; - - // Bind to localhost - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - // Spawn server task - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - // Two-phase: receive raw bytes + header, then parse full packet - let (raw_bytes, header) = handler.receive_raw_packet().await?; - let packet = parse_lp_packet(&raw_bytes, None).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)) - })?; - Ok::<_, GatewayError>((header, packet)) - }); - - // Connect as client - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - - // Send a valid packet from client side - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 0, - }, - LpMessage::Busy, - ); - write_lp_packet_to_stream(&mut client_stream, &packet) - .await - .unwrap(); - - // Handler should receive and parse it correctly - // Note: header is OuterHeader (receiver_idx + counter only), not LpHeader - let (header, received) = server_task.await.unwrap().unwrap(); - assert_eq!(header.receiver_idx, 42); - assert_eq!(header.counter, 0); - assert_eq!(received.header().protocol_version, 1); - assert_eq!(received.header().receiver_idx, 42); - assert_eq!(received.header().counter, 0); - } - - #[tokio::test] - async fn test_receive_raw_packet_exceeds_max_size() { - use tokio::net::{TcpListener, TcpStream}; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - handler.receive_raw_packet().await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - - // Send a packet size that exceeds MAX_PACKET_SIZE (64KB) - let oversized_len: u32 = 70000; // > 65536 - client_stream - .write_all(&oversized_len.to_be_bytes()) - .await - .unwrap(); - client_stream.flush().await.unwrap(); - - // Handler should reject it - let result = server_task.await.unwrap(); - assert!(result.is_err()); - let err_msg = format!("{:?}", result.unwrap_err()); - assert!(err_msg.contains("exceeds maximum")); - } - - #[tokio::test] - async fn test_send_lp_packet_valid() { - use tokio::net::{TcpListener, TcpStream}; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let receiver_idx = 99; - let sessions = SessionsMock::mock_post_handshake(receiver_idx); - let init = sessions.initiator; - let resp = sessions.responder; - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - add_dummy_lp_state(&mut handler, resp); - - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx, - counter: 5, - }, - LpMessage::Busy, - ); - handler.send_lp_packet(packet).await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - - // Wait for server to send - server_task.await.unwrap().unwrap(); - - // Client should receive it correctly - let received = read_lp_packet_from_stream(&mut client_stream, init.outer_aead_key()) - .await - .unwrap(); - assert_eq!(received.header().receiver_idx, receiver_idx); - assert_eq!(received.header().counter, 5); - } - - #[tokio::test] - async fn test_send_receive_encrypted_data_message() { - use tokio::net::{TcpListener, TcpStream}; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let receiver_idx = 200; - let sessions = SessionsMock::mock_post_handshake(receiver_idx); - let init = sessions.initiator; - let resp = sessions.responder; - - let encrypted_payload = vec![42u8; 256]; - let expected_payload = encrypted_payload.clone(); - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - add_dummy_lp_state(&mut handler, resp); - - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx, - counter: 20, - }, - LpMessage::EncryptedData(EncryptedDataPayload(encrypted_payload)), - ); - handler.send_lp_packet(packet).await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - server_task.await.unwrap().unwrap(); - - let received = read_lp_packet_from_stream(&mut client_stream, init.outer_aead_key()) - .await - .unwrap(); - assert_eq!(received.header().receiver_idx, 200); - assert_eq!(received.header().counter, 20); - match received.message() { - LpMessage::EncryptedData(data) => { - assert_eq!(data, &EncryptedDataPayload(expected_payload)) - } - _ => panic!("Expected EncryptedData message"), - } - } -} diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index cf8d303300..eacd7cf658 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -16,9 +16,8 @@ use nym_credential_verification::ecash::{ use nym_credential_verification::upgrade_mode::{ UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, }; -use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::IpPacketRouter; -use nym_lp::peer::LpLocalPeer; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::NRServiceProviderBuilder; @@ -33,14 +32,12 @@ use rand::thread_rng; use std::net::IpAddr; use std::path::PathBuf; use std::sync::Arc; -use tokio::sync::Semaphore; use tracing::*; use zeroize::Zeroizing; pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher; use crate::node::wireguard::{PeerManager, PeerRegistrator}; pub use client_handling::active_clients::ActiveClientsStore; -pub use lp_listener::LpConfig; pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender; pub use nym_gateway_stats_storage::PersistentStatsStorage; pub use nym_gateway_storage::{ @@ -52,7 +49,6 @@ pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgen pub(crate) mod client_handling; pub(crate) mod internal_service_providers; -pub mod lp_listener; mod stale_data_cleaner; pub mod upgrade_mode; pub mod wireguard; @@ -95,12 +91,6 @@ pub struct GatewayTasksBuilder { /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, - /// x25519 keypair used within KTT exchange - x25519_keypair: Arc, - - /// x25519 (for now, to be changed into MlKem) keypair used for the PSQ derivation - kem_psq_keys: Arc, - storage: GatewayStorage, mix_packet_sender: MixForwardingSender, @@ -116,6 +106,8 @@ pub struct GatewayTasksBuilder { shutdown_tracker: ShutdownTracker, // populated and cached as necessary + use_mock_ecash: bool, + ecash_manager: Option>, @@ -129,8 +121,6 @@ impl GatewayTasksBuilder { pub fn new( config: Config, identity: Arc, - x25519: Arc, - kem_psq_keys: Arc, storage: GatewayStorage, mix_packet_sender: MixForwardingSender, metrics_sender: MetricEventsSender, @@ -138,6 +128,7 @@ impl GatewayTasksBuilder { mnemonic: Arc>, user_agent: UserAgent, upgrade_mode_state: UpgradeModeState, + use_mock_ecash: bool, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -148,8 +139,6 @@ impl GatewayTasksBuilder { wireguard_data: None, user_agent, identity_keypair: identity, - x25519_keypair: x25519, - kem_psq_keys, storage, mix_packet_sender, metrics_sender, @@ -157,6 +146,7 @@ impl GatewayTasksBuilder { upgrade_mode_state, mnemonic, shutdown_tracker, + use_mock_ecash, ecash_manager: None, wireguard_peers: None, wireguard_networks: None, @@ -235,7 +225,7 @@ impl GatewayTasksBuilder { GatewayError, > { // Check if we should use mock ecash for testing - if self.config.lp.debug.use_mock_ecash { + if self.use_mock_ecash { warn!("Using MockEcashManager for LP testing (credentials NOT verified)"); let mock_manager = MockEcashManager::new(Box::new(self.storage.clone())); return Ok(Arc::new(mock_manager) @@ -344,36 +334,6 @@ impl GatewayTasksBuilder { )) } - pub async fn build_lp_listener( - &mut self, - peer_registrator: Option, - active_clients_store: ActiveClientsStore, - ) -> Result { - let handler_state = lp_listener::LpHandlerState { - ecash_verifier: self.ecash_manager().await?, - storage: self.storage.clone(), - local_lp_peer: LpLocalPeer::new( - self.identity_keypair.clone(), - self.x25519_keypair.clone(), - ) - .with_kem_psq_key(self.kem_psq_keys.clone()), - metrics: self.metrics.clone(), - active_clients_store, - peer_registrator, - lp_config: self.config.lp, - outbound_mix_sender: self.mix_packet_sender.clone(), - session_states: Arc::new(dashmap::DashMap::new()), - forward_semaphore: Arc::new(Semaphore::new( - self.config.lp.debug.max_concurrent_forwards, - )), - }; - - Ok(lp_listener::LpListener::new( - handler_state, - self.shutdown_tracker.clone(), - )) - } - fn build_network_requester( &mut self, topology_provider: Box, diff --git a/gateway/src/node/wireguard/new_peer_registration/lp.rs b/gateway/src/node/wireguard/new_peer_registration/lp.rs index dcc505ab94..9e7a45d057 100644 --- a/gateway/src/node/wireguard/new_peer_registration/lp.rs +++ b/gateway/src/node/wireguard/new_peer_registration/lp.rs @@ -1,13 +1,13 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::pending::{ PendingRegistration, PendingRegistrationData, }; use crate::node::wireguard::{GatewayWireguardError, PeerRegistrator}; use defguard_wireguard_rs::host::Peer; use defguard_wireguard_rs::key::Key; +use nym_lp::peer_config::LpReceiverIndex; use nym_registration_common::{LpRegistrationResponse, WireguardRegistrationData}; use nym_wireguard::ip_pool::{allocated_ip_pair, IpPair}; use nym_wireguard_types::PeerPublicKey; @@ -58,9 +58,10 @@ impl PeerRegistrator { pub(super) async fn check_pending_lp_registration( &self, - sender: ReceiverIndex, + receiver_index: LpReceiverIndex, ) -> Result, GatewayWireguardError> { - let Some(pending_registration) = self.pending_registrations.check_lp(sender).await else { + let Some(pending_registration) = self.pending_registrations.check_lp(receiver_index).await + else { return Ok(None); }; @@ -104,7 +105,7 @@ impl PeerRegistrator { pub(super) async fn process_fresh_initial_lp_registration( &self, - sender: ReceiverIndex, + receiver_index: LpReceiverIndex, remote_public: PeerPublicKey, psk: Key, ) -> Result { @@ -121,7 +122,7 @@ impl PeerRegistrator { .lp .write() .await - .insert(sender, pending); + .insert(receiver_index, pending); Ok(response) } diff --git a/gateway/src/node/wireguard/new_peer_registration/mod.rs b/gateway/src/node/wireguard/new_peer_registration/mod.rs index 43dcce7bb0..b2f89accf7 100644 --- a/gateway/src/node/wireguard/new_peer_registration/mod.rs +++ b/gateway/src/node/wireguard/new_peer_registration/mod.rs @@ -11,7 +11,6 @@ //! 2. Finalisation request message is received, where credential has to be attached is verified. //! Upon successful completion, pending registration is transformed into a properly inserted peer. -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::pending::{ PendingRegistration, PendingRegistrations, }; @@ -32,6 +31,7 @@ use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; use nym_crypto::asymmetric::x25519; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::models::PersistedBandwidth; +use nym_lp::peer_config::LpReceiverIndex; use nym_registration_common::dvpn::{ LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest, }; @@ -225,7 +225,7 @@ impl PeerRegistrator { Ok(()) } - pub(crate) async fn on_initial_authenticator_request( + pub async fn on_initial_authenticator_request( &mut self, init_message: Box, protocol: Protocol, @@ -262,7 +262,7 @@ impl PeerRegistrator { .await } - pub(crate) async fn on_final_authenticator_request( + pub async fn on_final_authenticator_request( &mut self, final_message: Box, protocol: Protocol, @@ -307,10 +307,10 @@ impl PeerRegistrator { ) } - pub(crate) async fn on_initial_lp_request( + pub async fn on_initial_lp_request( &self, init_msg: LpDvpnRegistrationInitialRequest, - sender: ReceiverIndex, + receiver_index: LpReceiverIndex, ) -> Result { let remote_public = init_msg.wg_public_key; let psk = Key::new(init_msg.psk); @@ -318,7 +318,9 @@ impl PeerRegistrator { // 1. check if there's any pending registration already in progress, // if so, return the same data again without additional processing, // but update stored PSK - if let Some(pending_registration) = self.check_pending_lp_registration(sender).await? { + if let Some(pending_registration) = + self.check_pending_lp_registration(receiver_index).await? + { self.update_peer_psk(remote_public, psk).await?; return Ok(pending_registration); } @@ -332,19 +334,19 @@ impl PeerRegistrator { } // 3. process fresh registration request - self.process_fresh_initial_lp_registration(sender, remote_public, psk) + self.process_fresh_initial_lp_registration(receiver_index, remote_public, psk) .await } - pub(crate) async fn on_final_lp_request( + pub async fn on_final_lp_request( &self, final_msg: LpDvpnRegistrationFinalisation, - sender: ReceiverIndex, + receiver_index: LpReceiverIndex, ) -> Result { // 1. check if there's any pending registration associated with this peer let pending_data = self .pending_registrations - .check_lp(sender) + .check_lp(receiver_index) .await .ok_or(GatewayWireguardError::RegistrationNotInProgress)? .clone(); @@ -356,7 +358,7 @@ impl PeerRegistrator { .await?; // 3 remove pending registration - self.pending_registrations.remove_lp(sender).await; + self.pending_registrations.remove_lp(receiver_index).await; // 4. construct and return the response Ok(pending_data.to_registered_lp_response(self.upgrade_mode_enabled())) diff --git a/gateway/src/node/wireguard/new_peer_registration/pending.rs b/gateway/src/node/wireguard/new_peer_registration/pending.rs index cb933514db..1899ccf8c8 100644 --- a/gateway/src/node/wireguard/new_peer_registration/pending.rs +++ b/gateway/src/node/wireguard/new_peer_registration/pending.rs @@ -1,7 +1,6 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::lp_listener::ReceiverIndex; use crate::node::wireguard::new_peer_registration::helpers::{ build_final_authenticator_response, build_pending_authenticator_response, }; @@ -9,6 +8,7 @@ use crate::node::wireguard::GatewayWireguardError; use defguard_wireguard_rs::key::Key; use nym_authenticator_requests::AuthenticatorVersion; use nym_crypto::asymmetric::x25519; +use nym_lp::peer_config::LpReceiverIndex; use nym_registration_common::{LpRegistrationResponse, WireguardRegistrationData}; use nym_sdk::mixnet::Recipient; use nym_wireguard::ip_pool::IpPair; @@ -116,9 +116,8 @@ pub(crate) struct PendingRegistrations { pub(crate) authenticator: Arc>>, /// Registrations in progress received from the LP Listener via the - /// [`crate::node::lp_listener::handler::LpConnectionHandler`] and handle through - /// [`crate::node::lp_listener::registration::LpHandlerState`] - pub(crate) lp: Arc>>, + /// `LpConnectionHandler` and handle through `LpHandlerState` + pub(crate) lp: Arc>>, } impl PendingRegistrations { @@ -133,13 +132,13 @@ impl PendingRegistrations { self.authenticator.write().await.remove(peer); } - pub(crate) async fn remove_lp(&self, receiver_index: ReceiverIndex) { + pub(crate) async fn remove_lp(&self, receiver_index: LpReceiverIndex) { self.lp.write().await.remove(&receiver_index); } pub(crate) async fn check_lp( &self, - receiver_index: ReceiverIndex, + receiver_index: LpReceiverIndex, ) -> Option { self.lp.read().await.get(&receiver_index).cloned() } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 3d825c7e73..aaeced8b6d 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -22,11 +22,13 @@ nym-credential-verification = { path = "../common/credential-verification" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-test-utils = { path = "../common/test-utils" } nym-registration-client = { path = "../nym-registration-client" } -nym-lp-transport = { path = "../common/nym-lp-transport", features = ["io-mocks"] } -nym-gateway = { path = "../gateway" } +nym-node = { path = "../nym-node" } +nym-lp = { path = "../common/nym-lp", features = ["mock"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } tracing = { workspace = true } futures = { workspace = true } +nym-kkt = { workspace = true } +nym-kkt-ciphersuite = { workspace = true } [lints] workspace = true diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index cf3658f8fe..444104f538 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -9,16 +9,20 @@ mod tests { use nym_credential_verification::upgrade_mode::testing::mock_dummy_upgrade_mode_details; use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; - use nym_gateway::GatewayError; - use nym_gateway::node::lp_listener::handler::LpConnectionHandler; - use nym_gateway::node::lp_listener::{ - LpDebug, LpHandlerState, LpLocalPeer, MixForwardingReceiver, PeerControlRequest, - WireguardGatewayData, mix_forwarding_channels, + use nym_kkt::key_utils::{ + generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, }; - use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator}; - use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig}; + use nym_kkt::keys::KEMKeys; + use nym_kkt_ciphersuite::Ciphersuite; + use nym_lp::peer::LpLocalPeer; + use nym_node::config::{LpConfig, LpDebug}; + use nym_node::node::GatewayStorage; + use nym_node::node::lp::error::LpHandlerError; + use nym_node::node::lp::handler::LpConnectionHandler; + use nym_node::node::lp::{LpHandlerState, MixForwardingReceiver, mix_forwarding_channels}; + use nym_node::wireguard::{PeerManager, PeerRegistrator}; use nym_registration_client::{LpClientError, LpRegistrationClient}; - use nym_test_utils::helpers::{CryptoRng, RngCore, u64_seeded_rng}; + use nym_test_utils::helpers::{CryptoRng09, seeded_rng}; use nym_test_utils::mocks::async_read_write::MockIOStream; use nym_test_utils::traits::Timeboxed; use nym_wireguard::peer_controller::IpPair; @@ -26,11 +30,10 @@ mod tests { Key, KeyWrapper, MockPeerController, MockPeerControllerState, PeerControlRequestType, RegisteredResponse, mock_peer_controller, }; - use nym_wireguard::{IpPool, WireguardConfig}; + use nym_wireguard::{IpPool, PeerControlRequest, WireguardConfig, WireguardGatewayData}; use std::mem; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; - use std::time::Duration; use tokio::sync::Semaphore; use tokio::sync::mpsc::Receiver; use tokio::task::JoinHandle; @@ -48,6 +51,7 @@ mod tests { } struct Party { + identity: ed25519::KeyPair, peer: LpLocalPeer, x25519_wg_keys: Arc, socket_addr: SocketAddr, @@ -55,20 +59,31 @@ mod tests { } impl Party { - fn generate(rng: &mut (impl RngCore + CryptoRng)) -> Self { + fn generate(rng: &mut impl CryptoRng09) -> Self { let mut ip = [0u8; 4]; let mut port = [0u8; 2]; + // generate a valid instance of rand08 + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + let mut rng08 = seeded_rng(seed); + rng.fill_bytes(&mut ip); rng.fill_bytes(&mut port); - let ed25519_keys = Arc::new(ed25519::KeyPair::new(rng)); - let x25519_wg_keys = Arc::new(x25519::KeyPair::new(rng)); + let ed25519_keys = ed25519::KeyPair::new(&mut rng08); + let x25519_wg_keys = Arc::new(x25519::KeyPair::new(&mut rng08)); - let lp_x25519_keys = Arc::new(ed25519_keys.to_x25519()); + let lp_x25519_keys = Arc::new(generate_lp_keypair_x25519(rng)); + let mlkem_keypair = generate_keypair_mlkem(rng); + let mceliece_keypair = generate_keypair_mceliece(rng); + let lp_kem_keys = KEMKeys::new(mceliece_keypair, mlkem_keypair); + + let ciphersuite = Ciphersuite::default(); Party { - peer: LpLocalPeer::new(ed25519_keys, lp_x25519_keys.clone()) - .with_kem_psq_key(lp_x25519_keys), + identity: ed25519_keys, + peer: LpLocalPeer::new(ciphersuite, lp_x25519_keys.clone()) + .with_kem_keys(lp_kem_keys), x25519_wg_keys, socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))), lp_version: 1, @@ -83,7 +98,7 @@ mod tests { } impl Client { - fn mock(rng: &mut (impl RngCore + CryptoRng)) -> Self { + fn mock(rng: &mut impl CryptoRng09) -> Self { Client { base: Party::generate(rng), ticket_provider: Default::default(), @@ -108,7 +123,7 @@ mod tests { handler: LpConnectionHandler, }, Running { - handle: JoinHandle>>, + handle: JoinHandle>>, }, Finished, } @@ -177,7 +192,7 @@ mod tests { Ok(GatewayStorage::from_connection_pool(conn_pool, 100).await?) } - async fn mock(rng: &mut (impl RngCore + CryptoRng)) -> anyhow::Result { + async fn mock(rng: &mut impl CryptoRng09) -> anyhow::Result { let base = Party::generate(rng); // 1. create in-memory gateway storage @@ -188,7 +203,6 @@ mod tests { let lp_config = LpConfig { debug: LpDebug { - timestamp_tolerance: Duration::from_secs(30), ..Default::default() }, ..Default::default() @@ -218,19 +232,10 @@ mod tests { ); let lp_state = LpHandlerState { - // use mock instance of ecash verifier - ecash_verifier, - - // use in-memory database (no need for persistency) - storage, - local_lp_peer: base.peer.clone(), metrics: Default::default(), - // no clients at the beginning - active_clients_store: ActiveClientsStore::new(), - // use default lp config (with enabled flag) lp_config, @@ -381,107 +386,115 @@ mod tests { #[cfg(test)] mod using_lp_registration_client { use super::*; + use nym_kkt_ciphersuite::{IntoEnumIterator, KEM}; use nym_registration_client::NestedLpSession; + use nym_test_utils::helpers::u64_seeded_rng_09; use nym_wireguard::DefguardPeer; #[tokio::test] async fn test_basic_lp_entry_registration() -> anyhow::Result<()> { - // nym_test_utils::helpers::setup_test_logger(); - // initialise random, but deterministic, keys, addresses, etc. for the parties - let mut client_rng = u64_seeded_rng(0); - let mut gateway_rng = u64_seeded_rng(1); + for kem in KEM::iter() { + let ciphersuite = Ciphersuite::default().with_kem(kem); - let client_data = Client::mock(&mut client_rng); - let client_key = *client_data.base.x25519_wg_keys.public_key(); - let mut entry = Gateway::mock(&mut gateway_rng).await?; + // nym_test_utils::helpers::setup_test_logger(); + // initialise random, but deterministic, keys, addresses, etc. for the parties + let mut client_rng = u64_seeded_rng_09(0); + let mut gateway_rng = u64_seeded_rng_09(1); - let mut client = LpRegistrationClient::::new_with_default_config( - client_data.base.peer.ed25519().clone(), - entry.base.peer.as_remote(), - entry.base.socket_addr, - entry.base.lp_version, - ); + let client_data = Client::mock(&mut client_rng); + let client_key = *client_data.base.x25519_wg_keys.public_key(); + let mut entry = Gateway::mock(&mut gateway_rng).await?; - // 1. establish mock connection between client and gateway and retrieve gateway's handle - client.ensure_connected().await?; - let gateway_conn = client - .connection() - .as_ref() - .context("mock connection has failed!")? - .try_get_remote_handle(); + let mut client = LpRegistrationClient::::new_with_default_config( + client_data.base.peer.x25519().clone(), + entry.base.peer.as_remote(), + entry.base.socket_addr, + ciphersuite, + entry.base.lp_version, + ); - // 2. create and spawn gateway handler for the client connection - entry.create_lp_handler(gateway_conn, client_data.base.socket_addr); - entry.spawn_lp_handler(); + // 1. establish mock connection between client and gateway and retrieve gateway's handle + client.ensure_connected().await?; + let gateway_conn = client + .connection() + .as_ref() + .context("mock connection has failed!")? + .try_get_remote_handle(); - // 3. register all needed responses for the dvpn registration that will reach the peer controller - // 1) peer registration - ip pair allocation - let ip_pair = entry.pre_allocate_ip_pair(); - let reg_res = Ok::<_, nym_wireguard::Error>(ip_pair); + // 2. create and spawn gateway handler for the client connection + entry.create_lp_handler(gateway_conn, client_data.base.socket_addr); + entry.spawn_lp_handler(); - entry - .register_peer_controller_response( - PeerControlRequestType::AllocatePeerIpPair {}, - reg_res, - ) - .await; + // 3. register all needed responses for the dvpn registration that will reach the peer controller + // 1) peer registration - ip pair allocation + let ip_pair = entry.pre_allocate_ip_pair(); + let reg_res = Ok::<_, nym_wireguard::Error>(ip_pair); - // 2) new peer inclusion - in non-mock system it would spawn handlers, - // here we'll just set a flag and say it's all fine - let public_key = client_key.to_wg_key(); - let add_res = Ok::<_, nym_wireguard::Error>(()); - entry - .register_peer_controller_response( - PeerControlRequestType::AddPeer { public_key }, - add_res, - ) - .await; + entry + .register_peer_controller_response( + PeerControlRequestType::AllocatePeerIpPair {}, + reg_res, + ) + .await; - // 3) peer query - check for prior registrations - let query_res = Ok::<_, nym_wireguard::Error>(Option::::None); - let key = client_key.to_wg_key(); - entry - .register_peer_controller_response( - PeerControlRequestType::QueryPeer { key }, - query_res, - ) - .await; + // 2) new peer inclusion - in non-mock system it would spawn handlers, + // here we'll just set a flag and say it's all fine + let public_key = client_key.to_wg_key(); + let add_res = Ok::<_, nym_wireguard::Error>(()); + entry + .register_peer_controller_response( + PeerControlRequestType::AddPeer { public_key }, + add_res, + ) + .await; - // 4. spawn peer controller to be able to handle dvpn registration requests - entry.spawn_peer_controller(); + // 3) peer query - check for prior registrations + let query_res = Ok::<_, nym_wireguard::Error>(Option::::None); + let key = client_key.to_wg_key(); + entry + .register_peer_controller_response( + PeerControlRequestType::QueryPeer { key }, + query_res, + ) + .await; - // 5. perform client handshake - client.perform_handshake().timeboxed().await??; + // 4. spawn peer controller to be able to handle dvpn registration requests + entry.spawn_peer_controller(); - // 6. perform registration with entry only - let wg_keypair = client_data.base.x25519_wg_keys; - let gateway_identity = entry.base.peer.ed25519().public_key(); - let registration_result = client - .register_dvpn( - &mut client_rng, - &wg_keypair, - gateway_identity, - &client_data.ticket_provider, - TicketType::V1WireguardEntry, - ) - .timeboxed() - .await??; + // 5. perform client handshake + client.perform_handshake().timeboxed().await??; - // 7. verify registration result - let peers_guard = entry.mock_peer_controller_state.peers.read().await; - let peer = peers_guard.get_by_x25519_key(&client_key).unwrap().clone(); - drop(peers_guard); - assert!(peer.add_success); + // 6. perform registration with entry only + let wg_keypair = client_data.base.x25519_wg_keys; + let gateway_identity = entry.base.identity.public_key(); + let registration_result = client + .register_dvpn( + &mut client_rng, + &wg_keypair, + gateway_identity, + &client_data.ticket_provider, + TicketType::V1WireguardEntry, + ) + .timeboxed() + .await??; - assert_eq!(registration_result.private_ipv4, ip_pair.ipv4); - assert_eq!(registration_result.private_ipv6, ip_pair.ipv6); - assert_eq!( - registration_result.public_key, - *entry.base.x25519_wg_keys.public_key() - ); + // 7. verify registration result + let peers_guard = entry.mock_peer_controller_state.peers.read().await; + let peer = peers_guard.get_by_x25519_key(&client_key).unwrap().clone(); + drop(peers_guard); + assert!(peer.add_success); + + assert_eq!(registration_result.private_ipv4, ip_pair.ipv4); + assert_eq!(registration_result.private_ipv6, ip_pair.ipv6); + assert_eq!( + registration_result.public_key, + *entry.base.x25519_wg_keys.public_key() + ); + + // 8. stop the gateway task and finish the test + entry.stop_tasks().await?; + } - // 8. stop the gateway task and finish the test - entry.stop_tasks().await?; Ok(()) } @@ -489,16 +502,19 @@ mod tests { async fn registration_is_not_allowed_without_prior_handshake() -> anyhow::Result<()> { // nym_test_utils::helpers::setup_test_logger(); // initialise random, but deterministic, keys, addresses, etc. for the parties - let mut client_rng = u64_seeded_rng(0); - let mut gateway_rng = u64_seeded_rng(1); + let mut client_rng = u64_seeded_rng_09(0); + let mut gateway_rng = u64_seeded_rng_09(1); let client_data = Client::mock(&mut client_rng); let mut entry = Gateway::mock(&mut gateway_rng).await?; + let ciphersuite = Ciphersuite::default(); + let mut client = LpRegistrationClient::::new_with_default_config( - client_data.base.peer.ed25519().clone(), + client_data.base.peer.x25519().clone(), entry.base.peer.as_remote(), entry.base.socket_addr, + ciphersuite, entry.base.lp_version, ); @@ -521,7 +537,7 @@ mod tests { // 4. perform registration with entry only // but WITHOUT performing the handshake let wg_keypair = client_data.base.x25519_wg_keys; - let gateway_identity = entry.base.peer.ed25519().public_key(); + let gateway_identity = entry.base.identity.public_key(); let registration_result = client .register_dvpn( &mut client_rng, @@ -534,13 +550,9 @@ mod tests { .await? .unwrap_err(); - let LpClientError::Transport(err) = registration_result else { + let LpClientError::IncompleteHandshake = registration_result else { panic!("unexpected error"); }; - assert_eq!( - err, - "State machine not available - has the handshake been completed?" - ); // 5. stop the gateway task and finish the test entry.stop_tasks().await?; @@ -550,10 +562,16 @@ mod tests { #[tokio::test] async fn test_basic_lp_exit_registration() -> anyhow::Result<()> { // nym_test_utils::helpers::setup_test_logger(); + + // TODO: update the test once mceliece works + let kem = KEM::MlKem768; + + let ciphersuite = Ciphersuite::default().with_kem(kem); + // initialise random, but deterministic, keys, addresses, etc. for the parties - let mut client_rng = u64_seeded_rng(0); - let mut entry_rng = u64_seeded_rng(1); - let mut exit_rng = u64_seeded_rng(2); + let mut client_rng = u64_seeded_rng_09(0); + let mut entry_rng = u64_seeded_rng_09(1); + let mut exit_rng = u64_seeded_rng_09(2); let client_data = Client::mock(&mut client_rng); let client_key = *client_data.base.x25519_wg_keys.public_key(); @@ -561,9 +579,10 @@ mod tests { let mut exit = Gateway::mock(&mut exit_rng).await?; let mut entry_client = LpRegistrationClient::::new_with_default_config( - client_data.base.peer.ed25519().clone(), + client_data.base.peer.x25519().clone(), entry.base.peer.as_remote(), entry.base.socket_addr, + ciphersuite, entry.base.lp_version, ); @@ -678,18 +697,21 @@ mod tests { // but crypto is going to work the same let mut nested_session = NestedLpSession::new( exit.base.socket_addr, - client_data.base.peer.ed25519().clone(), + client_data.base.peer.x25519().clone(), exit.base.peer.as_remote(), + ciphersuite, exit.base.lp_version, ); // 13. Perform handshake and registration with exit gateway (all via entry forwarding) + nested_session.perform_handshake(&mut entry_client).await?; + let exit_registration_result = nested_session - .handshake_and_register_dvpn( + .register_dvpn( &mut entry_client, &mut client_rng, &client_data.base.x25519_wg_keys, - exit.base.peer.ed25519().public_key(), + exit.base.identity.public_key(), &client_data.ticket_provider, TicketType::V1WireguardExit, ) @@ -701,7 +723,7 @@ mod tests { .register_dvpn( &mut client_rng, &client_data.base.x25519_wg_keys, - entry.base.peer.ed25519().public_key(), + entry.base.identity.public_key(), &client_data.ticket_provider, TicketType::V1WireguardEntry, ) diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index edb25c2972..176eb0920f 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -52,7 +52,6 @@ nym-ecash-signer-check-types = { workspace = true } nym-kkt-ciphersuite = { workspace = true } [dev-dependencies] -rand_chacha = { workspace = true } nym-crypto = { workspace = true, features = ["rand"] } nym-test-utils = { workspace = true } diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index 7b4898518c..6db039abdd 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -790,13 +790,7 @@ mod tests { use nym_compact_ecash::scheme::keygen::KeyPairUser; use nym_compact_ecash::withdrawal_request; use nym_ecash_time::{ecash_today_date, EcashTime}; - use rand_chacha::rand_core::SeedableRng; - use rand_chacha::ChaCha20Rng; - - pub fn test_rng() -> ChaCha20Rng { - let dummy_seed = [42u8; 32]; - ChaCha20Rng::from_seed(dummy_seed) - } + use nym_test_utils::helpers::deterministic_rng; // had some issues with `Date` and serde... // so might as well leave this unit test in case we do something to the helper @@ -821,7 +815,7 @@ mod tests { #[test] fn decoding_attribute_commitments() { - let mut rng = test_rng(); + let mut rng = deterministic_rng(); let keys = ed25519::KeyPair::new(&mut rng); let dummy_sig = keys.private_key().sign("foomp"); let dummy_keypair = KeyPairUser::new(); diff --git a/nym-api/nym-api-requests/src/models/described/type_translation.rs b/nym-api/nym-api-requests/src/models/described/type_translation.rs index 415184c980..37653a2d14 100644 --- a/nym-api/nym-api-requests/src/models/described/type_translation.rs +++ b/nym-api/nym-api-requests/src/models/described/type_translation.rs @@ -5,15 +5,16 @@ //! and defines required conversion methods use celes::Country; -use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; -use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use nym_crypto::asymmetric::ed25519::serde_helpers::{bs58_ed25519_pubkey, bs58_ed25519_signature}; +use nym_crypto::asymmetric::x25519::serde_helpers::{bs58_dh_public_key, bs58_x25519_pubkey}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_kkt_ciphersuite::{HashFunction, SignatureScheme, KEM}; use nym_network_defaults::{WG_METADATA_PORT, WG_TUNNEL_PORT}; +use nym_node_requests::api::SignedData; use nym_noise_keys::VersionedNoiseKeyV1; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::net::IpAddr; use strum_macros::{Display, EnumString}; use thiserror::Error; @@ -200,6 +201,26 @@ pub struct WireguardDetailsV1 { // even if you put `#[serde(default)]` all over the place #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] pub struct LewesProtocolDetailsV1 { + pub content: LewesProtocolDetailsDataV1, + + #[serde(with = "bs58_ed25519_signature")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub signature: ed25519::Signature, +} + +impl LewesProtocolDetailsV1 { + pub fn verify(&self, key: &ed25519::PublicKey) -> bool { + let Ok(plaintext) = serde_json::to_string(&self.content) else { + return false; + }; + + key.verify(plaintext, &self.signature).is_ok() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] +pub struct LewesProtocolDetailsDataV1 { /// Helper field that specifies whether the LP listener(s) is enabled on this node. /// It is directly controlled by the node's role (i.e. it is enabled if it supports 'entry' mode) pub enabled: bool, @@ -210,28 +231,23 @@ pub struct LewesProtocolDetailsV1 { /// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP pub data_port: u16, - #[serde(with = "bs58_x25519_pubkey")] + #[serde(with = "bs58_dh_public_key")] #[schemars(with = "String")] #[schema(value_type = String)] /// LP public key - pub x25519: x25519::PublicKey, + pub x25519: x25519::DHPublicKey, /// Digests of the KEM keys available to this node alongside hashing algorithms used /// for their computation. /// note: digests are hex encoded - pub kem_keys: HashMap>, - - /// Digests of the signing keys available to this node alongside hashing algorithms used - /// for their computation. - /// note: digests are hex encoded - pub signing_keys: HashMap>, + pub kem_keys: BTreeMap>, } -impl LewesProtocolDetailsV1 { +impl LewesProtocolDetailsDataV1 { fn decode_digests( - digests: &HashMap, - ) -> Result>, MalformedLPData> { - let mut kem_digests = HashMap::new(); + digests: &BTreeMap, + ) -> Result>, MalformedLPData> { + let mut kem_digests = BTreeMap::new(); for (hash_function, digest) in digests { let digest = hex::decode(digest).map_err(|source| MalformedLPData::MalformedHash { value: digest.to_string(), @@ -244,31 +260,20 @@ impl LewesProtocolDetailsV1 { pub fn kem_keys( &self, - ) -> Result>>, MalformedLPData> { - let mut kem_keys = HashMap::new(); + ) -> Result>>, MalformedLPData> { + let mut kem_keys = BTreeMap::new(); for (kem, digests) in &self.kem_keys { let kem_digests = Self::decode_digests(digests)?; kem_keys.insert((*kem).try_into()?, kem_digests); } Ok(kem_keys) } - - pub fn signing_keys( - &self, - ) -> Result>>, MalformedLPData> { - let mut signing_keys = HashMap::new(); - for (signature_scheme, digests) in &self.signing_keys { - let kem_digests = Self::decode_digests(digests)?; - signing_keys.insert((*signature_scheme).try_into()?, kem_digests); - } - Ok(signing_keys) - } } /// Convert map of digests from `nym_node_requests` types into `nym-api-requests` types fn translate_digests( - digests: HashMap, -) -> HashMap { + digests: BTreeMap, +) -> BTreeMap { digests .into_iter() .map(|(hash_fn, digest)| (hash_fn.into(), digest)) @@ -289,13 +294,12 @@ fn translate_digests( Display, EnumString, ToSchema, + Ord, )] #[strum(serialize_all = "lowercase")] #[non_exhaustive] pub enum LPKEM { MlKem768, - XWing, - X25519, McEliece, } @@ -313,6 +317,7 @@ pub enum LPKEM { Display, EnumString, ToSchema, + Ord, )] #[strum(serialize_all = "lowercase")] #[non_exhaustive] @@ -455,25 +460,26 @@ impl From for WireguardD } } -impl From +impl From> for LewesProtocolDetailsV1 { - fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol) -> Self { + fn from( + value: SignedData, + ) -> Self { LewesProtocolDetailsV1 { - enabled: value.enabled, - control_port: value.control_port, - data_port: value.data_port, - x25519: value.x25519, - kem_keys: value - .kem_keys - .into_iter() - .map(|(kem, digests)| (kem.into(), translate_digests(digests))) - .collect(), - signing_keys: value - .signing_keys - .into_iter() - .map(|(scheme, digests)| (scheme.into(), translate_digests(digests))) - .collect(), + signature: value.signature, + content: LewesProtocolDetailsDataV1 { + enabled: value.enabled, + control_port: value.control_port, + data_port: value.data_port, + x25519: value.x25519, + kem_keys: value + .data + .kem_keys + .into_iter() + .map(|(kem, digests)| (kem.into(), translate_digests(digests))) + .collect(), + }, } } } @@ -482,8 +488,6 @@ impl From for LPKEM { fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LPKEM) -> Self { match value { nym_node_requests::api::v1::lewes_protocol::models::LPKEM::MlKem768 => LPKEM::MlKem768, - nym_node_requests::api::v1::lewes_protocol::models::LPKEM::XWing => LPKEM::XWing, - nym_node_requests::api::v1::lewes_protocol::models::LPKEM::X25519 => LPKEM::X25519, nym_node_requests::api::v1::lewes_protocol::models::LPKEM::McEliece => LPKEM::McEliece, } } @@ -543,8 +547,6 @@ impl TryFrom for KEM { fn try_from(value: LPKEM) -> Result { match value { LPKEM::MlKem768 => Ok(KEM::MlKem768), - LPKEM::XWing => Ok(KEM::XWing), - LPKEM::X25519 => Ok(KEM::X25519), LPKEM::McEliece => Ok(KEM::McEliece), // TODO: for backwards compatibility once variants within the LP crate change // other => Err(MalformedLPData::UnknownLpKEM { value: other }), @@ -576,3 +578,48 @@ impl TryFrom for SignatureScheme { } } } + +#[cfg(test)] +mod tests { + use super::*; + use nym_node_requests::api::SignedLewesProtocol; + + #[test] + fn signature_validity_after_conversion() { + use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM}; + + let signing_key = ed25519::PrivateKey::from_bytes(&[42u8; 32]).unwrap(); + let verification_key = signing_key.public_key(); + + let x25519_key = x25519::DHPublicKey::from_bytes(&[42u8; 32]); + + let mut dummy_kems = BTreeMap::new(); + for kem in [LPKEM::McEliece, LPKEM::McEliece] { + let mut kem_digests = BTreeMap::new(); + for sf in [ + LPHashFunction::Blake3, + LPHashFunction::Shake128, + LPHashFunction::Shake256, + LPHashFunction::Sha256, + ] { + kem_digests.insert(sf, "0xdeadbeef".to_string()); + } + dummy_kems.insert(kem, kem_digests); + } + + // make sure the serialisation stays the same and signature is still valid + let dummy_lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol { + enabled: false, + control_port: 123, + data_port: 345, + x25519: x25519_key, + kem_keys: dummy_kems, + }; + let dummy_signed_lp = SignedLewesProtocol::new(dummy_lp, &signing_key).unwrap(); + // sanity check + assert!(dummy_signed_lp.verify(&verification_key)); + + let converted = LewesProtocolDetailsV1::from(dummy_signed_lp); + assert!(converted.verify(&verification_key)); + } +} diff --git a/nym-api/nym-api-requests/src/models/described/v2.rs b/nym-api/nym-api-requests/src/models/described/v2.rs index 429fb2ed88..73f1356dbc 100644 --- a/nym-api/nym-api-requests/src/models/described/v2.rs +++ b/nym-api/nym-api-requests/src/models/described/v2.rs @@ -247,7 +247,7 @@ impl From for NymNodeDescriptionV2 { #[cfg(test)] pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 { - use crate::models::{LPHashFunction, LPSignatureScheme, LPKEM}; + use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM}; use nym_test_utils::helpers::{u64_seeded_rng, RngCore}; let mut rng = u64_seeded_rng(seed); @@ -257,22 +257,33 @@ pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 { // just reuse the same x25519 key for everything - this is just a data mock let x25519 = x25519::KeyPair::new(&mut rng); - let mut kem_hashes_wrapper = std::collections::HashMap::new(); - let mut signing_keys_hashes_wrapper = std::collections::HashMap::new(); - let mut kem_hashes = std::collections::HashMap::new(); - let mut signing_keys_hashes = std::collections::HashMap::new(); + let mut dummy_kems = std::collections::BTreeMap::new(); + for kem in [LPKEM::McEliece, LPKEM::McEliece] { + let mut kem_digests = std::collections::BTreeMap::new(); + for (i, sf) in [ + LPHashFunction::Blake3, + LPHashFunction::Shake128, + LPHashFunction::Shake256, + LPHashFunction::Sha256, + ] + .iter() + .enumerate() + { + kem_digests.insert(*sf, hex::encode([((seed + i as u64) % 256) as u8; 32])); + } + dummy_kems.insert(kem, kem_digests); + } - kem_hashes.insert( - LPHashFunction::Sha256, - hex::encode([(seed % 256) as u8; 32]), - ); - kem_hashes_wrapper.insert(LPKEM::X25519, kem_hashes); - - signing_keys_hashes.insert( - LPHashFunction::Sha256, - hex::encode([(seed % 256) as u8; 32]), - ); - signing_keys_hashes_wrapper.insert(LPSignatureScheme::Ed25519, signing_keys_hashes); + // make sure the serialisation stays the same and signature is still valid + let dummy_lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol { + enabled: false, + control_port: 123, + data_port: 345, + x25519: (*x25519.public_key()).into(), + kem_keys: dummy_kems, + }; + let dummy_signed_lp = + nym_node_requests::api::SignedLewesProtocol::new(dummy_lp, ed25519.private_key()).unwrap(); NymNodeDescriptionV2 { node_id: rng.next_u32(), @@ -339,14 +350,7 @@ pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 { metadata_port: 456, public_key: x25519.public_key().to_base58_string(), }), - lewes_protocol: Some(LewesProtocolDetailsV1 { - enabled: true, - control_port: 1234, - data_port: 2345, - x25519: *x25519.public_key(), - kem_keys: kem_hashes_wrapper, - signing_keys: signing_keys_hashes_wrapper, - }), + lewes_protocol: Some(dummy_signed_lp.into()), mixnet_websockets: WebSocketsV2 { ws_port: 9000, wss_port: None, diff --git a/nym-api/src/nym_nodes/handlers/v1.rs b/nym-api/src/nym_nodes/handlers/v1.rs index 8cd5b46368..ce004731ed 100644 --- a/nym-api/src/nym_nodes/handlers/v1.rs +++ b/nym-api/src/nym_nodes/handlers/v1.rs @@ -231,7 +231,7 @@ async fn get_bonded_nodes( ), params(PaginationRequest) )] -// #[deprecated(note = "use '/v2/nym-nodes/described' instead")] +#[deprecated(note = "use '/v2/nym-nodes/described' instead")] async fn get_described_nodes( State(state): State, Query(pagination): Query, diff --git a/nym-api/src/nym_nodes/handlers/v2.rs b/nym-api/src/nym_nodes/handlers/v2.rs index 502f2c6e1c..ee49e575fa 100644 --- a/nym-api/src/nym_nodes/handlers/v2.rs +++ b/nym-api/src/nym_nodes/handlers/v2.rs @@ -1,14 +1,14 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::node_status_api::models::AxumResult; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; use axum::extract::{Query, State}; use axum::routing::get; use axum::Router; use nym_api_requests::models::NymNodeDescriptionV2; -use nym_api_requests::pagination::PaginatedResponse; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_http_api_common::FormattedResponse; use tower_http::compression::CompressionLayer; @@ -36,23 +36,19 @@ async fn get_described_nodes( State(state): State, Query(pagination): Query, ) -> AxumResult>> { - let _ = state; + // TODO: implement it let _ = pagination; - Err(AxumErrorResponse::not_implemented()) + let output = pagination.output.unwrap_or_default(); - // // TODO: implement it - // let _ = pagination; - // let output = pagination.output.unwrap_or_default(); - // - // let cache = state.described_nodes_cache.get().await?; - // let descriptions = cache.all_nodes().cloned().collect::>(); - // - // Ok(output.to_response(PaginatedResponse { - // pagination: Pagination { - // total: descriptions.len(), - // page: 0, - // size: descriptions.len(), - // }, - // data: descriptions, - // })) + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes().cloned().collect::>(); + + Ok(output.to_response(PaginatedResponse { + pagination: Pagination { + total: descriptions.len(), + page: 0, + size: descriptions.len(), + }, + data: descriptions, + })) } diff --git a/nym-browser-extension/storage/.cargo/config.toml b/nym-browser-extension/storage/.cargo/config.toml index 435ed755ec..33e7edcb17 100644 --- a/nym-browser-extension/storage/.cargo/config.toml +++ b/nym-browser-extension/storage/.cargo/config.toml @@ -1,2 +1,4 @@ [build] -target = "wasm32-unknown-unknown" \ No newline at end of file +target = "wasm32-unknown-unknown" +target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] diff --git a/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml deleted file mode 100644 index 52baabd2cb..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "nym-vpn-api-lib-wasm" -version.workspace = true -authors.workspace = true -repository.workspace = true -homepage.workspace = true -documentation.workspace = true -edition.workspace = true -license.workspace = true -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -bs58 = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -time = { workspace = true, features = ["wasm-bindgen"] } -thiserror.workspace = true -zeroize = { workspace = true } - -# wasm-specific deps -getrandom = { workspace = true, features = ["js"] } -wasm-bindgen = { workspace = true } -js-sys = { workspace = true } -tsify = { workspace = true, features = ["js"] } -serde-wasm-bindgen = { workspace = true } - -# NYM: -nym-bin-common = { workspace = true } -nym-crypto = { workspace = true, features = ["asymmetric"] } -nym-compact-ecash = { workspace = true } -nym-credentials = { workspace = true } -nym-credentials-interface = { workspace = true } -nym-ecash-time = { workspace = true, features = ["expiration"] } -nym-credential-proxy-requests = { workspace = true, features = ["tsify"] } -nym-wasm-utils = { workspace = true } diff --git a/nym-credential-proxy/vpn-api-lib-wasm/Makefile b/nym-credential-proxy/vpn-api-lib-wasm/Makefile deleted file mode 100644 index 2edce6e5fc..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -build: - wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/nym-vpn-api-lib-wasm - wasm-opt -Oz -o ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm - diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js deleted file mode 100644 index 362cdb2431..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js +++ /dev/null @@ -1,2 +0,0 @@ -import("./index.js") - .catch(e => console.error("Error importing `index.js`:", e)); diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html deleted file mode 100644 index 5cd272596a..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - Nym WebAssembly Demo - - - - - -

everything happens in the console

- - - - \ No newline at end of file diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js deleted file mode 100644 index 3921411115..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import init, { NymIssuanceTicketbook } from "@nymproject/nym-vpn-api-lib-wasm"; - -async function main() { - await init(); - - let cryptoData = new NymIssuanceTicketbook({}); - - console.log("getting partial vks"); - const partialVksRes = await fetch("http://localhost:8080/api/v1/ticketbook/partial-verification-keys", { - headers: new Headers({ "Authorization": "Bearer foomp" }) - }); - const partialVks = await partialVksRes.json(); - console.debug(partialVks); - - console.log("getting master vk"); - const masterVkRes = await fetch("http://localhost:8080/api/v1/ticketbook/master-verification-key", { - headers: new Headers({ "Authorization": "Bearer foomp" }) - }); - const masterVk = await masterVkRes.json(); - console.debug(masterVk); - - let request = cryptoData.buildRequestPayload(false); - console.log(request); - - - console.log("getting blinded wallet shares"); - const sharesRes = await fetch("http://localhost:8080/api/v1/ticketbook/obtain?include-coin-index-signatures=true&include-expiration-date-signatures=true", { - method: "POST", - headers: new Headers( - { - "Authorization": "Bearer foomp", - "Content-Type": "application/json" - } - ), - body: request - }); - - const credentialShares = await sharesRes.json(); - console.log(credentialShares); - - console.log("unblinding shares"); - const unblinded = cryptoData.unblindWalletShares(credentialShares, partialVks, masterVk); - - const serialised = unblinded.serialise(); - console.log("serialised:\n", serialised); -} - - -main(); diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json deleted file mode 100644 index 2eb37c3aa2..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "create-wasm-app", - "version": "0.1.0", - "description": "create an app to consume rust-generated wasm packages", - "main": "index.js", - "bin": { - "create-wasm-app": ".bin/create-wasm-app.js" - }, - "scripts": { - "build": "webpack --config webpack.config.js", - "build:wasm": "cd ../ && make wasm-build", - "start": "webpack-dev-server --port 8001" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rustwasm/create-wasm-app.git" - }, - "keywords": [ - "webassembly", - "wasm", - "rust", - "webpack" - ], - "author": "Dave Hrycyszyn ", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/nymtech/nym/issues" - }, - "homepage": "https://nymtech.net/docs", - "devDependencies": { - "copy-webpack-plugin": "^11.0.0", - "hello-wasm-pack": "^0.1.0", - "webpack": "^5.70.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^5.2.1" - }, - "dependencies": { - "@nymproject/nym-vpn-api-lib-wasm": "file:../../../dist/wasm/nym-vpn-api-lib-wasm" - } -} diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js deleted file mode 100644 index d5f839485f..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js +++ /dev/null @@ -1,33 +0,0 @@ -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const path = require("path"); - -module.exports = { - performance: { - hints: false, - maxEntrypointSize: 512000, - maxAssetSize: 512000 - }, - entry: { - bootstrap: "./bootstrap.js" - // worker: "./worker.js" - }, - output: { - path: path.resolve(__dirname, "dist"), - filename: "[name].js" - }, - mode: "development", - // mode: 'production', - plugins: [ - new CopyWebpackPlugin({ - patterns: [ - "index.html", - { - from: "../../../dist/wasm/nym-vpn-api-lib-wasm/*.(js|wasm)", - to: "[name][ext]" - } - ] - }) - - ], - experiments: { syncWebAssembly: true } -}; diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock deleted file mode 100644 index 01cda5871e..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock +++ /dev/null @@ -1,2204 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@discoveryjs/json-ext@^0.5.0": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@jsonjoy.com/base64@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" - integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== - -"@jsonjoy.com/json-pack@^1.0.3": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz#e658900e81d194903171c42546e1aa27f446846a" - integrity sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA== - dependencies: - "@jsonjoy.com/base64" "^1.1.1" - "@jsonjoy.com/util" "^1.1.2" - hyperdyperid "^1.2.0" - thingies "^1.20.0" - -"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.6.0.tgz#23991b2fe12cb3a006573d9dc97c768d3ed2c9f1" - integrity sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A== - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" - integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@nymproject/nym-vpn-api-lib-wasm@file:../../../dist/wasm/nym-vpn-api-lib-wasm": - version "0.1.0" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.13": - version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" - integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== - dependencies: - "@types/node" "*" - -"@types/connect-history-api-fallback@^1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" - integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/eslint-scope@^3.7.7": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^4.17.21": - version "4.17.22" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.22.tgz#14cfcf120f7eb56ebb8ca77b7fa9a14d21de7c96" - integrity sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/http-proxy@^1.17.8": - version "1.17.15" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" - integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== - dependencies: - "@types/node" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== - dependencies: - "@types/node" "*" - -"@types/node@*": - version "22.5.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958" - integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg== - dependencies: - undici-types "~6.19.2" - -"@types/qs@*": - version "6.9.15" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" - integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/retry@0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" - integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== - -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-index@^1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" - integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.15.5": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/sockjs@^0.3.36": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" - integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== - dependencies: - "@types/node" "*" - -"@types/ws@^8.5.10": - version "8.18.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" - integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== - dependencies: - "@types/node" "*" - -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" - integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== - dependencies: - "@webassemblyjs/helper-numbers" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - -"@webassemblyjs/floating-point-hex-parser@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" - integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== - -"@webassemblyjs/helper-api-error@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" - integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== - -"@webassemblyjs/helper-buffer@1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" - integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== - -"@webassemblyjs/helper-numbers@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" - integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.13.2" - "@webassemblyjs/helper-api-error" "1.13.2" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" - integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== - -"@webassemblyjs/helper-wasm-section@1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" - integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/wasm-gen" "1.14.1" - -"@webassemblyjs/ieee754@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" - integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" - integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" - integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== - -"@webassemblyjs/wasm-edit@^1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" - integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/helper-wasm-section" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-opt" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - "@webassemblyjs/wast-printer" "1.14.1" - -"@webassemblyjs/wasm-gen@1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" - integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wasm-opt@1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" - integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" - integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-api-error" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wast-printer@1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" - integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@xtuc/long" "4.2.2" - -"@webpack-cli/configtest@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" - integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== - -"@webpack-cli/info@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" - integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== - dependencies: - envinfo "^7.7.3" - -"@webpack-cli/serve@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" - integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-import-phases@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" - integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== - -acorn@^8.15.0: - version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" - integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^8.0.0, ajv@^8.9.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -baseline-browser-mapping@^2.9.0: - version "2.9.19" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" - integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" - integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== - dependencies: - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.28.1: - version "4.28.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" - integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== - dependencies: - baseline-browser-mapping "^2.9.0" - caniuse-lite "^1.0.30001759" - electron-to-chromium "^1.5.263" - node-releases "^2.0.27" - update-browserslist-db "^1.2.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -bundle-name@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" - integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== - dependencies: - run-applescript "^7.0.0" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bound@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -caniuse-lite@^1.0.30001759: - version "1.0.30001769" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" - integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== - -chokidar@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -colorette@^2.0.10, colorette@^2.0.14: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== - -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^4.1.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - -default-browser-id@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" - integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== - -default-browser@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" - integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== - dependencies: - bundle-name "^4.1.0" - default-browser-id "^5.0.0" - -define-lazy-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" - integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-packet@^5.2.2: - version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" - integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.5.263: - version "1.5.286" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" - integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -enhanced-resolve@^5.17.4: - version "5.19.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c" - integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.3.0" - -envinfo@^7.7.3: - version "7.13.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" - integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-module-lexer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1" - integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -express@^4.21.2: - version "4.21.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.11, fast-glob@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-uri@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" - integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== - -fastest-levenshtein@^1.0.12: - version "1.0.16" - resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" - integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -globby@^13.1.1: - version "13.2.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" - integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.3.0" - ignore "^5.2.4" - merge2 "^1.4.1" - slash "^4.0.0" - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hello-wasm-pack@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz#482a2e3371828056ac35f5b5fec76c0b99dcd530" - integrity sha512-3hx0GDkDLf/a9ThCMV2qG4mwza8N/MCtm8aeFFc/cdBCL2zMJ1kW1wjNl7xPqD1lz8Yl5+uhnc/cpui4dLwz/w== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-middleware@^2.0.7: - version "2.0.9" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" - integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -hyperdyperid@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" - integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore@^5.2.4: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -import-local@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -interpret@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - -is-docker@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" - integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-inside-container@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" - integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== - dependencies: - is-docker "^3.0.0" - -is-network-error@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" - integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-wsl@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" - integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== - dependencies: - is-inside-container "^1.0.0" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -launch-editor@^2.6.1: - version "2.10.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.10.0.tgz#5ca3edfcb9667df1e8721310f3a40f1127d4bc42" - integrity sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" - -loader-runner@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" - integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^4.6.0: - version "4.17.2" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.17.2.tgz#1f71a6d85c8c53b4f1b388234ed981a690c7e227" - integrity sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg== - dependencies: - "@jsonjoy.com/json-pack" "^1.0.3" - "@jsonjoy.com/util" "^1.3.0" - tree-dump "^1.0.1" - tslib "^2.0.0" - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -node-forge@^1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.2.tgz#d0d2659a26eef778bf84d73e7f55c08144ee7750" - integrity sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw== - -node-releases@^2.0.27: - version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -object-inspect@^1.13.3: - version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1, on-finished@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -open@^10.0.3: - version "10.1.2" - resolved "https://registry.yarnpkg.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925" - integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw== - dependencies: - default-browser "^5.2.1" - define-lazy-prop "^3.0.0" - is-inside-container "^1.0.0" - is-wsl "^3.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-retry@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af" - integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ== - dependencies: - "@types/retry" "0.12.2" - is-network-error "^1.0.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" - integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== - dependencies: - resolve "^1.9.0" - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve@^1.9.0: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-applescript@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" - integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" - integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== - dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" - -send@0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - side-channel-map "^1.0.1" - -side-channel@^1.0.6: - version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - side-channel-list "^1.0.0" - side-channel-map "^1.0.1" - side-channel-weakmap "^1.0.2" - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tapable@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" - integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== - -terser-webpack-plugin@^5.3.16: - version "5.3.16" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330" - integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q== - dependencies: - "@jridgewell/trace-mapping" "^0.3.25" - jest-worker "^27.4.5" - schema-utils "^4.3.0" - serialize-javascript "^6.0.2" - terser "^5.31.1" - -terser@^5.31.1: - version "5.46.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695" - integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.15.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -thingies@^1.20.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" - integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -tree-dump@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.3.tgz#2f0e42e77354714418ed7ab44291e435ccdb0f80" - integrity sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg== - -tslib@^2.0.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" - integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -watchpack@^2.4.4: - version "2.5.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.1.tgz#dd38b601f669e0cbf567cb802e75cead82cde102" - integrity sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -webpack-cli@^4.9.2: - version "4.10.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" - integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== - dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^1.2.0" - "@webpack-cli/info" "^1.5.0" - "@webpack-cli/serve" "^1.7.0" - colorette "^2.0.14" - commander "^7.0.0" - cross-spawn "^7.0.3" - fastest-levenshtein "^1.0.12" - import-local "^3.0.2" - interpret "^2.2.0" - rechoir "^0.7.0" - webpack-merge "^5.7.3" - -webpack-dev-middleware@^7.4.2: - version "7.4.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz#40e265a3d3d26795585cff8207630d3a8ff05877" - integrity sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA== - dependencies: - colorette "^2.0.10" - memfs "^4.6.0" - mime-types "^2.1.31" - on-finished "^2.4.1" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz#049072d6e19cbda8cf600b9e364e6662d61218ba" - integrity sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ== - dependencies: - "@types/bonjour" "^3.5.13" - "@types/connect-history-api-fallback" "^1.5.4" - "@types/express" "^4.17.21" - "@types/express-serve-static-core" "^4.17.21" - "@types/serve-index" "^1.9.4" - "@types/serve-static" "^1.15.5" - "@types/sockjs" "^0.3.36" - "@types/ws" "^8.5.10" - ansi-html-community "^0.0.8" - bonjour-service "^1.2.1" - chokidar "^3.6.0" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - express "^4.21.2" - graceful-fs "^4.2.6" - http-proxy-middleware "^2.0.7" - ipaddr.js "^2.1.0" - launch-editor "^2.6.1" - open "^10.0.3" - p-retry "^6.2.0" - schema-utils "^4.2.0" - selfsigned "^2.4.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^7.4.2" - ws "^8.18.0" - -webpack-merge@^5.7.3: - version "5.10.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" - integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.0" - -webpack-sources@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" - integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== - -webpack@^5.70.0: - version "5.104.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.104.1.tgz#94bd41eb5dbf06e93be165ba8be41b8260d4fb1a" - integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA== - dependencies: - "@types/eslint-scope" "^3.7.7" - "@types/estree" "^1.0.8" - "@types/json-schema" "^7.0.15" - "@webassemblyjs/ast" "^1.14.1" - "@webassemblyjs/wasm-edit" "^1.14.1" - "@webassemblyjs/wasm-parser" "^1.14.1" - acorn "^8.15.0" - acorn-import-phases "^1.0.3" - browserslist "^4.28.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.4" - es-module-lexer "^2.0.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.11" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.3.1" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^4.3.3" - tapable "^2.3.0" - terser-webpack-plugin "^5.3.16" - watchpack "^2.4.4" - webpack-sources "^3.3.3" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wildcard@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" - integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== - -ws@^8.18.0: - version "8.18.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" - integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== diff --git a/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs deleted file mode 100644 index 5e1255bdbc..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use nym_wasm_utils::wasm_error; -use serde_wasm_bindgen::Error; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum VpnApiLibError { - #[error("{0}")] - Json(String), - - #[error("[ecash] cryptographic failure: {source}")] - EcashFailure { - #[from] - source: nym_compact_ecash::CompactEcashError, - }, - - #[error("provided invalid ticket type")] - MalformedTicketType, - - #[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")] - InconsistentEpochId { shares: u64, issuers: u64 }, - - #[error("failed to recover ed25519 private key from its base58 representation")] - MalformedEd25519Key, -} - -wasm_error!(VpnApiLibError); - -impl From for VpnApiLibError { - fn from(value: Error) -> Self { - VpnApiLibError::Json(value.to_string()) - } -} diff --git a/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs deleted file mode 100644 index 00ca53805a..0000000000 --- a/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::VpnApiLibError; -use nym_compact_ecash::scheme::keygen::KeyPairUser; -use nym_compact_ecash::scheme::withdrawal::RequestInfo; -use nym_compact_ecash::{ - Base58, BlindedSignature, VerificationKeyAuth, WithdrawalRequest, aggregate_wallets, - issue_verify, withdrawal_request, -}; -use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, -}; -use nym_credentials::{ - AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, - IssuedTicketBook, -}; -use nym_credentials_interface::TicketType; -use nym_crypto::asymmetric::ed25519; -use nym_ecash_time::{EcashTime, ecash_default_expiration_date}; -use nym_wasm_utils::console_error; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use time::Date; -use tsify::Tsify; -use wasm_bindgen::prelude::*; -use zeroize::Zeroizing; - -pub mod error; - -#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct WalletShares(Vec); - -pub type WalletIssuers = PartialVerificationKeysResponse; - -impl From> for WalletShares { - fn from(shares: Vec) -> Self { - WalletShares(shares) - } -} - -#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct NymIssuanceTicketbookOpts { - #[tsify(optional)] - pub ticketbook_type: Option, - - // bs58-encoded user secret key used for seeding the ecash crypto keypair generation - // I reiterate, this is a **SECRET** key, not a public key. - #[tsify(optional)] - pub user_secret_key: Option, -} - -#[wasm_bindgen] -#[derive(Debug)] -#[allow(dead_code)] -pub struct NymIssuanceTicketbook { - /// ecash keypair related to the credential - ecash_keypair: KeyPairUser, - - withdrawal_request: WithdrawalRequest, - - ticketbook_type: TicketType, - - expiration_date: Date, - - request_info: Zeroizing, -} - -#[wasm_bindgen] -impl NymIssuanceTicketbook { - #[wasm_bindgen(constructor)] - pub fn new(opts: NymIssuanceTicketbookOpts) -> Result { - let ecash_keypair = match opts.user_secret_key { - None => KeyPairUser::new(), - Some(maybe_sk) => { - let pk = ed25519::PrivateKey::from_base58_string(maybe_sk) - .map(Zeroizing::new) - .map_err(|_| VpnApiLibError::MalformedEd25519Key)?; - let bytes = Zeroizing::new(pk.to_bytes()); - KeyPairUser::new_seeded(&bytes) - } - }; - - let ticketbook_type = match opts.ticketbook_type { - None => TicketType::V1MixnetEntry, - Some(typ) => typ - .parse() - .map_err(|_| VpnApiLibError::MalformedTicketType)?, - }; - - let expiration_date = ecash_default_expiration_date(); - - let (withdrawal_request, request_info) = withdrawal_request( - ecash_keypair.secret_key(), - expiration_date.ecash_unix_timestamp(), - ticketbook_type.encode(), - )?; - - Ok(NymIssuanceTicketbook { - ecash_keypair, - withdrawal_request, - ticketbook_type, - expiration_date, - request_info: Zeroizing::new(request_info), - }) - } - - #[wasm_bindgen(js_name = "buildRequestPayload")] - pub fn build_request_payload(&self, is_freepass_request: bool) -> String { - serde_json::to_string(&TicketbookRequest { - withdrawal_request: self.withdrawal_request.clone().into(), - ecash_pubkey: self.ecash_keypair.public_key(), - expiration_date: self.expiration_date, - ticketbook_type: self.ticketbook_type, - is_freepass_request, - }) - .unwrap() - } - - #[wasm_bindgen(js_name = "getWithdrawalRequest")] - pub fn get_encoded_withdrawal_request(&self) -> String { - self.withdrawal_request.to_bs58() - } - - #[wasm_bindgen(js_name = "getEncodedPublicKey")] - pub fn get_encoded_public_key(&self) -> String { - self.ecash_keypair.public_key().to_bs58() - } - - // - // #[wasm_bindgen(js_name = "unblindShare")] - // pub fn unblind_share(&self, share: UnblindableShare) -> Result { - // let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; - // let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; - // - // Ok(blinded_sig - // .unblind(&vk, &self.pedersen_commitments_openings) - // .into()) - // } - // - #[wasm_bindgen(js_name = "unblindWalletShares")] - pub fn unblind_wallet_shares( - self, - shares: JsValue, - issuers: WalletIssuers, - master_key: MasterVerificationKeyResponse, - ) -> Result { - // we couldn't derive all the required abi traits due to crypto types deep in the stack - let shares: TicketbookWalletSharesResponse = serde_wasm_bindgen::from_value(shares)?; - - if shares.epoch_id != issuers.epoch_id { - console_error!( - "the provided shares and issuers are not from the same epoch! {} and {}", - shares.epoch_id, - issuers.epoch_id - ); - return Err(VpnApiLibError::InconsistentEpochId { - shares: shares.epoch_id, - issuers: issuers.epoch_id, - }); - } - - let master_vk = VerificationKeyAuth::try_from_bs58(master_key.bs58_encoded_key)?; - - let mut decoded_keys = HashMap::new(); - for key in issuers.keys { - let vk = VerificationKeyAuth::try_from_bs58(key.bs58_encoded_key)?; - decoded_keys.insert(key.node_index, vk); - } - - let mut partial_wallets = Vec::new(); - for share in shares.shares { - let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; - let Some(vk) = decoded_keys.get(&share.node_index) else { - console_error!( - "received a share from issuer {} but did not receive a corresponding verification key!", - share.node_index - ); - continue; - }; - - match issue_verify( - vk, - self.ecash_keypair.secret_key(), - &blinded_sig, - &self.request_info, - share.node_index, - ) { - Ok(partial_wallet) => partial_wallets.push(partial_wallet), - Err(err) => { - console_error!( - "failed to unblind partial wallet corresponding to index {}: {err}", - share.node_index - ) - } - } - } - - let aggregated_wallet = aggregate_wallets( - &master_vk, - self.ecash_keypair.secret_key(), - &partial_wallets, - &self.request_info, - )?; - - Ok(NymIssuedTicketbook { - inner_ticketbook: IssuedTicketBook::new( - aggregated_wallet.into_wallet_signatures(), - shares.epoch_id, - self.ecash_keypair.into(), - self.ticketbook_type, - self.expiration_date, - ), - master_vk: EpochVerificationKey { - epoch_id: shares.epoch_id, - key: master_vk, - }, - expiration_date_signatures: shares - .aggregated_expiration_date_signatures - .map(|s| s.signatures), - coin_index_signatures: shares - .aggregated_coin_index_signatures - .map(|s| s.signatures), - }) - } -} - -#[wasm_bindgen] -pub struct NymIssuedTicketbook { - inner_ticketbook: IssuedTicketBook, - - master_vk: EpochVerificationKey, - expiration_date_signatures: Option, - coin_index_signatures: Option, -} - -#[wasm_bindgen] -impl NymIssuedTicketbook { - pub fn serialise(self) -> FullSerialisedNymIssuedTicketbook { - let serialised = self - .inner_ticketbook - .begin_export() - .with_master_verification_key(&self.master_vk) - .with_maybe_expiration_date_signatures(&self.expiration_date_signatures) - .with_maybe_coin_index_signatures(&self.coin_index_signatures) - .finalize_export(); - - FullSerialisedNymIssuedTicketbook { - serialisation_revision: serialised.revision, - bs58_encoded_data: bs58::encode(serialised.data).into_string(), - } - } -} - -#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct FullSerialisedNymIssuedTicketbook { - pub serialisation_revision: u8, - pub bs58_encoded_data: String, -} - -#[wasm_bindgen(start)] -pub fn main() { - nym_wasm_utils::console_log!("[rust main]: rust module loaded"); - nym_wasm_utils::console_log!( - "vpn-api-lib version used:\n{}", - nym_bin_common::bin_info!().pretty_print() - ); - nym_wasm_utils::console_log!("[rust main]: setting panic hook"); - nym_wasm_utils::set_panic_hook(); -} diff --git a/nym-gateway-probe/Cargo.toml b/nym-gateway-probe/Cargo.toml index bf8c621e59..b18927be15 100644 --- a/nym-gateway-probe/Cargo.toml +++ b/nym-gateway-probe/Cargo.toml @@ -22,6 +22,7 @@ hex.workspace = true tracing.workspace = true pnet_packet.workspace = true rand.workspace = true +rand09.workspace = true reqwest = { workspace = true, features = ["socks"] } serde.workspace = true serde_json.workspace = true diff --git a/nym-gateway-probe/src/common/helpers.rs b/nym-gateway-probe/src/common/helpers.rs index 5693b8a7a9..7f90fca60a 100644 --- a/nym-gateway-probe/src/common/helpers.rs +++ b/nym-gateway-probe/src/common/helpers.rs @@ -1,12 +1,9 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::common::nodes::TestedNodeLpDetails; -use nym_crypto::asymmetric::ed25519; use nym_ip_packet_requests::v8::response::{ ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData, }; -use nym_lp::peer::LpRemotePeer; use nym_sdk::{ DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails, TopologyProvider, mixnet::ReconstructedMessage, @@ -15,13 +12,6 @@ use nym_topology::NymTopology; use tracing::*; use url::Url; -pub fn to_lp_remote_peer(identity: ed25519::PublicKey, data: TestedNodeLpDetails) -> LpRemotePeer { - LpRemotePeer::new(identity, data.x25519).with_key_digests( - data.expected_kem_key_hashes, - data.expected_signing_key_hashes, - ) -} - pub fn mixnet_debug_config( min_gateway_performance: Option, ignore_egress_epoch_role: bool, diff --git a/nym-gateway-probe/src/common/nodes.rs b/nym-gateway-probe/src/common/nodes.rs index 4e79e84ce6..04b2b10cac 100644 --- a/nym-gateway-probe/src/common/nodes.rs +++ b/nym-gateway-probe/src/common/nodes.rs @@ -3,25 +3,25 @@ use anyhow::{Context, anyhow, bail}; use nym_api_requests::models::{ - AuthenticatorDetailsV1, DeclaredRolesV1, DescribedNodeTypeV1, HostInformationV1, - IpPacketRouterDetailsV1, NetworkRequesterDetailsV1, NymNodeDataV1, - OffsetDateTimeJsonSchemaWrapper, WebSocketsV1, WireguardDetailsV1, + AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2, + IpPacketRouterDetailsV2, NetworkRequesterDetailsV2, NymNodeDataV2, + OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2, }; use nym_authenticator_requests::AuthenticatorVersion; use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_crypto::asymmetric::x25519; use nym_http_api_client::UserAgent; -use nym_kkt_ciphersuite::SignatureScheme; +use nym_kkt_ciphersuite::Ciphersuite; use nym_kkt_ciphersuite::{KEM, KEMKeyDigests}; +use nym_lp::peer::{DHPublicKey, LpRemotePeer}; use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails; use nym_sdk::mixnet::NodeIdentity; use nym_sdk::mixnet::Recipient; use nym_validator_client::client::NymApiClientExt; -use nym_validator_client::models::NymNodeDescriptionV1; +use nym_validator_client::models::NymNodeDescriptionV2; use rand::prelude::IteratorRandom; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::net::{IpAddr, SocketAddr}; use std::time::Duration; use time::OffsetDateTime; @@ -90,7 +90,7 @@ use url::Url; #[derive(Clone)] pub struct DirectoryNode { - described: NymNodeDescriptionV1, + described: NymNodeDescriptionV2, } impl DirectoryNode { @@ -237,11 +237,11 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result anyhow::Result = - nr_result.map(|nr| NetworkRequesterDetailsV1 { + let network_requester: Option = + nr_result.map(|nr| NetworkRequesterDetailsV2 { address: nr.address, uses_exit_policy: false, // Field not availabe, to change if it becomes useful here }); - let ip_packet_router: Option = - ipr_result.map(|ipr| IpPacketRouterDetailsV1 { + let ip_packet_router: Option = + ipr_result.map(|ipr| IpPacketRouterDetailsV2 { address: ipr.address, }); - let authenticator: Option = - authenticator_result.map(|auth| AuthenticatorDetailsV1 { + let authenticator: Option = + authenticator_result.map(|auth| AuthenticatorDetailsV2 { address: auth.address, }); #[allow(deprecated)] - let wireguard: Option = - wireguard_result.map(|wg| WireguardDetailsV1 { + let wireguard: Option = + wireguard_result.map(|wg| WireguardDetailsV2 { port: wg.tunnel_port, // Use tunnel_port for deprecated port field tunnel_port: wg.tunnel_port, metadata_port: wg.metadata_port, @@ -284,14 +284,14 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result anyhow::Result, - pub expected_signing_key_hashes: HashMap, - pub x25519: x25519::PublicKey, + pub expected_kem_key_hashes: BTreeMap, + pub x25519: DHPublicKey, pub lp_version: u8, + pub ciphersuite: Ciphersuite, +} + +impl TestedNodeLpDetails { + pub fn into_remote_peer(self) -> LpRemotePeer { + LpRemotePeer::new(self.x25519).with_key_digests(self.expected_kem_key_hashes) + } } diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index 0ee8140f77..158cbfc011 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -28,10 +28,12 @@ use nym_credentials_interface::{CredentialSpendingData, TicketType}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_ip_packet_client::IprClientConnect; use nym_ip_packet_requests::{IpPair, codec::MultiIpPacketCodec}; +use nym_lp::peer::DHKeyPair; use nym_registration_client::{LpRegistrationClient, NestedLpSession}; use nym_sdk::NymNetworkDetails; use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5}; use nym_topology::{HardcodedTopologyProvider, NymTopology}; +use rand09::SeedableRng; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, @@ -163,23 +165,25 @@ pub async fn lp_registration_probe( ) -> anyhow::Result { let lp_address = gateway_lp_data.address; let lp_version = gateway_lp_data.lp_version; - let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data); + let lp_ciphersuite = gateway_lp_data.ciphersuite; + let peer = gateway_lp_data.into_remote_peer(); info!("Starting LP registration probe for gateway at {lp_address}"); let mut lp_outcome = LpProbeResults::default(); // Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP) - let mut rng = rand::thread_rng(); - let client_ed25519_keypair = std::sync::Arc::new(ed25519::KeyPair::new(&mut rng)); + let mut rng09 = rand09::rngs::StdRng::from_os_rng(); + let client_x25519_keypair = Arc::new(DHKeyPair::new(&mut rng09)); // Step 0: Derive X25519 keys from Ed25519 for the gateways // Create LP registration client (uses Ed25519 keys directly, derives X25519 internally) let mut client = LpRegistrationClient::::new_with_default_config( - client_ed25519_keypair, + client_x25519_keypair, peer, lp_address, + lp_ciphersuite, lp_version, ); @@ -209,23 +213,13 @@ pub async fn lp_registration_probe( let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng); // Convert gateway identity to ed25519 public key - let gateway_ed25519_pubkey = match nym_crypto::asymmetric::ed25519::PublicKey::from_bytes( - &gateway_identity.to_bytes(), - ) { - Ok(key) => key, - Err(e) => { - let error_msg = format!("Failed to convert gateway identity: {}", e); - error!("{}", error_msg); - lp_outcome.error = Some(error_msg); - return Ok(lp_outcome); - } - }; + let gateway_ed25519_pubkey = gateway_identity; // Register using the new packet-per-connection API (returns GatewayData directly) let ticket_type = TicketType::V1WireguardEntry; let gateway_data = match client .register_dvpn( - &mut rng, + &mut rng09, &wg_keypair, &gateway_ed25519_pubkey, bandwidth_controller, @@ -299,21 +293,26 @@ pub async fn wg_probe_lp( let entry_lp_version = entry_lp_data.lp_version; let exit_lp_version = exit_lp_data.lp_version; + let entry_lp_ciphersuite = entry_lp_data.ciphersuite; + let exit_lp_ciphersuite = exit_lp_data.ciphersuite; + info!("Starting LP-based WireGuard probe (entry→exit via forwarding)"); let mut wg_outcome = WgProbeResults::default(); - // Generate Ed25519 keypairs for LP protocol - let mut rng = rand::thread_rng(); - let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); - let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); + // Generate x25519 keypairs for LP protocol + let mut rng09 = rand09::rngs::StdRng::from_os_rng(); + + let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09)); + let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09)); // Generate WireGuard keypairs for VPN registration + let mut rng = rand::rngs::OsRng; let entry_wg_keypair = x25519::KeyPair::new(&mut rng); let exit_wg_keypair = x25519::KeyPair::new(&mut rng); - let entry_peer = helpers::to_lp_remote_peer(entry_gateway.identity, entry_lp_data); - let exit_peer = helpers::to_lp_remote_peer(exit_gateway.identity, exit_lp_data); + let entry_peer = entry_lp_data.into_remote_peer(); + let exit_peer = exit_lp_data.into_remote_peer(); // STEP 1: Establish outer LP session with entry gateway // LpRegistrationClient uses packet-per-connection model - connect() is gone, @@ -323,6 +322,7 @@ pub async fn wg_probe_lp( entry_lp_keypair, entry_peer, entry_address, + entry_lp_ciphersuite, entry_lp_version, ); @@ -335,16 +335,26 @@ pub async fn wg_probe_lp( // STEP 2: Use nested session to register with exit gateway via forwarding info!("Registering with exit gateway via entry forwarding..."); - let mut nested_session = - NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_version); + let mut nested_session = NestedLpSession::new( + exit_address, + exit_lp_keypair, + exit_peer, + exit_lp_ciphersuite, + exit_lp_version, + ); let exit_gateway_pubkey = exit_gateway.identity; // Perform handshake and registration with exit gateway via forwarding + if let Err(err) = nested_session.perform_handshake(&mut entry_client).await { + error!("Failed to perform handshake with exit gateway: {err}"); + return Ok(wg_outcome); + }; + let exit_gateway_data = match nested_session - .handshake_and_register_dvpn( + .register_dvpn( &mut entry_client, - &mut rng, + &mut rng09, &exit_wg_keypair, &exit_gateway_pubkey, bandwidth_controller, @@ -370,7 +380,7 @@ pub async fn wg_probe_lp( // Use packet-per-connection register() which returns GatewayData directly let entry_gateway_data = match entry_client .register_dvpn( - &mut rng, + &mut rng09, &entry_wg_keypair, &entry_gateway_pubkey, bandwidth_controller, diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 00340e45cb..4845997d7b 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -13,6 +13,8 @@ license = "GPL-3.0" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" [dependencies] async-trait = { workspace = true } @@ -27,6 +29,7 @@ colored = { workspace = true } console-subscriber = { workspace = true, optional = true } csv = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } +dashmap = { workspace = true } futures = { workspace = true } hex = { workspace = true } humantime-serde = { workspace = true } @@ -34,6 +37,7 @@ human-repr = { workspace = true } ipnetwork = { workspace = true } indicatif = { workspace = true } rand = { workspace = true } +rand09 = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror.workspace = true @@ -112,6 +116,11 @@ nym-gateway = { path = "../gateway" } nym-network-requester = { path = "../service-providers/network-requester" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } +# LP dependencies +nym-lp = { path = "../common/nym-lp" } +nym-registration-common = { path = "../common/registration" } +bincode = { workspace = true } + # throughput tester to recreate lioness # we don't care about particular versions - just pull whatever is used by sphinx @@ -133,7 +142,7 @@ cargo_metadata = { workspace = true } [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } -rand_chacha = { workspace = true } +nym-test-utils = { workspace = true } [features] tokio-console = ["console-subscriber", "nym-task/tokio-tracing"] diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 6c311152ab..b051086220 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -26,6 +26,7 @@ url = { workspace = true, features = ["serde"] } nym-crypto = { workspace = true, features = [ "asymmetric", "serde", + "libcrux_x25519" ] } nym-exit-policy = { workspace = true } nym-noise-keys = { workspace = true } @@ -47,7 +48,7 @@ nym-bin-common = { workspace = true, features = [ [dev-dependencies] tokio = { workspace = true, features = ["full"] } -rand_chacha = { workspace = true } +nym-test-utils = { workspace = true } nym-crypto = { workspace = true, features = ["rand"] } diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index dd014851cc..6e3b93eeca 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -1,24 +1,24 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use super::v1::gateway::models::Wireguard; +use super::v1::metrics::models::SessionStats; +use crate::api::SignedLewesProtocol; +use crate::api::v1::authenticator::models::Authenticator; use crate::api::v1::gateway::models::WebSockets; +use crate::api::v1::health::models::NodeHealth; +use crate::api::v1::ip_packet_router::models::IpPacketRouter; +use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy; +use crate::api::v1::network_requester::models::NetworkRequester; use crate::api::v1::node::models::{ AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, }; +use crate::api::v1::node_load::models::NodeLoad; use crate::routes; use async_trait::async_trait; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_http_api_client::{ApiClient, HttpClientError}; -use super::v1::gateway::models::Wireguard; -use super::v1::metrics::models::SessionStats; -use crate::api::v1::authenticator::models::Authenticator; -use crate::api::v1::health::models::NodeHealth; -use crate::api::v1::ip_packet_router::models::IpPacketRouter; -use crate::api::v1::lewes_protocol::models::LewesProtocol; -use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy; -use crate::api::v1::network_requester::models::NetworkRequester; -use crate::api::v1::node_load::models::NodeLoad; pub use nym_http_api_client::Client; pub type NymNodeApiClientError = HttpClientError; @@ -98,7 +98,7 @@ pub trait NymNodeApiClientExt: ApiClient { .await } - async fn get_lewes_protocol(&self) -> Result { + async fn get_lewes_protocol(&self) -> Result { self.get_json_from(routes::api::v1::lp_absolute()).await } } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index ba32570c2e..d9c9835af0 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -20,6 +20,7 @@ pub use client::Client; // create the type alias manually if openapi is not enabled pub type SignedHostInformation = SignedData; +pub type SignedLewesProtocol = SignedData; #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct SignedDataHostInfo { @@ -28,11 +29,20 @@ pub struct SignedDataHostInfo { pub signature: String, } +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SignedLewesProtocolInfo { + // #[serde(flatten)] + pub data: crate::api::v1::lewes_protocol::models::LewesProtocol, + pub signature: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignedData { // #[serde(flatten)] pub data: T, - pub signature: String, + + #[serde(with = "ed25519::bs58_ed25519_signature")] + pub signature: ed25519::Signature, } impl SignedData { @@ -42,7 +52,7 @@ impl SignedData { { let plaintext = serde_json::to_string(&data)?; - let signature = key.sign(plaintext).to_base58_string(); + let signature = key.sign(plaintext); Ok(SignedData { data, signature }) } @@ -54,11 +64,7 @@ impl SignedData { return false; }; - let Ok(signature) = ed25519::Signature::from_base58_string(&self.signature) else { - return false; - }; - - key.verify(plaintext, &signature).is_ok() + key.verify(plaintext, &self.signature).is_ok() } } @@ -72,7 +78,7 @@ impl SignedHostInformation { let legacy_v3 = SignedData { data: LegacyHostInformationV3::from(self.data.clone()), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v3.verify(&self.keys.ed25519_identity) { @@ -82,7 +88,7 @@ impl SignedHostInformation { // attempt to verify legacy signatures let legacy_v3 = SignedData { data: LegacyHostInformationV3::from(self.data.clone()), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v3.verify(&self.keys.ed25519_identity) { @@ -91,7 +97,7 @@ impl SignedHostInformation { let legacy_v2 = SignedData { data: LegacyHostInformationV2::from(legacy_v3.data), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v2.verify(&self.keys.ed25519_identity) { @@ -100,7 +106,7 @@ impl SignedHostInformation { SignedData { data: LegacyHostInformationV1::from(legacy_v2.data), - signature: self.signature.clone(), + signature: self.signature, } .verify(&self.keys.ed25519_identity) } @@ -128,16 +134,15 @@ impl Display for ErrorResponse { #[allow(deprecated)] #[cfg(test)] mod tests { - use super::*; use crate::api::v1::node::models::{HostKeys, SphinxKey}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1}; - use rand_chacha::rand_core::SeedableRng; + use nym_test_utils::helpers::deterministic_rng; #[test] fn dummy_signed_host_verification() { - let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let mut rng = deterministic_rng(); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); let x25519_sphinx2 = x25519::KeyPair::new(&mut rng); @@ -237,7 +242,7 @@ mod tests { #[test] fn dummy_legacy_v3_signed_host_verification() { - let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let mut rng = deterministic_rng(); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); let x25519_noise = x25519::KeyPair::new(&mut rng); @@ -331,7 +336,7 @@ mod tests { #[test] fn dummy_legacy_v2_signed_host_verification() { - let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let mut rng = deterministic_rng(); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); let x25519_noise = x25519::KeyPair::new(&mut rng); @@ -420,7 +425,7 @@ mod tests { #[test] fn dummy_legacy_v1_signed_host_verification() { - let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let mut rng = deterministic_rng(); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); diff --git a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs index 17900472b6..fe1ea68bd5 100644 --- a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use nym_crypto::asymmetric::x25519; -use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_dh_public_key; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; use strum_macros::{Display, EnumIter, EnumString}; #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] @@ -21,21 +21,16 @@ pub struct LewesProtocol { /// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP pub data_port: u16, - #[serde(with = "bs58_x25519_pubkey")] + #[serde(with = "bs58_dh_public_key")] #[schemars(with = "String")] - #[schema(value_type = String)] + #[cfg_attr(feature = "openapi", schema(value_type = String))] /// LP public key - pub x25519: x25519::PublicKey, + pub x25519: x25519::DHPublicKey, /// Digests of the KEM keys available to this node alongside hashing algorithms used /// for their computation. /// note: digests are hex encoded - pub kem_keys: HashMap>, - - /// Digests of the signing keys available to this node alongside hashing algorithms used - /// for their computation. - /// note: digests are hex encoded - pub signing_keys: HashMap>, + pub kem_keys: BTreeMap>, } impl LewesProtocol { @@ -43,9 +38,8 @@ impl LewesProtocol { enabled: bool, control_port: u16, data_port: u16, - x25519: x25519::PublicKey, - kem_keys: HashMap>, - signing_keys: HashMap>, + x25519: x25519::DHPublicKey, + kem_keys: BTreeMap>, ) -> Self { LewesProtocol { enabled, @@ -53,7 +47,6 @@ impl LewesProtocol { data_port, x25519, kem_keys, - signing_keys, } } } @@ -76,13 +69,12 @@ impl LewesProtocol { PartialOrd, Display, EnumString, + Ord, )] #[strum(serialize_all = "lowercase")] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub enum LPKEM { MlKem768, - XWing, - X25519, McEliece, } @@ -90,8 +82,6 @@ impl From for nym_kkt_ciphersuite::KEM { fn from(lpkem: LPKEM) -> Self { match lpkem { LPKEM::MlKem768 => nym_kkt_ciphersuite::KEM::MlKem768, - LPKEM::XWing => nym_kkt_ciphersuite::KEM::XWing, - LPKEM::X25519 => nym_kkt_ciphersuite::KEM::X25519, LPKEM::McEliece => nym_kkt_ciphersuite::KEM::McEliece, } } @@ -101,8 +91,6 @@ impl From for LPKEM { fn from(kem: nym_kkt_ciphersuite::KEM) -> Self { match kem { nym_kkt_ciphersuite::KEM::MlKem768 => LPKEM::MlKem768, - nym_kkt_ciphersuite::KEM::XWing => LPKEM::XWing, - nym_kkt_ciphersuite::KEM::X25519 => LPKEM::X25519, nym_kkt_ciphersuite::KEM::McEliece => LPKEM::McEliece, } } @@ -122,6 +110,7 @@ impl From for LPKEM { Display, EnumString, EnumIter, + Ord, )] #[strum(serialize_all = "lowercase")] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] @@ -199,8 +188,6 @@ mod tests { #[test] fn kem_display() { assert_eq!(LPKEM::MlKem768.to_string(), "mlkem768"); - assert_eq!(LPKEM::XWing.to_string(), "xwing"); - assert_eq!(LPKEM::X25519.to_string(), "x25519"); assert_eq!(LPKEM::McEliece.to_string(), "mceliece"); } diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 8c214f5e5e..2fbc4d3623 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::{ - ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, MetricsArgs, MixnetArgs, - VerlocArgs, WireguardArgs, + ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, LpArgs, MetricsArgs, + MixnetArgs, VerlocArgs, WireguardArgs, }; use crate::config::persistence::NymNodePaths; use crate::config::{Config, ConfigBuilder, NodeMode, NodeModes}; @@ -125,6 +125,9 @@ pub(crate) struct Args { #[clap(flatten)] exit_gateway: ExitGatewayArgs, + + #[clap(flatten)] + lp: LpArgs, } impl Args { @@ -174,6 +177,7 @@ impl Args { .with_metrics(self.metrics.build_config_section()) .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)?) .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) + .with_lp(self.lp.build_config_section()) .build() } @@ -193,6 +197,7 @@ impl Args { config.service_providers = self .exit_gateway .override_config_section(config.service_providers); + config.lp = self.lp.override_config_section(config.lp); config } } diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 00193698bf..147a5a0129 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -458,15 +458,6 @@ pub(crate) struct EntryGatewayArgs { )] #[zeroize(skip)] pub(crate) upgrade_mode_attester_public_key: Option, - - /// Use mock ecash manager for LP testing. - /// WARNING: Only use this for local testing! Never enable in production. - /// When enabled, the LP listener will accept any credential without blockchain verification. - #[clap( - long, - env = NYMNODE_LP_USE_MOCK_ECASH_ARG - )] - pub(crate) lp_use_mock_ecash: Option, } impl EntryGatewayArgs { @@ -500,9 +491,6 @@ impl EntryGatewayArgs { if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key { section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key } - if let Some(use_mock_ecash) = self.lp_use_mock_ecash { - section.lp.debug.use_mock_ecash = use_mock_ecash - } section } @@ -549,3 +537,62 @@ impl ExitGatewayArgs { section } } + +#[derive(clap::Args, Debug)] +pub(crate) struct LpArgs { + /// Bind address for the TCP LP control traffic. + /// default: `[::]:41264` + #[clap( + long, + env = NYMNODE_LP_CONTROL_BIND_ADDRESS_ARG + )] + pub(crate) lp_control_bind_address: Option, + + /// Custom announced port for listening for the TCP LP control traffic. + /// If unspecified, the value from the `lp_control_bind_address` will be used instead + #[clap( + long, + env = NYMNODE_LP_CONTROL_ANNOUNCE_PORT_ARG + )] + pub(crate) lp_control_announce_port: Option, + + /// Bind address for the UDP LP data traffic. + /// default: `[::]:51264` + #[clap( + long, + env = NYMNODE_LP_DATA_BIND_ADDRESS_ARG + )] + pub(crate) lp_data_bind_address: Option, + + /// Custom announced port for listening for the UDP LP data traffic. + /// If unspecified, the value from the `lp_data_bind_address` will be used instead + #[clap( + long, + env = NYMNODE_LP_DATA_ANNOUNCE_PORT_ARG + )] + pub(crate) lp_data_announce_port: Option, + + /// Use mock ecash manager for LP testing. + /// WARNING: Only use this for local testing! Never enable in production. + /// When enabled, the LP listener will accept any credential without blockchain verification. + #[clap( + long, + env = NYMNODE_LP_USE_MOCK_ECASH_ARG + )] + pub(crate) lp_use_mock_ecash: Option, +} + +impl LpArgs { + // TODO: could we perhaps make a clap error here and call `safe_exit` instead? + pub(crate) fn build_config_section(self) -> config::LpConfig { + self.override_config_section(config::LpConfig::default()) + } + + pub(crate) fn override_config_section(self, mut section: config::LpConfig) -> config::LpConfig { + if let Some(use_mock_ecash) = self.lp_use_mock_ecash { + section.debug.use_mock_ecash = use_mock_ecash + } + + section + } +} diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 99577f82ca..ed6335fed8 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -46,9 +46,6 @@ pub struct GatewayTasksConfig { pub upgrade_mode: UpgradeModeWatcher, - #[serde(default)] - pub lp: nym_gateway::node::LpConfig, - #[serde(default)] pub debug: Debug, } @@ -228,7 +225,6 @@ impl GatewayTasksConfig { announce_ws_port: None, announce_wss_port: None, upgrade_mode: UpgradeModeWatcher::new()?, - lp: Default::default(), debug: Default::default(), }) } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 20aac7cf0c..7c2e96215b 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -27,7 +27,6 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { enabled: config.service_providers.network_requester.debug.enabled, }, config.gateway_tasks.upgrade_mode.clone(), - config.gateway_tasks.lp, nym_gateway::config::Debug { client_bandwidth_max_flushing_rate: config .gateway_tasks @@ -92,8 +91,6 @@ pub struct GatewayTasksConfig { pub auth_opts: Option, #[allow(dead_code)] pub wg_opts: LocalWireguardOpts, - #[allow(dead_code)] - pub lp: nym_gateway::node::LpConfig, } // that function is rather disgusting, but I hope it's not going to live for too long @@ -227,7 +224,6 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { ipr_opts: Some(ipr_opts), auth_opts: Some(auth_opts), wg_opts, - lp: config.gateway_tasks.lp, } } diff --git a/nym-node/src/config/lp.rs b/nym-node/src/config/lp.rs new file mode 100644 index 0000000000..260fea64b1 --- /dev/null +++ b/nym-node/src/config/lp.rs @@ -0,0 +1,153 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_config::serde_helpers::de_maybe_port; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::time::Duration; + +/// Configuration for LP listener +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpConfig { + /// Bind address for the TCP LP control traffic. + /// default: `[::]:41264` + pub control_bind_address: SocketAddr, + + /// Bind address for the UDP LP data traffic. + /// default: `[::]:51264` + pub data_bind_address: SocketAddr, + + /// Custom announced port for listening for the TCP LP control traffic. + /// If unspecified, the value from the `control_bind_address` will be used instead + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_control_port: Option, + + /// Custom announced port for listening for the UDP LP data traffic. + /// If unspecified, the value from the `data_bind_address` will be used instead + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_data_port: Option, + + /// Auxiliary configuration + #[serde(default)] + pub debug: LpDebug, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpDebug { + /// Maximum concurrent connections + pub max_connections: usize, + + /// Use mock ecash manager for testing (default: false) + /// + /// When enabled, the LP listener will use a mock ecash verifier that + /// accepts any credential without blockchain verification. This is + /// useful for testing the LP protocol implementation without requiring + /// a full blockchain/contract setup. + /// + /// WARNING: Only use this for local testing! Never enable in production. + pub use_mock_ecash: bool, + + /// Maximum age of in-progress handshakes before cleanup (default: 90s) + /// + /// Handshakes should complete quickly (3-5 packets). This TTL accounts for: + /// - Network latency and retransmits + /// - Slow clients + /// - Clock skew tolerance + /// + /// Stale handshakes are removed by the cleanup task to prevent memory leaks. + #[serde(with = "humantime_serde")] + pub handshake_ttl: Duration, + + /// Maximum age of established sessions before cleanup (default: 24h) + /// + /// Sessions can be long-lived for dVPN tunnels. This TTL should be set + /// high enough to accommodate expected usage patterns: + /// - dVPN sessions: hours to days + /// - Registration: minutes + /// + /// Sessions with no activity for this duration are removed by the cleanup task. + #[serde(with = "humantime_serde")] + pub session_ttl: Duration, + + /// How often to run the state cleanup task (default: 5 minutes) + /// + /// The cleanup task scans for and removes stale handshakes and sessions. + /// Lower values = more frequent cleanup but higher overhead. + /// Higher values = less overhead but slower memory reclamation. + #[serde(with = "humantime_serde")] + pub state_cleanup_interval: Duration, + + /// Maximum concurrent forward connections (default: 1000) + /// + /// Limits simultaneous outbound connections when forwarding LP packets to other gateways + /// during telescope setup. This prevents file descriptor exhaustion under high load. + /// + /// When at capacity, new forward requests return an error, signaling the client + /// to choose a different gateway. + pub max_concurrent_forwards: usize, +} + +impl LpConfig { + pub const DEFAULT_CONTROL_PORT: u16 = 41264; + pub const DEFAULT_DATA_PORT: u16 = 51264; + + pub fn announced_control_port(&self) -> u16 { + self.announce_control_port + .unwrap_or(self.control_bind_address.port()) + } + + pub fn announced_data_port(&self) -> u16 { + self.announce_data_port + .unwrap_or(self.data_bind_address.port()) + } +} + +impl Default for LpConfig { + fn default() -> Self { + LpConfig { + control_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_CONTROL_PORT, + ), + data_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_DATA_PORT, + ), + announce_control_port: None, + announce_data_port: None, + debug: Default::default(), + } + } +} + +impl LpDebug { + pub const DEFAULT_MAX_CONNECTIONS: usize = 10000; + + // 90 seconds - handshakes should complete quickly + pub const DEFAULT_HANDSHAKE_TTL: Duration = Duration::from_secs(90); + + // 24 hours - for long-lived dVPN sessions + pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(86400); + + // 5 minutes - balances memory reclamation with task overhead + pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); + + // Limits concurrent outbound connections to prevent fd exhaustion + pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000; +} + +impl Default for LpDebug { + fn default() -> Self { + LpDebug { + max_connections: Self::DEFAULT_MAX_CONNECTIONS, + use_mock_ecash: false, + handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL, + session_ttl: Self::DEFAULT_SESSION_TTL, + state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL, + max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS, + } + } +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 064efc8b6a..14b00b7f5e 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -4,6 +4,7 @@ use crate::config::persistence::{NymNodePaths, ReplayProtectionPaths}; use crate::config::template::CONFIG_TEMPLATE; use crate::error::NymNodeError; +use crate::node::replay_protection::{bitmap_size, items_in_bloomfilter}; use celes::Country; use clap::ValueEnum; use human_repr::HumanCount; @@ -35,6 +36,7 @@ use url::Url; pub mod authenticator; pub mod gateway_tasks; pub mod helpers; +pub mod lp; pub mod metrics; mod old_configs; pub mod persistence; @@ -43,9 +45,9 @@ mod template; pub mod upgrade_helpers; pub use crate::config::gateway_tasks::GatewayTasksConfig; +pub use crate::config::lp::{LpConfig, LpDebug}; pub use crate::config::metrics::MetricsConfig; pub use crate::config::service_providers::ServiceProvidersConfig; -use crate::node::replay_protection::{bitmap_size, items_in_bloomfilter}; const DEFAULT_NYMNODES_DIR: &str = "nym-nodes"; @@ -186,6 +188,8 @@ pub struct ConfigBuilder { pub metrics: Option, + pub lp: Option, + pub logging: Option, } @@ -205,6 +209,7 @@ impl ConfigBuilder { gateway_tasks: None, service_providers: None, metrics: None, + lp: None, logging: None, } } @@ -234,6 +239,11 @@ impl ConfigBuilder { self } + pub fn with_lp(mut self, section: impl Into>) -> Self { + self.lp = section.into(); + self + } + pub fn with_wireguard(mut self, section: impl Into>) -> Self { self.wireguard = section.into(); self @@ -284,6 +294,7 @@ impl ConfigBuilder { .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), metrics: self.metrics.unwrap_or_default(), + lp: self.lp.unwrap_or_default(), gateway_tasks, service_providers: self .service_providers @@ -323,6 +334,9 @@ pub struct Config { pub wireguard: Wireguard, + #[serde(default)] + pub lp: LpConfig, + #[serde(alias = "entry_gateway")] pub gateway_tasks: GatewayTasksConfig, diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index c46a72d1a1..2116f94ec8 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -4,6 +4,7 @@ mod old_config_v1; mod old_config_v10; mod old_config_v11; +mod old_config_v12; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -24,3 +25,4 @@ pub use old_config_v8::try_upgrade_config_v8; pub use old_config_v9::try_upgrade_config_v9; pub use old_config_v10::try_upgrade_config_v10; pub use old_config_v11::try_upgrade_config_v11; +pub use old_config_v12::try_upgrade_config_v12; diff --git a/nym-node/src/config/old_configs/old_config_v11.rs b/nym-node/src/config/old_configs/old_config_v11.rs index 6d3a380357..6e1abb705d 100644 --- a/nym-node/src/config/old_configs/old_config_v11.rs +++ b/nym-node/src/config/old_configs/old_config_v11.rs @@ -1,24 +1,11 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, -}; -use crate::config::persistence::{ - AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, - NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, -}; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, -}; -use crate::config::{ - Config, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, Mixnet, MixnetDebug, - NodeModes, ReplayProtection, ReplayProtectionDebug, ServiceProvidersConfig, Verloc, - VerlocDebug, Wireguard, gateway_tasks, service_providers, +use crate::config::old_configs::old_config_v12::{ + ConfigV12, DebugV12, GatewayTasksConfigDebugV12, GatewayTasksConfigV12, LpConfigV12, + UpgradeModeWatcherV12, WireguardV12, }; use crate::error::NymNodeError; -use nym_bin_common::logging::LoggingSettings; use nym_config::read_config_from_toml_file; use serde::{Deserialize, Serialize}; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -185,7 +172,7 @@ impl ConfigV11 { pub async fn try_upgrade_config_v11>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v11 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -197,144 +184,16 @@ pub async fn try_upgrade_config_v11>( // for future reference: when creating v12 migration, // look at how v10 -> v11 is implemented // you might be able to create a bunch of type aliases again to save you some headache - let cfg = Config { + let cfg = ConfigV12 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { - mixnode: old_cfg.modes.mixnode, - entry: old_cfg.modes.entry, - exit: old_cfg.modes.exit, - }, - host: Host { - public_ips: old_cfg.host.public_ips, - hostname: old_cfg.host.hostname, - location: old_cfg.host.location, - }, - mixnet: Mixnet { - bind_address: old_cfg.mixnet.bind_address, - announce_port: old_cfg.mixnet.announce_port, - nym_api_urls: old_cfg.mixnet.nym_api_urls, - nyxd_urls: old_cfg.mixnet.nyxd_urls, - replay_protection: ReplayProtection { - storage_paths: ReplayProtectionPaths { - current_bloomfilters_directory: old_cfg - .mixnet - .replay_protection - .storage_paths - .current_bloomfilters_directory, - }, - debug: ReplayProtectionDebug { - unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, - maximum_replay_detection_deferral: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_deferral, - maximum_replay_detection_pending_packets: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_pending_packets, - false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, - initial_expected_packets_per_second: old_cfg - .mixnet - .replay_protection - .debug - .initial_expected_packets_per_second, - bloomfilter_minimum_packets_per_second_size: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_minimum_packets_per_second_size, - bloomfilter_size_multiplier: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_size_multiplier, - bloomfilter_disk_flushing_rate: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_disk_flushing_rate, - }, - }, - key_rotation: KeyRotation { - debug: KeyRotationDebug { - rotation_state_poling_interval: old_cfg - .mixnet - .key_rotation - .debug - .rotation_state_poling_interval, - }, - }, - debug: MixnetDebug { - maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, - packet_forwarding_initial_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_initial_backoff, - packet_forwarding_maximum_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_maximum_backoff, - initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, - maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, - unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, - use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, - }, - }, - storage_paths: NymNodePaths { - keys: KeysPaths { - private_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .public_ed25519_identity_key_file, - primary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .primary_x25519_sphinx_key_file, - private_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .private_x25519_noise_key_file, - public_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .public_x25519_noise_key_file, - secondary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .secondary_x25519_sphinx_key_file, - }, - description: old_cfg.storage_paths.description, - }, - http: Http { - bind_address: old_cfg.http.bind_address, - landing_page_assets_path: old_cfg.http.landing_page_assets_path, - access_token: old_cfg.http.access_token, - expose_system_info: old_cfg.http.expose_system_info, - expose_system_hardware: old_cfg.http.expose_system_hardware, - expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, - node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, - }, - verloc: Verloc { - bind_address: old_cfg.verloc.bind_address, - announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { - packets_per_node: old_cfg.verloc.debug.packets_per_node, - connection_timeout: old_cfg.verloc.debug.connection_timeout, - packet_timeout: old_cfg.verloc.debug.packet_timeout, - delay_between_packets: old_cfg.verloc.debug.delay_between_packets, - tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, - testing_interval: old_cfg.verloc.debug.testing_interval, - retry_timeout: old_cfg.verloc.debug.retry_timeout, - }, - }, - wireguard: Wireguard { + modes: old_cfg.modes, + host: old_cfg.host, + mixnet: old_cfg.mixnet, + storage_paths: old_cfg.storage_paths, + http: old_cfg.http, + verloc: old_cfg.verloc, + wireguard: WireguardV12 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, @@ -343,259 +202,48 @@ pub async fn try_upgrade_config_v11>( announced_metadata_port: old_cfg.wireguard.announced_metadata_port, private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + // \/ ADDED use_userspace: false, - storage_paths: WireguardPaths { - private_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .private_diffie_hellman_key_file, - public_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .public_diffie_hellman_key_file, - }, + // /\ ADDED + storage_paths: old_cfg.wireguard.storage_paths, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { - clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, - stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, - cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, - bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, - }, + gateway_tasks: GatewayTasksConfigV12 { + storage_paths: old_cfg.gateway_tasks.storage_paths, enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, // \/ ADDED - upgrade_mode: UpgradeModeWatcher::new() + upgrade_mode: UpgradeModeWatcherV12::new() .inspect_err(|_| { error!( "failed to set custom upgrade mode configuration - falling back to mainnet" ) }) - .unwrap_or(UpgradeModeWatcher::new_mainnet()), - lp: Default::default(), + .unwrap_or(UpgradeModeWatcherV12::new_mainnet()), + lp: LpConfigV12::default(), // /\ ADDED - debug: gateway_tasks::Debug { + debug: GatewayTasksConfigDebugV12 { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { - cleaner_run_interval: old_cfg - .gateway_tasks - .debug - .stale_messages - .cleaner_run_interval, - max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, - }, - client_bandwidth: ClientBandwidthDebug { - max_flushing_rate: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_flushing_rate, - max_delta_flushing_amount: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_delta_flushing_amount, - }, - zk_nym_tickets: ZkNymTicketHandlerDebug { - revocation_bandwidth_penalty: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .revocation_bandwidth_penalty, - pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, - minimum_api_quorum: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_api_quorum, - minimum_redemption_tickets: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_redemption_tickets, - maximum_time_between_redemption: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .maximum_time_between_redemption, - }, + stale_messages: old_cfg.gateway_tasks.debug.stale_messages, + client_bandwidth: old_cfg.gateway_tasks.debug.client_bandwidth, + zk_nym_tickets: old_cfg.gateway_tasks.debug.zk_nym_tickets, + // \/ ADDED (be explicit about the value rather than using ..Default::default() - upgrade_mode_min_staleness_recheck: gateway_tasks::Debug::default() + upgrade_mode_min_staleness_recheck: GatewayTasksConfigDebugV12::default() .upgrade_mode_min_staleness_recheck, // /\ ADDED }, }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { - clients_storage: old_cfg.service_providers.storage_paths.clients_storage, - stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .network_requester - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .network_requester - .gateway_registrations, - }, - ip_packet_router: IpPacketRouterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .gateway_registrations, - }, - authenticator: AuthenticatorPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .authenticator - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .authenticator - .gateway_registrations, - }, - }, - open_proxy: old_cfg.service_providers.open_proxy, - upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { - enabled: old_cfg.service_providers.network_requester.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .network_requester - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .network_requester - .debug - .client_debug, - }, - }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { - enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .ip_packet_router - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .ip_packet_router - .debug - .client_debug, - }, - }, - authenticator: Authenticator { - debug: AuthenticatorDebug { - enabled: old_cfg.service_providers.authenticator.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .authenticator - .debug - .disable_poisson_rate, - client_debug: old_cfg.service_providers.authenticator.debug.client_debug, - }, - }, - debug: service_providers::Debug { - message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, - }, - }, - metrics: Default::default(), - logging: LoggingSettings {}, - debug: Default::default(), + service_providers: old_cfg.service_providers, + metrics: old_cfg.metrics, + logging: old_cfg.logging, + // \/ FIXED + debug: DebugV12::default(), + // /\ FIXED }; Ok(cfg) } diff --git a/nym-node/src/config/old_configs/old_config_v12.rs b/nym-node/src/config/old_configs/old_config_v12.rs new file mode 100644 index 0000000000..268da56802 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v12.rs @@ -0,0 +1,1001 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, UpgradeModeWatcherDebug, + ZkNymTicketHandlerDebug, +}; +use crate::config::helpers::log_error_and_return; +use crate::config::persistence::{ + AuthenticatorPaths, DEFAULT_MCELIECE_PRIVATE_KEY_FILENAME, + DEFAULT_MCELIECE_PUBLIC_KEY_FILENAME, DEFAULT_MLKEM768_PRIVATE_KEY_FILENAME, + DEFAULT_MLKEM768_PUBLIC_KEY_FILENAME, DEFAULT_X25519_PRIVATE_LP_KEY_FILENAME, + DEFAULT_X25519_PUBLIC_LP_KEY_FILENAME, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, + NetworkRequesterPaths, NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, + WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + Config, Debug, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, MetricsConfig, + Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, + ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, gateway_tasks, metrics, + service_providers, +}; +use crate::error::NymNodeError; +use crate::node::helpers::{ + store_mceliece_keypair, store_mlkem768_keypair, store_x25519_lp_keypair, +}; +use nym_bin_common::logging::LoggingSettings; +use nym_config::defaults::{mainnet, var_names}; +use nym_config::read_config_from_toml_file; +use nym_config::serde_helpers::de_maybe_port; +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_kkt::key_utils::{ + generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, +}; +use rand09::SeedableRng; +use serde::{Deserialize, Serialize}; +use std::env; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::{debug, info, instrument}; +use url::Url; + +use crate::config::lp::{LpConfig, LpDebug}; +pub use unchanged_v12_types::*; + +// (while some of those are technically unused, they might be needed in future migrations, +// thus allow them to exist) +#[allow(dead_code)] +pub mod unchanged_v12_types { + use crate::config::old_configs::old_config_v11::{ + AuthenticatorDebugV11, AuthenticatorPathsV11, AuthenticatorV11, ClientBandwidthDebugV11, + GatewayTasksPathsV11, HostV11, HttpV11, IpPacketRouterDebugV11, IpPacketRouterPathsV11, + IpPacketRouterV11, KeyRotationDebugV11, KeyRotationV11, KeysPathsV11, LoggingSettingsV11, + MetricsConfigV11, MetricsDebugV11, MixnetDebugV11, MixnetV11, NetworkRequesterDebugV11, + NetworkRequesterPathsV11, NetworkRequesterV11, NodeModeV11, NodeModesV11, NymNodePathsV11, + ReplayProtectionDebugV11, ReplayProtectionPathsV11, ReplayProtectionV11, + ServiceProvidersConfigDebugV11, ServiceProvidersConfigV11, ServiceProvidersPathsV11, + StaleMessageDebugV11, VerlocDebugV11, VerlocV11, WireguardPathsV11, + ZkNymTicketHandlerDebugV11, + }; + + pub type WireguardPathsV12 = WireguardPathsV11; + pub type NodeModeV12 = NodeModeV11; + pub type NodeModesV12 = NodeModesV11; + pub type HostV12 = HostV11; + pub type KeyRotationDebugV12 = KeyRotationDebugV11; + pub type KeyRotationV12 = KeyRotationV11; + pub type MixnetDebugV12 = MixnetDebugV11; + pub type MixnetV12 = MixnetV11; + pub type ReplayProtectionV12 = ReplayProtectionV11; + pub type ReplayProtectionPathsV12 = ReplayProtectionPathsV11; + pub type ReplayProtectionDebugV12 = ReplayProtectionDebugV11; + pub type KeysPathsV12 = KeysPathsV11; + pub type NymNodePathsV12 = NymNodePathsV11; + pub type HttpV12 = HttpV11; + pub type VerlocDebugV12 = VerlocDebugV11; + pub type VerlocV12 = VerlocV11; + pub type ZkNymTicketHandlerDebugV12 = ZkNymTicketHandlerDebugV11; + pub type NetworkRequesterPathsV12 = NetworkRequesterPathsV11; + pub type IpPacketRouterPathsV12 = IpPacketRouterPathsV11; + pub type AuthenticatorPathsV12 = AuthenticatorPathsV11; + pub type AuthenticatorV12 = AuthenticatorV11; + pub type AuthenticatorDebugV12 = AuthenticatorDebugV11; + pub type IpPacketRouterDebugV12 = IpPacketRouterDebugV11; + pub type IpPacketRouterV12 = IpPacketRouterV11; + pub type NetworkRequesterDebugV12 = NetworkRequesterDebugV11; + pub type NetworkRequesterV12 = NetworkRequesterV11; + pub type GatewayTasksPathsV12 = GatewayTasksPathsV11; + pub type StaleMessageDebugV12 = StaleMessageDebugV11; + pub type ClientBandwidthDebugV12 = ClientBandwidthDebugV11; + pub type ServiceProvidersPathsV12 = ServiceProvidersPathsV11; + pub type ServiceProvidersConfigDebugV12 = ServiceProvidersConfigDebugV11; + pub type ServiceProvidersConfigV12 = ServiceProvidersConfigV11; + pub type MetricsConfigV12 = MetricsConfigV11; + pub type MetricsDebugV12 = MetricsDebugV11; + pub type LoggingSettingsV12 = LoggingSettingsV11; +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV12 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. + /// Useful in containerized environments without kernel WireGuard support. + /// default: `false` + #[serde(default)] + pub use_userspace: bool, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV12, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct GatewayTasksConfigDebugV12 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + #[serde(with = "humantime_serde")] + pub upgrade_mode_min_staleness_recheck: Duration, + + pub stale_messages: StaleMessageDebugV12, + + pub client_bandwidth: ClientBandwidthDebugV12, + + pub zk_nym_tickets: ZkNymTicketHandlerDebugV12, +} + +impl GatewayTasksConfigDebugV12 { + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30); +} + +impl Default for GatewayTasksConfigDebugV12 { + fn default() -> Self { + GatewayTasksConfigDebugV12 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, + stale_messages: Default::default(), + client_bandwidth: Default::default(), + zk_nym_tickets: Default::default(), + upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct UpgradeModeWatcherDebugV12 { + /// Default polling interval + #[serde(with = "humantime_serde")] + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + #[serde(with = "humantime_serde")] + pub expedited_poll_interval: Duration, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpgradeModeWatcherV12 { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + pub attestation_url: Url, + + /// Expected public key of the attester providing the upgrade mode attestation + /// on the specified endpoint + #[serde(with = "bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, + + #[serde(default)] + pub debug: UpgradeModeWatcherDebugV12, +} + +impl UpgradeModeWatcherV12 { + pub fn new_mainnet() -> UpgradeModeWatcherV12 { + info!("using mainnet configuration for the upgrade mode:"); + info!("\t- url: {}", mainnet::UPGRADE_MODE_ATTESTATION_URL); + info!( + "\t- attester public key: {}", + mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + ); + + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + let attestation_url = mainnet::UPGRADE_MODE_ATTESTATION_URL + .parse() + .expect("invalid default upgrade mode attestation URL"); + + #[allow(clippy::expect_used)] + let attester_public_key = mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + .parse() + .expect("invalid default upgrade mode attester public key"); + + UpgradeModeWatcherV12 { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebugV12::default(), + } + } + + pub fn new() -> Result { + // if env is configured, extract relevant values from there, otherwise fallback to mainnet + if env::var(var_names::CONFIGURED).is_err() { + return Ok(Self::new_mainnet()); + } + + // if env is configured, the relevant values should be set + let Ok(env_attestation_url) = env::var(var_names::UPGRADE_MODE_ATTESTATION_URL) else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTATION_URL + )); + }; + + let Ok(env_attester_pubkey) = + env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + )); + }; + + let attestation_url = match env_attestation_url.parse() { + Ok(url) => url, + Err(err) => { + return log_error_and_return(format!( + "provided attestation url {env_attestation_url} is invalid: {err}!" + )); + } + }; + + let attester_public_key = match env_attester_pubkey.parse() { + Ok(public_key) => public_key, + Err(err) => { + return log_error_and_return(format!( + "provided attester public key {env_attester_pubkey} is invalid: {err}!" + )); + } + }; + + Ok(UpgradeModeWatcherV12 { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebugV12::default(), + }) + } +} + +impl UpgradeModeWatcherDebugV12 { + const DEFAULT_REGULAR_POLLING_INTERVAL: Duration = Duration::from_secs(15 * 60); + const DEFAULT_EXPEDITED_POLL_INTERVAL: Duration = Duration::from_secs(2 * 60); +} + +impl Default for UpgradeModeWatcherDebugV12 { + fn default() -> Self { + UpgradeModeWatcherDebugV12 { + regular_polling_interval: Self::DEFAULT_REGULAR_POLLING_INTERVAL, + expedited_poll_interval: Self::DEFAULT_EXPEDITED_POLL_INTERVAL, + } + } +} + +/// Configuration for LP listener +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpConfigV12 { + pub control_bind_address: SocketAddr, + + pub data_bind_address: SocketAddr, + + #[serde(deserialize_with = "de_maybe_port")] + pub announce_control_port: Option, + + #[serde(deserialize_with = "de_maybe_port")] + pub announce_data_port: Option, + + #[serde(default)] + pub debug: LpDebugV12, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct LpDebugV12 { + pub max_connections: usize, + #[serde(with = "humantime_serde")] + pub timestamp_tolerance: Duration, + pub use_mock_ecash: bool, + #[serde(with = "humantime_serde")] + pub handshake_ttl: Duration, + #[serde(with = "humantime_serde")] + pub session_ttl: Duration, + #[serde(with = "humantime_serde")] + pub state_cleanup_interval: Duration, + pub max_concurrent_forwards: usize, +} + +impl LpConfigV12 { + pub const DEFAULT_CONTROL_PORT: u16 = 41264; + pub const DEFAULT_DATA_PORT: u16 = 51264; +} + +impl Default for LpConfigV12 { + fn default() -> Self { + LpConfigV12 { + control_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_CONTROL_PORT, + ), + data_bind_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + Self::DEFAULT_DATA_PORT, + ), + announce_control_port: None, + announce_data_port: None, + debug: Default::default(), + } + } +} + +impl LpDebugV12 { + pub const DEFAULT_MAX_CONNECTIONS: usize = 10000; + pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30); + pub const DEFAULT_HANDSHAKE_TTL: Duration = Duration::from_secs(90); + pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(86400); + pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); + pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000; +} + +impl Default for LpDebugV12 { + fn default() -> Self { + LpDebugV12 { + max_connections: Self::DEFAULT_MAX_CONNECTIONS, + timestamp_tolerance: Self::DEFAULT_TIMESTAMP_TOLERANCE, + use_mock_ecash: false, + handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL, + session_ttl: Self::DEFAULT_SESSION_TTL, + state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL, + max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksConfigV12 { + pub storage_paths: GatewayTasksPathsV12, + + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub ws_bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + pub upgrade_mode: UpgradeModeWatcherV12, + + #[serde(default)] + pub lp: LpConfigV12, + + #[serde(default)] + pub debug: GatewayTasksConfigDebugV12, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV12 { + /// Specifies the time to live of the internal topology provider cache. + #[serde(with = "humantime_serde")] + pub topology_cache_ttl: Duration, + + /// Specifies the time between attempting to resolve any pending unknown nodes in the routing filter + #[serde(with = "humantime_serde")] + pub routing_nodes_check_interval: Duration, + + /// Specifies whether this node runs in testnet mode thus allowing it to route packets on local interfaces + pub testnet: bool, +} + +impl DebugV12 { + pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(10 * 60); + pub const DEFAULT_ROUTING_NODES_CHECK_INTERVAL: Duration = Duration::from_secs(5 * 60); +} + +impl Default for DebugV12 { + fn default() -> Self { + DebugV12 { + topology_cache_ttl: Self::DEFAULT_TOPOLOGY_CACHE_TTL, + routing_nodes_check_interval: Self::DEFAULT_ROUTING_NODES_CHECK_INTERVAL, + testnet: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV12 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV12, + + pub host: HostV12, + + pub mixnet: MixnetV12, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV12, + + #[serde(default)] + pub http: HttpV12, + + #[serde(default)] + pub verloc: VerlocV12, + + pub wireguard: WireguardV12, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV12, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV12, + + #[serde(default)] + pub metrics: MetricsConfigV12, + + #[serde(default)] + pub logging: LoggingSettingsV12, + + #[serde(default)] + pub debug: DebugV12, +} + +impl ConfigV12 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV12 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v12>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v12 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV12::read_from_path(&path)? + }; + + info!("migrating the old config (v12)..."); + let keys_dir = old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file + .parent() + .ok_or(NymNodeError::DataDirDerivationFailure)? + .to_path_buf(); + + let updated_keys = KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file: old_cfg.storage_paths.keys.primary_x25519_sphinx_key_file, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg.storage_paths.keys.private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg.storage_paths.keys.public_x25519_noise_key_file, + private_x25519_lp_key_file: keys_dir.join(DEFAULT_X25519_PRIVATE_LP_KEY_FILENAME), + public_x25519_lp_key_file: keys_dir.join(DEFAULT_X25519_PUBLIC_LP_KEY_FILENAME), + private_mlkem768_lp_key_file: keys_dir.join(DEFAULT_MLKEM768_PRIVATE_KEY_FILENAME), + public_mlkem768_lp_key_file: keys_dir.join(DEFAULT_MLKEM768_PUBLIC_KEY_FILENAME), + private_mceliece_lp_key_file: keys_dir.join(DEFAULT_MCELIECE_PRIVATE_KEY_FILENAME), + public_mceliece_lp_key_file: keys_dir.join(DEFAULT_MCELIECE_PUBLIC_KEY_FILENAME), + }; + + let mut rng = rand09::rngs::StdRng::from_os_rng(); + + // generate new keys for LP + info!("generating new LP x25519 DH keypair"); + let x25519 = generate_lp_keypair_x25519(&mut rng); + let paths = updated_keys.x25519_lp_key_paths(); + store_x25519_lp_keypair(&x25519, &paths)?; + + info!("generating new mlkem768 keypair"); + let mlkem = generate_keypair_mlkem(&mut rng); + let paths = updated_keys.mlkem768_key_paths(); + store_mlkem768_keypair(&mlkem, &paths)?; + + info!("generating new mceliece keypair (this might take a while)"); + let mceliece = generate_keypair_mceliece(&mut rng); + let paths = updated_keys.mceliece_key_paths(); + store_mceliece_keypair(&mceliece, &paths)?; + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + storage_paths: NymNodePaths { + keys: updated_keys, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_tunnel_port: old_cfg.wireguard.announced_tunnel_port, + announced_metadata_port: old_cfg.wireguard.announced_metadata_port, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + use_userspace: old_cfg.wireguard.use_userspace, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + lp: LpConfig { + control_bind_address: old_cfg.gateway_tasks.lp.control_bind_address, + data_bind_address: old_cfg.gateway_tasks.lp.data_bind_address, + announce_control_port: old_cfg.gateway_tasks.lp.announce_control_port, + announce_data_port: old_cfg.gateway_tasks.lp.announce_data_port, + debug: LpDebug { + max_connections: old_cfg.gateway_tasks.lp.debug.max_connections, + use_mock_ecash: old_cfg.gateway_tasks.lp.debug.use_mock_ecash, + handshake_ttl: old_cfg.gateway_tasks.lp.debug.handshake_ttl, + session_ttl: old_cfg.gateway_tasks.lp.debug.session_ttl, + state_cleanup_interval: old_cfg.gateway_tasks.lp.debug.state_cleanup_interval, + max_concurrent_forwards: old_cfg.gateway_tasks.lp.debug.max_concurrent_forwards, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + upgrade_mode: UpgradeModeWatcher { + enabled: old_cfg.gateway_tasks.upgrade_mode.enabled, + attestation_url: old_cfg.gateway_tasks.upgrade_mode.attestation_url, + attester_public_key: old_cfg.gateway_tasks.upgrade_mode.attester_public_key, + debug: UpgradeModeWatcherDebug { + regular_polling_interval: old_cfg + .gateway_tasks + .upgrade_mode + .debug + .regular_polling_interval, + expedited_poll_interval: old_cfg + .gateway_tasks + .upgrade_mode + .debug + .expedited_poll_interval, + }, + }, + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + upgrade_mode_min_staleness_recheck: old_cfg + .gateway_tasks + .debug + .upgrade_mode_min_staleness_recheck, + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: MetricsConfig { + debug: metrics::Debug { + log_stats_to_console: false, + aggregator_update_rate: Default::default(), + stale_mixnet_metrics_cleaner_rate: Default::default(), + global_prometheus_counters_update_rate: Default::default(), + pending_egress_packets_update_rate: Default::default(), + clients_sessions_update_rate: Default::default(), + console_logging_update_interval: Default::default(), + legacy_mixing_metrics_update_rate: Default::default(), + }, + }, + logging: LoggingSettings {}, + debug: Debug { + topology_cache_ttl: old_cfg.debug.topology_cache_ttl, + routing_nodes_check_interval: old_cfg.debug.routing_nodes_check_interval, + testnet: old_cfg.debug.testnet, + }, + }; + Ok(cfg) +} diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs index 332fbf2c1d..4d0a5c645c 100644 --- a/nym-node/src/config/persistence.rs +++ b/nym-node/src/config/persistence.rs @@ -19,6 +19,14 @@ pub const DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME: &str = "x25519_noise"; pub const DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME: &str = "x25519_noise.pub"; pub const DEFAULT_NYMNODE_DESCRIPTION_FILENAME: &str = "description.toml"; +// Global/LP: +pub const DEFAULT_X25519_PRIVATE_LP_KEY_FILENAME: &str = "x25519_lp"; +pub const DEFAULT_X25519_PUBLIC_LP_KEY_FILENAME: &str = "x25519_lp.pub"; +pub const DEFAULT_MLKEM768_PRIVATE_KEY_FILENAME: &str = "mlkem768"; +pub const DEFAULT_MLKEM768_PUBLIC_KEY_FILENAME: &str = "mlkem768.pub"; +pub const DEFAULT_MCELIECE_PRIVATE_KEY_FILENAME: &str = "mceliece"; +pub const DEFAULT_MCELIECE_PUBLIC_KEY_FILENAME: &str = "mceliece.pub"; + // Mixnode: // Entry Gateway: @@ -99,6 +107,26 @@ pub struct KeysPaths { /// Path to file containing x25519 noise public key. pub public_x25519_noise_key_file: PathBuf, + + // >> LP KEYS START: + /// Path to file containing x25519 lp private key. + pub private_x25519_lp_key_file: PathBuf, + + /// Path to file containing x25519 lp public key. + pub public_x25519_lp_key_file: PathBuf, + + /// Path to file containing mlkem768 lp private key. + pub private_mlkem768_lp_key_file: PathBuf, + + /// Path to file containing mlkem768 lp public key. + pub public_mlkem768_lp_key_file: PathBuf, + + /// Path to file containing mceliece lp private key. + pub private_mceliece_lp_key_file: PathBuf, + + /// Path to file containing mceliece lp public key. + pub public_mceliece_lp_key_file: PathBuf, + // >> LP KEYS END } impl KeysPaths { @@ -116,6 +144,12 @@ impl KeysPaths { .join(DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME), private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), + private_x25519_lp_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_LP_KEY_FILENAME), + public_x25519_lp_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_LP_KEY_FILENAME), + private_mlkem768_lp_key_file: data_dir.join(DEFAULT_MLKEM768_PRIVATE_KEY_FILENAME), + public_mlkem768_lp_key_file: data_dir.join(DEFAULT_MLKEM768_PUBLIC_KEY_FILENAME), + private_mceliece_lp_key_file: data_dir.join(DEFAULT_MCELIECE_PRIVATE_KEY_FILENAME), + public_mceliece_lp_key_file: data_dir.join(DEFAULT_MCELIECE_PUBLIC_KEY_FILENAME), } } @@ -132,6 +166,27 @@ impl KeysPaths { &self.public_x25519_noise_key_file, ) } + + pub fn x25519_lp_key_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_lp_key_file, + &self.public_x25519_lp_key_file, + ) + } + + pub fn mlkem768_key_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_mlkem768_lp_key_file, + &self.public_mlkem768_lp_key_file, + ) + } + + pub fn mceliece_key_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_mceliece_lp_key_file, + &self.public_mceliece_lp_key_file, + ) + } } #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index 8177119780..6b5c1e39b7 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -96,6 +96,23 @@ private_x25519_noise_key_file = '{{ storage_paths.keys.private_x25519_noise_key_ # Path to file containing x25519 noise public key. public_x25519_noise_key_file = '{{ storage_paths.keys.public_x25519_noise_key_file }}' +# Path to file containing x25519 lp private key. +private_x25519_lp_key_file = '{{ storage_paths.keys.private_x25519_lp_key_file }}' + +# Path to file containing x25519 lp public key. +public_x25519_lp_key_file = '{{ storage_paths.keys.public_x25519_lp_key_file }}' + +# Path to file containing mlkem768 lp private key. +private_mlkem768_lp_key_file = '{{ storage_paths.keys.private_mlkem768_lp_key_file }}' + +# Path to file containing mlkem768 lp public key. +public_mlkem768_lp_key_file = '{{ storage_paths.keys.public_mlkem768_lp_key_file }}' + +# Path to file containing mceliece lp private key. +private_mceliece_lp_key_file = '{{ storage_paths.keys.private_mceliece_lp_key_file }}' + +# Path to file containing mceliece lp public key. +public_mceliece_lp_key_file = '{{ storage_paths.keys.public_mceliece_lp_key_file }}' ##### http-API nym-node config options ##### @@ -166,6 +183,29 @@ private_diffie_hellman_key_file = '{{ wireguard.storage_paths.private_diffie_hel # Path to file containing wireguard x25519 diffie hellman public key. public_diffie_hellman_key_file = '{{ wireguard.storage_paths.public_diffie_hellman_key_file }}' +##### Lewes Protocol config options ##### + +[lp] +# Bind address for the TCP LP control traffic. +# default: `[::]:41264` +control_bind_address = '{{ lp.control_bind_address }}' + +# Bind address for the UDP LP data traffic. +# default: `[::]:51264` +data_bind_address = '{{ lp.data_bind_address }}' + +# Custom announced port for listening for the TCP LP control traffic. +# If unspecified, the value from the `control_bind_address` will be used instead +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_control_port ={{#if lp.announce_control_port }} {{ lp.announce_control_port }} {{else}} 0 {{/if}} + +# Custom announced port for listening for the UDP LP data traffic. +# If unspecified, the value from the `data_bind_address` will be used instead +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_data_port ={{#if lp.announce_data_port }} {{ lp.announce_data_port }} {{else}} 0 {{/if}} + ##### verloc config options ##### diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index b2b8018231..8c556b409d 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -18,7 +18,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v8(path, cfg).await.ok(); let cfg = try_upgrade_config_v9(path, cfg).await.ok(); let cfg = try_upgrade_config_v10(path, cfg).await.ok(); - match try_upgrade_config_v11(path, cfg).await { + let cfg = try_upgrade_config_v11(path, cfg).await.ok(); + match try_upgrade_config_v12(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade: {e}"); diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 318f96999f..01f8a7113d 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -66,9 +66,15 @@ pub mod vars { "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str = "NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY"; - pub const NYMNODE_LP_USE_MOCK_ECASH_ARG: &str = "NYMNODE_LP_USE_MOCK_ECASH"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; pub const NYMNODE_OPEN_PROXY_ARG: &str = "NYMNODE_OPEN_PROXY"; + + // LP: + pub const NYMNODE_LP_CONTROL_BIND_ADDRESS_ARG: &str = "NYMNODE_LP_CONTROL_BIND_ADDRESS"; + pub const NYMNODE_LP_CONTROL_ANNOUNCE_PORT_ARG: &str = "NYMNODE_LP_CONTROL_ANNOUNCE_PORT"; + pub const NYMNODE_LP_DATA_BIND_ADDRESS_ARG: &str = "NYMNODE_LP_DATA_BIND_ADDRESS"; + pub const NYMNODE_LP_DATA_ANNOUNCE_PORT_ARG: &str = "NYMNODE_LP_DATA_ANNOUNCE_PORT"; + pub const NYMNODE_LP_USE_MOCK_ECASH_ARG: &str = "NYMNODE_LP_USE_MOCK_ECASH"; } diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index abe336dc44..f1328c61c9 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::error::NymNodeHttpError; +use crate::node::lp::error::LpHandlerError; use crate::wireguard::error::WireguardError; use nym_http_api_client::HttpClientError; use nym_ip_packet_router::error::ClientCoreError; +use nym_kkt::keys::storage_wrappers::MalformedStoredKeyError; use nym_validator_client::ValidatorClientError; use nym_validator_client::nyxd::error::NyxdError; use std::io; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use thiserror::Error; @@ -231,6 +233,18 @@ pub enum NymNodeError { #[error("failed upgrade")] FailedUpgrade, + + #[error(transparent)] + MalformedStoredKey(#[from] MalformedStoredKeyError), + + #[error("failed to bind LP to {address}: {source}")] + LpBindFailure { + address: SocketAddr, + source: io::Error, + }, + + #[error(transparent)] + LpFailure(#[from] LpHandlerError), } impl From for NymNodeError { diff --git a/nym-node/src/lib.rs b/nym-node/src/lib.rs new file mode 100644 index 0000000000..9882b20cb7 --- /dev/null +++ b/nym-node/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// allow dead code in the lib imports +#![allow(dead_code)] + +pub mod config; +pub mod error; +pub mod node; +pub mod wireguard; diff --git a/nym-node/src/main.rs b/nym-node/src/main.rs index 8c7a8aa2e2..3b83a1ee96 100644 --- a/nym-node/src/main.rs +++ b/nym-node/src/main.rs @@ -10,13 +10,13 @@ use nym_bin_common::logging::maybe_print_banner; use nym_config::defaults::setup_env; mod cli; -pub(crate) mod config; +pub mod config; mod env; -pub(crate) mod error; +pub mod error; mod logging; -pub(crate) mod node; +pub mod node; pub(crate) mod throughput_tester; -pub(crate) mod wireguard; +pub mod wireguard; fn main() -> anyhow::Result<()> { // std::env::set_var( diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index b0112a7004..dfe41d67ad 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -6,6 +6,10 @@ use crate::error::{KeyIOFailure, NymNodeError}; use crate::node::key_rotation::key::{SphinxPrivateKey, SphinxPublicKey}; use crate::node::nym_apis_client::NymApisClient; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_kkt::keys::storage_wrappers::StorableKey; +use nym_kkt::keys::{ + DHKeyPair, DHPrivateKey, MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey, mceliece, +}; use nym_node_requests::api::v1::node::models::NodeDescription; use nym_pemstore::KeyPairPath; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; @@ -169,6 +173,34 @@ pub(crate) fn load_x25519_wireguard_keypair( Ok(load_keypair(paths, "x25519-wireguard")?) } +fn load_lp_key(path: P, name: &'static str) -> Result +where + P: AsRef, + S: StorableKey, +{ + let repr = load_key::<::StorableRepresentation<'_>, _>(path, name)?; + Ok(S::from_storable(repr)?) +} + +pub(crate) fn load_x25519_lp_keypair(paths: &KeyPairPath) -> Result { + let pk: DHPrivateKey = load_lp_key(&paths.private_key_path, "x25519-lp-private-key")?; + Ok(DHKeyPair::from(pk)) +} + +pub(crate) fn load_mlkem768_keypair(paths: &KeyPairPath) -> Result { + let sk: MlKem768PrivateKey = load_lp_key(&paths.private_key_path, "mlkem768-private-key")?; + let pk: MlKem768PublicKey = load_lp_key(&paths.public_key_path, "mlkem768-public-key")?; + Ok(MlKem768KeyPair::from(sk, pk)) +} + +pub(crate) fn load_mceliece_keypair( + paths: &KeyPairPath, +) -> Result { + let sk: mceliece::SecretKey = load_lp_key(&paths.private_key_path, "mceliece-private-key")?; + let pk: mceliece::PublicKey = load_lp_key(&paths.public_key_path, "mceliece-public-key")?; + Ok(mceliece::KeyPair { sk, pk }) +} + pub(crate) fn store_ed25519_identity_keypair( keys: &ed25519::KeyPair, paths: &KeyPairPath, @@ -183,6 +215,57 @@ pub(crate) fn store_x25519_noise_keypair( Ok(store_keypair(keys, paths, "x25519-noise")?) } +pub(crate) fn store_x25519_lp_keypair( + keys: &DHKeyPair, + paths: &KeyPairPath, +) -> Result<(), NymNodeError> { + store_key( + &keys.pk.to_storable(), + &paths.public_key_path, + "x25519-lp-public-key", + )?; + store_key( + &keys.sk().to_storable(), + &paths.private_key_path, + "x25519-lp-private-key", + )?; + Ok(()) +} + +pub(crate) fn store_mlkem768_keypair( + keys: &MlKem768KeyPair, + paths: &KeyPairPath, +) -> Result<(), NymNodeError> { + store_key( + &keys.public_key().to_storable(), + &paths.public_key_path, + "mlkem768-public-key", + )?; + store_key( + &keys.private_key().to_storable(), + &paths.private_key_path, + "mlkem768-private-key", + )?; + Ok(()) +} + +pub(crate) fn store_mceliece_keypair( + keys: &mceliece::KeyPair, + paths: &KeyPairPath, +) -> Result<(), NymNodeError> { + store_key( + &keys.pk.to_storable(), + &paths.public_key_path, + "mceliece-public-key", + )?; + store_key( + &keys.sk.to_storable(), + &paths.private_key_path, + "mceliece-private-key", + )?; + Ok(()) +} + pub(crate) async fn get_current_rotation_id( nym_apis: &[Url], fallback_nyxd: &[Url], diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs index 36d8dc785d..6e0aa38e58 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs @@ -3,13 +3,13 @@ use axum::Router; use axum::routing::get; -use nym_node_requests::api::v1::lewes_protocol::models; +use nym_node_requests::api::SignedLewesProtocol; pub mod root; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Config { - pub details: Option, + pub details: SignedLewesProtocol, } pub(crate) fn routes(config: Config) -> Router { diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs index b056db5a90..06b2961717 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs @@ -4,30 +4,29 @@ use axum::extract::Query; use axum::http::StatusCode; use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol; +use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo}; /// Returns root Lewes Protocol information #[utoipa::path( get, - path = "", - context_path = "/api/v1/lewes-protocol", + path = "/lewes-protocol", + context_path = "/api/v1", tag = "Lewes Protocol", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( - (LewesProtocol = "application/json"), - (LewesProtocol = "application/yaml"), - (LewesProtocol = "application/bincode") + (SignedLewesProtocolInfo = "application/json"), + (SignedLewesProtocolInfo = "application/yaml"), + (SignedLewesProtocolInfo = "application/bincode") )) ), params(OutputParams) )] pub(crate) async fn root_lewes_protocol( - config: Option, + config: SignedLewesProtocol, Query(output): Query, ) -> Result { - let config = config.ok_or(StatusCode::NOT_IMPLEMENTED)?; Ok(output.to_response(config)) } -pub type LewesProtocolResponse = FormattedResponse; +pub type LewesProtocolResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs index 53427f0cdf..8e828f8590 100644 --- a/nym-node/src/node/http/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -16,7 +16,8 @@ use nym_node_requests::api::{SignedDataHostInfo, v1::node::models::SignedHostInf responses( (status = 200, content( (SignedDataHostInfo = "application/json"), - (SignedDataHostInfo = "application/yaml") + (SignedDataHostInfo = "application/yaml"), + (SignedDataHostInfo = "application/bincode") )) ), params(OutputParams) diff --git a/nym-node/src/node/http/router/mod.rs b/nym-node/src/node/http/router/mod.rs index dbaee0e8d5..4c3618c76f 100644 --- a/nym-node/src/node/http/router/mod.rs +++ b/nym-node/src/node/http/router/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::NymNodeHttpServer; +use crate::node::http::api::v1::lewes_protocol; use crate::node::http::error::NymNodeHttpError; use crate::node::http::state::AppState; use axum::Router; @@ -9,6 +10,7 @@ use axum::response::Redirect; use axum::routing::get; use nym_bin_common::bin_info_owned; use nym_http_api_common::middleware::logging; +use nym_node_requests::api::SignedLewesProtocol; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::{Bridges, Gateway}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; @@ -33,7 +35,7 @@ pub struct HttpServerConfig { } impl HttpServerConfig { - pub fn new() -> Self { + pub fn new(signed_lewes_protocol: SignedLewesProtocol) -> Self { HttpServerConfig { landing: Default::default(), api: api::Config { @@ -52,7 +54,9 @@ impl HttpServerConfig { network_requester: Default::default(), ip_packet_router: Default::default(), authenticator: Default::default(), - lewes_protocol: Default::default(), + lewes_protocol: lewes_protocol::Config { + details: signed_lewes_protocol, + }, }, }, } diff --git a/nym-node/src/node/key_rotation/key.rs b/nym-node/src/node/key_rotation/key.rs index 7d31455ae5..350cae8a7f 100644 --- a/nym-node/src/node/key_rotation/key.rs +++ b/nym-node/src/node/key_rotation/key.rs @@ -113,14 +113,11 @@ impl PemStorableKey for SphinxPrivateKey { #[cfg(test)] mod tests { use super::*; - use rand::SeedableRng; - use rand_chacha::ChaCha20Rng; + use nym_test_utils::helpers::deterministic_rng; #[test] fn private_key_bytes_convertion() { - // Set up a deterministic RNG. - let seed = [42u8; 32]; - let mut rng = ChaCha20Rng::from_seed(seed); + let mut rng = deterministic_rng(); let key = SphinxPrivateKey { rotation_id: 42, diff --git a/nym-node/src/node/lp/data_handler.rs b/nym-node/src/node/lp/data_handler.rs new file mode 100644 index 0000000000..9c842fd159 --- /dev/null +++ b/nym-node/src/node/lp/data_handler.rs @@ -0,0 +1,237 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! LP Data Handler - UDP listener for LP data plane (port 51264) +//! +//! This module handles the data plane for LP clients that have completed registration +//! via the control plane (TCP:41264). LP-wrapped Sphinx packets arrive here, get +//! decrypted, and are forwarded into the mixnet. +//! +//! # Packet Flow +//! +//! ```text +//! LP Client → UDP:51264 → LP Data Handler → Mixnet Entry +//! LP(Sphinx) decrypt LP forward Sphinx +//! ``` +//! + +use super::LpHandlerState; +use crate::error::NymNodeError; +use crate::node::lp::error::LpHandlerError; +use nym_lp::packet::OuterHeader; +use nym_metrics::inc; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use tracing::*; + +/// Maximum UDP packet size we'll accept +/// Sphinx packets are typically ~2KB, LP overhead is ~50 bytes, so 4KB is plenty +const MAX_UDP_PACKET_SIZE: usize = 4096; + +/// LP Data Handler for UDP data plane +pub struct LpDataHandler { + /// UDP socket for receiving LP-wrapped Sphinx packets + socket: Arc, + + /// Shared state with TCP control plane + #[allow(dead_code)] + state: LpHandlerState, + + /// Shutdown token + shutdown: nym_task::ShutdownToken, +} + +impl LpDataHandler { + /// Create a new LP data handler + pub async fn new( + bind_addr: SocketAddr, + state: LpHandlerState, + shutdown: nym_task::ShutdownToken, + ) -> Result { + let socket = UdpSocket::bind(bind_addr).await.map_err(|source| { + error!("Failed to bind LP data socket to {bind_addr}: {source}"); + NymNodeError::LpBindFailure { + address: bind_addr, + source, + } + })?; + + info!("LP data handler listening on UDP {bind_addr}"); + + Ok(Self { + socket: Arc::new(socket), + state, + shutdown, + }) + } + + /// Run the UDP packet receive loop + pub async fn run(self) -> Result<(), LpHandlerError> { + let mut buf = vec![0u8; MAX_UDP_PACKET_SIZE]; + + loop { + tokio::select! { + biased; + + _ = self.shutdown.cancelled() => { + info!("LP data handler: received shutdown signal"); + break; + } + + result = self.socket.recv_from(&mut buf) => { + match result { + Ok((len, src_addr)) => { + // Process packet in place (no spawn - UDP is fast) + if let Err(e) = self.handle_packet(&buf[..len], src_addr).await { + debug!("LP data packet error from {src_addr}: {e}"); + inc!("lp_data_packet_errors"); + } + } + Err(e) => { + warn!("LP data socket recv error: {e}"); + inc!("lp_data_recv_errors"); + } + } + } + } + } + + info!("LP data handler shutdown complete"); + Ok(()) + } + + /// Handle a single UDP packet + /// + /// # Packet Processing Steps + /// 1. Parse LP header to get receiver_idx (for routing) + /// 2. Look up session state machine by receiver_idx + /// 3. Process packet through state machine (handles decryption + replay protection) + /// 4. Forward decrypted Sphinx packet to mixnet + /// + /// # Security + /// The state machine's `process_input()` method handles replay protection by: + /// - Checking packet counter against receiving window + /// - Marking counter as used after successful decryption + /// + /// This prevents replay attacks where captured packets are re-sent. + async fn handle_packet( + &self, + packet: &[u8], + src_addr: SocketAddr, + ) -> Result<(), LpHandlerError> { + inc!("lp_data_packets_received"); + + let _ = OuterHeader::parse(packet)?; + trace!( + "received {} bytes from {src_addr} on the unimplemented LP Data endpoint", + packet.len() + ); + + Err(LpHandlerError::UnimplementedDataChannel) + // leave old code for future reference + + // + // // Step 1: Parse LP header (always cleartext for routing) + // let header = nym_lp::codec::parse_lp_header_only(packet).map_err(|e| { + // LpHandlerError::LpProtocolError(format!("Failed to parse LP header: {}", e)) + // })?; + // + // let receiver_idx = header.receiver_idx; + // let counter = header.counter; + // let len = packet.len(); + // + // trace!("LP data packet from {src_addr} (receiver_idx={receiver_idx}, counter={counter}, len={len})"); + // + // // Step 2: Look up session state machine by receiver_idx (mutable for state updates) + // let mut state_entry = self + // .state + // .session_states + // .get_mut(&receiver_idx) + // .ok_or_else(|| { + // inc!("lp_data_unknown_session"); + // LpHandlerError::LpProtocolError(format!( + // "Unknown session for receiver_idx {receiver_idx}" + // )) + // })?; + // + // // Update last activity timestamp + // state_entry.value().touch(); + // + // // Step 3: Get outer AEAD key for packet parsing + // let outer_key = state_entry + // .value() + // .state + // .session() + // .map_err(|e| LpHandlerError::LpProtocolError(format!("Session error: {e}")))? + // .outer_aead_key(); + // + // // Parse full packet with outer AEAD decryption + // let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(outer_key)).map_err(|e| { + // inc!("lp_data_decrypt_errors"); + // LpHandlerError::LpProtocolError(format!("Failed to decrypt LP packet: {}", e)) + // })?; + // + // // Step 4: Process packet through state machine + // // This handles: + // // - Replay protection (counter check + mark) + // // - Inner Noise decryption + // // - Subsession handling if applicable + // let state_machine = &mut state_entry.value_mut().state; + // + // let action = state_machine + // .process_input(LpInput::ReceivePacket(lp_packet)) + // .ok_or_else(|| { + // LpHandlerError::LpProtocolError("State machine returned no action".to_string()) + // })? + // .map_err(|e| { + // inc!("lp_data_state_machine_errors"); + // LpHandlerError::LpProtocolError(format!("State machine error: {}", e)) + // })?; + // + // // Release session lock before forwarding + // drop(state_entry); + // + // // Step 5: Handle the action from state machine + // match action { + // LpAction::DeliverData(data) => { + // // Decrypted application data - forward as Sphinx packet + // self.forward_sphinx_packet(&data.content).await?; + // inc!("lp_data_packets_forwarded"); + // Ok(()) + // } + // LpAction::SendPacket(_response_packet) => { + // // UDP is connectionless - we can't send responses back easily + // // For subsession rekeying, the client should use TCP control plane + // debug!( + // "Ignoring SendPacket action on UDP (receiver_idx={receiver_idx}) - use TCP for rekeying", + // ); + // inc!("lp_data_ignored_send_actions"); + // Ok(()) + // } + // other => { + // warn!( + // "Unexpected action on UDP data plane from {}: {:?}", + // src_addr, other + // ); + // inc!("lp_data_unexpected_actions"); + // Err(LpHandlerError::LpProtocolError(format!( + // "Unexpected state machine action on UDP: {:?}", + // other + // ))) + // } + // } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Sphinx packets are typically around 2KB + // LP overhead is small (~50 bytes header + AEAD tag) + // 4KB should be plenty with room to spare + const _: () = { + assert!(MAX_UDP_PACKET_SIZE >= 2048 + 100); + }; +} diff --git a/nym-node/src/node/lp/error.rs b/nym-node/src/node/lp/error.rs new file mode 100644 index 0000000000..d8c9da439a --- /dev/null +++ b/nym-node/src/node/lp/error.rs @@ -0,0 +1,72 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::lp::LpReceiverIndex; +use nym_lp::state_machine::{LpAction, LpDataKind}; +use nym_lp::transport::LpTransportError; +use nym_lp::{LpError, packet::MalformedLpPacketError}; +use std::net::SocketAddr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LpHandlerError { + #[error("failed to establish egress connection to {egress}: {reason}")] + ConnectionFailure { egress: SocketAddr, reason: String }, + + #[error(transparent)] + LpTransportError(#[from] LpTransportError), + + #[error("missing session state for {receiver_index} - has it been removed due to inactivity?")] + MissingLpSession { receiver_index: LpReceiverIndex }, + + #[error(transparent)] + LpProtocolError(#[from] LpError), + + #[error("the initial KKT/PSQ handshake has not been completed")] + IncompleteHandshake, + + #[error("receiver_idx mismatch: connection bound to {established}, packet has {received}")] + MismatchedReceiverIndex { + established: LpReceiverIndex, + received: LpReceiverIndex, + }, + + #[error("no action has been emitted from the LP State Machine")] + UnexpectedStateMachineHalt, + + #[error("the state machine instructed an unexpected action: {action:?}")] + UnexpectedStateMachineAction { action: LpAction }, + + #[error("received registration request was malformed: {source}")] + MalformedRegistrationRequest { source: bincode::Error }, + + #[error("received a malformed packet: {0}")] + MalformedLpPacket(#[from] MalformedLpPacketError), + + #[error("received payload type of an unexpected type: {typ:?}")] + UnexpectedLpPayload { typ: LpDataKind }, + + #[error("timed out while attempting to send to/receive from the connection")] + ConnectionTimeout, + + #[error("data channel is not yet implemented")] + UnimplementedDataChannel, + + #[error("{0}")] + Other(String), +} + +impl LpHandlerError { + pub fn is_connection_closed(&self) -> bool { + match self { + LpHandlerError::LpTransportError(transport_err) => { + matches!(transport_err, LpTransportError::ConnectionClosed) + } + _ => false, + } + } + + pub fn other(msg: impl Into) -> Self { + LpHandlerError::Other(msg.into()) + } +} diff --git a/nym-node/src/node/lp/handler.rs b/nym-node/src/node/lp/handler.rs new file mode 100644 index 0000000000..7adbeb57e6 --- /dev/null +++ b/nym-node/src/node/lp/handler.rs @@ -0,0 +1,821 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use super::{LpHandlerState, LpReceiverIndex, TimestampedState}; +use crate::node::lp::error::LpHandlerError; +use dashmap::mapref::one::RefMut; +use nym_lp::packet::{EncryptedLpPacket, ForwardPacketData}; +use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput}; +use nym_lp::transport::LpHandshakeChannel; +use nym_lp::transport::traits::LpTransportChannel; +use nym_lp::{LpSession, LpStateMachine, packet::message::ExpectedResponseSize}; +use nym_metrics::{add_histogram_obs, inc}; +use nym_registration_common::{LpRegistrationRequest, RegistrationStatus}; +use std::net::SocketAddr; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::time::timeout; +use tracing::*; + +// Histogram buckets for LP operation duration (legacy - used by unused forwarding methods) +const LP_DURATION_BUCKETS: &[f64] = &[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]; + +// Timeout for forward I/O operations (send + receive on exit stream) +// Must be long enough to cover exit gateway processing time +const FORWARD_IO_TIMEOUT_SECS: u64 = 30; + +// Histogram buckets for LP connection lifecycle duration +// LP connections can be very short (registration only: ~1s) or very long (dVPN sessions: hours/days) +// Covers full range from seconds to 24 hours +const LP_CONNECTION_DURATION_BUCKETS: &[f64] = &[ + 1.0, // 1 second + 5.0, // 5 seconds + 10.0, // 10 seconds + 30.0, // 30 seconds + 60.0, // 1 minute + 300.0, // 5 minutes + 600.0, // 10 minutes + 1800.0, // 30 minutes + 3600.0, // 1 hour + 7200.0, // 2 hours + 14400.0, // 4 hours + 28800.0, // 8 hours + 43200.0, // 12 hours + 86400.0, // 24 hours +]; + +/// Connection lifecycle statistics tracking +struct ConnectionStats { + /// When the connection started + start_time: std::time::Instant, + /// Total bytes received (including protocol framing) + bytes_received: u64, + /// Total bytes sent (including protocol framing) + bytes_sent: u64, +} + +impl ConnectionStats { + fn new() -> Self { + Self { + start_time: std::time::Instant::now(), + bytes_received: 0, + bytes_sent: 0, + } + } + + fn record_bytes_received(&mut self, bytes: usize) { + self.bytes_received += bytes as u64; + } + + fn record_bytes_sent(&mut self, bytes: usize) { + self.bytes_sent += bytes as u64; + } +} + +pub struct LpConnectionHandler { + stream: S, + remote_addr: SocketAddr, + state: LpHandlerState, + stats: ConnectionStats, + + // /// Flag indicating whether this is a connection from an entry gateway serving as a proxy + // forwarded_connection: bool, + /// Bound receiver_idx for this connection (set after first packet). + /// All subsequent packets on this connection must use this receiver_idx. + /// Set from ClientHello's proposed receiver_index, or from header for non-bootstrap packets. + bound_receiver_idx: Option, + + /// Persistent connection to exit gateway for forwarding. + /// Opened on first forward, reused for subsequent forwards, closed when client disconnects. + /// Tuple contains (stream, target_address) to verify subsequent forwards go to same exit. + exit_stream: Option<(S, SocketAddr)>, +} + +impl LpConnectionHandler +where + S: LpTransportChannel + LpHandshakeChannel + Unpin, +{ + pub fn new( + stream: S, + // forwarded_connection: bool, + remote_addr: SocketAddr, + state: LpHandlerState, + ) -> Self { + Self { + stream, + remote_addr, + // forwarded_connection, + state, + stats: ConnectionStats::new(), + bound_receiver_idx: None, + exit_stream: None, + } + } + + /// Get the mutable reference to the state machine associated with this client. + /// It is vital it's never held across await points or this might lead to a deadlock. + fn state_entry_mut( + &self, + ) -> Result>, LpHandlerError> { + let receiver_index = self.bound_receiver_index()?; + self.state + .session_states + .get_mut(&receiver_index) + .ok_or_else(|| LpHandlerError::MissingLpSession { receiver_index }) + } + + /// AIDEV-NOTE: Stream-oriented packet loop + /// This handler processes multiple packets on a single TCP connection. + /// Connection lifecycle: handshake + registration, then client closes. + /// First packet binds the connection to a receiver_idx (session-affine). + /// Binding is set by handle_client_hello() from payload's receiver_index, + /// or by validate_or_set_binding() for non-bootstrap first packets. + pub async fn handle(mut self) -> Result<(), LpHandlerError> { + debug!("Handling LP connection from {}", self.remote_addr); + + // Track total LP connections handled + inc!("lp_connections_total"); + + // ============================================================ + // STREAM-ORIENTED PROCESSING: Loop until connection closes + // State persists in LpHandlerState maps across packets + // ============================================================ + + // 1. complete KKT/PSQ handshake before doing anything else. + // bail if it takes too long + let timeout = self.state.lp_config.debug.handshake_ttl; + let local_peer = self.state.local_lp_peer.clone(); + let stream = &mut self.stream; + + let session = match tokio::time::timeout(timeout, async move { + LpSession::psq_handshake_responder(stream, local_peer) + .complete_handshake() + .await + }) + .await + { + Err(_timeout) => { + debug!( + "timed out attempting to complete KTT/PSQ handshake with {}", + self.remote_addr + ); + self.emit_lifecycle_metrics(false); + return Ok(()); + } + Ok(Err(handshake_failure)) => { + debug!( + "failed to complete KKT/PSQ handshake with {}: {handshake_failure}", + self.remote_addr + ); + self.emit_lifecycle_metrics(false); + return Ok(()); + } + Ok(Ok(session)) => session, + }; + let receiver_idx = session.receiver_index(); + + // 2. insert the state machine into the shared state + let state_machine = LpStateMachine::new(session); + self.state + .session_states + .insert(receiver_idx, TimestampedState::new(state_machine)); + self.bound_receiver_idx = Some(receiver_idx); + + // 3. handle any new incoming packet + loop { + // Step 1: Receive raw packet bytes and parse header only (for routing) + let encrypted_packet = match self.receive_raw_packet().await { + Ok(result) => result, + Err(err) => { + if err.is_connection_closed() { + // Graceful EOF - client closed connection + trace!("Connection closed by {} (EOF)", self.remote_addr); + break; + } else { + inc!("lp_errors_receive_packet"); + self.emit_lifecycle_metrics(false); + return Err(err); + } + } + }; + + let receiver_idx = encrypted_packet.outer_header().receiver_idx; + + // Step 2: Validate the binding + if let Err(e) = self.validate_binding(receiver_idx) { + self.emit_lifecycle_metrics(false); + return Err(e); + } + + // Step 3: Process the packet + if let Err(e) = self.process_packet(encrypted_packet).await { + self.emit_lifecycle_metrics(false); + return Err(e); + } + } + + self.emit_lifecycle_metrics(true); + Ok(()) + } + + fn bound_receiver_index(&self) -> Result { + self.bound_receiver_idx + .ok_or_else(|| LpHandlerError::IncompleteHandshake) + } + + /// Validate that the receiver_idx matches the bound session. + fn validate_binding(&self, receiver_idx: LpReceiverIndex) -> Result<(), LpHandlerError> { + let bound_receiver_idx = self.bound_receiver_index()?; + + if bound_receiver_idx != receiver_idx { + warn!( + "Receiver_idx mismatch from {}: expected {bound_receiver_idx}, got {receiver_idx}", + self.remote_addr + ); + inc!("lp_errors_receiver_idx_mismatch"); + return Err(LpHandlerError::MismatchedReceiverIndex { + established: bound_receiver_idx, + received: receiver_idx, + }); + } + + Ok(()) + } + + /// Process a single packet: lookup session, parse, route to handler. + /// Individual handlers do NOT emit lifecycle metrics - the main loop handles that. + /// + /// This handles packets on established sessions, which can be either: + /// EncryptedData containing LpRegistrationRequest or ForwardPacketData + /// + /// We process all transport packets through the state machine. + /// The state machine returns appropriate actions: + /// - DeliverData: decrypted application data to process + /// - SendPacket: response to send + async fn process_packet( + &mut self, + encrypted_packet: EncryptedLpPacket, + ) -> Result<(), LpHandlerError> { + let receiver_index = encrypted_packet.outer_header().receiver_idx; + + let mut state_entry = self.state_entry_mut()?; + + // Update last activity timestamp + state_entry.value().touch(); + + let state_machine = &mut state_entry.value_mut().state; + + trace!( + "Received packet from {} (receiver_idx={receiver_index}, counter={})", + self.remote_addr, + encrypted_packet.outer_header().counter, + ); + + // Process packet through state machine + let action = state_machine + .process_input(LpInput::ReceivePacket(encrypted_packet)) + .ok_or(LpHandlerError::UnexpectedStateMachineHalt)??; + + drop(state_entry); + + match action { + LpAction::SendPacket(response_packet) => { + self.send_serialised_packet(&response_packet).await?; + Ok(()) + } + LpAction::DeliverData(data) => { + // Decrypted application data - process as registration/forwarding + self.handle_decrypted_payload(receiver_index, data).await + } + other @ LpAction::ConnectionClosed => { + warn!( + "Unexpected action in transport from {}: {other:?}", + self.remote_addr + ); + Err(LpHandlerError::UnexpectedStateMachineAction { action: other }) + } + } + } + + /// Handle decrypted transport payload (registration or forwarding request) + async fn handle_decrypted_payload( + &mut self, + receiver_idx: LpReceiverIndex, + decrypted_data: LpData, + ) -> Result<(), LpHandlerError> { + let remote = self.remote_addr; + + let bytes = decrypted_data.content; + match decrypted_data.kind { + LpDataKind::Registration => { + let request = LpRegistrationRequest::try_deserialise(&bytes) + .map_err(|source| LpHandlerError::MalformedRegistrationRequest { source })?; + + debug!( + "LP registration request from {remote} (receiver_idx={receiver_idx}): mode={:?}", + request.mode() + ); + + self.handle_registration_request(receiver_idx, request) + .await + } + LpDataKind::Forward => { + let forward_data = ForwardPacketData::decode(&bytes)?; + + self.handle_forwarding_request(receiver_idx, forward_data) + .await + } + typ @ LpDataKind::Opaque => { + // Neither registration nor forwarding - unknown payload type + warn!( + "Unknown transport payload type from {remote} (receiver_idx={receiver_idx}). dropping {} bytes", + bytes.len() + ); + inc!("lp_errors_unknown_payload_type"); + Err(LpHandlerError::UnexpectedLpPayload { typ }) + } + } + } + + /// Attempt to wrap and send specified response back to the client + async fn send_response_packet( + &mut self, + serialised_response: Vec, + response_kind: LpDataKind, + ) -> Result<(), LpHandlerError> { + let mut state_entry = self.state_entry_mut()?; + + // Access session via state machine for subsession support + let state_machine = &mut state_entry.value_mut().state; + + let wrapped_lp_data = LpData::new(response_kind, serialised_response); + + // Process packet through state machine + let action = state_machine + .process_input(LpInput::SendData(wrapped_lp_data)) + .ok_or(LpHandlerError::UnexpectedStateMachineHalt)??; + + let packet = match action { + LpAction::SendPacket(packet) => packet, + action => return Err(LpHandlerError::UnexpectedStateMachineAction { action }), + }; + + drop(state_entry); + + self.send_serialised_packet(&packet).await?; + Ok(()) + } + + /// Handle registration request on an established session + async fn handle_registration_request( + &mut self, + receiver_idx: LpReceiverIndex, + request: LpRegistrationRequest, + ) -> Result<(), LpHandlerError> { + // Process registration (might modify state) + let response = self.state.process_registration(receiver_idx, request).await; + let response_bytes = response + .serialise() + .map_err(|source| LpHandlerError::MalformedRegistrationRequest { source })?; + + self.send_response_packet(response_bytes, LpDataKind::Registration) + .await?; + + match response.status { + RegistrationStatus::Completed => { + info!("LP registration successful for {}", self.remote_addr); + } + RegistrationStatus::Failed => { + warn!( + "LP registration failed for {}: {:?}", + self.remote_addr, + response.error_message() + ); + } + RegistrationStatus::PendingMoreData => { + info!( + "we required more deta from {} to complete registration", + self.remote_addr + ); + } + } + + Ok(()) + } + + /// Handle forwarding request on an established session + /// + /// Entry gateway receives ForwardPacketData from client, forwards inner packet + /// to exit gateway, receives response, encrypts it, and sends back to client. + async fn handle_forwarding_request( + &mut self, + receiver_idx: LpReceiverIndex, + forward_data: ForwardPacketData, + ) -> Result<(), LpHandlerError> { + // Forward the packet to the target gateway and retrieve its response + let response_bytes = self.handle_forward_packet(forward_data).await?; + + self.send_response_packet(response_bytes, LpDataKind::Forward) + .await?; + + debug!( + "LP forwarding completed for {} (receiver_idx={receiver_idx})", + self.remote_addr + ); + + Ok(()) + } + + /// Returns reference to the established forwarding channel to the exit. + #[allow(dead_code)] + pub fn forwarding_channel(&self) -> &Option<(S, SocketAddr)> { + &self.exit_stream + } + + /// This method establishes connection to the target gateway in order to + /// forward received packets and retrieve any responses + // + // In the future it will also perform identity validation to make sure + // the target node is a valid gateway present in the network + // + // Do not manually call this function. It is only exposed for the purposes of integration tests + #[doc(hidden)] + pub async fn establish_exit_stream( + &mut self, + target_addr: SocketAddr, + ) -> Result<(), LpHandlerError> { + // Acquire semaphore permit to limit concurrent connection opens (FD exhaustion protection) + // Permit is scoped to this block - only protects the connect() call, not stream reuse + let _permit = match self.state.forward_semaphore.try_acquire() { + Ok(permit) => permit, + Err(_) => { + inc!("lp_forward_rejected"); + return Err(LpHandlerError::other("Gateway at forward capacity")); + } + }; + + // Connect to target gateway with timeout + let stream = match timeout(Duration::from_secs(5), S::connect(target_addr)).await { + Ok(Ok(stream)) => stream, + Ok(Err(e)) => { + inc!("lp_forward_failed"); + return Err(LpHandlerError::ConnectionFailure { + egress: target_addr, + reason: e.to_string(), + }); + } + Err(_) => { + inc!("lp_forward_failed"); + return Err(LpHandlerError::ConnectionFailure { + egress: target_addr, + reason: "target gateway connection timeout".to_string(), + }); + } + }; + + debug!("Opened persistent exit connection to {target_addr} for forwarding"); + self.exit_stream = Some((stream, target_addr)); + + Ok(()) + } + + /// Forward an LP packet to another gateway + /// + /// This method connects to the target gateway, forwards the inner packet bytes, + /// receives the response, and returns it. Used for telescoping (hiding client IP). + /// + /// # Arguments + /// * `forward_data` - ForwardPacketData containing target gateway info and inner packet + /// + /// # Returns + /// * `Ok(Vec)` - Raw response bytes from target gateway + /// * `Err(LpHandlerError)` - If forwarding fails + /// + /// AIDEV-NOTE: Persistent exit stream forwarding + /// Uses self.exit_stream to maintain a persistent connection to the exit gateway. + /// First forward opens the connection, subsequent forwards reuse it. + /// Connection errors clear exit_stream, causing reconnection on next forward. + /// + /// Semaphore rationale: The forward_semaphore limits concurrent connection OPENS + /// (FD exhaustion protection), not concurrent operations. Since: + /// 1. Each LpConnectionHandler owns its exit_stream exclusively + /// 2. The handler loop processes packets sequentially (no concurrent access) + /// 3. Only connection opens consume new FDs + /// + /// The semaphore is only acquired when opening a new connection, not for reuse. + async fn handle_forward_packet( + &mut self, + forward_data: ForwardPacketData, + ) -> Result, LpHandlerError> { + inc!("lp_forward_total"); + let start = std::time::Instant::now(); + + // Parse target gateway address + let target_addr = forward_data.target_lp_address; + + // Check if we need to open a new connection + let need_new_connection = match &self.exit_stream { + Some((_, existing_addr)) if *existing_addr == target_addr => false, + Some((_, existing_addr)) => { + // Target mismatch - this shouldn't happen in normal operation + // (client should only forward to one exit gateway) + // Return error to prevent silent behavior changes that could mask bugs + inc!("lp_forward_failed"); + return Err(LpHandlerError::other(format!( + "Forward target mismatch: session bound to {existing_addr}, got request for {target_addr}" + ))); + } + None => true, + }; + + if need_new_connection { + self.establish_exit_stream(target_addr).await?; + } + + // Get mutable reference to the exit stream + #[allow(clippy::unwrap_used)] + let (target_stream, _) = self.exit_stream.as_mut().unwrap(); + + debug!( + "Forwarding packet to {} ({} bytes)", + target_addr, + forward_data.inner_packet_bytes.len() + ); + + // Wrap all I/O in timeout to prevent hanging on unresponsive exit gateway + let io_timeout = Duration::from_secs(FORWARD_IO_TIMEOUT_SECS); + let inner_bytes = &forward_data.inner_packet_bytes; + + let io_result: Result, LpHandlerError> = timeout(io_timeout, async { + // Forward inner packet bytes. + // it's up to the client to ensure correct formatting, + // i.e. relevant headers or length-prefixes + target_stream.write_all_and_flush(inner_bytes).await?; + + // attempt to read response based on the provided information + + let response = match forward_data.expected_response_size { + ExpectedResponseSize::Handshake(size) => { + // client told us exactly how many bytes to expect + target_stream.read_n_bytes(size as usize).await? + } + ExpectedResponseSize::Transport => { + // transport packets are length-prefixed + target_stream + .receive_length_prefixed_transport_bytes() + .await? + } + }; + Ok(response) + }) + .await + .map_err(|_| LpHandlerError::ConnectionTimeout)?; + + // Handle result - clear exit_stream on any error + let response_buf = match io_result { + Ok(buf) => buf, + Err(e) => { + inc!("lp_forward_failed"); + self.exit_stream = None; + return Err(e); + } + }; + + // Record metrics + let duration = start.elapsed().as_secs_f64(); + add_histogram_obs!("lp_forward_duration_seconds", duration, LP_DURATION_BUCKETS); + + inc!("lp_forward_success"); + debug!( + "Forwarding successful to {} ({} bytes response, {:.3}s)", + target_addr, + response_buf.len(), + duration + ); + + Ok(response_buf) + } + + /// Receive raw packet bytes and parse outer header only (for routing before session lookup). + /// + /// Returns the raw packet bytes and parsed outer header (receiver_idx + counter). + /// The caller should look up the session to get outer_aead_key, then call + /// `parse_lp_packet()` with the key. + async fn receive_raw_packet(&mut self) -> Result { + let packet = self + .stream + .receive_length_prefixed_transport_packet() + .await?; + + // Track bytes sent (4 byte header + packet data) + self.stats + .record_bytes_received(4 + packet.encoded_length()); + + Ok(packet) + } + + /// Send a serialised LP packet over the stream with proper length-prefixed framing. + async fn send_serialised_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result<(), LpHandlerError> { + self.stream + .send_length_prefixed_transport_packet(packet) + .await?; + + // Track bytes sent (4 byte header + packet data) + self.stats.record_bytes_sent(4 + packet.encoded_length()); + + Ok(()) + } + + /// Emit connection lifecycle metrics + fn emit_lifecycle_metrics(&self, graceful: bool) { + use nym_metrics::inc_by; + + // Track connection duration + let duration = self.stats.start_time.elapsed().as_secs_f64(); + add_histogram_obs!( + "lp_connection_duration_seconds", + duration, + LP_CONNECTION_DURATION_BUCKETS + ); + + // Track bytes transferred + inc_by!( + "lp_connection_bytes_received_total", + self.stats.bytes_received as i64 + ); + inc_by!( + "lp_connection_bytes_sent_total", + self.stats.bytes_sent as i64 + ); + + // Track completion type + if graceful { + inc!("lp_connections_completed_gracefully"); + } else { + inc!("lp_connections_completed_with_error"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::lp::LpDebug; + use crate::node::lp::LpConfig; + use nym_lp::peer::{KEMKeys, LpLocalPeer, generate_keypair_mceliece, generate_keypair_mlkem}; + use nym_lp::{Ciphersuite, SessionManager, sessions_for_tests}; + use nym_test_utils::helpers::{deterministic_rng, deterministic_rng_09}; + use std::sync::Arc; + // ==================== Test Helpers ==================== + + /// Create a minimal test state for handler tests + async fn create_minimal_test_state() -> LpHandlerState { + use nym_crypto::asymmetric::ed25519; + + let mut rng = deterministic_rng(); + let mut rng09 = deterministic_rng_09(); + + let lp_config = LpConfig { + debug: LpDebug { + ..Default::default() + }, + ..Default::default() + }; + let forward_semaphore = Arc::new(tokio::sync::Semaphore::new( + lp_config.debug.max_concurrent_forwards, + )); + + // Create mix forwarding channel (unused in tests but required by struct) + let (mix_sender, _mix_receiver) = nym_mixnet_client::forwarder::mix_forwarding_channels(); + + let id_keys = Arc::new(ed25519::KeyPair::new(&mut rng)); + let x_keys = Arc::new(id_keys.to_x25519().try_into().unwrap()); + + let kem_keys = KEMKeys::new( + generate_keypair_mceliece(&mut rng09), + generate_keypair_mlkem(&mut rng09), + ); + let lp_peer = LpLocalPeer::new(Ciphersuite::default(), x_keys).with_kem_keys(kem_keys); + + LpHandlerState { + lp_config, + local_lp_peer: lp_peer, + metrics: nym_node_metrics::NymNodeMetrics::default(), + outbound_mix_sender: mix_sender, + session_states: Arc::new(dashmap::DashMap::new()), + peer_registrator: None, + forward_semaphore, + } + } + + // ==================== Existing Tests ==================== + + // ==================== Packet I/O Tests ==================== + + #[tokio::test] + async fn test_receive_raw_packet_valid() { + use tokio::net::{TcpListener, TcpStream}; + + let (init, resp) = sessions_for_tests(); + let mut init_sm = SessionManager::new(); + let mut resp_sm = SessionManager::new(); + resp_sm.create_session_state_machine(resp).unwrap(); + let id = init_sm.create_session_state_machine(init).unwrap(); + + // Bind to localhost + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + // Spawn server task + let server_task = tokio::spawn(async move { + let (stream, remote_addr) = listener.accept().await.unwrap(); + let state = create_minimal_test_state().await; + let mut handler = LpConnectionHandler::new(stream, remote_addr, state); + // Two-phase: receive raw bytes + header, then parse full packet + let packet = handler.receive_raw_packet().await?; + let header = packet.outer_header(); + assert_eq!(packet.outer_header().receiver_idx, id); + let Some(LpAction::DeliverData(data)) = resp_sm.receive_packet(id, packet).unwrap() + else { + panic!("illegal state") + }; + Ok::<_, LpHandlerError>((header, data)) + }); + + // Connect as client + let mut client_stream = TcpStream::connect(addr).await.unwrap(); + + // Send a valid packet from client side + let LpAction::SendPacket(packet) = init_sm + .send_data(id, LpData::new_opaque(b"foomp".to_vec())) + .unwrap() + else { + panic!("illegal state") + }; + + client_stream + .send_length_prefixed_transport_packet(&packet) + .await + .unwrap(); + + // Handler should receive and parse it correctly + // Note: header is OuterHeader (receiver_idx + counter only), not LpHeader + let (header, received) = server_task.await.unwrap().unwrap(); + assert_eq!(header.receiver_idx, id); + assert_eq!(header.counter, 0); + assert_eq!(received.content.as_ref(), b"foomp"); + } + + #[tokio::test] + async fn test_send_lp_packet_valid() { + use tokio::net::{TcpListener, TcpStream}; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let (init, resp) = sessions_for_tests(); + let mut init_sm = SessionManager::new(); + let mut resp_sm = SessionManager::new(); + resp_sm.create_session_state_machine(resp).unwrap(); + let id = init_sm.create_session_state_machine(init).unwrap(); + + let server_task = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + + let LpAction::SendPacket(packet) = resp_sm + .send_data(id, LpData::new_opaque(b"foomp".to_vec())) + .unwrap() + else { + panic!("illegal state") + }; + + stream + .send_length_prefixed_transport_packet(&packet) + .await + .unwrap(); + }); + + let mut client_stream = TcpStream::connect(addr).await.unwrap(); + + // Wait for server to send + server_task.await.unwrap(); + + // Client should receive it correctly + let received = client_stream + .receive_length_prefixed_transport_packet() + .await + .unwrap(); + let header = received.outer_header(); + let Some(LpAction::DeliverData(data)) = init_sm.receive_packet(id, received).unwrap() + else { + panic!("illegal state") + }; + + assert_eq!(header.receiver_idx, id); + assert_eq!(header.counter, 0); + assert_eq!(data.content.as_ref(), b"foomp"); + } +} diff --git a/gateway/src/node/lp_listener/mod.rs b/nym-node/src/node/lp/mod.rs similarity index 64% rename from gateway/src/node/lp_listener/mod.rs rename to nym-node/src/node/lp/mod.rs index 976eb2afa7..78e1eaee80 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/nym-node/src/node/lp/mod.rs @@ -67,209 +67,30 @@ // To view metrics, the nym-metrics registry automatically collects all metrics. // They can be exported via Prometheus format using the metrics endpoint. -use crate::error::GatewayError; -use crate::node::wireguard::PeerRegistrator; -use crate::node::ActiveClientsStore; +use crate::config::lp::LpConfig; +use crate::error::NymNodeError; use dashmap::DashMap; -use nym_config::serde_helpers::de_maybe_port; -use nym_credential_verification::ecash::traits::EcashManager; -use nym_gateway_storage::GatewayStorage; +use nym_gateway::node::wireguard::PeerRegistrator; +use nym_lp::peer::LpLocalPeer; +use nym_lp::peer_config::LpReceiverIndex; use nym_lp::state_machine::LpStateMachine; +use nym_mixnet_client::forwarder::MixForwardingSender; use nym_node_metrics::NymNodeMetrics; use nym_task::ShutdownTracker; -use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::Semaphore; use tracing::*; -pub use nym_lp::peer::LpLocalPeer; -pub use nym_mixnet_client::forwarder::{ - mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, -}; -pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData}; +pub use nym_mixnet_client::forwarder::{MixForwardingReceiver, mix_forwarding_channels}; mod data_handler; +pub mod error; pub mod handler; mod registration; -pub type ReceiverIndex = u32; - -/// Configuration for LP listener -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct LpConfig { - /// Bind address for the TCP LP control traffic. - /// default: `[::]:41264` - pub control_bind_address: SocketAddr, - - /// Bind address for the UDP LP data traffic. - /// default: `[::]:51264` - pub data_bind_address: SocketAddr, - - /// Custom announced port for listening for the TCP LP control traffic. - /// If unspecified, the value from the `control_bind_address` will be used instead - /// (default: None) - #[serde(deserialize_with = "de_maybe_port")] - pub announce_control_port: Option, - - /// Custom announced port for listening for the UDP LP data traffic. - /// If unspecified, the value from the `data_bind_address` will be used instead - /// (default: None) - #[serde(deserialize_with = "de_maybe_port")] - pub announce_data_port: Option, - - /// Auxiliary configuration - #[serde(default)] - pub debug: LpDebug, -} - -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct LpDebug { - /// Maximum concurrent connections - pub max_connections: usize, - - /// Maximum acceptable age of ClientHello timestamp in seconds (default: 30) - /// - /// ClientHello messages with timestamps older than this will be rejected - /// to prevent replay attacks. Value should be: - /// - Large enough to account for clock skew and network latency - /// - Small enough to limit replay attack window - /// - /// Recommended: 30-60 seconds - #[serde(with = "humantime_serde")] - pub timestamp_tolerance: Duration, - - /// Use mock ecash manager for testing (default: false) - /// - /// When enabled, the LP listener will use a mock ecash verifier that - /// accepts any credential without blockchain verification. This is - /// useful for testing the LP protocol implementation without requiring - /// a full blockchain/contract setup. - /// - /// WARNING: Only use this for local testing! Never enable in production. - pub use_mock_ecash: bool, - - /// Maximum age of in-progress handshakes before cleanup (default: 90s) - /// - /// Handshakes should complete quickly (3-5 packets). This TTL accounts for: - /// - Network latency and retransmits - /// - Slow clients - /// - Clock skew tolerance - /// - /// Stale handshakes are removed by the cleanup task to prevent memory leaks. - #[serde(with = "humantime_serde")] - pub handshake_ttl: Duration, - - /// Maximum age of established sessions before cleanup (default: 24h) - /// - /// Sessions can be long-lived for dVPN tunnels. This TTL should be set - /// high enough to accommodate expected usage patterns: - /// - dVPN sessions: hours to days - /// - Registration: minutes - /// - /// Sessions with no activity for this duration are removed by the cleanup task. - #[serde(with = "humantime_serde")] - pub session_ttl: Duration, - - /// Maximum age of demoted (read-only) sessions before cleanup (default: 60s) - /// - /// After subsession promotion, old sessions enter ReadOnlyTransport state. - /// They only need to stay alive briefly to drain in-flight packets. - /// This shorter TTL prevents memory buildup from frequent rekeying. - #[serde(with = "humantime_serde")] - pub demoted_session_ttl: Duration, - - /// How often to run the state cleanup task (default: 5 minutes) - /// - /// The cleanup task scans for and removes stale handshakes and sessions. - /// Lower values = more frequent cleanup but higher overhead. - /// Higher values = less overhead but slower memory reclamation. - #[serde(with = "humantime_serde")] - pub state_cleanup_interval: Duration, - - /// Maximum concurrent forward connections (default: 1000) - /// - /// Limits simultaneous outbound connections when forwarding LP packets to other gateways - /// during telescope setup. This prevents file descriptor exhaustion under high load. - /// - /// When at capacity, new forward requests return an error, signaling the client - /// to choose a different gateway. - pub max_concurrent_forwards: usize, -} - -impl LpConfig { - pub const DEFAULT_CONTROL_PORT: u16 = 41264; - pub const DEFAULT_DATA_PORT: u16 = 51264; - - pub fn announced_control_port(&self) -> u16 { - self.announce_control_port - .unwrap_or(self.control_bind_address.port()) - } - - pub fn announced_data_port(&self) -> u16 { - self.announce_data_port - .unwrap_or(self.data_bind_address.port()) - } -} - -impl Default for LpConfig { - fn default() -> Self { - LpConfig { - control_bind_address: SocketAddr::new( - IpAddr::V6(Ipv6Addr::UNSPECIFIED), - Self::DEFAULT_CONTROL_PORT, - ), - data_bind_address: SocketAddr::new( - IpAddr::V6(Ipv6Addr::UNSPECIFIED), - Self::DEFAULT_DATA_PORT, - ), - announce_control_port: None, - announce_data_port: None, - debug: Default::default(), - } - } -} - -impl LpDebug { - pub const DEFAULT_MAX_CONNECTIONS: usize = 10000; - - // 30 seconds - balances security vs clock skew tolerance - pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30); - - // 90 seconds - handshakes should complete quickly - pub const DEFAULT_HANDSHAKE_TTL: Duration = Duration::from_secs(90); - - // 24 hours - for long-lived dVPN sessions - pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(86400); - - // 1 minute - enough to drain in-flight packets after subsession promotion - pub const DEFAULT_DEMOTED_SESSION_TTL: Duration = Duration::from_secs(60); - - // 5 minutes - balances memory reclamation with task overhead - pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); - - // Limits concurrent outbound connections to prevent fd exhaustion - pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000; -} - -impl Default for LpDebug { - fn default() -> Self { - LpDebug { - max_connections: Self::DEFAULT_MAX_CONNECTIONS, - timestamp_tolerance: Self::DEFAULT_TIMESTAMP_TOLERANCE, - use_mock_ecash: false, - handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL, - session_ttl: Self::DEFAULT_SESSION_TTL, - demoted_session_ttl: Self::DEFAULT_DEMOTED_SESSION_TTL, - state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL, - max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS, - } - } -} - /// Wrapper for state entries with timestamp tracking for cleanup /// /// This wrapper adds `created_at` and `last_activity` timestamps to state entries, @@ -315,6 +136,7 @@ impl TimestampedState { } /// Get age since creation + #[allow(dead_code)] pub fn age(&self) -> Duration { self.created_at.elapsed() } @@ -335,21 +157,12 @@ impl TimestampedState { /// Shared state for LP connection handlers #[derive(Clone)] pub struct LpHandlerState { - /// Ecash verifier for bandwidth credentials - pub ecash_verifier: Arc, - - /// Storage backend for persistence - pub storage: GatewayStorage, - /// Encapsulates all required key information of a local Lewes Protocol Peer. pub local_lp_peer: LpLocalPeer, /// Metrics collection pub metrics: NymNodeMetrics, - /// Active clients tracking - pub active_clients_store: ActiveClientsStore, - /// Handle registering new wireguard peers pub peer_registrator: Option, @@ -360,6 +173,7 @@ pub struct LpHandlerState { /// /// Used by the LP data handler (UDP:51264) to forward decrypted Sphinx packets /// from LP clients into the mixnet for routing. + #[allow(dead_code)] pub outbound_mix_sender: MixForwardingSender, /// Established sessions keyed by session_id @@ -374,7 +188,7 @@ pub struct LpHandlerState { /// subsession/rekeying support. The state machine handles subsession initiation /// (SubsessionKK1/KK2/Ready) during transport phase, allowing long-lived connections /// to rekey without re-authentication. - pub session_states: Arc>>, + pub session_states: Arc>>, /// Semaphore limiting concurrent forward connections /// @@ -423,16 +237,18 @@ impl LpListener { self.handler_state.lp_config } - pub async fn run(&mut self) -> Result<(), GatewayError> { + pub async fn run(&mut self) -> Result<(), NymNodeError> { let control_bind_address = self.lp_config().control_bind_address; let data_bind_address = self.lp_config().data_bind_address; - let listener = TcpListener::bind(control_bind_address).await.map_err(|e| { - error!("Failed to bind LP listener to {control_bind_address}: {e}",); - GatewayError::ListenerBindFailure { - address: control_bind_address.to_string(), - source: Box::new(e), - } - })?; + let listener = TcpListener::bind(control_bind_address) + .await + .map_err(|source| { + error!("Failed to bind LP listener to {control_bind_address}: {source}",); + NymNodeError::LpBindFailure { + address: control_bind_address, + source, + } + })?; let shutdown_token = self.shutdown.clone_shutdown_token(); @@ -449,7 +265,6 @@ impl LpListener { loop { tokio::select! { biased; - _ = shutdown_token.cancelled() => { trace!("LP listener: received shutdown signal"); break; @@ -457,12 +272,8 @@ impl LpListener { result = listener.accept() => { match result { - Ok((stream, addr)) => { - self.handle_connection(stream, addr); - } - Err(e) => { - warn!("Failed to accept LP connection: {}", e); - } + Ok((stream, addr)) => self.handle_connection(stream, addr), + Err(e) => warn!("Failed to accept LP connection: {e}") } } } @@ -520,7 +331,7 @@ impl LpListener { /// The data handler listens on UDP port 51264 and processes LP-wrapped Sphinx packets /// from registered clients. It decrypts the LP layer and forwards the Sphinx packets /// into the mixnet. - async fn spawn_data_handler(&self) -> Result, GatewayError> { + async fn spawn_data_handler(&self) -> Result, NymNodeError> { // Create data handler let data_handler = data_handler::LpDataHandler::new( self.lp_config().data_bind_address, @@ -555,14 +366,15 @@ impl LpListener { let handshake_ttl = dbg_cfg.handshake_ttl; let session_ttl = dbg_cfg.session_ttl; - let demoted_session_ttl = dbg_cfg.demoted_session_ttl; let interval = dbg_cfg.state_cleanup_interval; let shutdown = self.shutdown.clone_shutdown_token(); let metrics = self.handler_state.metrics.clone(); info!( - "Starting LP state cleanup task (handshake_ttl={}s, session_ttl={}s, demoted_ttl={}s, interval={}s)", - handshake_ttl.as_secs(), session_ttl.as_secs(), demoted_session_ttl.as_secs(), interval.as_secs() + "Starting LP state cleanup task (handshake_ttl={}s, session_ttl={}s, interval={}s)", + handshake_ttl.as_secs(), + session_ttl.as_secs(), + interval.as_secs() ); self.shutdown.try_spawn_named( @@ -580,9 +392,9 @@ impl LpListener { } pub(crate) mod cleanup_task { - use crate::node::lp_listener::{LpDebug, TimestampedState}; + use crate::config::lp::LpDebug; + use crate::node::lp::{LpReceiverIndex, TimestampedState}; use dashmap::DashMap; - use nym_lp::state_machine::LpStateBare; use nym_lp::LpStateMachine; use nym_metrics::inc_by; use nym_node_metrics::NymNodeMetrics; @@ -590,42 +402,29 @@ pub(crate) mod cleanup_task { use tracing::{debug, info}; async fn perform_cleanup( - session_states: &Arc>>, + session_states: &Arc>>, cfg: LpDebug, ) { let session_ttl = cfg.session_ttl; - let demoted_session_ttl = cfg.demoted_session_ttl; let start = std::time::Instant::now(); let mut ss_removed = 0u64; - let mut demoted_removed = 0u64; // Remove stale sessions (based on time since last activity) // Use shorter TTL for demoted (ReadOnlyTransport) sessions session_states.retain(|_, timestamped| { - let is_demoted = timestamped.state.bare_state() == LpStateBare::ReadOnlyTransport; - let ttl = if is_demoted { - demoted_session_ttl - } else { - session_ttl - }; - - if timestamped.since_activity() > ttl { - if is_demoted { - demoted_removed += 1; - } else { - ss_removed += 1; - } + if timestamped.since_activity() > session_ttl { + ss_removed += 1; false } else { true } }); - if ss_removed > 0 || demoted_removed > 0 { + if ss_removed > 0 { let duration = start.elapsed(); info!( - "LP state cleanup: {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)", + "LP state cleanup: {ss_removed} sessions (took {:.3}s)", duration.as_secs_f64() ); @@ -633,9 +432,6 @@ pub(crate) mod cleanup_task { if ss_removed > 0 { inc_by!("lp_states_cleanup_session_removed", ss_removed as i64); } - if demoted_removed > 0 { - inc_by!("lp_states_cleanup_demoted_removed", demoted_removed as i64); - } } } @@ -647,7 +443,7 @@ pub(crate) mod cleanup_task { /// Demoted sessions (ReadOnlyTransport) use shorter TTL since they /// only need to drain in-flight packets after subsession promotion. pub(crate) async fn cleanup_loop( - session_states: Arc>>, + session_states: Arc>>, cfg: LpDebug, shutdown: nym_task::ShutdownToken, _metrics: NymNodeMetrics, diff --git a/gateway/src/node/lp_listener/registration.rs b/nym-node/src/node/lp/registration.rs similarity index 96% rename from gateway/src/node/lp_listener/registration.rs rename to nym-node/src/node/lp/registration.rs index 7f2e7b875c..52b6975c5b 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/nym-node/src/node/lp/registration.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::lp_listener::{LpHandlerState, ReceiverIndex}; +use crate::node::lp::{LpHandlerState, LpReceiverIndex}; use nym_metrics::{add_histogram_obs, inc}; use nym_registration_common::dvpn::{ LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest, @@ -31,7 +31,7 @@ const LP_REGISTRATION_DURATION_BUCKETS: &[f64] = &[ impl LpHandlerState { async fn process_dvpn_initial_registration( &self, - sender: ReceiverIndex, + sender: LpReceiverIndex, request: LpDvpnRegistrationInitialRequest, ) -> LpRegistrationResponse { let Some(registrator) = self.peer_registrator.as_ref() else { @@ -54,7 +54,7 @@ impl LpHandlerState { async fn process_dvpn_registration_finalisation( &self, - sender: ReceiverIndex, + sender: LpReceiverIndex, request: LpDvpnRegistrationFinalisation, ) -> LpRegistrationResponse { let Some(registrator) = self.peer_registrator.as_ref() else { @@ -77,7 +77,7 @@ impl LpHandlerState { async fn process_dvpn_registration( &self, - sender: ReceiverIndex, + sender: LpReceiverIndex, request: Box, ) -> LpRegistrationResponse { // Track dVPN registration attempts @@ -108,7 +108,7 @@ impl LpHandlerState { /// Process an LP registration request pub async fn process_registration( &self, - sender: ReceiverIndex, + sender: LpReceiverIndex, request: LpRegistrationRequest, ) -> LpRegistrationResponse { let registration_start = std::time::Instant::now(); diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 460889d3db..59213d0a3b 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -10,7 +10,9 @@ use crate::error::{EntryGatewayError, NymNodeError, ServiceProvidersError}; use crate::node::description::{load_node_description, save_node_description}; use crate::node::helpers::{ DisplayDetails, get_current_rotation_id, load_ed25519_identity_keypair, load_key, + load_mceliece_keypair, load_mlkem768_keypair, load_x25519_lp_keypair, load_x25519_noise_keypair, store_ed25519_identity_keypair, store_key, store_keypair, + store_mceliece_keypair, store_mlkem768_keypair, store_x25519_lp_keypair, store_x25519_noise_keypair, }; use crate::node::http::api::api_requests; @@ -20,6 +22,7 @@ use crate::node::http::{HttpServerConfig, NymNodeHttpServer, NymNodeRouter}; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::key_rotation::controller::KeyRotationController; use crate::node::key_rotation::manager::SphinxKeyManager; +use crate::node::lp::{LpHandlerState, LpListener}; use crate::node::metrics::aggregator::MetricsAggregator; use crate::node::metrics::console_logger::ConsoleLogger; use crate::node::metrics::handler::client_sessions::GatewaySessionStatsHandler; @@ -41,7 +44,14 @@ use crate::node::shared_network::{ use nym_bin_common::bin_info; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; +use nym_gateway::node::wireguard::PeerRegistrator; +use nym_gateway::node::{GatewayTasksBuilder, UpgradeModeCheckRequestSender}; +use nym_kkt::key_utils::{ + generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519, +}; +use nym_kkt::keys::{DHKeyPair, KEMKeys}; +use nym_lp::Ciphersuite; +use nym_lp::peer::LpLocalPeer; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -50,6 +60,8 @@ use nym_network_requester::{ }; use nym_node_metrics::NymNodeMetrics; use nym_node_metrics::events::MetricEventsSender; +use nym_node_requests::api::SignedData; +use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM, LewesProtocol}; use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_noise::config::{NoiseConfig, NoiseNetworkView}; use nym_noise_keys::VersionedNoiseKeyV1; @@ -62,19 +74,25 @@ use nym_verloc::{self, measurements::VerlocMeasurer}; use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; +use rand09::SeedableRng; +use std::collections::BTreeMap; use std::net::SocketAddr; use std::ops::Deref; use std::path::Path; use std::sync::Arc; -use tokio::sync::mpsc; +use tokio::sync::{Semaphore, mpsc}; use tracing::{debug, info, trace}; use zeroize::Zeroizing; +pub use nym_gateway::node::ActiveClientsStore; +pub use nym_gateway::node::GatewayStorage; + pub mod bonding_information; pub mod description; pub mod helpers; pub(crate) mod http; pub(crate) mod key_rotation; +pub mod lp; pub(crate) mod metrics; pub(crate) mod mixnet; mod nym_apis_client; @@ -84,8 +102,7 @@ mod shared_network; pub struct GatewayTasksData { mnemonic: Arc>, - psq_kem_key: Arc, - client_storage: nym_gateway::node::GatewayStorage, + client_storage: GatewayStorage, stats_storage: nym_gateway::node::PersistentStatsStorage, } @@ -106,12 +123,8 @@ impl GatewayTasksData { Ok(()) } - async fn new( - config: &GatewayTasksConfig, - // this argument is temporary while we still derive KEM x25519 out of identity ed25519 - ed25519_identity: &ed25519::KeyPair, - ) -> Result { - let client_storage = nym_gateway::node::GatewayStorage::init( + async fn new(config: &GatewayTasksConfig) -> Result { + let client_storage = GatewayStorage::init( &config.storage_paths.clients_storage, config.debug.message_retrieval_limit, ) @@ -125,7 +138,6 @@ impl GatewayTasksData { Ok(GatewayTasksData { mnemonic: Arc::new(config.storage_paths.load_mnemonic_from_file()?), - psq_kem_key: Arc::new(ed25519_identity.to_x25519()), client_storage, stats_storage, }) @@ -373,7 +385,7 @@ impl From for nym_wireguard::WireguardData { } } -pub(crate) struct NymNode { +pub struct NymNode { config: Config, accepted_operator_terms_and_conditions: bool, shutdown_manager: ShutdownManager, @@ -396,8 +408,10 @@ pub(crate) struct NymNode { ed25519_identity_keys: Arc, sphinx_key_manager: Option, - // to be used when noise is integrated x25519_noise_keys: Arc, + + psq_kem_keys: KEMKeys, + x25519_lp_keys: Arc, } impl NymNode { @@ -405,12 +419,19 @@ impl NymNode { config: &Config, custom_mnemonic: Option>, ) -> Result<(), NymNodeError> { - debug!("initialising nym-node with id: {}", config.id); + info!("initialising nym-node with id: {}", config.id); let mut rng = OsRng; + let mut rng09 = rand09::rngs::StdRng::from_os_rng(); // global initialisation + info!("generating new node keys (this might take a while)"); let ed25519_identity_keys = ed25519::KeyPair::new(&mut rng); let x25519_noise_keys = x25519::KeyPair::new(&mut rng); + + let x25519_lp_keys = generate_lp_keypair_x25519(&mut rng09); + let mlkem = generate_keypair_mlkem(&mut rng09); + let mceliece = generate_keypair_mceliece(&mut rng09); + let current_rotation_id = get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?; let _ = SphinxKeyManager::initialise_new( @@ -432,6 +453,18 @@ impl NymNode { &config.storage_paths.keys.x25519_noise_storage_paths(), )?; + trace!("attempting to x25519 lp keypair"); + store_x25519_lp_keypair( + &x25519_lp_keys, + &config.storage_paths.keys.x25519_lp_key_paths(), + )?; + + trace!("attempting to mlkem768 keypair"); + store_mlkem768_keypair(&mlkem, &config.storage_paths.keys.mlkem768_key_paths())?; + + trace!("attempting to mceliece keypair"); + store_mceliece_keypair(&mceliece, &config.storage_paths.keys.mceliece_key_paths())?; + trace!("creating description file"); save_node_description( &config.storage_paths.description, @@ -454,6 +487,30 @@ impl NymNode { config.save() } + pub async fn build_lp_listener( + &mut self, + peer_registrator: Option, + mix_packet_sender: MixForwardingSender, + ) -> Result { + let handler_state = LpHandlerState { + local_lp_peer: LpLocalPeer::new(Ciphersuite::default(), self.x25519_lp_keys.clone()) + .with_kem_keys(self.psq_kem_keys.clone()), + metrics: self.metrics.clone(), + peer_registrator, + lp_config: self.config.lp, + outbound_mix_sender: mix_packet_sender, + session_states: Arc::new(dashmap::DashMap::new()), + forward_semaphore: Arc::new(Semaphore::new( + self.config.lp.debug.max_concurrent_forwards, + )), + }; + + Ok(LpListener::new( + handler_state, + self.shutdown_manager.shutdown_tracker().clone(), + )) + } + pub(crate) async fn new(config: Config) -> Result { let wireguard_data = WireguardData::new(&config.wireguard)?; let current_rotation_id = @@ -462,8 +519,12 @@ impl NymNode { let ed25519_identity_keys = load_ed25519_identity_keypair( &config.storage_paths.keys.ed25519_identity_storage_paths(), )?; - let entry_gateway = - GatewayTasksData::new(&config.gateway_tasks, &ed25519_identity_keys).await?; + let entry_gateway = GatewayTasksData::new(&config.gateway_tasks).await?; + let x25519_lp_keys = + load_x25519_lp_keypair(&config.storage_paths.keys.x25519_lp_key_paths())?; + let mlkem = load_mlkem768_keypair(&config.storage_paths.keys.mlkem768_key_paths())?; + let mceliece = load_mceliece_keypair(&config.storage_paths.keys.mceliece_key_paths())?; + let psq_kem_keys = KEMKeys::new(mceliece, mlkem); Ok(NymNode { ed25519_identity_keys: Arc::new(ed25519_identity_keys), @@ -475,6 +536,7 @@ impl NymNode { x25519_noise_keys: Arc::new(load_x25519_noise_keypair( &config.storage_paths.keys.x25519_noise_storage_paths(), )?), + psq_kem_keys, description: load_node_description(&config.storage_paths.description)?, metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), @@ -488,6 +550,7 @@ impl NymNode { accepted_operator_terms_and_conditions: false, shutdown_manager: ShutdownManager::build_new_default() .map_err(|source| NymNodeError::ShutdownSignalFailure { source })?, + x25519_lp_keys: Arc::new(x25519_lp_keys), }) } @@ -640,15 +703,14 @@ impl NymNode { let mut gateway_tasks_builder = GatewayTasksBuilder::new( config.gateway, self.ed25519_identity_keys.clone(), - self.x25519_noise_keys.clone(), - self.entry_gateway.psq_kem_key.clone(), self.entry_gateway.client_storage.clone(), - mix_packet_sender, + mix_packet_sender.clone(), metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), Self::user_agent(), self.upgrade_mode_state.clone(), + self.config.lp.debug.use_mock_ecash, self.shutdown_tracker().clone(), ); @@ -706,16 +768,13 @@ impl NymNode { // Start LP listener if enabled info!( "starting the LP listener on {} (data handler on: {})", - self.config.gateway_tasks.lp.control_bind_address, - self.config.gateway_tasks.lp.data_bind_address, + self.config.lp.control_bind_address, self.config.lp.data_bind_address, ); - let lp_listener = gateway_tasks_builder - .build_lp_listener(wg_peer_registrator.clone(), active_clients_store.clone()) + let mut lp_listener = self + .build_lp_listener(wg_peer_registrator.clone(), mix_packet_sender) .await?; - // make sure lp listener can be built, but do not start it - let _ = lp_listener; - // self.shutdown_tracker() - // .try_spawn_named(async move { lp_listener.run().await }, "LpListener"); + self.shutdown_tracker() + .try_spawn_named(async move { lp_listener.run().await }, "LpListener"); } else { info!("node not running in entry mode: the websocket and LP will remain closed"); } @@ -791,40 +850,24 @@ impl NymNode { Ok(()) } - // - // fn compute_kem_key_hashes(&self) -> HashMap> { - // let kem = LPKEM::X25519; - // - // let kem_key_bytes = self.entry_gateway.psq_kem_key.public_key().as_bytes(); - // - // // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` - // let digests = produce_key_digests(kem_key_bytes.as_ref()) - // .into_iter() - // .map(|(f, digest)| (f.into(), hex::encode(&digest))) - // .collect(); - // - // let mut hashes = HashMap::new(); - // hashes.insert(kem, digests); - // hashes - // } - // - // fn compute_signing_key_hashes( - // &self, - // ) -> HashMap> { - // let scheme = LPSignatureScheme::Ed25519; - // - // let kem_key_bytes = self.ed25519_identity_keys.public_key().as_bytes(); - // - // // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` - // let digests = produce_key_digests(kem_key_bytes.as_ref()) - // .into_iter() - // .map(|(f, digest)| (f.into(), hex::encode(&digest))) - // .collect(); - // - // let mut hashes = HashMap::new(); - // hashes.insert(scheme, digests); - // hashes - // } + + fn compute_kem_key_hashes(&self) -> BTreeMap> { + let digests = self.psq_kem_keys.encapsulation_keys_digests(); + + // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` + digests + .into_iter() + .map(|(kem, kem_digests)| { + ( + kem.into(), + kem_digests + .into_iter() + .map(|(f, digest)| (f.into(), hex::encode(&digest))) + .collect(), + ) + }) + .collect() + } pub(crate) async fn build_http_server(&self) -> Result { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { @@ -899,7 +942,21 @@ impl NymNode { policy: None, }; - let mut config = HttpServerConfig::new() + let lewes_protocol = LewesProtocol { + enabled: self.modes().entry, + control_port: self.config.lp.announced_control_port(), + data_port: self.config.lp.announced_data_port(), + x25519: self.x25519_lp_keys.pk, + kem_keys: self.compute_kem_key_hashes(), + }; + + // SAFETY: the only way for this call to fail is if serialisation of LewesProtocol fails. + // however, that conversion is stable and infallible + #[allow(clippy::unwrap_used)] + let signed_lewes_protocol = + SignedData::new(lewes_protocol, self.ed25519_identity_keys.private_key()).unwrap(); + + let mut config = HttpServerConfig::new(signed_lewes_protocol) .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) .with_mixnode_details(mixnode_details) .with_gateway_details(gateway_details) @@ -1345,7 +1402,7 @@ impl NymNode { Ok(self.shutdown_manager) } - pub(crate) async fn run(mut self) -> Result<(), NymNodeError> { + pub async fn run(mut self) -> Result<(), NymNodeError> { let mut shutdown_signals = self.shutdown_manager.detach_shutdown_signals(); // listen for shutdown signal in case we received it when attempting to spawn all the tasks diff --git a/nym-node/src/wireguard/mod.rs b/nym-node/src/wireguard/mod.rs index 03d1b32e4b..46db2413ef 100644 --- a/nym-node/src/wireguard/mod.rs +++ b/nym-node/src/wireguard/mod.rs @@ -5,3 +5,6 @@ // but let's start putting everything in here pub mod error; + +pub use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator}; +pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData}; diff --git a/nym-outfox/Cargo.toml b/nym-outfox/Cargo.toml index 8d7855bc48..d3ea59291d 100644 --- a/nym-outfox/Cargo.toml +++ b/nym-outfox/Cargo.toml @@ -10,18 +10,13 @@ repository = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rayon = { workspace = true } blake3 = { workspace = true } zeroize = { workspace = true } chacha20 = { workspace = true, features = ["std"] } x25519-dalek = { workspace = true } chacha20poly1305 = { workspace = true } -getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } sphinx-packet = { workspace = true } -rand = { workspace = true } -log = { workspace = true } [dev-dependencies] -criterion = { workspace = true } fastrand = { workspace = true } diff --git a/nym-registration-client/Cargo.toml b/nym-registration-client/Cargo.toml index 3209fc7e47..a1c3aa335a 100644 --- a/nym-registration-client/Cargo.toml +++ b/nym-registration-client/Cargo.toml @@ -14,26 +14,28 @@ publish = false workspace = true [dependencies] +bincode.workspace = true bytes.workspace = true futures.workspace = true -rand.workspace = true +rand09.workspace = true thiserror.workspace = true tokio.workspace = true tokio-util.workspace = true tracing.workspace = true typed-builder.workspace = true -url.workspace = true - nym-authenticator-client = { workspace = true } nym-bandwidth-controller = { workspace = true } nym-credential-storage = { workspace = true } nym-credentials-interface = { workspace = true } -nym-crypto = { workspace = true } +nym-crypto = { workspace = true, features = ["asymmetric", "libcrux_x25519"] } nym-ip-packet-client = { workspace = true } nym-lp = { path = "../common/nym-lp" } -nym-lp-transport = { path = "../common/nym-lp-transport" } nym-registration-common = { workspace = true } nym-sdk = { workspace = true } nym-validator-client = { workspace = true } nym-wireguard-types = { path = "../common/wireguard-types" } + +[dev-dependencies] +nym-kkt.workspace = true +nym-test-utils.workspace = true \ No newline at end of file diff --git a/nym-registration-client/src/clients/lp.rs b/nym-registration-client/src/clients/lp.rs index 3f3ce0abbc..3b6131db76 100644 --- a/nym-registration-client/src/clients/lp.rs +++ b/nym-registration-client/src/clients/lp.rs @@ -13,10 +13,10 @@ use crate::types::{LpRegistrationResult, RegistrationResult}; use nym_bandwidth_controller::BandwidthTicketProvider; use nym_credentials_interface::TicketType; -use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use nym_crypto::asymmetric::ed25519; -use rand::rngs::OsRng; +use nym_lp::peer::DHKeyPair; +use rand09::{CryptoRng, RngCore}; use std::sync::Arc; use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; @@ -54,19 +54,22 @@ impl LpBasedRegistrationClient { let entry_lp_protocol = entry_lp_data.lp_protocol_version; let exit_lp_protocol = exit_lp_data.lp_protocol_version; + let entry_ciphersuite = entry_lp_data.ciphersuite; + let exit_ciphersuite = exit_lp_data.ciphersuite; + let entry_address = entry_lp_data.address; let exit_address = exit_lp_data.address; tracing::debug!("Entry gateway LP address: {entry_address}"); tracing::debug!("Exit gateway LP address: {exit_address}"); - // Generate fresh Ed25519 keypairs for LP registration - // These are ephemeral and used only for the LP handshake protocol - let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); - let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); + // Generate fresh x25519 keypairs for LP registration + // TODO: persist them for the duration of the sessions + let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng())); + let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng())); - let entry_peer = to_lp_remote_peer(self.config.entry.node.identity, entry_lp_data); - let exit_peer = to_lp_remote_peer(self.config.exit.node.identity, exit_lp_data); + let entry_peer = to_lp_remote_peer(entry_lp_data); + let exit_peer = to_lp_remote_peer(exit_lp_data); // STEP 1: Establish outer session with entry gateway // This creates the LP session that will be used to forward packets to exit. @@ -76,6 +79,7 @@ impl LpBasedRegistrationClient { entry_lp_keypair.clone(), entry_peer, entry_address, + entry_ciphersuite, entry_lp_protocol, self.config.lp_registration_config, ); @@ -94,8 +98,13 @@ impl LpBasedRegistrationClient { // STEP 2: Use nested session to register with exit gateway via forwarding // This hides the client's IP address from the exit gateway tracing::info!("Registering with exit gateway via entry forwarding"); - let mut nested_session = - NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_protocol); + let mut nested_session = NestedLpSession::new( + exit_address, + exit_lp_keypair, + exit_peer, + exit_ciphersuite, + exit_lp_protocol, + ); // Perform handshake and registration with exit gateway (all via entry forwarding) let exit_gateway_data = nested_session @@ -149,7 +158,7 @@ impl LpBasedRegistrationClient { } async fn register_wg(self) -> Result { - let mut rng = rand::rngs::OsRng; + let mut rng = rand09::rng(); self.register_wg_with_rng(&mut rng).await } diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index 25f3265878..81d6c5f67d 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -5,29 +5,26 @@ use super::config::LpRegistrationConfig; use super::error::{LpClientError, Result}; -use crate::lp_client::helpers::{ - LpDataDeliverExt, LpDataSendExt, convert_forward_data, try_convert_forward_response, -}; +use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt}; use crate::lp_client::nested_session::connection::NestedConnection; use crate::lp_client::state_machine_helpers::{extract_forwarded_response, prepare_send_packet}; -use bytes::BytesMut; use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND}; use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; -use nym_lp::message::ForwardPacketData; -use nym_lp::packet::version; -use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; -use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine}; -use nym_lp::{LpPacket, LpSession}; -use nym_lp_transport::traits::LpTransport; +use nym_lp::LpSession; +use nym_lp::peer::{DHKeyPair, LpLocalPeer, LpRemotePeer}; +use nym_lp::peer_config::LpReceiverIndex; +use nym_lp::state_machine::LpStateMachine; +use nym_lp::transport::traits::LpTransportChannel; +use nym_lp::transport::{LpHandshakeChannel, LpTransportError}; +use nym_lp::{Ciphersuite, packet::EncryptedLpPacket, packet::version}; use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent; use nym_registration_common::{ LpRegistrationRequest, LpRegistrationResponse, WireguardConfiguration, WireguardRegistrationData, }; use nym_wireguard_types::PeerPublicKey; -use rand::{CryptoRng, RngCore}; +use rand09::{CryptoRng, Rng, RngCore}; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -62,7 +59,7 @@ pub struct LpRegistrationClient { gateway_supported_lp_protocol_version: u8, /// LP state machine for managing connection lifecycle. - /// Created during handshake initiation. Persists across packet-per-connection calls. + /// Created during handshake initiation. state_machine: Option, /// Configuration for timeouts and TCP parameters. @@ -75,23 +72,25 @@ pub struct LpRegistrationClient { impl LpRegistrationClient where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { /// Creates a new LP registration client. /// /// # Arguments - /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair + /// * `local_x25519_keypair` - Client's x25519 keypair /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol /// * `gateway_lp_address` - Gateway's LP listener socket address + /// * `ciphersuite` - the set of cryptographic protocols to use when negotiating the session with the node /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version /// * `config` - Configuration for timeouts and TCP parameters (use `LpConfig::default()`) /// /// # Note /// This creates the client. Call `perform_handshake()` to establish the LP session. pub fn new( - local_ed25519_keypair: Arc, + local_x25519_keypair: Arc, gateway_lp_peer: LpRemotePeer, gateway_lp_address: SocketAddr, + ciphersuite: Ciphersuite, gateway_supported_lp_protocol_version: u8, config: LpRegistrationConfig, ) -> Self { @@ -105,8 +104,7 @@ where gateway_supported_lp_protocol_version }; - let local_x25519_keypair = local_ed25519_keypair.to_x25519(); - let lp_local_peer = LpLocalPeer::new(local_ed25519_keypair, Arc::new(local_x25519_keypair)); + let lp_local_peer = LpLocalPeer::new(ciphersuite, local_x25519_keypair); Self { lp_local_peer, gateway_lp_peer, @@ -119,13 +117,8 @@ where } /// Attempt to use this `LpRegistrationClient` as transport for `NestedSession` - pub fn as_nested_connection( - &mut self, - exit_identity: ed25519::PublicKey, - exit_address: SocketAddr, - ) -> NestedConnection<'_, S> { + pub fn as_nested_connection(&mut self, exit_address: SocketAddr) -> NestedConnection<'_, S> { NestedConnection { - exit_identity, exit_address, outer_client: self, } @@ -134,41 +127,46 @@ where /// Creates a new LP registration client with default configuration. /// /// # Arguments - /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair + /// * `local_x25519_keypair` - Client's x25519 keypair /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol /// * `gateway_lp_address` - Gateway's LP listener socket address + /// * `ciphersuite` - the set of cryptographic protocols to use when negotiating the session with the node /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version /// /// Uses default config (LpConfig::default()) with sane timeout and TCP parameters. /// PSK is derived automatically during handshake inside the state machine. /// For custom config, use `new()` directly. pub fn new_with_default_config( - local_ed25519_keypair: Arc, + local_x25519_keypair: Arc, gateway_lp_peer: LpRemotePeer, gateway_lp_address: SocketAddr, + ciphersuite: Ciphersuite, gateway_supported_lp_protocol_version: u8, ) -> Self { Self::new( - local_ed25519_keypair, + local_x25519_keypair, gateway_lp_peer, gateway_lp_address, + ciphersuite, gateway_supported_lp_protocol_version, LpRegistrationConfig::default(), ) } + pub(crate) fn state_machine(&self) -> Result<&LpStateMachine> { + self.state_machine + .as_ref() + .ok_or(LpClientError::IncompleteHandshake) + } + pub(crate) fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> { - self.state_machine.as_mut().ok_or_else(|| { - LpClientError::transport( - "State machine not available - has the handshake been completed?", - ) - }) + self.state_machine + .as_mut() + .ok_or(LpClientError::IncompleteHandshake) } fn stream_mut(&mut self) -> Result<&mut S> { - self.stream - .as_mut() - .ok_or_else(|| LpClientError::transport("Cannot send: not connected")) + self.stream.as_mut().ok_or(LpClientError::NotConnected) } /// Returns whether the client has completed the handshake and is ready for registration. @@ -216,10 +214,10 @@ where .await .map_err(|_| LpClientError::TcpConnection { address: self.gateway_lp_address.to_string(), - source: std::io::Error::new( - std::io::ErrorKind::TimedOut, - format!("Connection timeout after {:?}", self.config.connect_timeout), - ), + source: LpTransportError::ConnectionFailure(format!( + "Connection timeout after {:?}", + self.config.connect_timeout + )), })? .map_err(|source| LpClientError::TcpConnection { address: self.gateway_lp_address.to_string(), @@ -253,7 +251,10 @@ where /// /// # Errors /// Returns an error if not connected or if send or receive fails. - async fn send_and_receive_packet(&mut self, packet: &LpPacket) -> Result { + async fn send_and_receive_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result { self.try_send_packet(packet).await?; self.try_receive_packet().await } @@ -262,22 +263,21 @@ where /// and attempt to immediately read a response /// within the provided timeout. /// - /// Both packets are going to be optionally encrypted/decrypted based on the availability of keys - /// within the internal `LpStateMachine` + /// Both packets are going to be encrypted /// /// # Arguments - /// * `packet` - The LP packet to send + /// * `packet` - The encrypted LP packet to send /// /// # Errors /// Returns an error if not connected, the timeout has been reached, or if send or receive fails. - async fn send_and_receive_packet_with_timeout( + async fn send_and_receive_data_packet_with_timeout( &mut self, - packet: &LpPacket, + packet: &EncryptedLpPacket, timeout: Duration, - ) -> Result { + ) -> Result { tokio::time::timeout(timeout, self.send_and_receive_packet(packet)) .await - .map_err(|_| LpClientError::ResponseReceiveTimeout { timeout })? + .map_err(|_| LpClientError::ConnectionTimeout)? } /// Sends an LP packet on the persistent stream. @@ -287,42 +287,25 @@ where /// /// # Errors /// Returns an error if not connected or if send fails. - pub(crate) async fn try_send_packet(&mut self, packet: &LpPacket) -> Result<()> { + pub(crate) async fn try_send_packet(&mut self, packet: &EncryptedLpPacket) -> Result<()> { // can't use getters due to borrow checker (i.e. requiring full borrows for function calls) - let stream = self - .stream - .as_mut() - .ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?; - - let state_machine = self.state_machine.as_ref().ok_or_else(|| { - LpClientError::transport( - "State machine not available - has the handshake been completed?", - ) - })?; - - let outer_key = state_machine.session()?.outer_aead_key(); - Self::send_packet_with_key(stream, packet, outer_key).await + self.stream_mut()? + .send_length_prefixed_transport_packet(packet) + .await?; + Ok(()) } /// Receives an LP packet from the persistent stream. /// /// # Errors /// Returns an error if not connected or if receive fails. - pub(crate) async fn try_receive_packet(&mut self) -> Result { - let stream = self - .stream - .as_mut() - .ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?; + pub(crate) async fn try_receive_packet(&mut self) -> Result { + let encrypted_packet = self + .stream_mut()? + .receive_length_prefixed_transport_packet() + .await?; - let state_machine = self.state_machine.as_ref().ok_or_else(|| { - LpClientError::transport( - "State machine not available - has the handshake been completed?", - ) - })?; - - let outer_key = state_machine.session()?.outer_aead_key(); - - Self::receive_packet_with_key(stream, outer_key).await + Ok(encrypted_packet) } /// Closes the persistent connection. @@ -372,7 +355,7 @@ where /// /// The connection remains open after handshake for registration/forwarding. pub async fn perform_handshake(&mut self) -> Result<()> { - // Apply handshake timeout (nym-102) + // Apply handshake timeout let result = tokio::time::timeout( self.config.handshake_timeout, self.perform_handshake_inner(), @@ -388,10 +371,7 @@ where } Err(_) => { self.close(); - Err(LpClientError::Transport(format!( - "Handshake timeout after {:?}", - self.config.handshake_timeout - ))) + Err(LpClientError::HandshakeTimeout) } } } @@ -413,15 +393,13 @@ where let connection = self.stream_mut()?; // TODO: - let ciphersuite = LpSession::default_ciphersuite(); - let session = LpSession::complete_as_initiator( + let session = LpSession::psq_handshake_initiator( connection, - ciphersuite, local_peer, remote_peer, protocol_version, ) - .complete_as_initiator() + .complete_handshake() .await?; // Store the state machine (with established session) for later use @@ -429,57 +407,6 @@ where Ok(()) } - /// Sends an LP packet over a TCP stream with length-prefixed framing. - /// - /// Format: 4-byte big-endian u32 length + packet bytes - /// - /// # Arguments - /// * `stream` - TCP stream to send on - /// * `packet` - The LP packet to send - /// * `outer_key` - Optional outer AEAD key for encryption - /// - /// # Errors - /// Returns an error if serialization or network transmission fails. - async fn send_packet_with_key( - stream: &mut S, - packet: &LpPacket, - outer_key: &OuterAeadKey, - ) -> Result<()> { - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(packet, &mut packet_buf, Some(outer_key)) - .map_err(|e| LpClientError::Transport(format!("Failed to serialize packet: {e}")))?; - - stream - .send_serialised_packet(&packet_buf) - .await - .map_err(|err| LpClientError::Transport(err.to_string())) - } - - /// Receives an LP packet from a TCP stream with length-prefixed framing. - /// - /// Format: 4-byte big-endian u32 length + packet bytes - /// - /// # Arguments - /// * `stream` - TCP stream to receive from - /// * `outer_key` - Optional outer AEAD key for decryption - /// - /// # Errors - /// Returns an error if: - /// - Network read fails - /// - Packet size exceeds maximum (64KB) - /// - Packet parsing/decryption fails - async fn receive_packet_with_key(stream: &mut S, outer_key: &OuterAeadKey) -> Result { - let packet_buf = stream - .receive_raw_packet() - .await - .map_err(|err| LpClientError::transport(err.to_string()))?; - - let packet = parse_lp_packet(&packet_buf, Some(outer_key)) - .map_err(|e| LpClientError::Transport(format!("Failed to parse packet: {e}")))?; - - Ok(packet) - } - /// This is an internal method only meant to be called by `Self::register_dvpn` if the gateway /// responds with a credential request. This is expected in every initial interaction with a particular gateway. /// @@ -539,7 +466,10 @@ where // 5. Send initial request and receive response on persistent connection with timeout let response_packet = self - .send_and_receive_packet_with_timeout(&request_packet, self.config.registration_timeout) + .send_and_receive_data_packet_with_timeout( + &request_packet, + self.config.registration_timeout, + ) .await?; // 6. Decrypt via state machine (re-borrow) @@ -623,7 +553,10 @@ where // 4. Send initial request and receive response on persistent connection with timeout let response_packet = self - .send_and_receive_packet_with_timeout(&request_packet, self.config.registration_timeout) + .send_and_receive_data_packet_with_timeout( + &request_packet, + self.config.registration_timeout, + ) .await?; // 5. Decrypt via state machine (re-borrow) @@ -721,7 +654,7 @@ where if attempt > 0 { // Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped) let base_delay_ms = 100u64 * (1 << attempt.min(4)); - let jitter_ms = rand::random::() % (base_delay_ms / 4 + 1); + let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1)); let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms); tracing::info!("Retrying registration (attempt {attempt_display}) after {delay:?}"); tokio::time::sleep(delay).await; @@ -764,174 +697,9 @@ where } } - Err(last_error - .unwrap_or_else(|| LpClientError::transport("Registration failed after all retries"))) - } - - /// Sends a ForwardPacket message to the entry gateway for forwarding to the exit gateway. - /// - /// This method constructs a ForwardPacket containing the target gateway's identity, - /// address, and the inner LP packet bytes, encrypts it through the outer session - /// (client-entry), and receives the response from the exit gateway via the entry gateway. - /// - /// Uses the persistent TCP connection established during handshake. - /// Multiple forward packets can be sent on the same connection. - /// - /// # Arguments - /// * `forward_data` - encapsulated target gateway's ed25519 identity, socket address and serialised inner LP packet - /// - /// # Returns - /// * `Ok(Vec)` - Decrypted response bytes from the exit gateway - /// - /// # Errors - /// Returns an error if: - /// - Handshake has not been completed - /// - Serialization fails - /// - Encryption or network transmission fails - /// - Response decryption fails - /// - /// # Example Flow - /// ```ignore - /// // Construct inner packet for exit gateway (ClientHello, handshake, etc.) - /// let inner_packet = LpPacket::new(...); - /// let inner_bytes = serialize_lp_packet(&inner_packet, &mut BytesMut::new())?; - /// - /// // Forward through entry gateway - /// let response_bytes = client.send_forward_packet( - /// exit_identity, - /// "2.2.2.2:41264".to_string(), - /// inner_bytes.to_vec(), - /// ).await?; - /// ``` - pub async fn send_forward_packet_with_response( - &mut self, - forward_data: ForwardPacketData, - ) -> Result> { - let target_address = forward_data.target_lp_address; - - tracing::debug!( - "Sending ForwardPacket to {target_address} ({} inner bytes, persistent connection)", - forward_data.inner_packet_bytes.len() - ); - - // 1. Serialize the ForwardPacketData - let input = convert_forward_data(forward_data)?; - - // 2. Encrypt and prepare packet via state machine - let state_machine = self.state_machine_mut()?; - - let action = state_machine - .process_input(input) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| { - LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}")) - })?; - - let forward_packet = match action { - LpAction::SendPacket(packet) => packet, - other => { - return Err(LpClientError::Transport(format!( - "Unexpected action when sending ForwardPacket: {:?}", - other - ))); - } - }; - - // 3. Send and receive on persistent connection with timeout - let response_packet = tokio::time::timeout(self.config.forward_timeout, async { - self.try_send_packet(&forward_packet).await?; - self.try_receive_packet().await - }) - .await - .map_err(|_| { - LpClientError::Transport(format!( - "Forward packet timeout after {:?}", - self.config.forward_timeout - )) - })??; - tracing::trace!("Received response packet from entry gateway"); - - // 4. Decrypt via state machine (re-borrow) - let state_machine = self - .state_machine - .as_mut() - .ok_or_else(|| LpClientError::transport("State machine disappeared unexpectedly"))?; - let action = state_machine - .process_input(LpInput::ReceivePacket(response_packet)) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| { - LpClientError::Transport(format!("Failed to decrypt forward response: {e}")) - })?; - - // 5. Extract decrypted response data - let response_data = try_convert_forward_response(action)?; - - tracing::debug!( - "Successfully received forward response from {target_address} ({} bytes)", - response_data.len() - ); - - Ok(response_data) - } - - /// Wrap data in an LP packet for UDP transmission to the data plane (port 51264). - /// - /// This method encrypts the provided data using the established LP session - /// and returns serialized LP packet bytes ready to send over UDP. - /// - /// # Prerequisites - /// - Handshake must be completed (`perform_handshake()`) - /// - /// # Arguments - /// * `data` - Raw application data to wrap (e.g., Sphinx packet bytes) - /// - /// # Returns - /// * `Ok(Vec)` - Serialized LP packet bytes (outer header + encrypted payload) - /// - /// # Wire Format - /// The returned bytes are in LP wire format: - /// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext - /// - Encrypted payload: proto(1) + reserved(3) + msg_type(4) + content + tag(16) - /// - /// # Usage - /// After LP handshake, wrap Sphinx packets before sending to gateway's LP data port: - /// ```ignore - /// client.perform_handshake().await?; - /// let sphinx_bytes = build_sphinx_packet(...); - /// let lp_bytes = client.wrap_data(&sphinx_bytes)?; - /// socket.send_to(&lp_bytes, gateway_lp_data_address).await?; // UDP:51264 - /// ``` - pub fn wrap_data(&mut self, data: &[u8]) -> Result> { - let state_machine = self - .state_machine - .as_mut() - .ok_or_else(|| LpClientError::transport("Cannot wrap data: handshake not completed"))?; - - // Process data through state machine to create LP packet - let action = state_machine - .process_input(LpInput::SendData(LpData::new_opaque(data.to_vec()))) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| LpClientError::Transport(format!("Failed to encrypt data: {e}")))?; - - let packet = match action { - LpAction::SendPacket(packet) => packet, - other => { - return Err(LpClientError::Transport(format!( - "Unexpected action when wrapping data: {:?}", - other - ))); - } - }; - - // Get outer AEAD key for encryption - let outer_key = state_machine.session()?.outer_aead_key(); - - // Serialize the packet with outer AEAD encryption - let mut buf = BytesMut::new(); - serialize_lp_packet(&packet, &mut buf, Some(outer_key)) - .map_err(|e| LpClientError::Transport(format!("Failed to serialize LP packet: {e}")))?; - - Ok(buf.to_vec()) + Err(last_error.unwrap_or(LpClientError::RegistrationFailure { + message: "Registration failed after all retries".to_string(), + })) } /// Get the LP session ID (receiver_idx) for this client. @@ -940,41 +708,39 @@ where /// the gateway to look up the session for decryption. /// /// # Returns - /// * `Ok(u32)` - The session ID + /// * `Ok(LpReceiverIndex)` - The session ID /// /// # Errors /// Returns an error if handshake has not been completed. - pub fn session_id(&self) -> Result { - let state_machine = self.state_machine.as_ref().ok_or_else(|| { - LpClientError::transport("Cannot get session ID: handshake not completed") - })?; - - state_machine + pub fn session_id(&self) -> Result { + self.state_machine()? .session() - .map(|s| s.id()) - .map_err(|e| LpClientError::Transport(format!("Failed to get session: {e}"))) + .map(|s| s.receiver_index()) + .map_err(Into::into) } } #[cfg(test)] mod tests { use super::*; + use nym_kkt::key_utils::generate_lp_keypair_x25519; use nym_lp::packet::version; + use nym_test_utils::helpers::deterministic_rng_09; #[test] fn test_client_creation() { - let mut rng = rand::thread_rng(); - let keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); - let gateway_ed_keys = ed25519::KeyPair::new(&mut rng); - let gateway_x_keys = gateway_ed_keys.to_x25519(); - let gateway_peer = - LpRemotePeer::new(*gateway_ed_keys.public_key(), *gateway_x_keys.public_key()); + let mut rng09 = deterministic_rng_09(); + let keypair = Arc::new(generate_lp_keypair_x25519(&mut rng09)); + + let gateway_x_keys = generate_lp_keypair_x25519(&mut rng09); + let gateway_peer = LpRemotePeer::from(gateway_x_keys.pk); let address = "127.0.0.1:41264".parse().unwrap(); let client = LpRegistrationClient::::new_with_default_config( keypair, gateway_peer, address, + Ciphersuite::default(), version::CURRENT, ); diff --git a/nym-registration-client/src/lp_client/error.rs b/nym-registration-client/src/lp_client/error.rs index 32443f4e01..681d34d01e 100644 --- a/nym-registration-client/src/lp_client/error.rs +++ b/nym-registration-client/src/lp_client/error.rs @@ -4,9 +4,9 @@ //! Error types for LP (Lewes Protocol) client operations. use nym_lp::LpError; -use nym_registration_common::BincodeError; -use std::io; -use std::time::Duration; +use nym_lp::packet::MalformedLpPacketError; +use nym_lp::state_machine::{LpAction, LpDataKind}; +use nym_lp::transport::LpTransportError; use thiserror::Error; /// Errors that can occur during LP client operations. @@ -17,12 +17,41 @@ pub enum LpClientError { TcpConnection { address: String, #[source] - source: io::Error, + source: LpTransportError, }, - /// Failed during LP handshake - #[error("LP handshake failed: {0}")] - HandshakeFailed(#[from] LpError), + #[error(transparent)] + LpTransportError(#[from] LpTransportError), + + #[error("the client has not opened a connection to the exit")] + NotConnected, + + #[error("the KKT/PSQ handshake does not appear to have been completed")] + IncompleteHandshake, + + #[error(transparent)] + LpProtocolError(#[from] LpError), + + #[error("no action has been emitted from the LP State Machine")] + UnexpectedStateMachineHalt, + + #[error("the state machine instructed an unexpected action: {action:?}")] + UnexpectedStateMachineAction { action: LpAction }, + + #[error("received registration data was malformed: {source}")] + MalformedRegistrationData { source: bincode::Error }, + + #[error("received a malformed packet: {0}")] + MalformedLpPacket(#[from] MalformedLpPacketError), + + #[error("received payload type of an unexpected type: {typ:?}")] + UnexpectedLpPayload { typ: LpDataKind }, + + #[error("timed out while attempting to finish the KKT/PSQ handshake")] + HandshakeTimeout, + + #[error("timed out while attempting to send to/receive from the connection")] + ConnectionTimeout, /// Failed to send registration request #[error("Failed to send registration request: {0}")] @@ -36,43 +65,20 @@ pub enum LpClientError { #[error("Gateway rejected registration: {reason}")] RegistrationRejected { reason: String }, - /// Failed to receive response within specified deadline - #[error("Failed to receive response within the set timeout: {timeout:?}")] - ResponseReceiveTimeout { timeout: Duration }, - - /// LP transport error - #[error("LP transport error: {0}")] - Transport(String), - - /// Invalid LP address format - #[error("Invalid LP address '{address}': {reason}")] - InvalidAddress { address: String, reason: String }, - - /// Serialization/deserialization error - #[error("Serialization error: {0}")] - Serialization(#[from] BincodeError), - - /// Connection closed unexpectedly - #[error("Connection closed unexpectedly")] - ConnectionClosed, - - /// Timeout waiting for response - #[error("Timeout waiting for {operation}")] - Timeout { operation: String }, + #[error("could not complete the registration: {message}")] + RegistrationFailure { message: String }, #[error("received an unexpected response: {message}")] UnexpectedResponse { message: String }, - /// Another uncategorized error + #[error("currently McEliece keys are not supported for nested registration")] + UnsupportedNestedMcEliece, + #[error("{0}")] Other(String), } impl LpClientError { - pub fn transport(message: impl Into) -> LpClientError { - LpClientError::Transport(message.into()) - } - pub fn unexpected_response(message: impl Into) -> LpClientError { LpClientError::UnexpectedResponse { message: message.into(), diff --git a/nym-registration-client/src/lp_client/helpers.rs b/nym-registration-client/src/lp_client/helpers.rs index 3eb53706fd..9de8cb5a19 100644 --- a/nym-registration-client/src/lp_client/helpers.rs +++ b/nym-registration-client/src/lp_client/helpers.rs @@ -4,8 +4,7 @@ #![allow(dead_code)] use crate::LpClientError; -use nym_crypto::asymmetric::ed25519; -use nym_lp::message::ForwardPacketData; +use nym_lp::packet::ForwardPacketData; use nym_lp::peer::LpRemotePeer; use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput}; use nym_registration_common::{ @@ -38,15 +37,11 @@ impl LpDataSendExt for LpRegistrationRequest { impl LpDataDeliverExt for LpRegistrationResponse { fn from_lp_data(data: LpData) -> Result { if data.kind != LpDataKind::Registration { - return Err(LpClientError::Transport(format!( - "did not receive a valid registration response. got {:?} instead", - data.kind - ))); + return Err(LpClientError::UnexpectedLpPayload { typ: data.kind }); } - let response = LpRegistrationResponse::try_deserialise(&data.content).map_err(|e| { - LpClientError::Transport(format!("Failed to deserialize registration response: {e}",)) - })?; + let response = LpRegistrationResponse::try_deserialise(&data.content) + .map_err(|source| LpClientError::MalformedRegistrationData { source })?; Ok(response) } @@ -72,30 +67,18 @@ pub(crate) fn convert_forward_data(request: ForwardPacketData) -> Result Result, LpClientError> { let response_data = match action { LpAction::DeliverData(data) => data, - other => { - return Err(LpClientError::Transport(format!( - "Unexpected action when receiving forward response: {:?}", - other - ))); - } + action => return Err(LpClientError::UnexpectedStateMachineAction { action }), }; if response_data.kind != LpDataKind::Forward { - return Err(LpClientError::Transport(format!( - "did not receive a valid forward response. got {:?} instead", - response_data.kind - ))); + return Err(LpClientError::UnexpectedLpPayload { + typ: response_data.kind, + }); } Ok(response_data.content.into()) } -pub(crate) fn to_lp_remote_peer( - identity: ed25519::PublicKey, - data: NymNodeLPInformation, -) -> LpRemotePeer { - LpRemotePeer::new(identity, data.x25519).with_key_digests( - data.expected_kem_key_hashes, - data.expected_signing_key_hashes, - ) +pub(crate) fn to_lp_remote_peer(data: NymNodeLPInformation) -> LpRemotePeer { + LpRemotePeer::new(data.x25519).with_key_digests(data.expected_kem_key_hashes) } diff --git a/nym-registration-client/src/lp_client/nested_session/connection.rs b/nym-registration-client/src/lp_client/nested_session/connection.rs index fd0f673677..70432e9bb0 100644 --- a/nym-registration-client/src/lp_client/nested_session/connection.rs +++ b/nym-registration-client/src/lp_client/nested_session/connection.rs @@ -3,60 +3,79 @@ use crate::lp_client::helpers::{convert_forward_data, try_convert_forward_response}; use crate::{LpClientError, LpRegistrationClient}; -use nym_crypto::asymmetric::ed25519; -use nym_lp::message::ForwardPacketData; +use bytes::{BufMut, BytesMut}; +use nym_lp::KEM; +use nym_lp::packet::{EncryptedLpPacket, ForwardPacketData, message::ExpectedResponseSize}; use nym_lp::state_machine::{LpAction, LpInput}; -use nym_lp_transport::traits::LpTransport; +use nym_lp::transport::traits::{HandshakeMessage, LpTransportChannel}; +use nym_lp::transport::{LpHandshakeChannel, LpTransportError}; use std::io; use std::net::SocketAddr; /// Attempt to treat the inner client as a LP connection pub struct NestedConnection<'a, S> { - /// Remote Ed25519 public key - pub(crate) exit_identity: ed25519::PublicKey, - /// Exit gateway's LP address (e.g., "2.2.2.2:41264") pub(crate) exit_address: SocketAddr, + // exact mechanisms of determining this value are TBD pub(crate) outer_client: &'a mut LpRegistrationClient, } impl<'a, S> NestedConnection<'a, S> { - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> Result<(), LpClientError> + fn prepare_handshake_message( + &self, + message: M, + handshake_kem: KEM, + ) -> Result { + let Some(response_size) = message.response_size(handshake_kem) else { + // this should NEVER happen for an initiator + return Err(LpClientError::Other("unexpected empty response".into())); + }; + + let expected_size = ExpectedResponseSize::Handshake(response_size as u32); + + Ok(ForwardPacketData::new( + self.exit_address, + expected_size, + message.into_bytes(), + )) + } + + fn prepare_transport_message(&self, packet: &EncryptedLpPacket) -> ForwardPacketData { + let mut buf = BytesMut::new(); + let len = packet.encoded_length() as u32; + buf.put_u32_le(len); + packet.encode(&mut buf); + ForwardPacketData::new( + self.exit_address, + ExpectedResponseSize::Transport, + buf.freeze().into(), + ) + } + + async fn send_forward_packet(&mut self, data: ForwardPacketData) -> Result<(), LpClientError> where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { - let forward_packet_data = - ForwardPacketData::new(self.exit_identity, self.exit_address, packet_data.to_vec()); - - let target_address = self.exit_address; - tracing::debug!( - "Sending ForwardPacket to {target_address} ({} inner bytes, persistent connection)", - forward_packet_data.inner_packet_bytes.len() + "Sending ForwardPacket to {} ({} inner bytes, persistent connection)", + data.target_lp_address, + data.inner_packet_bytes.len() ); // 1. Serialize the ForwardPacketData - let input = convert_forward_data(forward_packet_data)?; + let input = convert_forward_data(data)?; // 2. Encrypt and prepare packet via state machine let state_machine = self.outer_client.state_machine_mut()?; let action = state_machine .process_input(input) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| { - LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}")) - })?; + .ok_or(LpClientError::UnexpectedStateMachineHalt)??; let forward_packet = match action { LpAction::SendPacket(packet) => packet, - other => { - return Err(LpClientError::Transport(format!( - "Unexpected action when sending ForwardPacket: {:?}", - other - ))); - } + action => return Err(LpClientError::UnexpectedStateMachineAction { action }), }; // 3. Send the packet with timeout @@ -65,16 +84,14 @@ impl<'a, S> NestedConnection<'a, S> { self.outer_client.try_send_packet(&forward_packet).await }) .await - .map_err(|_| { - LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",)) - })??; + .map_err(|_| LpClientError::ConnectionTimeout)??; Ok(()) } - async fn receive_raw_packet(&mut self) -> Result, LpClientError> + async fn receive_forward_packet_data(&mut self) -> Result, LpClientError> where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { // 1. Receive the packet with timeout let timeout = self.outer_client.config.forward_timeout; @@ -82,18 +99,13 @@ impl<'a, S> NestedConnection<'a, S> { self.outer_client.try_receive_packet().await }) .await - .map_err(|_| { - LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",)) - })??; + .map_err(|_| LpClientError::ConnectionTimeout)??; // 2. Decrypt via state machine (re-borrow) let state_machine = self.outer_client.state_machine_mut()?; let action = state_machine .process_input(LpInput::ReceivePacket(response_packet)) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| { - LpClientError::Transport(format!("Failed to decrypt forward response: {e}")) - })?; + .ok_or(LpClientError::UnexpectedStateMachineHalt)??; // 3. Extract decrypted response data let response_data = try_convert_forward_response(action)?; @@ -108,28 +120,78 @@ impl<'a, S> NestedConnection<'a, S> { } } -impl<'a, S> LpTransport for NestedConnection<'a, S> +impl<'a, S> LpHandshakeChannel for NestedConnection<'a, S> where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { #[allow(clippy::unimplemented)] - async fn connect(_: SocketAddr) -> std::io::Result { + async fn write_all_and_flush(&mut self, _: &[u8]) -> Result<(), LpTransportError> { + // this is not being called instead we implement `send_handshake_message` directly + unimplemented!() + } + + #[allow(clippy::unimplemented)] + async fn read_n_bytes(&mut self, _: usize) -> Result, LpTransportError> { + // this is not being called instead we implement `receive_handshake_message` directly + unimplemented!() + } + + async fn send_handshake_message( + &mut self, + message: M, + handshake_kem: KEM, + ) -> Result<(), LpTransportError> { + let forward_data = self + .prepare_handshake_message(message, handshake_kem) + .map_err(|err| LpTransportError::TransportSendFailure(err.to_string()))?; + self.send_forward_packet(forward_data) + .await + .map_err(|err| LpTransportError::TransportSendFailure(err.to_string())) + } + + async fn receive_handshake_message( + &mut self, + _: usize, + ) -> Result { + let data = self + .receive_forward_packet_data() + .await + .map_err(|err| LpTransportError::TransportReceiveFailure(err.to_string()))?; + M::try_from_bytes(data) + } +} + +impl<'a, S> LpTransportChannel for NestedConnection<'a, S> +where + S: LpTransportChannel + LpHandshakeChannel + Unpin, +{ + #[allow(clippy::unimplemented)] + async fn connect(_: SocketAddr) -> Result { // this really breaks the pattern and should be refactored // since this function should never be called unimplemented!("cannot establish nested connection without an outer client") } - fn set_no_delay(&mut self, _: bool) -> std::io::Result<()> { + fn set_no_delay(&mut self, _: bool) -> Result<(), LpTransportError> { Ok(()) } - async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> { - self.send_serialised_packet(packet_data) + async fn send_length_prefixed_transport_packet( + &mut self, + packet: &EncryptedLpPacket, + ) -> Result<(), LpTransportError> { + let packet = self.prepare_transport_message(packet); + self.send_forward_packet(packet) .await .map_err(io::Error::other) + .map_err(LpTransportError::send_failure) } - async fn receive_raw_packet(&mut self) -> std::io::Result> { - self.receive_raw_packet().await.map_err(io::Error::other) + async fn receive_length_prefixed_transport_bytes( + &mut self, + ) -> Result, LpTransportError> { + self.receive_forward_packet_data() + .await + .map_err(|err| LpTransportError::TransportReceiveFailure(err.to_string())) } } diff --git a/nym-registration-client/src/lp_client/nested_session/mod.rs b/nym-registration-client/src/lp_client/nested_session/mod.rs index 7ee62c844c..a4e1c5681c 100644 --- a/nym-registration-client/src/lp_client/nested_session/mod.rs +++ b/nym-registration-client/src/lp_client/nested_session/mod.rs @@ -21,29 +21,27 @@ use super::client::LpRegistrationClient; use super::error::{LpClientError, Result}; use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt}; -use crate::lp_client::state_machine_helpers::{ - extract_forwarded_response, prepare_serialised_send_packet, -}; +use crate::lp_client::state_machine_helpers::{extract_forwarded_response, prepare_send_packet}; use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND}; use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_lp::codec::{OuterAeadKey, parse_lp_packet}; -use nym_lp::message::ForwardPacketData; +use nym_lp::packet::EncryptedLpPacket; use nym_lp::packet::version; -use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; +use nym_lp::peer::{DHKeyPair, LpLocalPeer, LpRemotePeer}; use nym_lp::state_machine::{LpData, LpStateMachine}; -use nym_lp::{LpPacket, LpSession}; -use nym_lp_transport::traits::LpTransport; +use nym_lp::transport::LpHandshakeChannel; +use nym_lp::transport::traits::LpTransportChannel; +use nym_lp::{Ciphersuite, KEM, LpSession}; use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent; use nym_registration_common::{ LpRegistrationRequest, LpRegistrationResponse, WireguardConfiguration, WireguardRegistrationData, }; use nym_wireguard_types::PeerPublicKey; -use rand::{CryptoRng, RngCore}; +use rand09::{CryptoRng, Rng, RngCore}; use std::net::SocketAddr; use std::sync::Arc; -use tracing::warn; +use tracing::{debug, warn}; pub(crate) mod connection; @@ -90,17 +88,18 @@ impl NestedLpSession { /// /// # Arguments /// * `exit_address` - Exit gateway's LP address (e.g., "2.2.2.2:41264") - /// * `client_keypair` - Client's Ed25519 keypair + /// * `client_keypair` - Client's x25519 keypair /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol + /// * `ciphersuite` - the set of cryptographic protocols to use when negotiating the session with the node /// * `gateway_supported_lp_protocol_version` - Gateway's LP protocol version pub fn new( exit_address: SocketAddr, - client_keypair: Arc, + client_keypair: Arc, gateway_lp_peer: LpRemotePeer, + ciphersuite: Ciphersuite, gateway_supported_lp_protocol_version: u8, ) -> Self { - let local_x25519_keypair = client_keypair.to_x25519(); - let lp_local_peer = LpLocalPeer::new(client_keypair, Arc::new(local_x25519_keypair)); + let lp_local_peer = LpLocalPeer::new(ciphersuite, client_keypair); let lp_protocol = if gateway_supported_lp_protocol_version > version::CURRENT { warn!( @@ -121,44 +120,22 @@ impl NestedLpSession { } } - fn state_machine(&self) -> Result<&LpStateMachine> { - self.state_machine.as_ref().ok_or_else(|| { - LpClientError::transport( - "State machine not available - has the handshake been completed?", - ) - }) - } - fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> { - self.state_machine.as_mut().ok_or_else(|| { - LpClientError::transport( - "State machine not available - has the handshake been completed?", - ) - }) + self.state_machine + .as_mut() + .ok_or(LpClientError::IncompleteHandshake) } - /// Attempt to parse received bytes into an LpPacket - fn parse_received_lp_packet(&self, response_bytes: Vec) -> Result { - let state_machine = self.state_machine()?; - let outer_key = state_machine.session()?.outer_aead_key(); - Self::parse_packet(&response_bytes, Some(outer_key)) - } - - /// Attempt to wrap the provided `LpData` into a `ForwardPacketData` + /// Attempt to wrap the provided `LpData` into a `EncryptedLpPacket` /// using the inner state machine. - fn prepare_forward_packet(&mut self, data: LpData) -> Result { + fn prepare_transport_packet(&mut self, data: LpData) -> Result { let state_machine = self.state_machine_mut()?; - let inner_packet_bytes = prepare_serialised_send_packet(data, state_machine)?; - Ok(ForwardPacketData::new( - self.gateway_lp_peer.ed25519(), - self.exit_address, - inner_packet_bytes, - )) + prepare_send_packet(data, state_machine) } - /// Attempt to recover received `LpData` from the received `LpPacket` + /// Attempt to recover received `LpData` from the received `EncryptedLpPacket` /// using the inner state machine. - fn extract_forwarded_response(&mut self, response_packet: LpPacket) -> Result { + fn extract_forwarded_response(&mut self, response_packet: EncryptedLpPacket) -> Result { let state_machine = self.state_machine_mut()?; extract_forwarded_response(response_packet, state_machine) } @@ -167,10 +144,8 @@ impl NestedLpSession { /// through the entry gateway. /// /// This method: - /// 1. Generates ClientHello for exit gateway - /// 2. Creates LP state machine for exit handshake - /// 3. Runs handshake loop, forwarding all packets through entry gateway - /// 4. Stores established session in internal state machine + /// 1. Runs handshake loop, forwarding all packets through entry gateway + /// 2. Stores established session in internal state machine /// /// # Arguments /// * `outer_client` - Connected LP client with established outer session to entry gateway @@ -181,38 +156,40 @@ impl NestedLpSession { /// - Forwarding through entry gateway fails /// - Exit gateway handshake fails /// - Cryptographic operations fail - async fn perform_handshake( + pub async fn perform_handshake( &mut self, outer_client: &mut LpRegistrationClient, ) -> Result<()> where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { + if self.lp_local_peer.ciphersuite().kem() == KEM::McEliece { + return Err(LpClientError::UnsupportedNestedMcEliece); + } + tracing::debug!( "Starting nested LP handshake with exit gateway {}", self.exit_address ); - let mut nested_connection = - outer_client.as_nested_connection(self.gateway_lp_peer.ed25519(), self.exit_address); + let mut nested_connection = outer_client.as_nested_connection(self.exit_address); let local_peer = self.lp_local_peer.clone(); let remote_peer = self.gateway_lp_peer.clone(); let protocol_version = self.gateway_supported_lp_protocol_version; - let ciphersuite = LpSession::default_ciphersuite(); - let session = LpSession::complete_as_initiator( + let session = LpSession::psq_handshake_initiator( &mut nested_connection, - ciphersuite, local_peer, remote_peer, protocol_version, ) - .complete_as_initiator() + .complete_handshake() .await?; // Store the state machine (with established session) for later use self.state_machine = Some(LpStateMachine::new(session)); + debug!("completed nested handshake"); Ok(()) } @@ -246,9 +223,10 @@ impl NestedLpSession { ticket_type: TicketType, ) -> Result where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, { tracing::debug!("Acquiring bandwidth credential for registration"); + let mut nested_connection = outer_client.as_nested_connection(self.exit_address); // Step 1: Get bandwidth credential from controller let credential_spending = bandwidth_controller @@ -276,18 +254,20 @@ impl NestedLpSession { let send_data = request.to_lp_data()?; // Step 4: Encrypt and prepare packet via state machine - let forward_packet = self.prepare_forward_packet(send_data)?; + let forward_packet = self.prepare_transport_packet(send_data)?; // Step 5: Send the encrypted packet via forwarding - let response_bytes = outer_client - .send_forward_packet_with_response(forward_packet) + nested_connection + .send_length_prefixed_transport_packet(&forward_packet) .await?; - // Step 6: Parse response bytes to LP packet - let response_packet = self.parse_received_lp_packet(response_bytes)?; + // Step 6: wait for response + let response = nested_connection + .receive_length_prefixed_transport_packet() + .await?; - // Step 7: Decrypt via state machine - let response_data = self.extract_forwarded_response(response_packet)?; + // Step 7: Process via state machine + let response_data = self.extract_forwarded_response(response)?; // Step 8: Extract decrypted data and deserialise the response let response = LpRegistrationResponse::from_lp_data(response_data)?; @@ -314,13 +294,12 @@ impl NestedLpSession { } } - /// Performs handshake and registration with the exit gateway via forwarding. + /// Performs dVPN registration with the exit gateway via forwarding. /// /// This is the main entry point for nested LP registration. It: - /// 1. Performs handshake with exit gateway (via `perform_handshake`) - /// 2. Builds and sends registration request through the forwarded connection - /// 3. Receives and processes registration response - /// 4. Returns gateway data on successful registration + /// 1. Builds and sends registration request through the forwarded connection + /// 2. Receives and processes registration response + /// 3. Returns gateway data on successful registration /// /// # Arguments /// * `outer_client` - Connected LP client with established outer session to entry gateway @@ -341,7 +320,7 @@ impl NestedLpSession { /// - Forwarding through entry gateway fails /// - Response decryption/deserialization fails /// - Gateway rejects the registration - pub async fn handshake_and_register_dvpn( + pub async fn register_dvpn( &mut self, outer_client: &mut LpRegistrationClient, rng: &mut R, @@ -351,11 +330,10 @@ impl NestedLpSession { ticket_type: TicketType, ) -> Result where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, R: RngCore + CryptoRng, { - // Step 1: Perform handshake with exit gateway via forwarding - self.perform_handshake(outer_client).await?; + let mut nested_connection = outer_client.as_nested_connection(self.exit_address); tracing::debug!("Building registration request for exit gateway"); @@ -370,20 +348,21 @@ impl NestedLpSession { let send_data = request.to_lp_data()?; // Step 4: Encrypt and prepare packet via state machine - let forward_packet = self.prepare_forward_packet(send_data)?; + let forward_packet = self.prepare_transport_packet(send_data)?; // Step 5: Send the encrypted packet via forwarding - let response_bytes = outer_client - .send_forward_packet_with_response(forward_packet) + nested_connection + .send_length_prefixed_transport_packet(&forward_packet) .await?; + // Step 6: wait for response + let response = nested_connection + .receive_length_prefixed_transport_packet() + .await?; tracing::trace!("Received registration response from exit gateway"); - // Step 6: Parse response bytes to LP packet - let response_packet = self.parse_received_lp_packet(response_bytes)?; - - // Step 7: Decrypt via state machine - let response_data = self.extract_forwarded_response(response_packet)?; + // Step 7: Process via state machine + let response_data = self.extract_forwarded_response(response)?; // Step 8: Extract decrypted data and deserialise the response let response = LpRegistrationResponse::from_lp_data(response_data)?; @@ -426,6 +405,60 @@ impl NestedLpSession { }) } + /// Performs handshake and registration with the exit gateway via forwarding. + /// + /// This is the main entry point for nested LP registration. It: + /// 1. Performs handshake with exit gateway (via `perform_handshake`) + /// 2. Builds and sends registration request through the forwarded connection + /// 3. Receives and processes registration response + /// 4. Returns gateway data on successful registration + /// + /// # Arguments + /// * `outer_client` - Connected LP client with established outer session to entry gateway + /// * `wg_keypair` - Client's WireGuard x25519 keypair + /// * `gateway_identity` - Exit gateway's Ed25519 identity (for credential verification) + /// * `bandwidth_controller` - Provider for bandwidth credentials + /// * `ticket_type` - Type of bandwidth ticket to use + /// * `client_ip` - Client IP address for registration metadata + /// + /// # Returns + /// * `Ok(GatewayData)` - Exit gateway configuration data on successful registration + /// + /// # Errors + /// Returns an error if: + /// - Handshake fails + /// - Credential acquisition fails + /// - Request serialization/encryption fails + /// - Forwarding through entry gateway fails + /// - Response decryption/deserialization fails + /// - Gateway rejects the registration + pub(crate) async fn handshake_and_register_dvpn( + &mut self, + outer_client: &mut LpRegistrationClient, + rng: &mut R, + wg_keypair: &x25519::KeyPair, + gateway_identity: &ed25519::PublicKey, + bandwidth_controller: &dyn BandwidthTicketProvider, + ticket_type: TicketType, + ) -> Result + where + S: LpTransportChannel + LpHandshakeChannel + Unpin, + R: RngCore + CryptoRng, + { + // Step 1: Perform handshake with exit gateway via forwarding + self.perform_handshake(outer_client).await?; + + self.register_dvpn( + outer_client, + rng, + wg_keypair, + gateway_identity, + bandwidth_controller, + ticket_type, + ) + .await + } + /// Performs handshake and registration with the exit gateway via forwarding, /// with automatic retry on network failure. /// @@ -466,7 +499,7 @@ impl NestedLpSession { max_retries: u32, ) -> Result where - S: LpTransport + Unpin, + S: LpTransportChannel + LpHandshakeChannel + Unpin, R: RngCore + CryptoRng, { tracing::debug!( @@ -479,14 +512,14 @@ impl NestedLpSession { if attempt > 0 { // Verify outer session is still usable before retry if !outer_client.is_handshake_complete() { - return Err(LpClientError::Transport( + return Err(LpClientError::Other( "Outer session lost during retry - caller must re-establish entry gateway connection".to_string() )); } // Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped) let base_delay_ms = 100u64 * (1 << attempt.min(4)); - let jitter_ms = rand::random::() % (base_delay_ms / 4 + 1); + let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1)); let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms); tracing::info!( "Retrying exit registration (attempt {}) after {:?}", @@ -526,24 +559,8 @@ impl NestedLpSession { } } - Err(last_error.unwrap_or_else(|| { - LpClientError::Transport("Exit registration failed after all retries".to_string()) + Err(last_error.unwrap_or(LpClientError::RegistrationFailure { + message: "Exit Registration failed after all retries".to_string(), })) } - - /// Parses an LP packet from bytes. - /// - /// # Arguments - /// * `bytes` - The bytes to parse - /// - /// # Returns - /// * `Ok(LpPacket)` - Parsed LP packet - /// - /// # Errors - /// Returns an error if parsing fails - fn parse_packet(bytes: &[u8], outer_key: Option<&OuterAeadKey>) -> Result { - // Use outer AEAD key when available (after PSK derivation) - parse_lp_packet(bytes, outer_key) - .map_err(|e| LpClientError::Transport(format!("Failed to parse LP packet: {e}"))) - } } diff --git a/nym-registration-client/src/lp_client/state_machine_helpers.rs b/nym-registration-client/src/lp_client/state_machine_helpers.rs index b8be242621..bfa3f9edc5 100644 --- a/nym-registration-client/src/lp_client/state_machine_helpers.rs +++ b/nym-registration-client/src/lp_client/state_machine_helpers.rs @@ -2,85 +2,37 @@ // SPDX-License-Identifier: Apache-2.0 use crate::LpClientError; -use bytes::BytesMut; -use nym_lp::codec::{OuterAeadKey, serialize_lp_packet}; use nym_lp::state_machine::{LpAction, LpData, LpInput}; -use nym_lp::{LpPacket, LpStateMachine}; - -/// Serializes an LP packet to bytes. -/// -/// # Arguments -/// * `packet` - The LP packet to serialize -/// -/// # Returns -/// * `Ok(Vec)` - Serialized packet bytes -/// -/// # Errors -/// Returns an error if serialization fails -pub(crate) fn serialize_packet( - packet: &LpPacket, - outer_key: Option<&OuterAeadKey>, -) -> Result, LpClientError> { - let mut buf = BytesMut::new(); - // Use outer AEAD key when available (after PSK derivation) - serialize_lp_packet(packet, &mut buf, outer_key) - .map_err(|e| LpClientError::Transport(format!("Failed to serialize LP packet: {}", e)))?; - Ok(buf.to_vec()) -} +use nym_lp::{LpStateMachine, packet::EncryptedLpPacket}; /// Attempt to prepare the provided data for sending by wrapping it in appropriate `LpAction`, -/// and attempting to extract `LpPacket` from the provided srtate machine. +/// and attempting to extract `EncryptedLpPacket` from the provided state machine. pub(crate) fn prepare_send_packet( data: LpData, state_machine: &mut LpStateMachine, -) -> Result { +) -> Result { let action = state_machine .process_input(LpInput::SendData(data)) - .ok_or_else(|| LpClientError::transport("State machine returned no action"))? - .map_err(|e| { - LpClientError::SendRegistrationRequest(format!( - "Failed to encrypt registration request: {e}", - )) - })?; + .ok_or(LpClientError::UnexpectedStateMachineHalt)??; match action { LpAction::SendPacket(packet) => Ok(packet), - other => Err(LpClientError::Transport(format!( - "Unexpected action when trying to send packet data: {other:?}", - ))), + action => Err(LpClientError::UnexpectedStateMachineAction { action }), } } -/// Attempt to prepare the provided data for sending by wrapping it in appropriate `LpAction`, -/// serialising and finally encrypting (if appropriate key is available) the resultant `LpPacket` -/// It uses the provided state machine. -pub(crate) fn prepare_serialised_send_packet( - data: LpData, - state_machine: &mut LpStateMachine, -) -> Result, LpClientError> { - let packet = prepare_send_packet(data, state_machine)?; - let send_key = state_machine.session()?.outer_aead_key(); - - serialize_packet(&packet, Some(send_key)) -} - /// Attempt to recover received `LpData` from the received `LpPacket` /// using the provided state machine. pub(crate) fn extract_forwarded_response( - response_packet: LpPacket, + response_packet: EncryptedLpPacket, state_machine: &mut LpStateMachine, ) -> Result { let action = state_machine .process_input(LpInput::ReceivePacket(response_packet)) - .ok_or_else(|| LpClientError::Transport("State machine returned no action".to_string()))? - .map_err(|e| { - LpClientError::Transport(format!("Failed to decrypt received response: {e}")) - })?; + .ok_or(LpClientError::UnexpectedStateMachineHalt)??; match action { LpAction::DeliverData(data) => Ok(data), - other => Err(LpClientError::Transport(format!( - "Unexpected action when receiving response: {other:?}" - ))), + action => Err(LpClientError::UnexpectedStateMachineAction { action }), } } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index dd7d19c7f7..7b049ffd8a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -1255,6 +1255,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-models" +version = "0.0.5" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.2", +] + [[package]] name = "cosmos-sdk-proto" version = "0.27.0" @@ -2601,15 +2611,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -2871,6 +2881,43 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "heck" version = "0.4.1" @@ -2943,7 +2990,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.2", "ring", "rustls 0.23.36", "thiserror 2.0.12", @@ -2968,7 +3015,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.0", + "rand 0.9.2", "resolv-conf", "rustls 0.23.36", "smallvec", @@ -3659,7 +3706,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -3804,6 +3851,240 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libcrux-aesgcm" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-ed25519" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", + "rand_core 0.9.3", + "tls_codec", +] + +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-curve25519", + "libcrux-ecdh", + "libcrux-ml-kem", + "libcrux-p256", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "quote", + "syn 2.0.100", +] + +[[package]] +name = "libcrux-ml-dsa" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "core-models", + "hax-lib", + "libcrux-intrinsics", + "libcrux-macros", + "libcrux-platform", + "libcrux-sha3", + "tls_codec", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-p256" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-psq" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-aesgcm", + "libcrux-chacha20poly1305", + "libcrux-ecdh", + "libcrux-ed25519", + "libcrux-hkdf", + "libcrux-hmac", + "libcrux-kem", + "libcrux-ml-dsa", + "libcrux-ml-kem", + "libcrux-sha2", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.7" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.6" +source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", +] + [[package]] name = "libloading" version = "0.7.4" @@ -4356,6 +4637,8 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "jwt-simple", + "libcrux-curve25519", + "libcrux-psq", "nym-pemstore", "rand 0.8.5", "serde", @@ -4480,6 +4763,7 @@ name = "nym-kkt-ciphersuite" version = "1.20.4" dependencies = [ "num_enum", + "semver", "strum", "strum_macros", "thiserror 2.0.12", @@ -5242,6 +5526,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -5711,6 +6001,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -5807,8 +6119,8 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.2", - "rand 0.9.0", + "getrandom 0.3.3", + "rand 0.9.2", "ring", "rustc-hash", "rustls 0.23.36", @@ -5876,13 +6188,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", ] [[package]] @@ -5939,7 +6250,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -7638,7 +7949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix 1.0.5", "windows-sys 0.59.0", @@ -7881,6 +8192,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "tokio" version = "1.47.1" @@ -8398,7 +8730,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "serde", ] @@ -8494,11 +8826,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -9408,13 +9749,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "wl-clipboard-rs" diff --git a/tools/nym-lp-client/Cargo.toml b/tools/nym-lp-client/Cargo.toml index d3e6b08bd5..e91e052662 100644 --- a/tools/nym-lp-client/Cargo.toml +++ b/tools/nym-lp-client/Cargo.toml @@ -15,6 +15,7 @@ anyhow = { workspace = true } bytes = { workspace = true } clap = { workspace = true, features = ["derive"] } rand = { workspace = true } +rand09 = { workspace = true } rand_chacha = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/tools/nym-lp-client/src/client.rs b/tools/nym-lp-client/src/client.rs index 3be0bc18b2..881a781392 100644 --- a/tools/nym-lp-client/src/client.rs +++ b/tools/nym-lp-client/src/client.rs @@ -20,7 +20,8 @@ use nym_sphinx_anonymous_replies::requests::{AnonymousSenderTag, RepliableMessag use nym_sphinx_anonymous_replies::{ReplySurb, SurbEncryptionKey}; use nym_sphinx_framing::codec::NymCodec; use nym_sphinx_framing::packet::FramedNymPacket; -use rand_chacha::rand_core::SeedableRng; +use rand::SeedableRng; +use rand09::SeedableRng as SeedableRng09; use rand_chacha::ChaCha8Rng; use std::net::SocketAddr; use std::sync::Arc; @@ -33,7 +34,7 @@ use tracing::{debug, info, trace}; use crate::topology::{GatewayInfo, SpeedtestTopology}; use nym_ip_packet_requests::v8::request::IpPacketRequest; use nym_lp::packet::version; -use nym_lp::peer::LpRemotePeer; +use nym_lp::peer::{DHKeyPair, LpRemotePeer}; use nym_sphinx::forwarding::packet::MixPacket; /// Conv ID for KCP - hash of source and destination addresses @@ -51,6 +52,8 @@ pub struct SpeedtestClient { identity_keypair: Arc, /// Client's x25519 encryption keypair (for SURBs) encryption_keypair: Arc, + /// Client's LP keypair + lp_keypair: Arc, /// Target gateway gateway: GatewayInfo, /// Network topology for routing @@ -86,11 +89,14 @@ impl SpeedtestClient { pub fn new(gateway: GatewayInfo, topology: Arc) -> Self { let identity_keypair = Arc::new(ed25519::KeyPair::new(&mut rand::rngs::OsRng)); let encryption_keypair = Arc::new(x25519::KeyPair::new(&mut rand::rngs::OsRng)); + let mut rng09 = rand09::rngs::StdRng::from_os_rng(); + let lp_keypair = DHKeyPair::new(&mut rng09); let rng = ChaCha8Rng::from_entropy(); Self { identity_keypair, encryption_keypair, + lp_keypair: Arc::new(lp_keypair), gateway, topology, socket: None, @@ -118,16 +124,14 @@ impl SpeedtestClient { self.gateway.lp_address ); - let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?) - .with_key_digests( - self.gateway.kem_key_hashes.clone(), - self.gateway.signing_key_hashes.clone(), - ); + let gw_peer = LpRemotePeer::new(self.gateway.lp_key) + .with_key_digests(self.gateway.kem_key_hashes.clone()); let mut lp_client = LpRegistrationClient::::new_with_default_config( - self.identity_keypair.clone(), + self.lp_keypair.clone(), gw_peer, self.gateway.lp_address, + self.gateway.ciphersuite, self.gateway.lp_version, ); @@ -165,16 +169,14 @@ impl SpeedtestClient { self.gateway.lp_address ); - let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?) - .with_key_digests( - self.gateway.kem_key_hashes.clone(), - self.gateway.signing_key_hashes.clone(), - ); + let gw_peer = LpRemotePeer::new(self.gateway.lp_key) + .with_key_digests(self.gateway.kem_key_hashes.clone()); let mut lp_client = LpRegistrationClient::new_with_default_config( - self.identity_keypair.clone(), + self.lp_keypair.clone(), gw_peer, self.gateway.lp_address, + self.gateway.ciphersuite, self.gateway.lp_version, ); @@ -440,61 +442,65 @@ impl SpeedtestClient { if !self.has_lp_session() { bail!("LP session not initialized - call init_lp_session() first"); } + let _ = payload; + let _ = num_surbs; + bail!("lp transport channel is not yet implemented"); - let prepared = self.prepare_sphinx_fragments(payload, num_surbs).await?; - - // Now get mutable references after prepare_sphinx_fragments is done - let lp_client = self.lp_client.as_mut().unwrap(); // safe: checked above - let socket = self.socket.as_ref().context("socket not initialized")?; - let lp_data_address = self.gateway.lp_data_address; - - let mut total_sent = 0usize; - let fragment_count = prepared.fragments.len(); - - for fragment in prepared.fragments { - let nym_packet = NymPacket::sphinx_build( - false, - PacketSize::RegularPacket.payload_size(), - fragment.into_bytes(), - &prepared.route, - &prepared.destination, - &prepared.delays, - )?; - - // Wrap in MixPacket v2: packet_type || key_rotation || next_hop || sphinx_data - let mix_packet = MixPacket::new( - prepared.first_hop_addr, - nym_packet, - PacketType::Mix, - SphinxKeyRotation::Unknown, - ); - - let mix_bytes = mix_packet - .into_v2_bytes() - .context("failed to serialize MixPacket")?; - - // Wrap in LP for UDP data plane - let lp_packet = lp_client - .wrap_data(&mix_bytes) - .context("failed to wrap in LP")?; - - // Send to gateway's LP data port (51264) with timeout - tokio::time::timeout( - Duration::from_secs(5), - socket.send_to(&lp_packet, lp_data_address), - ) - .await - .context("UDP send timed out")? - .context("UDP send failed")?; - total_sent += lp_packet.len(); - } - - info!( - "Sent {} bytes via LP ({} fragments, {} SURBs) to {}", - total_sent, fragment_count, num_surbs, lp_data_address - ); - - Ok(prepared.encryption_keys) + // leave the code for future reference + // let prepared = self.prepare_sphinx_fragments(payload, num_surbs).await?; + // + // // Now get mutable references after prepare_sphinx_fragments is done + // let lp_client = self.lp_client.as_mut().unwrap(); // safe: checked above + // let socket = self.socket.as_ref().context("socket not initialized")?; + // let lp_data_address = self.gateway.lp_data_address; + // + // let mut total_sent = 0usize; + // let fragment_count = prepared.fragments.len(); + // + // for fragment in prepared.fragments { + // let nym_packet = NymPacket::sphinx_build( + // false, + // PacketSize::RegularPacket.payload_size(), + // fragment.into_bytes(), + // &prepared.route, + // &prepared.destination, + // &prepared.delays, + // )?; + // + // // Wrap in MixPacket v2: packet_type || key_rotation || next_hop || sphinx_data + // let mix_packet = MixPacket::new( + // prepared.first_hop_addr, + // nym_packet, + // PacketType::Mix, + // SphinxKeyRotation::Unknown, + // ); + // + // let mix_bytes = mix_packet + // .into_v2_bytes() + // .context("failed to serialize MixPacket")?; + // + // // Wrap in LP for UDP data plane + // let lp_packet = lp_client + // .wrap_data(&mix_bytes) + // .context("failed to wrap in LP")?; + // + // // Send to gateway's LP data port (51264) with timeout + // tokio::time::timeout( + // Duration::from_secs(5), + // socket.send_to(&lp_packet, lp_data_address), + // ) + // .await + // .context("UDP send timed out")? + // .context("UDP send failed")?; + // total_sent += lp_packet.len(); + // } + // + // info!( + // "Sent {} bytes via LP ({} fragments, {} SURBs) to {}", + // total_sent, fragment_count, num_surbs, lp_data_address + // ); + // + // Ok(prepared.encryption_keys) } /// Receive UDP data with timeout diff --git a/tools/nym-lp-client/src/topology.rs b/tools/nym-lp-client/src/topology.rs index 3e0f76b6ff..91fccc9b8c 100644 --- a/tools/nym-lp-client/src/topology.rs +++ b/tools/nym-lp-client/src/topology.rs @@ -10,13 +10,14 @@ use nym_api_requests::models::{LPHashFunction, LPKEM}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::ed25519; use nym_http_api_client::UserAgent; -use nym_kkt_ciphersuite::{KEMKeyDigests, SignatureScheme, SigningKeyDigests, KEM}; +use nym_kkt_ciphersuite::{Ciphersuite, KEMKeyDigests, SignatureScheme, KEM}; +use nym_lp::peer::DHPublicKey; use nym_sphinx_types::Node as SphinxNode; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyMetadata}; use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::IteratorRandom; use rand::{CryptoRng, Rng}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::net::SocketAddr; use tracing::{debug, info}; use url::Url; @@ -30,8 +31,9 @@ const LP_DATA_PORT: u16 = 51264; #[derive(Debug, Clone)] pub struct GatewayInfo { pub identity: ed25519::PublicKey, - pub kem_key_hashes: HashMap, - pub signing_key_hashes: HashMap, + pub lp_key: DHPublicKey, + pub kem_key_hashes: BTreeMap, + pub ciphersuite: Ciphersuite, pub sphinx_key: nym_crypto::asymmetric::x25519::PublicKey, /// Mix host (IP:port for Sphinx mixing) diff --git a/wasm/client/.cargo/config.toml b/wasm/client/.cargo/config.toml index a7f24f6737..f506045a7f 100644 --- a/wasm/client/.cargo/config.toml +++ b/wasm/client/.cargo/config.toml @@ -1,6 +1,8 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] + [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' \ No newline at end of file diff --git a/wasm/client/Cargo.toml b/wasm/client/Cargo.toml index 3bc15e7ade..2377ddcb6d 100644 --- a/wasm/client/Cargo.toml +++ b/wasm/client/Cargo.toml @@ -44,6 +44,7 @@ nym-node-tester-wasm = { path = "../node-tester", optional = true } tokio_with_wasm = { workspace = true, features = ["full"] } + [dev-dependencies] wasm-bindgen-test = { workspace = true } diff --git a/wasm/full-nym-wasm/.cargo/config.toml b/wasm/full-nym-wasm/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/wasm/full-nym-wasm/.cargo/config.toml +++ b/wasm/full-nym-wasm/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/wasm/mix-fetch/.cargo/config.toml b/wasm/mix-fetch/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/wasm/mix-fetch/.cargo/config.toml +++ b/wasm/mix-fetch/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/wasm/mix-fetch/src/helpers.rs b/wasm/mix-fetch/src/helpers.rs index 9476d61715..719ab64559 100644 --- a/wasm/mix-fetch/src/helpers.rs +++ b/wasm/mix-fetch/src/helpers.rs @@ -25,6 +25,7 @@ pub(crate) async fn get_network_requester( url::Url::parse(&nym_api_url.unwrap_or(NYM_API_URL.to_string()))?, None, ); + #[allow(deprecated)] let nodes = client.get_all_described_nodes().await?; let providers: Vec<_> = nodes .iter() diff --git a/wasm/node-tester/.cargo/config.toml b/wasm/node-tester/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/wasm/node-tester/.cargo/config.toml +++ b/wasm/node-tester/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/wasm/zknym-lib/.cargo/config.toml b/wasm/zknym-lib/.cargo/config.toml index bdbe517658..e9c3438b6e 100644 --- a/wasm/zknym-lib/.cargo/config.toml +++ b/wasm/zknym-lib/.cargo/config.toml @@ -1,3 +1,4 @@ [build] target = "wasm32-unknown-unknown" target_arch = "wasm32" +rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] \ No newline at end of file diff --git a/wasm/zknym-lib/Cargo.toml b/wasm/zknym-lib/Cargo.toml index b9d086535e..385ed467f1 100644 --- a/wasm/zknym-lib/Cargo.toml +++ b/wasm/zknym-lib/Cargo.toml @@ -16,31 +16,21 @@ crate-type = ["cdylib", "rlib"] [dependencies] async-trait.workspace = true -bs58.workspace = true -getrandom = { workspace = true, features = ["js"] } js-sys.workspace = true wasm-bindgen.workspace = true serde = { workspace = true, features = ["derive"] } thiserror.workspace = true tsify = { workspace = true, features = ["js"] } uuid = { workspace = true, features = ["serde"] } -reqwest = { workspace = true } wasmtimer = { workspace = true } zeroize.workspace = true -rand = { workspace = true } - - nym-bin-common = { workspace = true } nym-compact-ecash = { workspace = true } -nym-credentials = { workspace = true } -nym-crypto = { workspace = true, features = ["asymmetric", "rand"] } nym-http-api-client = { workspace = true } nym-wasm-utils = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } -tokio = { workspace = true, features = ["full"] } [package.metadata.wasm-pack.profile.release]