diff --git a/.gitignore b/.gitignore index eddf33eadc..c4d8a91c01 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,6 @@ nym-api/redocly/formatted-openapi.json **/settings.sql **/enter_db.sh -.beads \ No newline at end of file +.beads +CLAUDE.md +docs \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a196d98d00..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,700 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Nym is a privacy platform that uses mixnet technology to protect against metadata surveillance. The platform consists of several key components: -- Mixnet nodes (mixnodes) for packet mixing -- Gateways (entry/exit points for the network) -- Clients for interacting with the network -- Network monitoring tools -- Validators for network consensus -- Various service providers and integrations - -## Navigation Aids - -This repository includes comprehensive navigation documents for efficient code exploration: - -- **[CODEMAP.md](./CODEMAP.md)**: Structural overview of the entire repository with directory hierarchy, package descriptions, and navigation hints. Use this to quickly understand the codebase layout and find specific components. - -- **[FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md)**: Comprehensive catalog of key functions, signatures, and API patterns across all major modules. Use this to quickly find available functions and understand their usage patterns. - -When working with this codebase, start by consulting these documents to understand the structure and available APIs before diving into specific files. - -## Build Commands - -### Rust Components - -```bash -# Default build (debug) -cargo build - -# Release build -cargo build --release - -# Build a specific package -cargo build -p - -# Build main components -make build - -# Build release versions of main binaries and contracts -make build-release - -# Build specific binaries -make build-nym-cli -cargo build -p nym-node --release -cargo build -p nym-api --release -``` - -### Testing - -```bash -# Run clippy, unit tests, and formatting -make test - -# Run all tests including slow tests -make test-all - -# Run clippy on all workspaces -make clippy - -# Run unit tests for a specific package -cargo test -p - -# Run only expensive/ignored tests -cargo test --workspace -- --ignored - -# Run API tests -dotenv -f envs/sandbox.env -- cargo test --test public-api-tests - -# Run tests with specific log level -RUST_LOG=debug cargo test -p - -# Run specific test scripts -./nym-node/tests/test_apis.sh -./scripts/wireguard-exit-policy/exit-policy-tests.sh -``` - -### Linting and Formatting - -```bash -# Run rustfmt on all code -make fmt - -# Check formatting without modifying -cargo fmt --all -- --check - -# Run clippy with all targets -cargo clippy --workspace --all-targets -- -D warnings - -# TypeScript linting -yarn lint -yarn lint:fix -yarn types:lint:fix - -# Check dependencies for security/licensing issues -cargo deny check -``` - -### WASM Components - -```bash -# Build all WASM components -make sdk-wasm-build - -# Build TypeScript SDK -yarn build:sdk -npx lerna run --scope @nymproject/sdk build --stream - -# Build and test WASM components -make sdk-wasm - -# Build specific WASM packages -cd wasm/client && make -cd wasm/mix-fetch && make -cd wasm/node-tester && make -``` - -### Contract Development - -```bash -# Build all contracts -make contracts - -# Build contracts in release mode -make build-release-contracts - -# Generate contract schemas -make contract-schema - -# Run wasm-opt on contracts -make wasm-opt-contracts - -# Check contracts with cosmwasm-check -make cosmwasm-check-contracts -``` - -### Running Components - -```bash -# Run nym-node as a mixnode -cargo run -p nym-node -- run --mode mixnode - -# Run nym-node as a gateway -cargo run -p nym-node -- run --mode gateway - -# Run the network monitor -cargo run -p nym-network-monitor - -# Run the API server -cargo run -p nym-api - -# Run with specific environment -dotenv -f envs/sandbox.env -- cargo run -p nym-api - -# Start a local network -./scripts/localnet_start.sh -``` - -## Architecture - -The Nym platform consists of various components organized as a monorepo. For a detailed structural overview with directory hierarchy and navigation hints, see [CODEMAP.md](./CODEMAP.md). - -1. **Core Mixnet Infrastructure**: - - `nym-node`: Core binary supporting mixnode and gateway modes - - `common/nymsphinx`: Implementation of the Sphinx packet format - - `common/topology`: Network topology management - - `common/types`: Shared data types across components - -2. **Network Monitoring**: - - `nym-network-monitor`: Monitors the network's reliability and performance - - `nym-api`: API server for network stats and monitoring data - - Metrics tracking for nodes, routes, and overall network health - -3. **Client Implementations**: - - `clients/native`: Native Rust client implementation - - `clients/socks5`: SOCKS5 proxy client for standard applications - - `wasm`: WebAssembly client implementations (for browsers) - - `nym-connect`: Desktop and mobile clients - -4. **Blockchain & Smart Contracts**: - - `common/cosmwasm-smart-contracts`: Smart contract implementations - - `contracts`: CosmWasm contracts for the Nym network - - `common/ledger`: Blockchain integration - -5. **Utilities & Tools**: - - `tools`: Various CLI tools and utilities - - `sdk`: SDKs for different languages and platforms - - `documentation`: Documentation generation and management - -## Packet System - -Nym uses a modified Sphinx packet format for its mixnet: - -1. **Message Chunking**: - - Messages are divided into "sets" and "fragments" - - Each fragment fits in a single Sphinx packet - - The `common/nymsphinx/chunking` module handles message fragmentation - -2. **Routing**: - - Packets traverse through 3 layers of mixnodes - - Routing information is encrypted in layers (onion routing) - - The final gateway receives and processes the messages - -3. **Monitoring**: - - Monitoring system tracks packet delivery through the network - - Routes are analyzed for reliability statistics - - Node performance metrics are collected - -## Network Protocol - -Nym implements the Loopix mixnet design with several key privacy features: - -1. **Continuous-time Mixing**: - - Each mixnode delays messages independently with an exponential distribution - - This creates random reordering of packets, destroying timing correlations - - Offers better anonymity properties than batch mixing approaches - -2. **Cover Traffic**: - - Clients and nodes generate dummy "loop" packets that circulate through the network - - These packets are indistinguishable from real traffic - - Creates a baseline level of traffic that hides actual communication patterns - - Provides unobservability (hiding when and how much real traffic is being sent) - -3. **Stratified Network Architecture**: - - Traffic flows through Entry Gateway → 3 Mixnode Layers → Exit Gateway - - Path selection is independent per-message (unlike Tor) - - Each node connects only to adjacent layers - -4. **Anonymous Replies**: - - Single-Use Reply Blocks (SURBs) allow receiving messages without revealing identity - - Enables bidirectional communication while maintaining privacy - -## Network Monitoring Architecture - -The network monitoring system is a core component that measures mixnet reliability: - -1. The `nym-network-monitor` sends test packets through the network -2. These packets follow predefined routes through multiple mixnodes -3. Metrics are collected about: - - Successful and failed packet deliveries - - Node reliability (percentage of successful packet handling) - - Route reliability (which specific route combinations work best) -4. Results are stored in the database and used by `nym-api` to: - - Present node performance statistics - - Determine network rewards - - Provide route selection guidance to clients - -In the current branch, metrics collection is being enhanced with a fanout approach to submit to multiple API endpoints. - -## Development Environment - -### Required Dependencies - -- Rust toolchain (stable, 1.80+) -- Node.js (v20+) and yarn for TypeScript components -- SQLite for local database development -- PostgreSQL for API database (optional, for full API functionality) -- CosmWasm tools for contract development -- For building contracts: `wasm-opt` tool from `binaryen` -- Python 3.8+ for some scripts -- Docker (optional, for containerized development) -- protoc (Protocol Buffers compiler) for some components - -### Environment Configurations - -The `envs/` directory contains pre-configured environments: - -#### Available Environments - -- **`local.env`**: Local development environment - - Points to local services (localhost) - - Uses test mnemonics and keys - - Ideal for testing without external dependencies - -- **`sandbox.env`**: Sandbox test network - - Public test network with real nodes - - Test tokens available from faucet - - Contract addresses for sandbox deployment - - API: https://sandbox-nym-api1.nymtech.net - -- **`mainnet.env`**: Production mainnet - - Real network with real tokens - - Production contract addresses - - API: https://validator.nymtech.net - - Use with caution! - -- **`canary.env`**: Canary deployment - - Pre-release testing environment - - Tests new features before mainnet - -- **`mainnet-local-api.env`**: Hybrid environment - - Uses mainnet contracts but local API - - Useful for API development against mainnet data - -#### Key Environment Variables - -```bash -# Network configuration -NETWORK_NAME=sandbox # Network identifier -BECH32_PREFIX=n # Address prefix (n for sandbox, n for mainnet) -NYM_API=https://sandbox-nym-api1.nymtech.net/api -NYXD=https://rpc.sandbox.nymtech.net -NYM_API_NETWORK=sandbox - -# Contract addresses (network-specific) -MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz -# ... other contract addresses - -# Mnemonic for testing (NEVER use in production) -MNEMONIC="clutch captain shoe salt awake harvest setup primary inmate ugly among become" - -# API Keys and tokens -IPINFO_API_TOKEN=your_token_here -AUTHENTICATOR_PASSWORD=password_here - -# Logging -RUST_LOG=info # Options: error, warn, info, debug, trace -RUST_BACKTRACE=1 # Enable backtraces - -# Database -DATABASE_URL=postgresql://user:pass@localhost/nym_api -``` - -#### Using Environment Files - -```bash -# Load environment and run command -dotenv -f envs/sandbox.env -- cargo run -p nym-api - -# Export to shell -source envs/sandbox.env - -# Use with make targets -dotenv -f envs/sandbox.env -- make run-api-tests -``` - -## Initial Setup - -### First Time Setup - -1. **Install Prerequisites** - ```bash - # Install Rust - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - - # Install Node.js and yarn - # Via nvm (recommended): - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash - nvm install 20 - npm install -g yarn - - # Install build tools - # Ubuntu/Debian: - sudo apt-get install build-essential pkg-config libssl-dev protobuf-compiler libpq-dev - - # macOS: - brew install protobuf postgresql - - # Install wasm-opt for contract builds - npm install -g wasm-opt - - # Add wasm target for Rust - rustup target add wasm32-unknown-unknown - ``` - -2. **Clone and Setup Repository** - ```bash - git clone https://github.com/nymtech/nym.git - cd nym/nym - - # Install JavaScript dependencies - yarn install - - # Build the project - make build - ``` - -3. **Database Setup (Optional, for API development)** - ```bash - # Install PostgreSQL - # Create database - createdb nym_api - - # Run migrations (from nym-api directory) - cd nym-api - sqlx migrate run - ``` - -### Quick Start - -```bash -# Run a mixnode locally -dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode mixnode --id my-mixnode - -# Run a gateway locally -dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode gateway --id my-gateway - -# Run the API server -dotenv -f envs/sandbox.env -- cargo run -p nym-api - -# Run a client -cargo run -p nym-client -- init --id my-client -cargo run -p nym-client -- run --id my-client -``` - -## CI/CD Pipeline - -The project uses GitHub Actions for CI/CD with several key workflows: - -1. **Build and Test**: - - `ci-build.yml`: Main build workflow for Rust components - - Tests are run on multiple platforms (Linux, Windows, macOS) - - Includes formatting check (rustfmt) and linting (clippy) - -2. **Release Process**: - - Binary artifacts are published on release tags - - Multiple platform builds are created - -3. **Documentation**: - - Documentation is automatically built and deployed - -## Database Structure - -The system uses SQLite databases with tables like: -- `mixnode_status`: Status information about mixnodes -- `gateway_status`: Status information about gateways -- `routes`: Route performance information (success/failure of specific paths) -- `monitor_run`: Information about monitoring test runs - -## Development Workflows - -**Note**: Before diving into specific workflows, consult [CODEMAP.md](./CODEMAP.md) to understand the repository structure and [FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md) to discover available APIs and functions. - -### Running a Node - -To run the mixnode or gateway: - -```bash -# Run nym-node as a mixnode with specified identity -cargo run -p nym-node -- run --mode mixnode --id my-mixnode - -# Run nym-node as a gateway -cargo run -p nym-node -- run --mode gateway --id my-gateway -``` - -### Configuration - -Nodes can be configured with files in various locations: -- Command-line arguments -- Environment variables -- `.env` files specified with `--config-env-file` - -### Monitoring - -To monitor the health of your node: -- View logs for real-time information -- Use the node's HTTP API for status information -- Check the explorer for public node statistics - -## Common Libraries - -For a comprehensive catalog of functions and APIs available in these libraries, see [FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md). - -- `common/types`: Shared data types across all components -- `common/crypto`: Cryptographic primitives and wrappers -- `common/client-core`: Core client functionality -- `common/gateway-client`: Client-gateway communication -- `common/task`: Task management and concurrency utilities -- `common/nymsphinx`: Sphinx packet implementation for mixnet -- `common/topology`: Network topology management -- `common/credentials`: Credential system for privacy-preserving authentication -- `common/bandwidth-controller`: Bandwidth management and accounting - -## Code Conventions - -- Error handling: Use anyhow/thiserror for structured error handling -- Logging: Use the tracing framework for logging and diagnostics -- State management: Generally use Tokio/futures for async code -- Configuration: Use the config crate and env vars with defaults -- Database: Use sqlx for type-safe database queries -- Follow clippy recommendations and rustfmt formatting -- Use semantic commit messages: feat, fix, docs, refactor, test, chore - -## When Making Changes - -- Run `make test` before submitting PRs -- Follow Rust naming conventions -- Use `clippy` to check for common issues -- Update SQLx query caches when modifying DB queries: `cargo sqlx prepare` -- Consider backward compatibility for protocol changes -- Use lefthook pre-commit hooks for TypeScript formatting -- Run `cargo deny check` to verify dependency compliance -- Test against both sandbox and local environments when possible -- Update relevant documentation and CHANGELOG.md - -## Development Tools - -### Useful Cargo Commands - -```bash -# Check for outdated dependencies -cargo outdated - -# Analyze binary size -cargo bloat --release -p nym-node - -# Generate dependency graph -cargo tree -p nym-api - -# Run with instrumentation -cargo run --features profiling -p nym-node - -# Check for security advisories -cargo audit -``` - -### Database Tools - -```bash -# SQLx CLI for migrations -cargo install sqlx-cli - -# Create new migration -cd nym-api && sqlx migrate add - -# Prepare query metadata for offline compilation -cargo sqlx prepare --workspace - -# View database schema -./nym-api/enter_db.sh -``` - -### Development Scripts - -- `scripts/build_topology.py`: Generate network topology files -- `scripts/node_api_check.py`: Verify node API endpoints -- `scripts/network_tunnel_manager.sh`: Manage network tunnels -- `scripts/localnet_start.sh`: Start a local test network -- Various deployment scripts in `deployment/` for different environments - -## Debugging - -- Enable more verbose logging with the RUST_LOG environment variable: - ``` - RUST_LOG=debug,nym_node=trace cargo run -p nym-node -- run --mode mixnode - ``` -- Use the HTTP API endpoints for status information -- Check monitoring data in the database for network performance metrics -- For complex issues, use tracing tools to follow packet flow -- Enable backtraces: `RUST_BACKTRACE=full` -- For WASM debugging: Use browser developer tools with source maps - -## Deployment and Advanced Configurations - -### Deployment Structure - -The `deployment/` directory contains Ansible playbooks and configurations for various deployment scenarios: - -- **`aws/`**: AWS-specific deployment configurations -- **`mixnode/`**: Mixnode deployment playbooks -- **`gateway/`**: Gateway deployment playbooks -- **`validator/`**: Validator node deployment -- **`sandbox-v2/`**: Complete sandbox environment setup -- **`big-dipper-2/`**: Block explorer deployment - -### Sandbox V2 Deployment - -The sandbox-v2 deployment (`deployment/sandbox-v2/`) provides a complete test environment: - -```bash -# Key playbooks: -- deploy.yaml # Main deployment orchestrator -- deploy-mixnodes.yaml # Deploy mixnodes -- deploy-gateways.yaml # Deploy gateways -- deploy-validators.yaml # Deploy validator nodes -- deploy-nym-api.yaml # Deploy API services -``` - -### Custom Environment Setup - -To create a custom environment: - -1. Copy an existing env file: `cp envs/sandbox.env envs/custom.env` -2. Modify the network endpoints and contract addresses -3. Update the `NETWORK_NAME` to your identifier -4. Set appropriate mnemonics and keys (use fresh ones for production!) - -### Contract Addresses - -Contract addresses are network-specific and defined in environment files: -- Mixnet contract: Manages mixnode/gateway registry -- Vesting contract: Handles token vesting schedules -- Coconut contracts: Privacy-preserving credentials -- Name service: Human-readable address mapping -- Ecash contract: Electronic cash functionality - -### Local Network Setup - -For a completely local network: -```bash -# Start local chain -./scripts/localnet_start.sh - -# Deploy contracts -cd contracts -make deploy-local - -# Start nodes with local config -dotenv -f envs/local.env -- cargo run -p nym-node -- run --mode mixnode -``` - -## Common Issues and Troubleshooting - -### Database Issues - -- When modifying database queries, you must update SQLx query caches: - ```bash - cargo sqlx prepare - ``` -- If you see SQLx errors about missing query files, this is likely the cause -- For "database is locked" errors with SQLite, ensure only one process accesses the DB -- For PostgreSQL connection issues, verify DATABASE_URL and that the server is running - -### API Connection Issues - -- Check the environment variables pointing to the APIs (NYM_API, NYXD) -- Verify network connectivity and API health endpoints -- For authentication issues, check node keys and credentials -- Common endpoints to verify: - - API health: `$NYM_API/health` - - Chain status: `$NYXD/status` - - Contract info: `$NYXD/cosmwasm/wasm/v1/contract/$CONTRACT_ADDRESS` - -### Build Problems - -- Clean dependencies with `cargo clean` for a fresh build -- Check for compatible Rust version (1.80+ recommended) -- For smart contract builds, ensure wasm-opt is installed: `npm install -g wasm-opt` -- For cross-compilation issues, check target-specific dependencies -- WASM build issues: Ensure wasm32-unknown-unknown target is installed: - ```bash - rustup target add wasm32-unknown-unknown - ``` -- For "cannot find -lpq" errors, install PostgreSQL development files: - ```bash - # Ubuntu/Debian - sudo apt-get install libpq-dev - # macOS - brew install postgresql - ``` - -### Environment Issues - -- Contract address mismatches: Ensure you're using the correct environment file -- "Account sequence mismatch": The account nonce is out of sync, wait and retry -- Token decimal issues: Sandbox uses different decimal places than mainnet -- API version mismatches: Ensure your local API version matches the network -- "Insufficient funds": Get test tokens from faucet (sandbox) or check balance -- Gateway/mixnode bonding issues: Verify minimum stake requirements - -## Working with Routes and Monitoring - -1. Route monitoring metrics are stored in a `routes` table with: - - Layer node IDs (layer1, layer2, layer3, gw) - - Success flag (boolean) - - Timestamp - -2. To analyze routes: - - Check `NetworkAccount` and `AccountingRoute` in `nym-network-monitor/src/accounting.rs` - - View monitoring logic in `common/nymsphinx/chunking/monitoring.rs` - - Observe how routes are submitted to the database in the `submit_accounting_routes_to_db` function - -## Performance Optimization - -### Profiling and Benchmarking - -```bash -# Run benchmarks -cargo bench -p nym-node - -# Profile with perf (Linux) -cargo build --release --features profiling -perf record --call-graph=dwarf ./target/release/nym-node run --mode mixnode -perf report - -# Generate flamegraph -cargo install flamegraph -cargo flamegraph --bin nym-node -- run --mode mixnode -``` - -### Common Performance Considerations - -- Use bounded channels for backpressure -- Batch database operations where possible -- Monitor memory usage with `RUST_LOG=nym_node::metrics=debug` -- Use connection pooling for database connections -- Consider using `jemalloc` for better memory allocation performance \ No newline at end of file diff --git a/CODEMAP.md b/CODEMAP.md deleted file mode 100644 index 37039c03c8..0000000000 --- a/CODEMAP.md +++ /dev/null @@ -1,452 +0,0 @@ -# Nym Repository Codemap - - - -## Quick Navigation Index - -| Component | Location | Purpose | -|-----------|----------|---------| -| [Main Executables](#main-executables) | Root directories | Core binaries and services | -| [Client Implementations](#client-implementations) | `/clients/` | Various client types | -| [Common Libraries](#common-libraries) | `/common/` | 70+ shared modules | -| [Smart Contracts](#smart-contracts) | `/contracts/` | CosmWasm contracts | -| [SDKs](#sdks) | `/sdk/` | Multi-language SDKs | -| [WASM Modules](#wasm-modules) | `/wasm/` | Browser implementations | -| [Service Providers](#service-providers) | `/service-providers/` | Exit nodes & routers | -| [Tools](#tools-and-utilities) | `/tools/` | CLI tools & utilities | -| [Configuration](#configuration-and-environments) | `/envs/` | Environment configs | - -## Repository Structure Overview - -``` -nym/ -├── Cargo.toml # Workspace manifest (170+ members) -├── Cargo.lock # Locked dependencies -├── Makefile # Build automation -├── CLAUDE.md # Development guidelines -├── envs/ # Environment configurations -│ ├── local.env # Local development -│ ├── sandbox.env # Test network -│ ├── mainnet.env # Production -│ └── canary.env # Pre-release -├── assets/ # Images, logos, fonts -├── docker/ # Docker configurations -└── scripts/ # Deployment & setup scripts -``` - - - -## Main Executables - -### Core Network Nodes - -#### **nym-node** (v1.19.0) - Universal Node Binary -- **Path**: `/nym-node/` -- **Entry**: `src/main.rs` -- **Modes**: `mixnode`, `gateway` -- **Key Modules**: - - `cli/` - Command-line interface - - `config/` - Configuration management - - `node/` - Core node logic - - `wireguard/` - WireGuard VPN integration - - `throughput_tester/` - Performance testing - - - -#### **nym-api** - Network API Server -- **Path**: `/nym-api/` -- **Entry**: `src/main.rs` -- **Database**: PostgreSQL with SQLx -- **Migrations**: `/migrations/` (25+ migration files) -- **Key Subsystems**: - - `circulating_supply_api/` - Token supply tracking - - `ecash/` - E-cash credential management - - `epoch_operations/` - Epoch advancement - - `network_monitor/` - Health monitoring - - `node_performance/` - Performance metrics - - `nym_nodes/` - Node registry - -#### **gateway** (Legacy, v1.1.36) -- **Path**: `/gateway/` -- **Status**: Being phased out for nym-node -- **New**: `src/node/lp_listener/` (branch: drazen/lp-reg) - -### Supporting Services - -| Service | Path | Purpose | -|---------|------|---------| -| `nym-network-monitor` | `/nym-network-monitor/` | Network reliability testing | -| `nym-validator-rewarder` | `/nym-validator-rewarder/` | Reward calculation | -| `nyx-chain-watcher` | `/nyx-chain-watcher/` | Blockchain monitoring | -| `nym-credential-proxy` | `/nym-credential-proxy/` | Credential services | -| `nym-statistics-api` | `/nym-statistics-api/` | Statistics aggregation | -| `nym-node-status-api` | `/nym-node-status-api/` | Node status tracking | - -## Client Implementations - -### Directory: `/clients/` - -``` -clients/ -├── native/ # Native Rust client -│ └── websocket-requests/ # WebSocket protocol -├── socks5/ # SOCKS5 proxy client -├── validator/ # Blockchain validator client -└── webassembly/ # Browser-based client -``` - - - -## Common Libraries - -### Directory: `/common/` (70+ modules) - -### Core Infrastructure -| Module | Purpose | Key Types | -|--------|---------|-----------| -| `nym-common` | Shared utilities | Constants, helpers | -| `types` | Common data types | NodeId, MixId | -| `config` | Configuration system | Config traits | -| `commands` | CLI structures | Command builders | -| `bin-common` | Binary utilities | Logging, banners | - -### Cryptography & Security -| Module | Purpose | Dependencies | -|--------|---------|-------------| -| `crypto` | Crypto primitives | Ed25519, X25519 | -| `credentials` | Credential system | BLS12-381 | -| `credentials-interface` | Interface definitions | - | -| `credential-verification` | Validation logic | - | -| `pemstore` | PEM storage | - | - -### Network Protocol (Sphinx) - - -``` -nymsphinx/ -├── types/ # Core types -├── chunking/ # Message fragmentation -├── forwarding/ # Packet forwarding -├── routing/ # Route selection -├── addressing/ # Address handling -├── anonymous-replies/ # SURB system -├── acknowledgements/ # ACK handling -├── cover/ # Cover traffic -├── params/ # Protocol parameters -└── framing/ # Wire format -``` - -### New Components (Branch: drazen/lp-reg) - - -| Module | Path | Status | -|--------|------|--------| -| `nym-lp` | `/common/nym-lp/` | New LP protocol | -| `nym-lp-common` | `/common/nym-lp-common/` | LP utilities | -| `nym-kcp` | `/common/nym-kcp/` | KCP protocol | - -### Client Systems -``` -client-core/ -├── config-types/ # Configuration types -├── gateways-storage/ # Gateway persistence -└── surb-storage/ # SURB storage - -client-libs/ -├── gateway-client/ # Gateway connection -├── mixnet-client/ # Mixnet interaction -└── validator-client/ # Blockchain queries -``` - -### Additional Common Modules - -**Storage & Data**: -- `statistics/` - Statistical collection -- `topology/` - Network topology -- `node-tester-utils/` - Testing utilities -- `ticketbooks-merkle/` - Merkle trees - -**Advanced Features**: -- `dkg/` - Distributed Key Generation -- `ecash-signer-check/` - E-cash validation -- `nym_offline_compact_ecash/` - Offline e-cash - -**Blockchain**: -- `ledger/` - Ledger operations -- `nyxd-scraper/` - Chain scraping -- `cosmwasm-smart-contracts/` - Contract interfaces - -**Utilities**: -- `task/` - Async task management -- `async-file-watcher/` - File watching -- `nym-cache/` - Caching layer -- `nym-metrics/` - Metrics (Prometheus) -- `bandwidth-controller/` - Bandwidth accounting - -## Smart Contracts - -### Directory: `/contracts/` - - - -``` -contracts/ -├── Cargo.toml # Workspace config -├── .cargo/config.toml # WASM build config -├── coconut-dkg/ # DKG contract -├── ecash/ # E-cash contract -├── mixnet/ # Node registry -├── vesting/ # Token vesting -├── nym-pool/ # Liquidity pool -├── multisig/ # Multi-sig wallet -├── performance/ # Performance tracking -└── mixnet-vesting-integration-tests/ -``` - -### Contract Build Process -```bash -make contracts # Build all -make contract-schema # Generate schemas -make wasm-opt-contracts # Optimize -``` - -## SDKs - -### Directory: `/sdk/` - -``` -sdk/ -├── rust/ -│ └── nym-sdk/ # Primary Rust SDK -├── typescript/ -│ ├── packages/ # NPM packages -│ ├── codegen/ # Code generation -│ └── examples/ # Usage examples -└── ffi/ - ├── cpp/ # C++ bindings - ├── go/ # Go bindings - └── shared/ # Shared FFI code -``` - -## WASM Modules - -### Directory: `/wasm/` - -| Module | Purpose | Build Command | -|--------|---------|---------------| -| `client` | Browser client | `make` in directory | -| `mix-fetch` | Privacy fetch API | `make` in directory | -| `node-tester` | Network testing | `make` in directory | -| `zknym-lib` | Zero-knowledge lib | `make` in directory | - - - -## Service Providers - -### Directory: `/service-providers/` - -``` -service-providers/ -├── network-requester/ # Exit node for external requests -├── ip-packet-router/ # IP packet routing (VPN-like) -└── common/ # Shared utilities -``` - -## Tools and Utilities - -### Directory: `/tools/` - -### Public Tools -| Tool | Path | Purpose | -|------|------|---------| -| `nym-cli` | `/tools/nym-cli/` | Node management CLI | -| `nym-id-cli` | `/tools/nym-id-cli/` | Identity management | -| `nymvisor` | `/tools/nymvisor/` | Process supervisor | -| `nym-nr-query` | `/tools/nym-nr-query/` | Network queries | -| `echo-server` | `/tools/echo-server/` | Testing server | - -### Internal Tools -``` -internal/ -├── mixnet-connectivity-check/ # Network diagnostics -├── contract-state-importer/ # Migration tools -├── validator-status-check/ # Validator health -├── ssl-inject/ # SSL injection -├── testnet-manager/ # Testnet management -└── sdk-version-bump/ # Version management -``` - -## Configuration and Environments - -### Environment Files: `/envs/` - - - -| Environment | File | API Endpoint | Use Case | -|------------|------|--------------|----------| -| Local | `local.env` | localhost | Development | -| Sandbox | `sandbox.env` | sandbox-nym-api1.nymtech.net | Testing | -| Mainnet | `mainnet.env` | validator.nymtech.net | Production | -| Canary | `canary.env` | - | Pre-release | - -### Key Environment Variables -```bash -NETWORK_NAME # Network identifier -NYM_API # API endpoint -NYXD # Blockchain RPC -MIXNET_CONTRACT_ADDRESS # Contract addresses -MNEMONIC # Test mnemonic (NEVER in production) -RUST_LOG # Logging level -DATABASE_URL # PostgreSQL connection -``` - -## Build System - -### Primary Build Commands -```bash -make build # Debug build -make build-release # Release build -make test # Run tests -make clippy # Lint code -make fmt # Format code -make contracts # Build contracts -make sdk-wasm-build # Build WASM -``` - -### Workspace Configuration - - - -**Root Cargo.toml Structure**: -- `[workspace]` - Lists all 170+ members -- `[workspace.dependencies]` - Shared dependency versions -- `[workspace.lints]` - Shared lint rules -- `[profile.*]` - Build profiles - -## Database Structure - -### SQLx Usage Pattern -- **Compile-time verified**: All queries checked at build -- **Migration files**: In package `/migrations/` directories -- **Query cache**: `.sqlx/` directory - -### Key Tables (nym-api) -```sql --- Network monitoring -mixnode_status -gateway_status -routes -monitor_run - --- Node registry -nym_nodes -node_descriptions - --- Performance -node_uptime -node_performance -``` - -## Current Branch Context (drazen/lp-reg) - -### New Additions -- `/common/nym-lp/` - Low-level protocol implementation -- `/common/nym-lp-common/` - LP common utilities -- `/common/nym-kcp/` - KCP protocol -- `/gateway/src/node/lp_listener/` - LP listener - -### Modified Files -``` -M Cargo.lock -M Cargo.toml -M common/registration/ -M common/wireguard/ -M gateway/ -M nym-node/ -M nym-node/nym-node-metrics/ -``` - -## Navigation Patterns - - - -### Finding Code by Type -| Code Type | Look In | -|-----------|---------| -| Main executables | Root directories with `src/main.rs` | -| Libraries | `/common/` with descriptive names | -| Contracts | `/contracts/[name]/src/contract.rs` | -| Tests | Colocated with source, `#[cfg(test)]` | -| Configurations | `/envs/` and `config/` subdirs | -| Database queries | Files with `.sql` or SQLx macros | -| API endpoints | `/nym-api/src/` subdirectories | -| CLI commands | `/cli/commands/` in executables | - -### Common Import Locations -```rust -// Crypto -use nym_crypto::asymmetric::{ed25519, x25519}; - -// Network -use nym_sphinx::forwarding::packet::MixPacket; -use nym_topology::NymTopology; - -// Client -use nym_client_core::client::Client; - -// Configuration -use nym_network_defaults::NymNetworkDetails; - -// Contracts -use nym_mixnet_contract_common::*; -``` - -## Module Relationships - - - -### Dependency Graph (Simplified) -``` -nym-node -├── common/nym-common -├── common/crypto -├── common/nymsphinx -├── common/topology -├── common/client-libs/validator-client -└── common/wireguard - -nym-api -├── common/nym-common -├── nym-api-requests -├── common/client-libs/validator-client -├── common/credentials -└── sqlx (database) - -clients/native -├── common/client-core -├── common/client-libs/gateway-client -├── common/nymsphinx -└── common/credentials -``` - -## Development Workflows - -### Adding New Feature -1. Check `/envs/` for configuration -2. Find similar code in `/common/` -3. Implement in appropriate module -4. Add tests colocated with code -5. Update `/nym-api/` if needed -6. Run `make test` and `make clippy` - -### Debugging Network Issues -1. Start with `/nym-network-monitor/` -2. Check `/common/topology/` for routing -3. Review `/common/nymsphinx/` for protocol -4. Examine logs with `RUST_LOG=debug` - -### Contract Development -1. Create in `/contracts/[name]/` -2. Use existing contracts as templates -3. Build with `make contracts` -4. Test with `cw-multi-test` \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4b9e037c37..cefa200acc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,16 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "classic-mceliece-rust" +version = "3.2.0" +source = "git+https://github.com/georgio/classic-mceliece-rust#f2f27048b621df103bbe64369a18174ffec04ae1" +dependencies = [ + "rand 0.9.2", + "sha3", + "zeroize", +] + [[package]] name = "coarsetime" version = "0.1.36" @@ -1447,6 +1457,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-models" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.2", +] + [[package]] name = "cosmos-sdk-proto" version = "0.26.1" @@ -1874,6 +1894,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", + "rand_core 0.6.4", "rustc_version 0.4.1", "serde", "subtle 2.6.1", @@ -3174,6 +3195,43 @@ dependencies = [ "hashbrown 0.15.4", ] +[[package]] +name = "hax-lib" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -4122,6 +4180,15 @@ dependencies = [ "signature", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "keystream" version = "1.0.0" @@ -4200,6 +4267,213 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-ed25519" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +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#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +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#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +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.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.2" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-psq" +version = "0.0.5" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-chacha20poly1305", + "libcrux-ecdh", + "libcrux-ed25519", + "libcrux-hkdf", + "libcrux-hmac", + "libcrux-kem", + "libcrux-ml-kem", + "libcrux-sha2", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.4" +source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", +] + [[package]] name = "libm" version = "0.2.15" @@ -4829,6 +5103,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -4840,7 +5136,7 @@ dependencies = [ [[package]] name = "nym-api" -version = "1.1.69" +version = "1.1.68" dependencies = [ "anyhow", "async-trait", @@ -5003,7 +5299,6 @@ dependencies = [ "nym-network-defaults", "nym-service-provider-requests-common", "nym-sphinx", - "nym-test-utils", "nym-wireguard-types", "rand 0.8.5", "semver 1.0.26", @@ -5011,7 +5306,6 @@ dependencies = [ "sha2 0.10.9", "strum_macros", "thiserror 2.0.12", - "tracing", "x25519-dalek", ] @@ -5020,16 +5314,21 @@ name = "nym-bandwidth-controller" version = "0.1.0" dependencies = [ "async-trait", + "bip39", "log", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-ecash-time", + "nym-network-defaults", "nym-task", "nym-validator-client", "rand 0.8.5", "thiserror 2.0.12", + "url", + "zeroize", ] [[package]] @@ -5063,7 +5362,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.66" +version = "1.1.65" dependencies = [ "anyhow", "base64 0.22.1", @@ -5146,7 +5445,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.66" +version = "1.1.65" dependencies = [ "bs58", "clap", @@ -5582,7 +5881,6 @@ dependencies = [ "sqlx", "sqlx-pool-guard", "thiserror 2.0.12", - "time", "tokio", "zeroize", ] @@ -5618,14 +5916,13 @@ dependencies = [ "nym-api-requests", "nym-credentials", "nym-credentials-interface", - "nym-crypto", "nym-ecash-contract-common", "nym-gateway-requests", "nym-gateway-storage", "nym-metrics", "nym-task", - "nym-upgrade-mode-check", "nym-validator-client", + "rand 0.8.5", "si-scale", "thiserror 2.0.12", "time", @@ -5665,7 +5962,6 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", - "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -5817,6 +6113,7 @@ dependencies = [ name = "nym-gateway" version = "1.1.36" dependencies = [ + "anyhow", "async-trait", "bincode", "bip39", @@ -5828,6 +6125,7 @@ dependencies = [ "futures", "ipnetwork", "mock_instant", + "nym-api-requests", "nym-authenticator-requests", "nym-client-core", "nym-credential-verification", @@ -5843,6 +6141,7 @@ dependencies = [ "nym-lp", "nym-metrics", "nym-mixnet-client", + "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", "nym-node-metrics", @@ -5853,18 +6152,20 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-topology", - "nym-upgrade-mode-check", + "nym-types", "nym-validator-client", "nym-wireguard", "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", + "sha2 0.10.9", "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tokio-tungstenite", + "tokio-util", "tracing", "url", "zeroize", @@ -6083,7 +6384,6 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "tracing-subscriber", "url", "wasmtimer", ] @@ -6247,6 +6547,35 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "nym-kkt" +version = "0.1.0" +dependencies = [ + "aead", + "arc-swap", + "blake3", + "bytes", + "classic-mceliece-rust", + "criterion", + "curve25519-dalek", + "futures", + "libcrux-ecdh", + "libcrux-kem", + "libcrux-ml-kem", + "libcrux-psq", + "libcrux-sha3", + "libcrux-traits", + "nym-crypto", + "pin-project", + "rand 0.9.2", + "strum", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "zeroize", +] + [[package]] name = "nym-ledger" version = "0.1.0" @@ -6268,16 +6597,24 @@ dependencies = [ "bytes", "criterion", "dashmap", + "libcrux-kem", + "libcrux-psq", + "libcrux-traits", + "num_enum", "nym-crypto", + "nym-kkt", "nym-lp-common", "nym-sphinx", "parking_lot", "rand 0.8.5", + "rand 0.9.2", "rand_chacha 0.3.1", "serde", "sha2 0.10.9", "snow", "thiserror 2.0.12", + "tls_codec", + "tracing", "utoipa", ] @@ -6428,7 +6765,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.67" +version = "1.1.66" dependencies = [ "addr", "anyhow", @@ -6478,7 +6815,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.21.0" +version = "1.20.0" dependencies = [ "anyhow", "arc-swap", @@ -6509,7 +6846,6 @@ dependencies = [ "nym-bin-common", "nym-client-core-config-types", "nym-config", - "nym-credential-verification", "nym-crypto", "nym-gateway", "nym-gateway-stats-storage", @@ -6582,13 +6918,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", + "humantime", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", - "nym-upgrade-mode-check", "nym-wireguard-types", "rand_chacha 0.3.1", "schemars 0.8.22", @@ -6599,7 +6935,6 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", - "url", "utoipa", ] @@ -7018,7 +7353,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.66" +version = "1.1.65" dependencies = [ "bs58", "clap", @@ -7652,24 +7987,36 @@ dependencies = [ name = "nym-wireguard" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", + "bincode", + "chrono", + "dashmap", "defguard_wireguard_rs", + "dyn-clone", "futures", "ip_network", + "ipnetwork", + "log", + "nym-authenticator-requests", "nym-credential-verification", "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", "nym-gateway-storage", + "nym-ip-packet-requests", "nym-metrics", "nym-network-defaults", "nym-node-metrics", "nym-task", "nym-wireguard-types", + "rand 0.8.5", "thiserror 2.0.12", + "time", "tokio", "tokio-stream", "tracing", + "x25519-dalek", ] [[package]] @@ -7721,20 +8068,15 @@ version = "1.0.0" dependencies = [ "async-trait", "axum", - "futures", "nym-credential-verification", "nym-credentials-interface", - "nym-crypto", "nym-http-api-client", "nym-http-api-common", - "nym-upgrade-mode-check", "nym-wireguard", "nym-wireguard-private-metadata-client", "nym-wireguard-private-metadata-server", "nym-wireguard-private-metadata-shared", - "time", "tokio", - "tower 0.5.2", "tower-http 0.5.2", "utoipa", ] @@ -7744,7 +8086,10 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", + "log", + "nym-config", "nym-crypto", + "nym-network-defaults", "rand 0.8.5", "serde", "thiserror 2.0.12", @@ -7753,7 +8098,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.31" +version = "0.1.30" dependencies = [ "anyhow", "bytes", @@ -8107,6 +8452,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "peg" version = "0.8.5" @@ -9852,6 +10203,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -10838,6 +11199,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.106", +] + [[package]] name = "tokio" version = "1.47.1" diff --git a/Cargo.toml b/Cargo.toml index d6e5f7464a..8bf7cc2533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ members = [ "common/nym-kcp", "common/nym-lp", "common/nym-lp-common", + "common/nym-kkt", "common/nym-metrics", "common/nym_offline_compact_ecash", "common/nymnoise", @@ -153,7 +154,7 @@ members = [ "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", "tools/internal/mixnet-connectivity-check", -# "tools/internal/sdk-version-bump", + # "tools/internal/sdk-version-bump", "tools/internal/ssl-inject", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", @@ -168,7 +169,7 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", - "nym-gateway-probe" + "nym-gateway-probe", ] default-members = [ @@ -288,7 +289,9 @@ inventory = "0.3.21" ip_network = "0.4.1" ipnetwork = "0.20" itertools = "0.14.0" -jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } +jwt-simple = { version = "0.12.12", default-features = false, features = [ + "pure-rust", +] } k256 = "0.13" lazy_static = "1.5.0" ledger-transport = "0.10.0" @@ -298,6 +301,7 @@ mime = "0.3.17" moka = { version = "0.12", features = ["future"] } nix = "0.27.1" notify = "5.1.0" +num_enum = "0.7.5" once_cell = "1.21.3" opentelemetry = "0.19.0" opentelemetry-jaeger = "0.18.0" @@ -344,6 +348,7 @@ test-with = { version = "0.15.4", default-features = false } tempfile = "3.20" thiserror = "2.0" time = "0.3.41" +tls_codec = "0.4.1" tokio = "1.47" tokio-postgres = "0.7" tokio-stream = "0.1.17" diff --git a/FUNCTION_LEXICON.md b/FUNCTION_LEXICON.md deleted file mode 100644 index d71fbc038f..0000000000 --- a/FUNCTION_LEXICON.md +++ /dev/null @@ -1,909 +0,0 @@ -# Nym Function Lexicon - - - -## Quick Reference Index - -| Category | Section | Key Operations | -|----------|---------|----------------| -| [Node Operations](#1-node-operations) | Mixnode & Gateway | Initialization, key management, tasks | -| [Sphinx Protocol](#2-sphinx-packet-protocol) | Packet Processing | Message creation, chunking, routing | -| [Client APIs](#3-client-apis) | Client Operations | Connection, sending, receiving | -| [Network Topology](#4-network-topology) | Routing | Topology queries, route selection | -| [Blockchain](#5-blockchain-operations) | Validator Client | Queries, transactions, contracts | -| [REST APIs](#6-rest-api-endpoints) | HTTP Handlers | API routes and responses | -| [Credentials](#7-credential--ecash) | E-cash | Credential creation, verification | -| [Smart Contracts](#8-smart-contracts) | CosmWasm | Entry points, messages | -| [Common Patterns](#9-common-patterns) | Conventions | Naming, errors, async | - ---- - -## 1. Node Operations - -### nym-node Core Functions - - -**Module**: `nym-node/src/node/mod.rs` - -```rust -// Node initialization -pub async fn initialise_node( - config: &Config, - rng: &mut impl CryptoRng + RngCore, -) -> Result - -// Key management -pub fn load_x25519_wireguard_keypair( - paths: &KeysPaths, -) -> Result - -pub fn load_ed25519_identity_keypair( - paths: &KeysPaths, -) -> Result - -// Gateway-specific initialization -impl GatewayTasksData { - pub async fn new( - config: &GatewayTasksConfig, - client_storage: ClientStorage, - ) -> Result - - pub fn initialise( - config: &GatewayTasksConfig, - force_init: bool, - ) -> Result<(), GatewayError> -} - -// Service provider initialization -impl ServiceProvidersData { - pub fn initialise_client_keys( - rng: &mut R, - gateway_paths: &GatewayPaths, - ) -> Result - - pub async fn initialise_network_requester( - rng: &mut R, - config: &Config, - ) -> Result, GatewayError> -} -``` - -### Gateway Task Builder Pattern -**Module**: `gateway/src/node/mod.rs` - -```rust -pub struct GatewayTasksBuilder { - // Builder methods - pub fn new( - identity_keypair: Arc, - config: Config, - client_storage: ClientStorage, - ) -> GatewayTasksBuilder - - pub fn set_network_requester_opts( - &mut self, - opts: Option - ) -> &mut Self - - pub fn set_ip_packet_router_opts( - &mut self, - opts: Option - ) -> &mut Self - - pub async fn build_and_run( - self, - shutdown: TaskManager, - ) -> Result<(), GatewayError> -} -``` - - - ---- - -## 2. Sphinx Packet Protocol - -### Message Construction & Processing -**Module**: `common/nymsphinx/src/message.rs` - -```rust -// Core message types -pub enum NymMessage { - Plain(Vec), - Repliable(RepliableMessage), - Reply(ReplyMessage), -} - -impl NymMessage { - // Constructors - pub fn new_plain(msg: Vec) -> NymMessage - pub fn new_repliable(msg: RepliableMessage) -> NymMessage - pub fn new_reply(msg: ReplyMessage) -> NymMessage - pub fn new_additional_surbs_request( - recipient: Recipient, - amount: u32 - ) -> NymMessage - - // Processing - pub fn pad_to_full_packet_lengths( - self, - plaintext_per_packet: usize - ) -> PaddedMessage - - pub fn split_into_fragments( - self, - rng: &mut R, - packet_size: PacketSize, - ) -> Vec - - pub fn remove_padding(self) -> Result - - // Queries - pub fn is_reply_surb_request(&self) -> bool - pub fn available_sphinx_plaintext_per_packet( - &self, - packet_size: PacketSize - ) -> usize - pub fn required_packets(&self, packet_size: PacketSize) -> usize -} -``` - -### Payload Building & Preparation -**Module**: `common/nymsphinx/src/preparer.rs` - -```rust -pub struct NymPayloadBuilder { - // Main preparation methods - pub async fn prepare_chunk_for_sending( - &mut self, - message: NymMessage, - topology: &NymTopology, - ) -> Result, NymPayloadBuilderError> - - pub async fn prepare_reply_chunk_for_sending( - &mut self, - reply: NymMessage, - reply_surb: ReplySurb, - ) -> Result, NymPayloadBuilderError> - - // SURB generation - pub fn generate_reply_surbs( - &mut self, - amount: u32, - topology: &NymTopology, - ) -> Result, NymPayloadBuilderError> - - // Fragment splitting - pub fn pad_and_split_message( - &mut self, - message: NymMessage, - ) -> Result, NymPayloadBuilderError> -} - -// Builder constructors -pub fn build_regular( - rng: R, - sender_address: Option, -) -> NymPayloadBuilder - -pub fn build_reply( - sender_address: Recipient, - sender_tag: AnonymousSenderTag, -) -> NymPayloadBuilder -``` - -### Chunking & Fragmentation -**Module**: `common/nymsphinx/chunking/src/lib.rs` - - - -```rust -// Main chunking function -pub fn split_into_sets( - message: &[u8], - max_plaintext_size: usize, - max_fragments_per_set: usize, -) -> Result>, ChunkingError> - -// Fragment monitoring (optional feature) -pub mod monitoring { - pub fn enable() - pub fn enabled() -> bool - pub fn fragment_received(fragment: &Fragment) - pub fn fragment_sent( - fragment: &Fragment, - client_nonce: i32, - destination: PublicKey - ) -} -``` - ---- - -## 3. Client APIs - -### Gateway Client -**Module**: `common/client-libs/gateway-client/src/lib.rs` - -```rust -pub struct GatewayClient { - // Connection management - pub async fn connect( - config: GatewayClientConfig, - ) -> Result - - pub async fn authenticate( - &mut self, - credentials: Credentials, - ) -> Result<(), GatewayClientError> - - // Message operations - pub async fn send_mix_packet( - &self, - packet: MixPacket, - ) -> Result<(), GatewayClientError> - - pub async fn receive_messages( - &mut self, - ) -> Result, GatewayClientError> -} - -// Packet routing -pub struct PacketRouter { - pub fn new( - mix_tx: MixnetMessageSender, - ack_tx: AcknowledgementSender, - ) -> PacketRouter - - pub async fn route_packet( - &self, - packet: MixPacket, - ) -> Result<(), PacketRouterError> -} -``` - -### Mixnet Client -**Module**: `common/client-libs/mixnet-client/src/lib.rs` - -```rust -pub struct Client { - // Core client operations - pub async fn new(config: Config) -> Result - - pub async fn send_message( - &mut self, - recipient: Recipient, - message: Vec, - ) -> Result<(), ClientError> - - pub async fn receive_message( - &mut self, - ) -> Result - - // Connection management - pub fn is_connected(&self) -> bool - pub async fn reconnect(&mut self) -> Result<(), ClientError> -} - -// Send without response trait -pub trait SendWithoutResponse { - fn send_without_response( - &self, - packet: MixPacket, - ) -> io::Result<()> -} -``` - - - -### Client Core Initialization -**Module**: `common/client-core/src/init.rs` - -```rust -// Key generation -pub fn generate_new_client_keys( - rng: &mut R, -) -> (ed25519::KeyPair, x25519::KeyPair) - -// Storage initialization -pub async fn init_storage( - paths: &ClientPaths, -) -> Result - -// Configuration setup -pub fn setup_client_config( - id: &str, - network: Network, -) -> Result -``` - ---- - -## 4. Network Topology - -### Topology Management -**Module**: `common/topology/src/lib.rs` - -```rust -pub struct NymTopology { - // Query methods - pub fn mixnodes(&self) -> &[RoutingNode] - pub fn gateways(&self) -> &[RoutingNode] - pub fn layer_nodes(&self, layer: MixLayer) -> Vec<&RoutingNode> - - // Route selection - pub fn random_route( - &self, - rng: &mut R, - ) -> Option> - - pub fn get_node_by_id(&self, node_id: NodeId) -> Option<&RoutingNode> -} - -// Route provider -pub struct NymRouteProvider { - pub fn new(topology: NymTopology) -> NymRouteProvider - - pub fn random_route( - &self, - rng: &mut R, - ) -> Option> -} - -// Topology provider trait -pub trait TopologyProvider { - async fn get_topology(&self) -> Result - async fn refresh_topology(&mut self) -> Result<(), NymTopologyError> -} -``` - -### Routing Node -**Module**: `common/topology/src/node.rs` - -```rust -pub struct RoutingNode { - pub fn node_id(&self) -> NodeId - pub fn identity_key(&self) -> &ed25519::PublicKey - pub fn sphinx_key(&self) -> &x25519::PublicKey - pub fn mix_host(&self) -> &SocketAddr - pub fn clients_ws_address(&self) -> Option<&Url> -} -``` - ---- - -## 5. Blockchain Operations - -### Validator Client -**Module**: `common/client-libs/validator-client/src/client.rs` - - - -```rust -pub struct Client { - // Contract queries - pub async fn query_contract_state( - &self, - contract: &str, - query: T, - ) -> Result - where T: Into - - // Transaction execution (requires signer) - pub async fn execute_contract_message( - &self, - contract: &str, - msg: M, - funds: Vec, - ) -> Result - where M: Into - - // Specific contract operations - pub async fn bond_mixnode( - &self, - mixnode: MixNode, - cost_params: MixNodeCostParams, - pledge: Coin, - ) -> Result - - pub async fn unbond_mixnode(&self) -> Result - - pub async fn delegate_to_mixnode( - &self, - mix_id: MixId, - amount: Coin, - ) -> Result -} - -// Nyxd-specific client -pub type DirectSigningHttpRpcNyxdClient = - nyxd::NyxdClient; -``` - -### Contract Queries -**Module**: `common/client-libs/validator-client/src/nyxd/contract_traits/` - -```rust -// Mixnet contract queries -pub trait MixnetQueryClient { - async fn get_mixnodes(&self) -> Result, NyxdError> - async fn get_gateways(&self) -> Result, NyxdError> - async fn get_current_epoch(&self) -> Result - async fn get_rewarded_set(&self) -> Result -} - -// Vesting contract queries -pub trait VestingQueryClient { - async fn get_vesting_details(&self, address: &str) - -> Result -} - -// E-cash contract queries -pub trait EcashQueryClient { - async fn get_deposit(&self, id: DepositId) - -> Result -} -``` - ---- - -## 6. REST API Endpoints - -### nym-api Main Routes -**Module**: `nym-api/src/main.rs` and submodules - - - -```rust -// Main API setup -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - // Router configuration - let app = Router::new() - .merge(api_routes()) - .merge(swagger_ui()) - .layer(cors_layer()) - .layer(trace_layer()); -} - -// Core API routes (various modules) -pub fn api_routes() -> Router { - Router::new() - .nest("/v1/status", status_routes()) - .nest("/v1/mixnodes", mixnode_routes()) - .nest("/v1/gateways", gateway_routes()) - .nest("/v1/network", network_routes()) - .nest("/v1/ecash", ecash_routes()) -} -``` - -### Status Routes -**Module**: `nym-api/src/status/mod.rs` - -```rust -pub async fn status_handler() -> impl IntoResponse { - Json(ApiStatusResponse { - status: "ok", - uptime: get_uptime(), - }) -} - -pub async fn health_check() -> impl IntoResponse { - StatusCode::OK -} -``` - -### Network Monitor Routes -**Module**: `nym-api/src/network_monitor/mod.rs` - -```rust -pub async fn get_monitor_report( - State(state): State, -) -> Result, ApiError> { - // Returns network reliability report -} - -pub async fn get_node_reliability( - Path(node_id): Path, - State(state): State, -) -> Result, ApiError> { - // Returns specific node reliability -} -``` - -### E-cash API -**Module**: `nym-api/src/ecash/mod.rs` - -```rust -pub async fn verify_credential( - Json(credential): Json, - State(state): State, -) -> Result, ApiError> { - // Verifies e-cash credentials -} - -pub async fn issue_credential( - Json(request): Json, - State(state): State, -) -> Result, ApiError> { - // Issues new e-cash credentials -} -``` - ---- - -## 7. Credential & E-cash - -### Credential Operations -**Module**: `common/credentials/src/ecash/mod.rs` - -```rust -// Credential spending -pub struct CredentialSpendingData { - pub fn new( - ticketbook: IssuedTicketBook, - gateway_identity: ed25519::PublicKey, - ) -> CredentialSpendingData - - pub fn prepare_for_spending( - &self, - request_id: i64, - ) -> PreparedCredential -} - -// Credential signing -pub struct CredentialSigningData { - pub fn sign_credential( - &self, - blinded_credential: BlindedCredential, - ) -> Result -} - -// Aggregation utilities -pub fn aggregate_verification_keys( - keys: Vec, -) -> AggregatedVerificationKey - -pub fn obtain_aggregate_wallet( - verification_keys: Vec, - commitments: Vec, -) -> Result -``` - -### Ticketbook Operations -**Module**: `common/credentials/src/ecash/bandwidth/mod.rs` - - - -```rust -pub struct IssuedTicketBook { - pub fn new( - tickets: Vec, - expiration: OffsetDateTime, - ) -> IssuedTicketBook - - pub fn total_bandwidth(&self) -> Bandwidth - pub fn is_expired(&self) -> bool - pub fn consume_ticket(&mut self) -> Option -} - -pub struct ImportableTicketBook { - pub fn try_from_base58(s: &str) -> Result - pub fn into_issued(self) -> Result -} -``` - ---- - -## 8. Smart Contracts - -### Mixnet Contract Entry Points -**Module**: `contracts/mixnet/src/contract.rs` - -```rust -#[entry_point] -pub fn instantiate( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, -) -> Result - -#[entry_point] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result - -#[entry_point] -pub fn query( - deps: Deps, - env: Env, - msg: QueryMsg, -) -> StdResult -``` - -### Execute Message Handlers -**Module**: `contracts/mixnet/src/contract.rs` - -```rust -// Node operations -fn try_bond_mixnode( - deps: DepsMut, - env: Env, - info: MessageInfo, - mixnode: MixNode, -) -> Result - -fn try_unbond_mixnode( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result - -// Delegation operations -fn try_delegate( - deps: DepsMut, - env: Env, - info: MessageInfo, - mix_id: MixId, -) -> Result - -fn try_undelegate( - deps: DepsMut, - env: Env, - info: MessageInfo, - mix_id: MixId, -) -> Result - -// Reward operations -fn try_reward_mixnode( - deps: DepsMut, - env: Env, - mix_id: MixId, - performance: Performance, -) -> Result -``` - -### Query Message Handlers -```rust -fn query_mixnode(deps: Deps, mix_id: MixId) -> StdResult -fn query_gateways(deps: Deps) -> StdResult> -fn query_rewarded_set(deps: Deps, epoch: Epoch) -> StdResult -fn query_current_epoch(deps: Deps) -> StdResult -``` - ---- - -## 9. Common Patterns - -### Function Naming Conventions - - -```rust -// Constructors -pub fn new(...) -> Self // Standard constructor -pub fn with_defaults() -> Self // Constructor with defaults -pub fn from_config(config: Config) -> Self // From configuration - -// Async initialization -pub async fn init(...) -> Result // Async initialization -pub async fn initialise(...) -> Result // British spelling variant -pub async fn setup(...) -> Result // Setup function - -// Builder pattern -pub fn builder() -> TBuilder // Create builder -pub fn set_field(mut self, val: T) -> Self // Builder setter -pub fn build(self) -> Result // Build final object - -// Getters -pub fn field(&self) -> &T // Immutable reference -pub fn field_mut(&mut self) -> &mut T // Mutable reference -pub fn into_inner(self) -> T // Consume and return inner - -// Queries -pub fn is_valid(&self) -> bool // Boolean check -pub fn has_field(&self) -> bool // Existence check -pub fn contains(&self, item: &T) -> bool // Contains check - -// Transformations -pub fn to_type(&self) -> Type // Convert to type -pub fn into_type(self) -> Type // Consume and convert -pub fn try_into_type(self) -> Result // Fallible conversion -``` - -### Error Handling Patterns - -```rust -// Custom error types with thiserror -#[derive(Error, Debug)] -pub enum ModuleError { - #[error("Network error: {0}")] - Network(#[from] NetworkError), - - #[error("Invalid configuration: {reason}")] - InvalidConfig { reason: String }, - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -// Result type alias -pub type Result = std::result::Result; - -// Error conversion -impl From for ModuleError { - fn from(err: io::Error) -> Self { - ModuleError::Io(err) - } -} -``` - -### Async Patterns - -```rust -// Async trait (with async-trait crate) -#[async_trait] -pub trait AsyncOperation { - async fn perform(&self) -> Result<()>; -} - -// Spawning tasks -tokio::spawn(async move { - // Task code -}); - -// Channels for communication -let (tx, mut rx) = mpsc::channel(100); - -// Select on multiple futures -tokio::select! { - result = future1 => { /* handle */ }, - result = future2 => { /* handle */ }, - _ = shutdown.recv() => { /* shutdown */ }, -} -``` - -### Storage Patterns - -```rust -// SQLx queries -sqlx::query!( - "SELECT * FROM nodes WHERE id = ?", - node_id -) -.fetch_optional(&pool) -.await?; - -// In-memory caching -use dashmap::DashMap; -let cache: DashMap = DashMap::new(); - -// File storage -use std::fs; -fs::write(path, data)?; -let content = fs::read_to_string(path)?; -``` - ---- - -## 10. Import Reference - -### Standard Imports by Category - -```rust -// Nym crypto -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_crypto::symmetric::stream_cipher; - -// Sphinx protocol -use nym_sphinx::forwarding::packet::MixPacket; -use nym_sphinx::framing::codec::NymCodec; -use nym_sphinx::addressing::nodes::NymNodeRoutingAddress; -use nym_sphinx::params::{PacketSize, DEFAULT_PACKET_SIZE}; - -// Client libraries -use nym_client_core::client::Client; -use nym_gateway_client::GatewayClient; -use nym_validator_client::ValidatorClient; - -// Topology -use nym_topology::{NymTopology, RoutingNode}; -use nym_mixnet_contract_common::NodeId; - -// Configuration -use nym_network_defaults::NymNetworkDetails; -use nym_config::defaults::NymNetwork; - -// Async runtime -use tokio::sync::{mpsc, RwLock, Mutex}; -use tokio::time::{sleep, Duration}; -use futures::{StreamExt, SinkExt}; - -// Error handling -use thiserror::Error; -use anyhow::{anyhow, Result, Context}; - -// Logging -use tracing::{debug, info, warn, error, instrument}; - -// Serialization -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; - -// Web framework (API) -use axum::{Router, extract::{Path, Query, State}, response::IntoResponse}; -use axum::Json; -``` - ---- - -## 11. Feature Flags - -### Common Feature Gates - -```rust -// Client-specific features -#[cfg(feature = "client")] -#[cfg(feature = "cli")] - -// Platform-specific -#[cfg(not(target_arch = "wasm32"))] -#[cfg(target_arch = "wasm32")] - -// Testing -#[cfg(test)] -#[cfg(feature = "testing")] -#[cfg(feature = "contract-testing")] - -// Storage backends -#[cfg(feature = "fs-surb-storage")] -#[cfg(feature = "fs-credentials-storage")] - -// Network features -#[cfg(feature = "http-client")] -#[cfg(feature = "websocket")] -``` - ---- - -## Quick Lookup Tables - -### Async vs Sync Functions - -| Operation Type | Typically Async | Typically Sync | -|---------------|-----------------|----------------| -| Network I/O | ✓ | | -| Database queries | ✓ | | -| Contract execution | ✓ | | -| Cryptographic ops | | ✓ | -| Message construction | | ✓ | -| Configuration parsing | | ✓ | -| Topology queries | Both | Both | - -### Return Type Patterns - -| Pattern | Usage | Example | -|---------|-------|---------| -| `Result` | Fallible operations | `connect() -> Result` | -| `Option` | May not exist | `get_node() -> Option` | -| `impl Trait` | Return trait impl | `handler() -> impl IntoResponse` | -| `Box` | Dynamic dispatch | `create() -> Box` | -| Direct type | Infallible ops | `new() -> Self` | - -### Module Organization - -| Module Type | Location Pattern | Naming Convention | -|------------|------------------|-------------------| -| Binary entry | `/src/main.rs` | - | -| Library root | `/src/lib.rs` | - | -| Submodules | `/src/module/mod.rs` | snake_case | -| Tests | `/src/module/tests.rs` | #[cfg(test)] | -| Errors | `/src/error.rs` | ModuleError | -| Config | `/src/config.rs` | Config struct | - ---- - - \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NodeConfigUpdate.ts b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NodeConfigUpdate.ts index b39d3997e4..34c6534fdc 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NodeConfigUpdate.ts +++ b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NodeConfigUpdate.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type NodeConfigUpdate = { host: string | null, custom_http_port: number | null, restore_default_http_port: boolean, }; +export type NodeConfigUpdate = { host: string | null, custom_http_port: number | null, restore_default_http_port: boolean, +/** + * LP listener address for direct gateway connections (format: "host:port") + */ +lp_address: string | null, restore_default_lp_address: boolean, }; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NymNode.ts b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NymNode.ts index a113827957..9cdd09cbb5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NymNode.ts +++ b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/NymNode.ts @@ -17,4 +17,9 @@ custom_http_port: number | null, /** * Base58-encoded ed25519 EdDSA public key. */ -identity_key: string, }; +identity_key: string, +/** + * Optional LP (Lewes Protocol) listener address for direct gateway connections. + * Format: "host:port", for example "1.1.1.1:41264" or "gateway.example.com:41264" + */ +lp_address: string | null, }; diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 6a0187dcd7..cafc5bfcfd 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -23,9 +23,8 @@ pub mod error; pub mod upgrade_mode; // Histogram buckets for ecash verification duration (in seconds) -const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] = &[ - 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, -]; +const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] = + &[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]; pub struct CredentialVerifier { credential: CredentialSpendingRequest, @@ -148,7 +147,10 @@ impl CredentialVerifier { // Track epoch ID - use dynamic metric name via registry let epoch_id = self.credential.data.epoch_id; - let epoch_metric = format!("nym_credential_verification_ecash_epoch_{}_verifications", epoch_id); + let epoch_metric = format!( + "nym_credential_verification_ecash_epoch_{}_verifications", + epoch_id + ); nym_metrics::metrics_registry().maybe_register_and_inc(&epoch_metric, None); // Check verification result after timing diff --git a/common/nym-kcp/CLAUDE.md b/common/nym-kcp/CLAUDE.md deleted file mode 100644 index 910c68e8ae..0000000000 --- a/common/nym-kcp/CLAUDE.md +++ /dev/null @@ -1,81 +0,0 @@ -# CLAUDE.md - nym-kcp - -KCP (Fast and Reliable ARQ Protocol) implementation providing reliability over UDP for the Nym network. This crate ensures ordered, reliable delivery of packets. - -## Architecture Overview - -### Core Components - -**KcpDriver** (src/driver.rs) -- High-level interface for KCP operations -- Manages single KCP session and I/O buffer -- Handles packet encoding/decoding - -**KcpSession** (src/session.rs) -- Core KCP state machine -- Manages send/receive windows, RTT, congestion control -- Implements ARQ (Automatic Repeat Request) logic - -**KcpPacket** (src/packet.rs) -- Wire format: conv(4B) | cmd(1B) | frg(1B) | wnd(2B) | ts(4B) | sn(4B) | una(4B) | len(4B) | data -- Commands: PSH (data), ACK, WND (window probe), ERR - -## Key Concepts - -### Conversation ID (conv) -- Unique identifier for each KCP connection -- Generated from hash of destination in nym-lp-node -- Must match on both ends for successful communication - -### Packet Flow -1. **Send Path**: `send()` → Queue in send buffer → `fetch_outgoing()` → Wire -2. **Receive Path**: Wire → `input()` → Process ACKs/data → Application buffer -3. **Update Loop**: Call `update()` regularly to handle timeouts/retransmissions - -### Reliability Mechanisms -- **Sequence Numbers (sn)**: Track packet ordering -- **Fragment Numbers (frg)**: Handle message fragmentation -- **UNA (Unacknowledged)**: Cumulative ACK up to this sequence -- **Selective ACK**: Via individual ACK packets -- **Fast Retransmit**: Triggered by duplicate ACKs -- **RTO Calculation**: Smoothed RTT with variance - -## Configuration Parameters - -```rust -// In KcpSession -MSS: 1400 // Maximum segment size -WINDOW_SIZE: 128 // Send/receive window -RTO_MIN: 100ms // Minimum retransmission timeout -RTO_MAX: 60000ms // Maximum retransmission timeout -FAST_RESEND: 2 // Fast retransmit threshold -``` - -## Common Operations - -### Processing Incoming Data -```rust -driver.input(data)?; // Decode and process packets -let packets = driver.fetch_outgoing(); // Get any response packets -``` - -### Sending Data -```rust -driver.send(&data); // Queue for sending -driver.update(current_time); // Trigger flush -let packets = driver.fetch_outgoing(); // Get packets to send -``` - -## Debugging Tips - -- Enable `trace!` logs to see packet-level details -- Monitor `ts_flush` vs `ts_current` for timing issues -- Check `snd_wnd` and `rcv_wnd` for flow control problems -- Watch for "fast retransmit" messages indicating packet loss - -## Integration Notes - -- AIDEV-NOTE: MSS must account for Sphinx packet overhead -- AIDEV-NOTE: Window size affects memory usage and throughput -- Update frequency impacts latency vs CPU usage tradeoff -- Conv ID must be consistent across session lifecycle \ No newline at end of file diff --git a/common/nym-kcp/Cargo.toml b/common/nym-kcp/Cargo.toml index 43cafb1414..beafab5ee9 100644 --- a/common/nym-kcp/Cargo.toml +++ b/common/nym-kcp/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "nym-kcp" version = "0.1.0" -edition = "2021" +edition = { workspace = true } +license = { workspace = true } [lib] name = "nym_kcp" diff --git a/common/nym-kcp/src/session.rs b/common/nym-kcp/src/session.rs index 2e56c9073f..f72cd19ef7 100644 --- a/common/nym-kcp/src/session.rs +++ b/common/nym-kcp/src/session.rs @@ -490,8 +490,16 @@ impl KcpSession { post_retain_sns ); // Corrected format string arguments for the removed count log - debug!("[ConvID: {}, Thread: {:?}] parse_una(una={}): Removed {} segment(s) from snd_buf ({} -> {}). Remaining sns: {:?}", - self.conv, thread::current().id(), una, removed_count, original_len, self.snd_buf.len(), post_retain_sns); + debug!( + "[ConvID: {}, Thread: {:?}] parse_una(una={}): Removed {} segment(s) from snd_buf ({} -> {}). Remaining sns: {:?}", + self.conv, + thread::current().id(), + una, + removed_count, + original_len, + self.snd_buf.len(), + post_retain_sns + ); if removed_count > 0 { // Use trace level if no segments were removed but buffer wasn't empty @@ -955,7 +963,7 @@ mod tests { // Check that snd_buf now contains segments up to the new cwnd (8) // The total number of segments should be 7 (initial 5 - 1 acked + 3 moved from queue) - let expected_buf_len_after_ack = initial_cwnd as usize - 1 + (8 - initial_cwnd as usize); + let _expected_buf_len_after_ack = initial_cwnd as usize - 1 + (8 - initial_cwnd as usize); assert_eq!( session.snd_buf.len(), 7, @@ -1028,7 +1036,7 @@ mod tests { .expect("Segment must be in buffer") .clone(); // Clone for inspection let initial_rto = session.rx_rto; - let expected_resendts = session.current + initial_rto; + let _expected_resendts = session.current + initial_rto; assert_eq!(segment.xmit, 1, "Initial transmit count should be 1"); assert_eq!( segment.rto, initial_rto, @@ -1255,11 +1263,11 @@ mod tests { session.set_mtu(50); // Send 5 segments (SN 0, 1, 2, 3, 4) - session.send(&vec![1u8; 30]); // sn=0 - session.send(&vec![2u8; 30]); // sn=1 - session.send(&vec![3u8; 30]); // sn=2 - session.send(&vec![4u8; 30]); // sn=3 - session.send(&vec![5u8; 30]); // sn=4 + session.send(&[1u8; 30]); // sn=0 + session.send(&[2u8; 30]); // sn=1 + session.send(&[3u8; 30]); // sn=2 + session.send(&[4u8; 30]); // sn=3 + session.send(&[5u8; 30]); // sn=4 assert_eq!(session.snd_queue.len(), 5); // Move all to snd_buf @@ -1616,9 +1624,9 @@ mod tests { debug!("Simulating loss of fragment sn={}", lost_packet_sn); // Deliver all packets *except* the lost one - for i in 0..num_fragments { + for (i, packet) in packets.iter().enumerate().take(num_fragments) { if i != 1 { - receiver.input(&packets[i]); + receiver.input(packet); } } receiver.update(0); // Process inputs diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml new file mode 100644 index 0000000000..3c717d5d41 --- /dev/null +++ b/common/nym-kkt/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "nym-kkt" +version = "0.1.0" +authors = ["Georgio Nicolas "] +edition = { workspace = true } +license.workspace = true + +[dependencies] +arc-swap = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +pin-project = { workspace = true } +blake3 = { workspace = true } +aead = { workspace = true } +strum = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-util = { workspace = true, features = ["codec"] } + + + +# internal +nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]} + +libcrux-traits = { git = "https://github.com/cryspen/libcrux" } +libcrux-kem = { git = "https://github.com/cryspen/libcrux" } +libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] } +libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" } +libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" } +libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"]} + +rand = "0.9.2" +curve25519-dalek = {version = "4.1.3", features = ["rand_core", "serde"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } +classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f","zeroize"]} + + +[dev-dependencies] +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 new file mode 100644 index 0000000000..8df4dd3e42 --- /dev/null +++ b/common/nym-kkt/benches/benches.rs @@ -0,0 +1,518 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +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 rand::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]; + rand::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 rand::rng()).unwrap() + }); + }); +} + +pub fn kkt_benchmark(c: &mut Criterion) { + let mut rng = rand::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!( + "{}, {} | Anonymous Initiator: Generate Request", + kem, hash_function + ), + |b| { + b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap()); + }, + ); + + let (mut i_context, i_frame) = + anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); + + c.bench_function( + &format!( + "{}, {} | Anonymous Initiator: Encode Frame - Request", + kem, hash_function + ), + |b| b.iter(|| i_frame.to_bytes()), + ); + + let i_frame_bytes = i_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Anonymous Initiator: Decode Frame - Request", + kem, hash_function + ), + |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!( + "{}, {} | Anonymous Initiator: Responder Ingest Frame", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap() + }); + }, + ); + + let (mut r_context, _) = + responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); + + c.bench_function( + &format!( + "{}, {} | Anonymous Initiator: Responder Generate Response", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap() + }); + }, + ); + let r_frame = responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + c.bench_function( + &format!( + "{}, {} | Anonymous Initiator: Responder Encode Frame", + kem, hash_function + ), + |b| b.iter(|| r_frame.to_bytes()), + ); + + let r_bytes = r_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Anonymous Initiator: Initiator Ingest Response", + kem, hash_function + ), + |b| { + b.iter(|| { + initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap() + }); + }, + ); + + let obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap(); + + assert_eq!(obtained_key.encode(), r_kem_key_bytes) + } + // Initiator, OneWay + { + let (mut i_context, i_frame) = initiator_process( + &mut rng, + KKTMode::OneWay, + ciphersuite, + initiator_ed25519_keypair.private_key(), + None, + ) + .unwrap(); + + c.bench_function( + &format!( + "{}, {} | Initiator OneWay: Generate Request", + kem, hash_function + ), + |b| { + b.iter(|| { + initiator_process( + &mut rng, + KKTMode::OneWay, + ciphersuite, + initiator_ed25519_keypair.private_key(), + None, + ) + .unwrap() + }); + }, + ); + + c.bench_function( + &format!( + "{}, {} | Initiator OneWay: Encode Frame - Request", + kem, hash_function + ), + |b| b.iter(|| i_frame.to_bytes()), + ); + + let i_frame_bytes = i_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Initiator OneWay: Decode Frame - Request", + kem, hash_function + ), + |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!( + "{}, {} | Initiator OneWay: Responder Ingest Frame", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_ingest_message( + &r_context, + Some(initiator_ed25519_keypair.public_key()), + None, + &i_frame_r, + ) + .unwrap() + }); + }, + ); + + let (mut 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!( + "{}, {} | Initiator OneWay: Responder Generate Response", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap() + }); + }, + ); + + let r_frame = responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + c.bench_function( + &format!( + "{}, {} | Initiator OneWay: Responder Encode Frame", + kem, hash_function + ), + |b| { + b.iter(|| r_frame.to_bytes()); + }, + ); + + let r_bytes = r_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Initiator OneWay: Initiator Ingest Response", + kem, hash_function + ), + |b| { + b.iter(|| { + initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap() + }); + }, + ); + + let i_obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap(); + + assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) + } + + // Initiator, Mutual + { + c.bench_function( + &format!( + "{}, {} | Initiator Mutual: Generate Request", + kem, hash_function + ), + |b| { + b.iter(|| { + initiator_process( + &mut rng, + KKTMode::Mutual, + ciphersuite, + initiator_ed25519_keypair.private_key(), + Some(&initiator_kem_public_key), + ) + .unwrap() + }); + }, + ); + + let (mut 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!( + "{}, {} | Initiator Mutual: Encode Frame - Request", + kem, hash_function + ), + |b| { + b.iter(|| i_frame.to_bytes()); + }, + ); + + let i_frame_bytes = i_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Initiator Mutual: Decode Frame - Request", + kem, hash_function + ), + |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!( + "{}, {} | Initiator Mutual: Responder Ingest Frame", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_ingest_message( + &r_context, + Some(initiator_ed25519_keypair.public_key()), + Some(&i_dir_hash), + &i_frame_r, + ) + .unwrap() + }); + }, + ); + + let (mut 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!( + "{}, {} | Initiator Mutual: Responder Generate Response", + kem, hash_function + ), + |b| { + b.iter(|| { + responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap() + }); + }, + ); + + let r_frame = responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + c.bench_function( + &format!( + "{}, {} | Initiator Mutual: Responder Encode Frame", + kem, hash_function + ), + |b| { + b.iter(|| { + r_frame.to_bytes(); + }); + }, + ); + + let r_bytes = r_frame.to_bytes(); + + c.bench_function( + &format!( + "{}, {} | Initiator Mutual: Initiator Ingest Response", + kem, hash_function + ), + |b| { + b.iter(|| { + initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap() + }); + }, + ); + + let obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .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/ciphersuite.rs b/common/nym-kkt/src/ciphersuite.rs new file mode 100644 index 0000000000..cc7877e690 --- /dev/null +++ b/common/nym-kkt/src/ciphersuite.rs @@ -0,0 +1,301 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use libcrux_kem::{Algorithm, MlKem768PublicKey}; +use nym_crypto::asymmetric::ed25519; + +use crate::error::KKTError; + +pub const HASH_LEN_256: u8 = 32; +pub const CIPHERSUITE_ENCODING_LEN: usize = 4; + +pub const CURVE25519_KEY_LEN: usize = 32; + +#[derive(Clone, Copy, Debug)] +pub enum HashFunction { + Blake3, + SHAKE128, + SHAKE256, + SHA256, +} +impl Display for HashFunction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + HashFunction::Blake3 => "Blake3", + HashFunction::SHAKE128 => "SHAKE128", + HashFunction::SHAKE256 => "SHAKE256", + HashFunction::SHA256 => "SHA256", + }) + } +} + +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()), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum SignatureScheme { + Ed25519, +} +impl Display for SignatureScheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SignatureScheme::Ed25519 => "Ed25519", + }) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum KEM { + MlKem768, + XWing, + X25519, + McEliece, +} + +impl Display for KEM { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + KEM::MlKem768 => "MlKem768", + KEM::XWing => "XWing", + KEM::X25519 => "x25519", + KEM::McEliece => "McEliece", + }) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Ciphersuite { + hash_function: HashFunction, + signature_scheme: SignatureScheme, + kem: KEM, + hash_length: u8, + encapsulation_key_length: usize, + signing_key_length: usize, + verification_key_length: usize, + signature_length: usize, +} + +impl Ciphersuite { + pub fn kem_key_len(&self) -> usize { + self.encapsulation_key_length + } + + pub fn signature_len(&self) -> usize { + self.signature_length + } + pub fn signing_key_len(&self) -> usize { + self.signing_key_length + } + pub fn verification_key_len(&self) -> usize { + self.verification_key_length + } + pub fn hash_function(&self) -> HashFunction { + self.hash_function + } + pub fn kem(&self) -> KEM { + self.kem + } + pub fn signature_scheme(&self) -> SignatureScheme { + self.signature_scheme + } + pub fn hash_len(&self) -> usize { + self.hash_length as usize + } + + pub fn resolve_ciphersuite( + kem: KEM, + hash_function: HashFunction, + signature_scheme: SignatureScheme, + // This should be None 99.9999% of the time + custom_hash_length: Option, + ) -> Result { + let hash_len = match custom_hash_length { + Some(l) => { + if l < 16 { + return Err(KKTError::InsecureHashLen); + } else { + l + } + } + None => HASH_LEN_256, + }; + Ok(Self { + hash_function, + signature_scheme, + kem, + hash_length: hash_len, + encapsulation_key_length: match kem { + // 1184 bytes + KEM::MlKem768 => MlKem768PublicKey::len(), + // 1216 bytes = 1184 + 32 + KEM::XWing => MlKem768PublicKey::len() + CURVE25519_KEY_LEN, + // 32 bytes + KEM::X25519 => CURVE25519_KEY_LEN, + // 524160 bytes + KEM::McEliece => classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES, + }, + signing_key_length: match signature_scheme { + // 32 bytes + SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH, + }, + verification_key_length: match signature_scheme { + // 32 bytes + SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH, + }, + signature_length: match signature_scheme { + // 64 bytes + SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH, + }, + }) + } + pub fn encode(&self) -> [u8; 4] { + // [kem, hash, hashlen, sig] + [ + match self.kem { + KEM::XWing => 0, + KEM::MlKem768 => 1, + KEM::McEliece => 2, + KEM::X25519 => 255, + }, + match self.hash_function { + HashFunction::Blake3 => 0, + HashFunction::SHAKE256 => 1, + HashFunction::SHAKE128 => 2, + HashFunction::SHA256 => 3, + }, + match self.hash_length { + HASH_LEN_256 => 0, + _ => self.hash_length, + }, + match self.signature_scheme { + SignatureScheme::Ed25519 => 0, + }, + ] + } + pub fn decode(encoding: &[u8]) -> Result { + if encoding.len() == 4 { + let kem = match encoding[0] { + 0 => KEM::XWing, + 1 => KEM::MlKem768, + 2 => KEM::McEliece, + 255 => KEM::X25519, + _ => { + return Err(KKTError::CiphersuiteDecodingError { + info: format!("Undefined KEM: {}", encoding[0]), + }); + } + }; + let hash_function = match encoding[1] { + 0 => HashFunction::Blake3, + 1 => HashFunction::SHAKE256, + 2 => HashFunction::SHAKE128, + 3 => HashFunction::SHA256, + _ => { + return Err(KKTError::CiphersuiteDecodingError { + info: format!("Undefined Hash Function: {}", encoding[1]), + }); + } + }; + + let custom_hash_length = match encoding[2] { + 0 => None, + _ => Some(encoding[2]), + }; + + let signature_scheme = match encoding[3] { + 0 => SignatureScheme::Ed25519, + _ => { + return Err(KKTError::CiphersuiteDecodingError { + info: format!("Undefined Signature Scheme: {}", encoding[3]), + }); + } + }; + + Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length) + } else { + Err(KKTError::CiphersuiteDecodingError { + info: format!( + "Incorrect Encoding Length: actual: {} != expected: {}", + encoding.len(), + CIPHERSUITE_ENCODING_LEN + ), + }) + } + } +} + +impl Display for Ciphersuite { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str( + &format!( + "{}_{}({})_{}", + self.kem, self.hash_function, self.hash_length, self.signature_scheme + ) + .to_ascii_lowercase(), + ) + } +} + +pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Algorithm { + match kem { + KEM::MlKem768 => Algorithm::MlKem768, + KEM::XWing => Algorithm::XWingKemDraft06, + KEM::X25519 => Algorithm::X25519, + KEM::McEliece => panic!("McEliece is not supported in libcrux_kem"), + } +} diff --git a/common/nym-kkt/src/context.rs b/common/nym-kkt/src/context.rs new file mode 100644 index 0000000000..da66bd3ae6 --- /dev/null +++ b/common/nym-kkt/src/context.rs @@ -0,0 +1,258 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN}; + +pub const KKT_CONTEXT_LEN: usize = 7; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum KKTStatus { + Ok, + InvalidRequestFormat, + InvalidResponseFormat, + InvalidSignature, + UnsupportedCiphersuite, + UnsupportedKKTVersion, + InvalidKey, + Timeout, +} + +impl Display for KKTStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + 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::Timeout => "Timeout", + }) + } +} +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum KKTRole { + Initiator, + AnonymousInitiator, + Responder, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum KKTMode { + OneWay, + Mutual, +} + +#[derive(Copy, Clone, Debug)] +pub struct KKTContext { + version: u8, + message_sequence: u8, + status: KKTStatus, + mode: KKTMode, + 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 { + version: KKT_VERSION, + message_sequence: 0, + status: KKTStatus::Ok, + mode, + role, + ciphersuite, + }) + } + + pub fn derive_responder_header(&self) -> Result { + let mut responder_header = *self; + + responder_header.increment_message_sequence_count()?; + responder_header.role = KKTRole::Responder; + + Ok(responder_header) + } + + pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> { + if self.message_sequence + 1 < (1 << 4) { + self.message_sequence += 1; + Ok(()) + } else { + Err(KKTError::MessageCountLimitReached) + } + } + + pub fn update_status(&mut self, status: KKTStatus) { + self.status = status; + } + pub fn version(&self) -> u8 { + self.version + } + pub fn status(&self) -> KKTStatus { + self.status + } + pub fn ciphersuite(&self) -> Ciphersuite { + self.ciphersuite + } + pub fn role(&self) -> KKTRole { + self.role + } + pub fn mode(&self) -> KKTMode { + self.mode + } + + pub fn body_len(&self) -> usize { + if self.status != KKTStatus::Ok + || (self.mode == KKTMode::OneWay + && (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator)) + { + 0 + } else { + self.ciphersuite.kem_key_len() + } + } + + pub fn signature_len(&self) -> usize { + match self.role { + KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(), + KKTRole::AnonymousInitiator => 0, + } + } + + pub fn header_len(&self) -> usize { + KKT_CONTEXT_LEN + } + + pub fn session_id_len(&self) -> usize { + // 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() + } + + pub fn encode(&self) -> Result, KKTError> { + let mut header_bytes: Vec = Vec::with_capacity(KKT_CONTEXT_LEN); + if self.message_sequence >= 1 << 4 { + return Err(KKTError::MessageCountLimitReached); + } + + header_bytes.push((KKT_VERSION << 4) + self.message_sequence); + + header_bytes.push( + match self.status { + KKTStatus::Ok => 0, + KKTStatus::InvalidRequestFormat => 0b0010_0000, + KKTStatus::InvalidResponseFormat => 0b0100_0000, + KKTStatus::InvalidSignature => 0b0110_0000, + KKTStatus::UnsupportedCiphersuite => 0b1000_0000, + KKTStatus::UnsupportedKKTVersion => 0b1010_0000, + KKTStatus::InvalidKey => 0b1100_0000, + KKTStatus::Timeout => 0b1110_0000, + } + match self.mode { + KKTMode::OneWay => 0, + KKTMode::Mutual => 0b0000_0100, + } + match self.role { + KKTRole::Initiator => 0, + KKTRole::Responder => 1, + KKTRole::AnonymousInitiator => 2, + }, + ); + + header_bytes.extend_from_slice(&self.ciphersuite.encode()); + header_bytes.push(0); + Ok(header_bytes) + } + + pub fn try_decode(header_bytes: &[u8]) -> Result { + if header_bytes.len() == KKT_CONTEXT_LEN { + let kkt_version = header_bytes[0] & 0b1111_0000; + + 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 >> 4) > KKT_VERSION { + return Err(KKTError::FrameDecodingError { + info: format!("Header - Invalid KKT Version: {}", kkt_version >> 4), + }); + } + + let status = match header_bytes[1] & 0b1110_0000 { + 0 => KKTStatus::Ok, + 0b0010_0000 => KKTStatus::InvalidRequestFormat, + 0b0100_0000 => KKTStatus::InvalidResponseFormat, + 0b0110_0000 => KKTStatus::InvalidSignature, + 0b1000_0000 => KKTStatus::UnsupportedCiphersuite, + 0b1010_0000 => KKTStatus::UnsupportedKKTVersion, + 0b1100_0000 => KKTStatus::InvalidKey, + 0b1110_0000 => KKTStatus::Timeout, + _ => { + return Err(KKTError::FrameDecodingError { + info: format!( + "Header - Invalid KKT Status: {}", + header_bytes[1] & 0b1110_0000 + ), + }); + } + }; + + let role = match header_bytes[1] & 0b0000_0011 { + 0 => KKTRole::Initiator, + 1 => KKTRole::Responder, + 2 => KKTRole::AnonymousInitiator, + _ => { + return Err(KKTError::FrameDecodingError { + info: format!( + "Header - Invalid KKT Role: {}", + header_bytes[1] & 0b0000_0011 + ), + }); + } + }; + + let mode = match (header_bytes[1] & 0b0001_1100) >> 2 { + 0 => KKTMode::OneWay, + 1 => KKTMode::Mutual, + _ => { + return Err(KKTError::FrameDecodingError { + info: format!( + "Header - Invalid KKT Mode: {}", + (header_bytes[1] & 0b0001_1100) >> 2 + ), + }); + } + }; + + Ok(KKTContext { + version: kkt_version, + status, + mode, + role, + ciphersuite: Ciphersuite::decode(&header_bytes[2..6])?, + message_sequence: message_sequence_counter, + }) + } else { + Err(KKTError::FrameDecodingError { + info: format!( + "Header - Invalid Header Length: actual: {} != expected: {}", + header_bytes.len(), + KKT_CONTEXT_LEN + ), + }) + } + } +} diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs new file mode 100644 index 0000000000..65ac46f0ac --- /dev/null +++ b/common/nym-kkt/src/encryption.rs @@ -0,0 +1,95 @@ +use core::hash; + +use blake3::{Hash, Hasher}; +use curve25519_dalek::digest::DynDigest; +use libcrux_psq::traits::Ciphertext; +use nym_crypto::symmetric::aead::{AeadKey, Nonce}; +use nym_crypto::{ + aes::Aes256, + asymmetric::x25519::{self, PrivateKey, PublicKey}, + generic_array::GenericArray, + Aes256GcmSiv, +}; +// use rand::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore}; + +use crate::error::KKTError; + +fn generate_round_trip_symmetric_key( + rng: &mut R, + remote_public_key: &PublicKey, +) -> ([u8; 64], [u8; 32]) +where + R: CryptoRng + RngCore, +{ + let mut s = x25519::PrivateKey::new(rng); + let gs = s.public_key(); + + let mut gbs = s.diffie_hellman(remote_public_key); + s.zeroize(); + + let mut message: [u8; 64] = [0u8; 64]; + message[0..32].clone_from_slice(gs.as_bytes()); + + let mut hasher = Hasher::new(); + + hasher.update(&gbs); + gbs.zeroize(); + let key: [u8; 32] = hasher.finalize().as_bytes().to_owned(); + + hasher.update(remote_public_key.as_bytes()); + hasher.update(gs.as_bytes()); + + hasher.finalize_into_reset(&mut message[32..64]); + + (message, key) +} + +fn extract_shared_secret(b: &PrivateKey, message: &[u8; 64]) -> Result<[u8; 32], KKTError> { + let gs = PublicKey::from_bytes(&message[0..32])?; + + let mut gsb = b.diffie_hellman(&gs); + + let mut hasher = Hasher::new(); + hasher.update(&gsb); + gsb.zeroize(); + let key: [u8; 32] = hasher.finalize().as_bytes().to_owned(); + + hasher.update(b.public_key().as_bytes()); + hasher.update(gs.as_bytes()); + + // This runs in constant time + if hasher.finalize() == message[32..64] { + Ok(key) + } else { + Err(KKTError::X25519Error { + info: format!("Symmetric Key Hash Validation Error"), + }) + } +} + +fn encrypt(mut key: [u8; 32], message: &[u8]) -> Result, KKTError> { + // The empty nonce is fine since we use the key once. + let nonce = Nonce::::from_slice(&[]); + + let ciphertext = + nym_crypto::symmetric::aead::encrypt::(&key.into(), nonce, message)?; + + key.zeroize(); + + Ok(ciphertext) +} + +fn decrypt(key: [u8; 32], ciphertext: Vec) -> Vec { + // The empty nonce is fine since we use the key once. + let nonce = Nonce::::from_slice(&[]); + + let ciphertext = + nym_crypto::symmetric::aead::encrypt::(&key.into(), nonce, message)?; + + key.zeroize(); + + Ok(ciphertext) +} diff --git a/common/nym-kkt/src/error.rs b/common/nym-kkt/src/error.rs new file mode 100644 index 0000000000..3e148d03e1 --- /dev/null +++ b/common/nym-kkt/src/error.rs @@ -0,0 +1,85 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +use crate::context::KKTStatus; + +#[derive(Error, Debug)] +pub enum KKTError { + #[error("Signature constructor error")] + SigConstructorError, + #[error("Signature verification error")] + SigVerifError, + #[error("Ciphersuite Decoding Error: {}", info)] + CiphersuiteDecodingError { info: String }, + #[error("Insecure Encapsulation Key Hash Length")] + InsecureHashLen, + + #[error("KKT Frame Decoding Error: {}", info)] + FrameDecodingError { info: String }, + + #[error("KKT Frame Encoding Error: {}", info)] + FrameEncodingError { info: String }, + + #[error("KKT Incompatibility Error: {}", info)] + IncompatibilityError { info: &'static str }, + + #[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 }, + + #[error("Local Function Input Error: {}", info)] + FunctionInputError { info: &'static str }, + + #[error("{}", info)] + X25519Error { info: &'static str }, + + #[error("Generic libcrux error")] + LibcruxError, +} + +impl From for KKTError { + fn from(err: libcrux_kem::Error) -> Self { + match err { + libcrux_kem::Error::EcDhError(_) => KKTError::KEMError { info: "ECDH Error" }, + libcrux_kem::Error::KeyGen => KKTError::KEMError { + info: "Key Generation Error", + }, + libcrux_kem::Error::Encapsulate => KKTError::KEMError { + info: "Encapsulation Error", + }, + libcrux_kem::Error::Decapsulate => KKTError::KEMError { + info: "Decapsulation Error", + }, + libcrux_kem::Error::UnsupportedAlgorithm => KKTError::KEMError { + info: "libcrux Unsupported Algorithm", + }, + libcrux_kem::Error::InvalidPrivateKey => KKTError::KEMError { + info: "Invalid Private Key", + }, + + libcrux_kem::Error::InvalidPublicKey => KKTError::KEMError { + info: "Invalid Public Key", + }, + libcrux_kem::Error::InvalidCiphertext => KKTError::KEMError { + info: "Invalid Ciphertext", + }, + } + } +} +impl From for KKTError { + fn from(err: libcrux_ecdh::Error) -> Self { + match err { + libcrux_ecdh::Error::InvalidPoint => KKTError::KEMError { + info: "Invalid Remote Public Key", + }, + _ => KKTError::LibcruxError, + } + } +} diff --git a/common/nym-kkt/src/frame.rs b/common/nym-kkt/src/frame.rs new file mode 100644 index 0000000000..1745b997f1 --- /dev/null +++ b/common/nym-kkt/src/frame.rs @@ -0,0 +1,129 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// | 0 | 1 | 2, 3, 4, 5 | 6 | 7 +// [0] => KKT version (4 bits) + Message Sequence Count (4 bits) +// [1] => Status (3 bits) + Mode (3 bits) + Role (2 bits) +// [2..=5] => Ciphersuite +// [6] => Reserved + +use crate::{ + context::{KKT_CONTEXT_LEN, KKTContext}, + error::KKTError, +}; + +pub const KKT_SESSION_ID_LEN: usize = 16; + +pub struct KKTFrame { + context: Vec, + session_id: Vec, + body: Vec, + signature: 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. + +impl KKTFrame { + pub fn new(context: &[u8], body: &[u8], session_id: &[u8], signature: &[u8]) -> Self { + Self { + context: Vec::from(context), + body: Vec::from(body), + session_id: Vec::from(session_id), + signature: Vec::from(signature), + } + } + pub fn context_ref(&self) -> &[u8] { + &self.context + } + pub fn signature_ref(&self) -> &[u8] { + &self.signature + } + pub fn body_ref(&self) -> &[u8] { + &self.body + } + + pub fn session_id_ref(&self) -> &[u8] { + &self.session_id + } + 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() + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.frame_length()); + bytes.extend_from_slice(&self.context); + bytes.extend_from_slice(&self.body); + bytes.extend_from_slice(&self.session_id); + bytes.extend_from_slice(&self.signature); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> { + if bytes.len() < KKT_CONTEXT_LEN { + Err(KKTError::FrameDecodingError { + info: format!( + "Frame is shorter than expected context length: actual {} != expected {}", + bytes.len(), + KKT_CONTEXT_LEN + ), + }) + } else { + let context_bytes = Vec::from(&bytes[0..KKT_CONTEXT_LEN]); + + let context = KKTContext::try_decode(&context_bytes)?; + + let (mut session_id, mut body, mut signature): (Vec, Vec, Vec) = + (vec![], vec![], vec![]); + + if bytes.len() == context.full_message_len() { + if context.body_len() > 0 { + body.extend_from_slice( + &bytes[KKT_CONTEXT_LEN..KKT_CONTEXT_LEN + context.body_len()], + ); + } + 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()], + ); + } + if context.signature_len() > 0 { + signature.extend_from_slice( + &bytes[KKT_CONTEXT_LEN + context.body_len() + context.session_id_len() + ..KKT_CONTEXT_LEN + + context.body_len() + + context.session_id_len() + + context.signature_len()], + ); + } + + Ok(( + KKTFrame::new(&context_bytes, &body, &session_id, &signature), + context, + )) + } else { + Err(KKTError::FrameDecodingError { + info: format!( + "Frame is shorter than expected: actual {} != expected {}", + bytes.len(), + context.full_message_len() + ), + }) + } + } + } +} diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs new file mode 100644 index 0000000000..1ab18934e0 --- /dev/null +++ b/common/nym-kkt/src/key_utils.rs @@ -0,0 +1,107 @@ +use crate::{ + ciphersuite::{HashFunction, KEM}, + error::KKTError, +}; + +use classic_mceliece_rust::keypair_boxed; +use libcrux_kem::{Algorithm, key_gen}; + +use libcrux_sha3; +use rand::{CryptoRng, RngCore}; + +// (decapsulation_key, encapsulation_key) +pub fn generate_keypair_libcrux( + rng: &mut R, + kem: KEM, +) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), KKTError> +where + R: RngCore + CryptoRng, +{ + match kem { + KEM::MlKem768 => Ok(key_gen(Algorithm::MlKem768, rng)?), + KEM::XWing => Ok(key_gen(Algorithm::XWingKemDraft06, rng)?), + KEM::X25519 => Ok(key_gen(Algorithm::X25519, rng)?), + _ => Err(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 + // this is annoying because mceliece lib uses rand 0.8.5... + R: RngCore + CryptoRng, +{ + let (encapsulation_key, decapsulation_key) = keypair_boxed(rng); + (decapsulation_key, encapsulation_key) +} + +pub fn hash_key_bytes( + hash_function: &HashFunction, + hash_length: usize, + key_bytes: &[u8], +) -> Vec { + let mut hashed_key: Vec = vec![0u8; hash_length]; + match hash_function { + HashFunction::Blake3 => { + let mut hasher = blake3::Hasher::new(); + hasher.update(key_bytes); + hasher.finalize_xof().fill(&mut hashed_key); + hasher.reset(); + } + HashFunction::SHAKE256 => { + libcrux_sha3::shake256_ema(&mut hashed_key, key_bytes); + } + HashFunction::SHAKE128 => { + libcrux_sha3::shake128_ema(&mut hashed_key, key_bytes); + } + HashFunction::SHA256 => { + libcrux_sha3::sha256_ema(&mut hashed_key, key_bytes); + } + } + + hashed_key +} + +/// This does NOT run in constant time. +// It's fine for KKT since we are comparing hashes. +fn compare_hashes(a: &[u8], b: &[u8]) -> bool { + a == b +} + +pub fn validate_encapsulation_key( + hash_function: &HashFunction, + hash_length: usize, + encapsulation_key: &[u8], + expected_hash_bytes: &[u8], +) -> bool { + compare_hashes( + &hash_encapsulation_key(hash_function, hash_length, encapsulation_key), + expected_hash_bytes, + ) +} + +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_length: usize, + encapsulation_key: &[u8], +) -> Vec { + hash_key_bytes(hash_function, hash_length, encapsulation_key) +} diff --git a/common/nym-kkt/src/kkt.rs b/common/nym-kkt/src/kkt.rs new file mode 100644 index 0000000000..7fcef8d3e3 --- /dev/null +++ b/common/nym-kkt/src/kkt.rs @@ -0,0 +1,355 @@ +// 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; +use rand::{CryptoRng, RngCore}; + +use crate::{ + ciphersuite::{Ciphersuite, EncapsulationKey}, + context::{KKTContext, KKTMode}, + error::KKTError, + frame::KKTFrame, +}; + +// 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, +}; + +/// Request 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 +/// +/// # Returns +/// * `KKTContext` - Context to use when validating the response +/// * `KKTFrame` - Signed request frame to send to responder +/// +/// # Example +/// ```ignore +/// let (context, request_frame) = request_kem_key( +/// &mut rng, +/// ciphersuite, +/// client_signing_key, +/// )?; +/// // Send request_frame to gateway +/// ``` +pub fn request_kem_key( + rng: &mut R, + ciphersuite: Ciphersuite, + signing_key: &ed25519::PrivateKey, +) -> Result<(KKTContext, KKTFrame), KKTError> { + // OneWay mode: client only wants responder's KEM key + // None: client doesn't send their own KEM key + initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None) +} + +/// Validate a 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 +/// * `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( +/// &mut context, +/// gateway_verification_key, +/// &expected_hash_from_directory, +/// &response_bytes, +/// )?; +/// // Use gateway_kem_key for PSQ +/// ``` +pub fn validate_kem_response<'a>( + context: &mut KKTContext, + responder_vk: &ed25519::PublicKey, + expected_key_hash: &[u8], + response_bytes: &[u8], +) -> Result, KKTError> { + initiator_ingest_response(context, responder_vk, expected_key_hash, response_bytes) +} + +/// Handle a 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 +/// * `request_frame` - Request frame received from initiator +/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous) +/// * `responder_signing_key` - Gateway's Ed25519 signing 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>( + request_frame: &KKTFrame, + initiator_vk: Option<&ed25519::PublicKey>, + responder_signing_key: &ed25519::PrivateKey, + responder_kem_key: &EncapsulationKey<'a>, +) -> Result { + // Parse context from the request frame + let request_bytes = request_frame.to_bytes(); + let (_, request_context) = KKTFrame::from_bytes(&request_bytes)?; + + // Validate the request (verifies signature if initiator_vk provided) + let (mut response_context, _) = responder_ingest_message( + &request_context, + initiator_vk, + None, // Not checking initiator's KEM key in OneWay mode + request_frame, + )?; + + // Generate signed response with our KEM public key + responder_process( + &mut response_context, + request_frame.session_id_ref(), + responder_signing_key, + responder_kem_key, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ciphersuite::{HashFunction, KEM, SignatureScheme}, + key_utils::{generate_keypair_libcrux, hash_encapsulation_key}, + }; + + #[test] + fn test_kkt_wrappers_oneway_authenticated() { + let mut rng = rand::rng(); + + // Generate Ed25519 keypairs for both parties + 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); + + // 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 (mut context, request_frame) = + request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap(); + + // Gateway: Handle request + let response_frame = handle_kem_request( + &request_frame, + Some(initiator_keypair.public_key()), // Authenticated + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Client: Validate response + let obtained_key = validate_kem_response( + &mut context, + responder_keypair.public_key(), + &key_hash, + &response_frame.to_bytes(), + ) + .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 = rand::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 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 (mut context, request_frame) = + anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); + + // Gateway: Handle anonymous request + let response_frame = handle_kem_request( + &request_frame, + None, // Anonymous - no verification key + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Initiator: Validate response + let obtained_key = validate_kem_response( + &mut context, + responder_keypair.public_key(), + &key_hash, + &response_frame.to_bytes(), + ) + .unwrap(); + + assert_eq!(obtained_key.encode(), responder_kem_key.encode()); + } + + #[test] + fn test_invalid_signature_rejected() { + let mut rng = rand::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); + + // 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 (_context, request_frame) = + request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap(); + + // Gateway handles request but we provide WRONG verification key + let result = handle_kem_request( + &request_frame, + Some(wrong_keypair.public_key()), // Wrong key! + responder_keypair.private_key(), + &responder_kem_key, + ); + + // Should fail signature verification + assert!(result.is_err()); + } + + #[test] + fn test_hash_mismatch_rejected() { + let mut rng = rand::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 (_, 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 (mut context, request_frame) = + request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap(); + + let response_frame = handle_kem_request( + &request_frame, + Some(initiator_keypair.public_key()), + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Client validates with WRONG hash + let result = validate_kem_response( + &mut context, + responder_keypair.public_key(), + &wrong_hash, // Wrong! + &response_frame.to_bytes(), + ); + + // Should fail hash validation + assert!(result.is_err()); + } +} diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs new file mode 100644 index 0000000000..348e8fb01c --- /dev/null +++ b/common/nym-kkt/src/lib.rs @@ -0,0 +1,232 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod ciphersuite; +pub mod context; +// pub mod encryption; +pub mod error; +pub mod frame; +pub mod key_utils; +pub mod kkt; +pub mod session; + +// pub mod psq; + +// This must be less than 4 bits +pub const KKT_VERSION: u8 = 1; +const _: () = assert!(KKT_VERSION < 1 << 4); + +#[cfg(test)] +mod test { + use nym_crypto::asymmetric::ed25519; + use rand::prelude::*; + + use crate::{ + ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM}, + 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, + }, + }; + + #[test] + fn test_kkt_psq_e2e_clear() { + let mut rng = rand::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, + 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 (mut 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 (mut r_context, _) = + responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); + + let r_frame = responder_process( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + let r_bytes = r_frame.to_bytes(); + + let obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap(); + + assert_eq!(obtained_key.encode(), r_kem_key_bytes) + } + // Initiator, OneWay + { + let (mut 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 (mut 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( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + let r_bytes = r_frame.to_bytes(); + + let i_obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap(); + + assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) + } + + // Initiator, Mutual + { + let (mut 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 (mut 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( + &mut r_context, + i_frame_r.session_id_ref(), + responder_ed25519_keypair.private_key(), + &responder_kem_public_key, + ) + .unwrap(); + + let r_bytes = r_frame.to_bytes(); + + let obtained_key = initiator_ingest_response( + &mut i_context, + responder_ed25519_keypair.public_key(), + &r_dir_hash, + &r_bytes, + ) + .unwrap(); + + assert_eq!(obtained_key.encode(), r_kem_key_bytes) + } + } + } + } +} diff --git a/common/nym-kkt/src/session.rs b/common/nym-kkt/src/session.rs new file mode 100644 index 0000000000..75492a6170 --- /dev/null +++ b/common/nym-kkt/src/session.rs @@ -0,0 +1,234 @@ +use nym_crypto::asymmetric::ed25519::{self, Signature}; +use rand::{CryptoRng, RngCore}; + +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: &mut KKTContext, + remote_verification_key: &ed25519::PublicKey, + expected_hash: &[u8], + message_bytes: &[u8], +) -> Result, KKTError> { + // sizes have to be correct + let (frame, remote_context) = KKTFrame::from_bytes(message_bytes)?; + + 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(frame.body_ref()); + bytes_to_verify.extend_from_slice(frame.session_id_ref()); + + match Signature::from_bytes(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(), + frame.body_ref(), + )?; + + match validate_encapsulation_key( + &own_context.ciphersuite().hash_function(), + own_context.ciphersuite().hash_len(), + 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: &mut KKTContext, + session_id: &[u8], + 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 index f3f23b8fdb..b70550c82b 100644 --- a/common/nym-lp-common/Cargo.toml +++ b/common/nym-lp-common/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "nym-lp-common" version = "0.1.0" -edition = "2021" +edition = { workspace = true } +license = { workspace = true } [dependencies] diff --git a/common/nym-lp/Cargo.toml b/common/nym-lp/Cargo.toml index 13b7bfd13e..ea88885e81 100644 --- a/common/nym-lp/Cargo.toml +++ b/common/nym-lp/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "nym-lp" version = "0.1.0" -edition = "2021" +edition = { workspace = true } +license = { workspace = true } [dependencies] bincode = { workspace = true } @@ -14,13 +15,26 @@ bytes = { workspace = true } dashmap = { workspace = true } sha2 = { workspace = true } ansi_term = { workspace = true } +tracing = { workspace = true } utoipa = { workspace = true, features = ["macros", "non_strict_integers"] } rand = { workspace = true } +# rand 0.9 for KKT integration (nym-kkt uses rand 0.9) +rand09 = { package = "rand", version = "0.9.2" } nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] } +nym-kkt = { path = "../nym-kkt" } nym-lp-common = { path = "../nym-lp-common" } nym-sphinx = { path = "../nymsphinx" } +# 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 } +num_enum = { workspace = true } + [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } rand_chacha = "0.3" diff --git a/common/nym-lp/benches/replay_protection.rs b/common/nym-lp/benches/replay_protection.rs index 72b44fbc78..562982e527 100644 --- a/common/nym-lp/benches/replay_protection.rs +++ b/common/nym-lp/benches/replay_protection.rs @@ -1,4 +1,4 @@ -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; use nym_lp::replay::ReceivingKeyCounterValidator; use parking_lot::Mutex; use rand::{Rng, SeedableRng}; diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index 6154cdba50..260d344ec6 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -1,9 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::message::{ClientHelloData, EncryptedDataPayload, HandshakeData, LpMessage, MessageType}; -use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; use crate::LpError; +use crate::message::{ + ClientHelloData, EncryptedDataPayload, HandshakeData, KKTRequestData, KKTResponseData, + LpMessage, MessageType, +}; +use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; use bytes::BytesMut; /// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload). @@ -43,7 +46,7 @@ pub fn parse_lp_packet(src: &[u8]) -> Result { if message_size != 0 { return Err(LpError::InvalidPayloadSize { expected: 0, - actual: message_size, + actual: message_size, }); } LpMessage::Busy @@ -63,6 +66,14 @@ pub fn parse_lp_packet(src: &[u8]) -> Result { .map_err(|e| LpError::DeserializationError(e.to_string()))?; LpMessage::ClientHello(data) } + MessageType::KKTRequest => { + // KKT request contains serialized KKTFrame bytes + LpMessage::KKTRequest(KKTRequestData(payload_slice.to_vec())) + } + MessageType::KKTResponse => { + // KKT response contains serialized KKTFrame bytes + LpMessage::KKTResponse(KKTResponseData(payload_slice.to_vec())) + } }; // Extract trailer @@ -105,9 +116,9 @@ mod tests { // Import standalone functions use super::{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 crate::LpError; use bytes::BytesMut; // === Updated Encode/Decode Tests === @@ -280,7 +291,7 @@ mod tests { 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::Handshake.to_u16().to_le_bytes()); // Handshake type - // No payload, no trailer. Length = 16+2=18. Min size = 34. + // No payload, no trailer. Length = 16+2=18. Min size = 34. let result_too_short = parse_lp_packet(&buf_too_short); assert!(result_too_short.is_err()); assert!(matches!( @@ -317,7 +328,7 @@ mod tests { buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter buf_too_short.extend_from_slice(&MessageType::Busy.to_u16().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. + // Length = 16 + 2 + 15 = 33. Min Size = 34. let result_too_short = parse_lp_packet(&buf_too_short); assert!( result_too_short.is_err(), @@ -337,7 +348,7 @@ mod tests { buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index buf.extend_from_slice(&123u64.to_le_bytes()); // Counter buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type - // Need payload and trailer to meet min_size requirement + // 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 @@ -390,9 +401,11 @@ mod tests { // Create ClientHelloData let client_key = [42u8; 32]; + let client_ed25519_key = [43u8; 32]; let salt = [99u8; 32]; let hello_data = ClientHelloData { client_lp_public_key: client_key, + client_ed25519_public_key: client_ed25519_key, salt, }; @@ -438,7 +451,8 @@ mod tests { // Create ClientHelloData with fresh salt let client_key = [7u8; 32]; - let hello_data = ClientHelloData::new_with_fresh_salt(client_key); + let client_ed25519_key = [8u8; 32]; + let hello_data = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); // Create a ClientHello message packet let packet = LpPacket { @@ -532,6 +546,7 @@ mod tests { let hello_data = ClientHelloData { client_lp_public_key: [version; 32], + client_ed25519_public_key: [version.wrapping_add(2); 32], salt: [version.wrapping_add(1); 32], }; diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index ecb8389323..456bc72154 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{noise_protocol::NoiseError, replay::ReplayError}; +use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; use thiserror::Error; #[derive(Error, Debug)] @@ -48,6 +49,9 @@ pub enum LpError { #[error("Deserialization error: {0}")] DeserializationError(String), + #[error("KKT protocol error: {0}")] + KKTError(String), + #[error(transparent)] InvalidBase58String(#[from] bs58::decode::Error), @@ -70,4 +74,8 @@ pub enum LpError { /// State machine not found. #[error("State machine not found for lp_id: {lp_id}")] StateMachineNotFound { lp_id: u32 }, + + /// Ed25519 to X25519 conversion error. + #[error("Ed25519 key conversion error: {0}")] + Ed25519RecoveryError(#[from] Ed25519RecoveryError), } diff --git a/common/nym-lp/src/keypair.rs b/common/nym-lp/src/keypair.rs index abcc14f24a..6f9546ba52 100644 --- a/common/nym-lp/src/keypair.rs +++ b/common/nym-lp/src/keypair.rs @@ -8,8 +8,15 @@ use utoipa::ToSchema; use crate::LpError; +#[derive(Clone)] pub struct PrivateKey(SphinxPrivateKey); +impl fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("PrivateKey").field(&"[REDACTED]").finish() + } +} + impl Deref for PrivateKey { type Target = SphinxPrivateKey; @@ -49,8 +56,17 @@ impl PrivateKey { } } +#[derive(Clone)] pub struct PublicKey(SphinxPublicKey); +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("PublicKey") + .field(&self.to_base58_string()) + .finish() + } +} + impl Deref for PublicKey { type Target = SphinxPublicKey; diff --git a/common/nym-lp/src/kkt_orchestrator.rs b/common/nym-lp/src/kkt_orchestrator.rs new file mode 100644 index 0000000000..5999a9929f --- /dev/null +++ b/common/nym-lp/src/kkt_orchestrator.rs @@ -0,0 +1,468 @@ +// 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 (client_context, request_data) = create_request( +//! ciphersuite, +//! &client_signing_key, +//! ).unwrap(); +//! +//! // Gateway: Handle request +//! let response_data = handle_request( +//! &request_data, +//! Some(&client_verification_key), +//! &gateway_signing_key, +//! &gateway_kem_public_key, +//! ).unwrap(); +//! +//! // Client: Process response +//! let gateway_kem_key = process_response( +//! client_context, +//! &gateway_verification_key, +//! &expected_key_hash, +//! &response_data, +//! ).unwrap(); +//! ``` + +use crate::LpError; +use crate::message::{KKTRequestData, KKTResponseData}; +use nym_crypto::asymmetric::ed25519; +use nym_kkt::ciphersuite::{Ciphersuite, EncapsulationKey}; +use nym_kkt::context::KKTContext; +use nym_kkt::frame::KKTFrame; +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 +/// +/// # Returns +/// * `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, +) -> Result<(KKTContext, KKTRequestData), LpError> { + // Note: Uses rand 0.9's thread_rng() to match nym-kkt's rand version + let mut rng = rand09::rng(); + let (context, frame) = request_kem_key(&mut rng, ciphersuite, signing_key) + .map_err(|e| LpError::KKTError(e.to_string()))?; + + let request_bytes = frame.to_bytes(); + Ok((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 +/// * `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, + responder_vk: &ed25519::PublicKey, + expected_key_hash: &[u8], + response_data: &KKTResponseData, +) -> Result, LpError> { + validate_kem_response( + &mut context, + 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_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_kem_key: &EncapsulationKey<'a>, +) -> Result { + // Deserialize request frame + let (request_frame, _) = KKTFrame::from_bytes(&request_data.0) + .map_err(|e| LpError::KKTError(format!("Failed to parse KKT request: {}", e)))?; + + // Handle the request and generate response + let response_frame = handle_kem_request( + &request_frame, + initiator_vk, + responder_signing_key, + responder_kem_key, + ) + .map_err(|e| LpError::KKTError(e.to_string()))?; + + let response_bytes = response_frame.to_bytes(); + Ok(KKTResponseData(response_bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme}; + use nym_kkt::key_utils::{generate_keypair_libcrux, hash_encapsulation_key}; + use rand09::RngCore; + + #[test] + fn test_kkt_roundtrip_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 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); + + // 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 (context, request_data) = + create_request(ciphersuite, initiator_keypair.private_key()).unwrap(); + + // Gateway: Handle request + let response_data = handle_request( + &request_data, + Some(initiator_keypair.public_key()), + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Client: Process response + let obtained_key = process_response( + context, + responder_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) + 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 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, // Anonymous - no verification key + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Initiator: Validate response + let obtained_key = validate_kem_response( + &mut context, + responder_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(); + + 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); + + // 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 (_context, request_data) = + create_request(ciphersuite, initiator_keypair.private_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_keypair.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 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 (_, 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 (context, request_data) = + create_request(ciphersuite, initiator_keypair.private_key()).unwrap(); + + let response_data = handle_request( + &request_data, + Some(initiator_keypair.public_key()), + responder_keypair.private_key(), + &responder_kem_key, + ) + .unwrap(); + + // Client validates with WRONG hash + let result = process_response( + context, + responder_keypair.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_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); + + // Create malformed request data (invalid bytes) + let malformed_request = KKTRequestData(vec![0xFF; 100]); + + let result = handle_request( + &malformed_request, + None, + responder_keypair.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(); + + 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 ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::X25519, + HashFunction::Blake3, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + + let (context, _request_data) = + create_request(ciphersuite, initiator_keypair.private_key()).unwrap(); + + // Create malformed response data + let malformed_response = KKTResponseData(vec![0xFF; 100]); + let key_hash = [0u8; 32]; + + let result = process_response( + context, + responder_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 243b238493..c23fec5341 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -4,6 +4,7 @@ pub mod codec; pub mod error; pub mod keypair; +pub mod kkt_orchestrator; pub mod message; pub mod noise_protocol; pub mod packet; @@ -19,9 +20,8 @@ pub use error::LpError; use keypair::PublicKey; pub use message::{ClientHelloData, LpMessage}; pub use packet::LpPacket; -pub use psk::derive_psk; pub use replay::{ReceivingKeyCounterValidator, ReplayError}; -pub use session::LpSession; +pub use session::{LpSession, generate_fresh_salt}; pub use session_manager::SessionManager; // Add the new state machine module @@ -34,35 +34,47 @@ pub const NOISE_PSK_INDEX: u8 = 3; #[cfg(test)] pub fn sessions_for_tests() -> (LpSession, LpSession) { use crate::{keypair::Keypair, make_lp_id}; + use nym_crypto::asymmetric::ed25519; + // X25519 keypairs for Noise protocol let keypair_1 = Keypair::default(); let keypair_2 = Keypair::default(); let id = make_lp_id(keypair_1.public_key(), keypair_2.public_key()); + // Ed25519 keypairs for PSQ authentication (placeholders for testing) + let ed25519_keypair_1 = ed25519::KeyPair::from_secret([1u8; 32], 0); + let ed25519_keypair_2 = ed25519::KeyPair::from_secret([2u8; 32], 1); + // Use consistent salt for deterministic tests let salt = [1u8; 32]; - // Initiator derives PSK from their perspective - let initiator_psk = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt); + // PSQ will always derive the PSK during handshake using X25519 as DHKEM let initiator_session = LpSession::new( id, true, - &keypair_1.private_key().to_bytes(), - &keypair_2.public_key().to_bytes(), - &initiator_psk, + ( + ed25519_keypair_1.private_key(), + ed25519_keypair_1.public_key(), + ), + keypair_1.private_key(), + ed25519_keypair_2.public_key(), + keypair_2.public_key(), + &salt, ) .expect("Test session creation failed"); - // Responder derives same PSK from their perspective - let responder_psk = derive_psk(keypair_2.private_key(), keypair_1.public_key(), &salt); - let responder_session = LpSession::new( id, false, - &keypair_2.private_key().to_bytes(), - &keypair_1.public_key().to_bytes(), - &responder_psk, + ( + ed25519_keypair_2.private_key(), + ed25519_keypair_2.public_key(), + ), + keypair_2.private_key(), + ed25519_keypair_1.public_key(), + keypair_1.public_key(), + &salt, ) .expect("Test session creation failed"); @@ -105,11 +117,11 @@ pub fn make_conv_id(src: &[u8], dst: &[u8]) -> u32 { #[cfg(test)] mod tests { - use crate::keypair::Keypair; + use crate::keypair::PublicKey; use crate::message::LpMessage; use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; use crate::session_manager::SessionManager; - use crate::{make_lp_id, sessions_for_tests, LpError}; + use crate::{LpError, make_lp_id, sessions_for_tests}; use bytes::BytesMut; // Import the new standalone functions @@ -216,28 +228,60 @@ mod tests { #[test] fn test_session_manager_integration() { + use nym_crypto::asymmetric::ed25519; + // Create session manager let local_manager = SessionManager::new(); let remote_manager = SessionManager::new(); - let local_keypair = Keypair::default(); - let remote_keypair = Keypair::default(); - let lp_id = make_lp_id(local_keypair.public_key(), remote_keypair.public_key()); + + // Generate Ed25519 keypairs for PSQ authentication + let ed25519_keypair_local = ed25519::KeyPair::from_secret([8u8; 32], 0); + let ed25519_keypair_remote = ed25519::KeyPair::from_secret([9u8; 32], 1); + + // Derive X25519 keys from Ed25519 (same as state machine does internally) + let x25519_pub_local = ed25519_keypair_local + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + let x25519_pub_remote = ed25519_keypair_remote + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + + // Convert to LP keypair types + let lp_pub_local = PublicKey::from_bytes(x25519_pub_local.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + let lp_pub_remote = PublicKey::from_bytes(x25519_pub_remote.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + + // Calculate lp_id (matches state machine's internal calculation) + let lp_id = make_lp_id(&lp_pub_local, &lp_pub_remote); + + // Test salt + let salt = [46u8; 32]; + // Create a session via manager let _ = local_manager .create_session_state_machine( - &local_keypair, - remote_keypair.public_key(), + ( + ed25519_keypair_local.private_key(), + ed25519_keypair_local.public_key(), + ), + ed25519_keypair_remote.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); let _ = remote_manager .create_session_state_machine( - &remote_keypair, - local_keypair.public_key(), + ( + ed25519_keypair_remote.private_key(), + ed25519_keypair_remote.public_key(), + ), + ed25519_keypair_local.public_key(), false, - &[2u8; 32], + &salt, ) .unwrap(); // === Packet 1 (Counter 0 - Should succeed) === diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index d308d353f2..bbc0ea9904 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -3,13 +3,16 @@ use std::fmt::{self, Display}; // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use bytes::{BufMut, BytesMut}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde::{Deserialize, Serialize}; /// Data structure for the ClientHello message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientHelloData { - /// Client's LP x25519 public key (32 bytes) + /// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key pub client_lp_public_key: [u8; 32], + /// Client's Ed25519 public key (32 bytes) - for PSQ authentication + pub client_ed25519_public_key: [u8; 32], /// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce) pub salt: [u8; 32], } @@ -20,9 +23,12 @@ impl ClientHelloData { /// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce /// /// # Arguments - /// * `client_lp_public_key` - Client's x25519 public key - /// * `protocol_version` - Protocol version number - pub fn new_with_fresh_salt(client_lp_public_key: [u8; 32]) -> Self { + /// * `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: [u8; 32], + client_ed25519_public_key: [u8; 32], + ) -> Self { use std::time::{SystemTime, UNIX_EPOCH}; // Generate salt: timestamp + nonce @@ -41,6 +47,7 @@ impl ClientHelloData { Self { client_lp_public_key, + client_ed25519_public_key, salt, } } @@ -56,33 +63,24 @@ impl ClientHelloData { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] #[repr(u16)] pub enum MessageType { Busy = 0x0000, Handshake = 0x0001, EncryptedData = 0x0002, ClientHello = 0x0003, + KKTRequest = 0x0004, + KKTResponse = 0x0005, } impl MessageType { pub(crate) fn from_u16(value: u16) -> Option { - match value { - 0x0000 => Some(MessageType::Busy), - 0x0001 => Some(MessageType::Handshake), - 0x0002 => Some(MessageType::EncryptedData), - 0x0003 => Some(MessageType::ClientHello), - _ => None, - } + MessageType::try_from(value).ok() } pub fn to_u16(&self) -> u16 { - match self { - MessageType::Busy => 0x0000, - MessageType::Handshake => 0x0001, - MessageType::EncryptedData => 0x0002, - MessageType::ClientHello => 0x0003, - } + u16::from(*self) } } @@ -92,12 +90,22 @@ pub struct HandshakeData(pub Vec); #[derive(Debug, Clone, PartialEq, Eq)] pub struct EncryptedDataPayload(pub Vec); +/// KKT request frame data (serialized KKTFrame bytes) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KKTRequestData(pub Vec); + +/// KKT response frame data (serialized KKTFrame bytes) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KKTResponseData(pub Vec); + #[derive(Debug, Clone)] pub enum LpMessage { Busy, Handshake(HandshakeData), EncryptedData(EncryptedDataPayload), ClientHello(ClientHelloData), + KKTRequest(KKTRequestData), + KKTResponse(KKTResponseData), } impl Display for LpMessage { @@ -107,6 +115,8 @@ impl Display for LpMessage { 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"), } } } @@ -118,6 +128,8 @@ impl LpMessage { LpMessage::Handshake(payload) => payload.0.as_slice(), LpMessage::EncryptedData(payload) => payload.0.as_slice(), LpMessage::ClientHello(_) => unimplemented!(), // Structured data, serialized in encode_content + LpMessage::KKTRequest(payload) => payload.0.as_slice(), + LpMessage::KKTResponse(payload) => payload.0.as_slice(), } } @@ -127,6 +139,8 @@ impl LpMessage { 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(), } } @@ -135,7 +149,9 @@ impl LpMessage { LpMessage::Busy => 0, LpMessage::Handshake(payload) => payload.0.len(), LpMessage::EncryptedData(payload) => payload.0.len(), - LpMessage::ClientHello(_) => 65, // 32 bytes key + 1 byte version + 32 bytes salt + LpMessage::ClientHello(_) => 97, // 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + 1 byte bincode overhead + LpMessage::KKTRequest(payload) => payload.0.len(), + LpMessage::KKTResponse(payload) => payload.0.len(), } } @@ -145,6 +161,8 @@ impl LpMessage { LpMessage::Handshake(_) => MessageType::Handshake, LpMessage::EncryptedData(_) => MessageType::EncryptedData, LpMessage::ClientHello(_) => MessageType::ClientHello, + LpMessage::KKTRequest(_) => MessageType::KKTRequest, + LpMessage::KKTResponse(_) => MessageType::KKTResponse, } } @@ -163,6 +181,12 @@ impl LpMessage { bincode::serialize(data).expect("Failed to serialize ClientHelloData"); dst.put_slice(&serialized); } + LpMessage::KKTRequest(payload) => { + dst.put_slice(&payload.0); + } + LpMessage::KKTResponse(payload) => { + dst.put_slice(&payload.0); + } } } } @@ -170,8 +194,8 @@ impl LpMessage { #[cfg(test)] mod tests { use super::*; - use crate::packet::{LpHeader, TRAILER_LEN}; use crate::LpPacket; + use crate::packet::{LpHeader, TRAILER_LEN}; #[test] fn encoding() { @@ -208,8 +232,9 @@ mod tests { #[test] fn test_client_hello_salt_generation() { let client_key = [1u8; 32]; - let hello1 = ClientHelloData::new_with_fresh_salt(client_key); - let hello2 = ClientHelloData::new_with_fresh_salt(client_key); + let client_ed25519_key = [2u8; 32]; + let hello1 = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); + let hello2 = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); // Different salts should be generated assert_ne!(hello1.salt, hello2.salt); @@ -223,7 +248,8 @@ mod tests { #[test] fn test_client_hello_timestamp_extraction() { let client_key = [2u8; 32]; - let hello = ClientHelloData::new_with_fresh_salt(client_key); + let client_ed25519_key = [3u8; 32]; + let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); let timestamp = hello.extract_timestamp(); let now = std::time::SystemTime::now() @@ -238,7 +264,8 @@ mod tests { #[test] fn test_client_hello_salt_format() { let client_key = [3u8; 32]; - let hello = ClientHelloData::new_with_fresh_salt(client_key); + let client_ed25519_key = [4u8; 32]; + let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); // First 8 bytes should be non-zero timestamp let timestamp_bytes = &hello.salt[..8]; diff --git a/common/nym-lp/src/noise_protocol.rs b/common/nym-lp/src/noise_protocol.rs index 06ec8b1346..42b5e0308f 100644 --- a/common/nym-lp/src/noise_protocol.rs +++ b/common/nym-lp/src/noise_protocol.rs @@ -3,7 +3,7 @@ //! Sans-IO Noise protocol state machine, adapted from noise-psq. -use snow::{params::NoiseParams, TransportState}; +use snow::{TransportState, params::NoiseParams}; use thiserror::Error; // --- Error Definition --- @@ -20,6 +20,9 @@ pub enum NoiseError { #[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), } @@ -255,6 +258,32 @@ impl NoiseProtocol { 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), + } + } } pub fn create_noise_state( diff --git a/common/nym-lp/src/packet.rs b/common/nym-lp/src/packet.rs index 98570154b1..193fa0704d 100644 --- a/common/nym-lp/src/packet.rs +++ b/common/nym-lp/src/packet.rs @@ -1,9 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::LpError; use crate::message::LpMessage; use crate::replay::ReceivingKeyCounterValidator; -use crate::LpError; use bytes::{BufMut, BytesMut}; use nym_lp_common::format_debug_bytes; use parking_lot::Mutex; diff --git a/common/nym-lp/src/psk.rs b/common/nym-lp/src/psk.rs index 676eb0e80a..f4f416b3a7 100644 --- a/common/nym-lp/src/psk.rs +++ b/common/nym-lp/src/psk.rs @@ -4,57 +4,403 @@ //! 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 using ECDH + Blake3 KDF. +//! derive the same PSK from their LP keypairs. +//! +//! 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 crate::keypair::{PrivateKey, PublicKey}; +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; +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. -const PSK_CONTEXT: &str = "nym-lp-psk-v1"; +/// Context string for Blake3 KDF domain separation (PSQ-enhanced). +const PSK_PSQ_CONTEXT: &str = "nym-lp-psk-psq-v1"; -/// Derives a PSK using Blake3 KDF from local private key, remote public key, and salt. +/// Session context for PSQ protocol. +const PSQ_SESSION_CONTEXT: &[u8] = b"nym-lp-psq-session"; + +/// 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 -/// shared_secret = ECDH(local_private, remote_public) -/// psk = Blake3_derive_key(context="nym-lp-psk-v1", input=shared_secret || salt) +/// 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 +/// ) /// ``` /// -/// # Properties -/// - **Identity-bound**: PSK is tied to the LP keypairs of both parties -/// - **Session-specific**: Different salts produce different PSKs -/// - **Symmetric**: Both sides derive the same PSK from their respective keys -/// /// # Arguments -/// * `local_private` - This side's LP private key -/// * `remote_public` - Peer's LP public key -/// * `salt` - 32-byte salt (timestamp + nonce from ClientHello) +/// * `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 -/// 32-byte PSK suitable for Noise protocol +/// * `Ok((psk, ciphertext))` - PSK and ciphertext to send to responder +/// * `Err(LpError)` - If PSQ encapsulation fails /// /// # Example /// ```ignore -/// // Client side -/// let client_private = client_keypair.private_key(); -/// let gateway_public = gateway_keypair.public_key(); -/// let salt = ClientHelloData::new_with_fresh_salt(...).salt; -/// let psk = derive_psk(&client_private, &gateway_public, &salt); -/// -/// // Gateway side (derives same PSK) -/// let gateway_private = gateway_keypair.private_key(); -/// let client_public = /* from ClientHello */; -/// let psk = derive_psk(&gateway_private, &client_public, &salt); +/// // 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( - local_private: &PrivateKey, - remote_public: &PublicKey, +pub fn derive_psk_with_psq_initiator( + local_x25519_private: &PrivateKey, + remote_x25519_public: &PublicKey, + remote_kem_public: &EncapsulationKey, salt: &[u8; 32], -) -> [u8; 32] { - // Perform ECDH to get shared secret - let shared_secret = local_private.diffie_hellman(remote_public); +) -> Result<([u8; 32], Vec), LpError> { + // Step 1: Classical ECDH for baseline security + let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); - // Derive PSK using Blake3 KDF with domain separation - nym_crypto::kdf::derive_key_blake3(PSK_CONTEXT, shared_secret.as_bytes(), salt) + // Step 2: PSQ encapsulation for post-quantum security + // Extract X25519 public 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(), + )); + } + }; + + 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.as_bytes()); + 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: &PrivateKey, + remote_x25519_public: &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.as_bytes()); + 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 +/// `(psk, psq_payload_bytes)` - PSK for Noise and serialized PSQ payload to embed +pub fn psq_initiator_create_message( + local_x25519_private: &PrivateKey, + remote_x25519_public: &PublicKey, + remote_kem_public: &EncapsulationKey, + client_ed25519_sk: &ed25519::PrivateKey, + client_ed25519_pk: &ed25519::PublicKey, + salt: &[u8; 32], + session_context: &[u8], +) -> 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 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) + let psq_psk = state.unregistered_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.as_bytes()); + 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((final_psk, msg_bytes)) +} + +/// 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 +/// `psk` - Derived PSK for Noise +/// Processes a PSQ initiator message and generates a PSK with encrypted handle. +/// +/// Returns a tuple of (derived_psk, responder_msg_bytes) where responder_msg_bytes +/// contains the encrypted PSK handle (ctxt_B) that should be sent to the initiator. +pub fn psq_responder_process_message( + local_x25519_private: &PrivateKey, + remote_x25519_public: &PublicKey, + local_kem_keypair: (&DecapsulationKey, &EncapsulationKey), + initiator_ed25519_pk: &ed25519::PublicKey, + psq_payload: &[u8], + salt: &[u8; 32], + session_context: &[u8], +) -> 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: 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 + let psq_psk = registered_psk.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.as_bytes()); + 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((final_psk, responder_msg_bytes)) } #[cfg(test)] @@ -62,30 +408,35 @@ mod tests { use super::*; use crate::keypair::Keypair; - #[test] - fn test_psk_derivation_is_deterministic() { - let keypair_1 = Keypair::default(); - let keypair_2 = Keypair::default(); - let salt = [1u8; 32]; - - // Derive PSK twice with same inputs - let psk1 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt); - let psk2 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt); - - assert_eq!(psk1, psk2, "Same inputs should produce same PSK"); - } - #[test] fn test_psk_derivation_is_symmetric() { let keypair_1 = Keypair::default(); let keypair_2 = Keypair::default(); 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 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt); + 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(keypair_2.private_key(), keypair_1.public_key(), &salt); + 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, @@ -100,9 +451,24 @@ mod tests { 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(keypair_1.private_key(), keypair_2.public_key(), &salt1); - let psk2 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt2); + 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"); } @@ -114,8 +480,24 @@ mod tests { let keypair_3 = Keypair::default(); let salt = [3u8; 32]; - let psk1 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt); - let psk2 = derive_psk(keypair_1.private_key(), keypair_3.public_key(), &salt); + 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, @@ -123,14 +505,198 @@ mod tests { ); } + // PSQ-enhanced PSK tests + use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey, KEM}; + use nym_kkt::key_utils::generate_keypair_libcrux; + #[test] - fn test_psk_output_length() { - let keypair_1 = Keypair::default(); - let keypair_2 = Keypair::default(); + fn test_psq_derivation_deterministic() { + let mut rng = rand09::rng(); + + // Generate X25519 keypairs for Noise + let client_keypair = Keypair::default(); + let gateway_keypair = Keypair::default(); + + // 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 = Keypair::default(); + let gateway_keypair = Keypair::default(); + + // 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 = Keypair::default(); + let gateway_keypair = Keypair::default(); + + // 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 = Keypair::default(); + let gateway_keypair = Keypair::default(); + + 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(keypair_1.private_key(), keypair_2.public_key(), &salt); + let (psk, _) = derive_psk_with_psq_initiator( + client_keypair.private_key(), + gateway_keypair.public_key(), + &enc_key, + &salt, + ) + .unwrap(); - assert_eq!(psk.len(), 32, "PSK should be exactly 32 bytes"); + 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 = Keypair::default(); + let gateway_keypair = Keypair::default(); + + 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/replay/error.rs b/common/nym-lp/src/replay/error.rs index 2a5affb986..6422eb8613 100644 --- a/common/nym-lp/src/replay/error.rs +++ b/common/nym-lp/src/replay/error.rs @@ -56,13 +56,9 @@ mod tests { #[test] fn test_replay_result() { let ok_result: ReplayResult<()> = Ok(()); - let err_result: ReplayResult<()> = Err(ReplayError::InvalidCounter); + let err = ReplayError::InvalidCounter; assert!(ok_result.is_ok()); - assert!(err_result.is_err()); - assert!(matches!( - err_result.unwrap_err(), - ReplayError::InvalidCounter - )); + assert!(matches!(err, ReplayError::InvalidCounter)); } } diff --git a/common/nym-lp/src/replay/simd/arm.rs b/common/nym-lp/src/replay/simd/arm.rs index 3d881396ab..cdf0302d6c 100644 --- a/common/nym-lp/src/replay/simd/arm.rs +++ b/common/nym-lp/src/replay/simd/arm.rs @@ -210,17 +210,20 @@ pub mod atomic { if first_full_word <= last_full_word { // Use NEON to set words faster // Safety: vdupq_n_u64 is safe to call with any u64 value - let ones_vec = vdupq_n_u64(u64::MAX); + let ones_vec = unsafe { vdupq_n_u64(u64::MAX) }; let mut idx = first_full_word; while idx + 2 <= last_full_word + 1 { // Safety: // - bitmap[idx..] is valid for reads/writes of at least 2 u64 words (16 bytes) // - We check that idx + 2 <= last_full_word + 1 to ensure we have 2 complete words - let current_vec = vld1q_u64(bitmap[idx..].as_ptr()); - // Safety: vorrq_u64 is safe when given valid vector values - let result_vec = vorrq_u64(current_vec, ones_vec); - vst1q_u64(bitmap[idx..].as_mut_ptr(), result_vec); + unsafe { + let current_vec = vld1q_u64(bitmap[idx..].as_ptr()); + // Safety: vorrq_u64 is safe when given valid vector values + let result_vec = vorrq_u64(current_vec, ones_vec); + vst1q_u64(bitmap[idx..].as_mut_ptr(), result_vec); + } + idx += 2; } diff --git a/common/nym-lp/src/replay/validator.rs b/common/nym-lp/src/replay/validator.rs index e3e1a8fe79..1d842b4913 100644 --- a/common/nym-lp/src/replay/validator.rs +++ b/common/nym-lp/src/replay/validator.rs @@ -467,9 +467,11 @@ mod tests { assert!(validator.mark_did_receive_branchless(1000 + 70).is_ok()); assert!(validator.mark_did_receive_branchless(1000 + 71).is_ok()); assert!(validator.mark_did_receive_branchless(1000 + 72).is_ok()); - assert!(validator - .mark_did_receive_branchless(1000 + 72 + 125) - .is_ok()); + assert!( + validator + .mark_did_receive_branchless(1000 + 72 + 125) + .is_ok() + ); assert!(validator.mark_did_receive_branchless(1000 + 63).is_ok()); // Check duplicates @@ -757,8 +759,8 @@ mod tests { ); // Verify minimum memory needed for different window sizes - for window_size in [64, 128, 256, 512, 1024, 2048] { - let words_needed = (window_size + WORD_SIZE - 1) / WORD_SIZE; // Ceiling division + for window_size in [64usize, 128, 256, 512, 1024, 2048] { + let words_needed = window_size.div_ceil(WORD_SIZE); let memory_needed = size_of::() * 2 + size_of::() * words_needed; println!( "Window size {}: {} bytes minimum", @@ -847,10 +849,11 @@ mod tests { #[test] fn test_clear_window_overflow() { - let mut validator = ReceivingKeyCounterValidator::default(); - // Set a very large next value, close to u64::MAX - validator.next = u64::MAX - 1000; + let mut validator = ReceivingKeyCounterValidator { + next: u64::MAX - 1000, + ..Default::default() + }; // Try to clear window with an even higher counter // This should exercise the potentially problematic code diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 3dddfdce88..e58a2200f1 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -6,19 +6,118 @@ //! This module implements session management functionality, including replay protection //! and Noise protocol state handling. +use crate::keypair::{PrivateKey, PublicKey}; use crate::message::{EncryptedDataPayload, HandshakeData}; use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult}; use crate::packet::LpHeader; +use crate::psk::{psq_initiator_create_message, psq_responder_process_message}; use crate::replay::ReceivingKeyCounterValidator; use crate::{LpError, LpMessage, LpPacket}; +use nym_crypto::asymmetric::ed25519; +use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; use parking_lot::Mutex; use snow::Builder; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +/// KKT (KEM Key Transfer) exchange state. +/// +/// Tracks the KKT protocol for obtaining the responder's KEM public key +/// before PSQ can begin. This allows post-quantum KEM algorithms to be +/// used even when keys are not pre-published. +/// +/// # State Transitions +/// +/// **Initiator path:** +/// ```text +/// NotStarted → InitiatorWaiting → Completed +/// ``` +/// +/// **Responder path:** +/// ```text +/// NotStarted → ResponderProcessed +/// ``` +pub enum KKTState { + /// KKT exchange not started. + NotStarted, + + /// Initiator sent KKT request and is waiting for responder's KEM key. + InitiatorWaiting { + /// KKT context for verifying the response + context: nym_kkt::context::KKTContext, + }, + + /// KKT exchange completed (initiator received and validated KEM key). + Completed { + /// Responder's KEM public key for PSQ encapsulation + kem_pk: Box>, + }, + + /// Responder processed a KKT request and sent response. + /// Responder uses their own KEM keypair, not the one from KKT. + ResponderProcessed, +} + +impl std::fmt::Debug for KKTState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotStarted => write!(f, "KKTState::NotStarted"), + Self::InitiatorWaiting { context } => f + .debug_struct("KKTState::InitiatorWaiting") + .field("context", context) + .finish(), + Self::Completed { .. } => write!(f, "KKTState::Completed {{ kem_pk: }}"), + Self::ResponderProcessed => write!(f, "KKTState::ResponderProcessed"), + } + } +} + +/// PSQ (Post-Quantum Secure PSK) handshake state. +/// +/// Tracks the PSQ protocol state machine through the session lifecycle. +/// +/// # State Transitions +/// +/// **Initiator path:** +/// ```text +/// NotStarted → InitiatorWaiting → Completed +/// ``` +/// +/// **Responder path:** +/// ```text +/// NotStarted → ResponderWaiting → Completed +/// ``` +#[derive(Debug)] +pub enum PSQState { + /// PSQ handshake not yet started. + NotStarted, + + /// Initiator has sent PSQ ciphertext and is waiting for confirmation. + /// Stores the ciphertext that was sent. + InitiatorWaiting { ciphertext: Vec }, + + /// Responder is ready to receive and decapsulate PSQ ciphertext. + ResponderWaiting, + + /// PSQ handshake completed successfully. + /// The PSK has been derived and registered with the Noise protocol. + Completed { + /// The derived post-quantum PSK + psk: [u8; 32], + }, +} /// A session in the Lewes Protocol, handling connection state with Noise. /// /// Sessions manage connection state, including LP replay protection and Noise cryptography. /// Each session has a unique receiving index and sending index for connection identification. +/// +/// ## PSK Injection Lifecycle +/// +/// 1. Session created with dummy PSK `[0u8; 32]` in Noise HandshakeState +/// 2. During handshake, PSQ runs and derives real post-quantum PSK +/// 3. Real PSK injected via `set_psk()` - `psk_injected` flag set to `true` +/// 4. Handshake completes, transport mode available +/// 5. Transport operations (`encrypt_data`/`decrypt_data`) check `psk_injected` flag for safety #[derive(Debug)] pub struct LpSession { id: u32, @@ -29,11 +128,71 @@ pub struct LpSession { /// Noise protocol state machine noise_state: Mutex, + /// KKT (KEM Key Transfer) exchange state + kkt_state: Mutex, + + /// PSQ (Post-Quantum Secure PSK) handshake state + psq_state: Mutex, + + /// PSK handle from responder (ctxt_B) for future re-registration + psk_handle: Mutex>>, + /// Counter for outgoing packets sending_counter: AtomicU64, /// Validator for incoming packet counters to prevent replay attacks receiving_counter: Mutex, + + /// Safety flag: `true` if real PSK was injected via PSQ, `false` if still using dummy PSK. + /// This prevents transport mode operations from running with the insecure dummy `[0u8; 32]` PSK. + psk_injected: AtomicBool, + + // PSQ-related keys stored for handshake + /// Local Ed25519 private key for PSQ authentication + local_ed25519_private: ed25519::PrivateKey, + + /// Local Ed25519 public key for PSQ authentication + local_ed25519_public: ed25519::PublicKey, + + /// Remote Ed25519 public key for PSQ authentication + remote_ed25519_public: ed25519::PublicKey, + + /// Local X25519 private key (Noise static key) + local_x25519_private: PrivateKey, + + /// Remote X25519 public key (Noise static key) + remote_x25519_public: PublicKey, + + /// Salt for PSK derivation + salt: [u8; 32], +} + +/// Generates a fresh salt for PSK derivation. +/// +/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce +/// +/// This ensures each session derives a unique PSK, even with the same key pairs. +/// The timestamp provides temporal uniqueness while the random nonce prevents collisions. +/// +/// # Returns +/// A 32-byte array containing fresh salt material +pub fn generate_fresh_salt() -> [u8; 32] { + use rand::RngCore; + use std::time::{SystemTime, UNIX_EPOCH}; + + let mut salt = [0u8; 32]; + + // First 8 bytes: current timestamp as u64 little-endian + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("System time before UNIX epoch") + .as_secs(); + salt[..8].copy_from_slice(×tamp.to_le_bytes()); + + // Last 24 bytes: random nonce + rand::thread_rng().fill_bytes(&mut salt[8..]); + + salt } impl LpSession { @@ -50,24 +209,36 @@ impl LpSession { self.is_initiator } + /// Returns the local X25519 public key derived from the private 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) -> PublicKey { + self.local_x25519_private.public_key() + } + /// Creates a new session and initializes the Noise protocol state. /// + /// PSQ always runs during the handshake to derive the real PSK from X25519 DHKEM. + /// The Noise protocol is initialized with a dummy PSK that gets replaced during handshake. + /// /// # Arguments /// - /// * `receiving_index` - Index used for receiving packets (becomes session ID). - /// * `sending_index` - Index used for sending packets to the peer. + /// * `id` - Session identifier /// * `is_initiator` - True if this side initiates the Noise handshake. - /// * `local_static_key` - This side's static private key (e.g., X25519). - /// * `remote_static_key` - The peer's static public key (required for initiator in some patterns like XK). - /// * `psk` - The pre-shared key established out-of-band. - /// * `pattern_name` - The Noise protocol pattern string (e.g., "Noise_XKpsk3_25519_ChaChaPoly_SHA256"). - /// * `psk_index` - The index/position where the PSK is mixed in according to the pattern. + /// * `local_ed25519_keypair` - This side's Ed25519 keypair for PSQ authentication + /// * `local_x25519_key` - This side's X25519 private key for Noise protocol and DHKEM + /// * `remote_ed25519_key` - Peer's Ed25519 public key for PSQ authentication + /// * `remote_x25519_key` - Peer's X25519 public key for Noise protocol and DHKEM + /// * `salt` - Salt for PSK derivation pub fn new( id: u32, is_initiator: bool, - local_private_key: &[u8], - remote_public_key: &[u8], - psk: &[u8], + local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey), + local_x25519_key: &PrivateKey, + remote_ed25519_key: &ed25519::PublicKey, + remote_x25519_key: &PublicKey, + salt: &[u8; 32], ) -> Result { // XKpsk3 pattern requires remote static key known upfront (XK) // and PSK mixed at position 3. This provides forward secrecy with PSK authentication. @@ -77,11 +248,16 @@ impl LpSession { let params = pattern_name.parse()?; let builder = Builder::new(params); - let builder = builder.local_private_key(local_private_key); + let local_key_bytes = local_x25519_key.to_bytes(); + let builder = builder.local_private_key(&local_key_bytes); - let builder = builder.remote_public_key(remote_public_key); + let remote_key_bytes = remote_x25519_key.to_bytes(); + let builder = builder.remote_public_key(&remote_key_bytes); - let builder = builder.psk(psk_index, psk); + // Initialize with dummy PSK - real PSK will be injected via set_psk() during handshake + // when PSQ runs using X25519 as DHKEM + let dummy_psk = [0u8; 32]; + let builder = builder.psk(psk_index, &dummy_psk); let initial_state = if is_initiator { builder.build_initiator().map_err(LpError::SnowKeyError)? @@ -91,12 +267,40 @@ impl LpSession { let noise_protocol = NoiseProtocol::new(initial_state); + // Initialize KKT state - both roles start at NotStarted + let kkt_state = KKTState::NotStarted; + + // Initialize PSQ state based on role + let psq_state = if is_initiator { + PSQState::NotStarted + } else { + PSQState::ResponderWaiting + }; + Ok(Self { id, is_initiator, noise_state: Mutex::new(noise_protocol), + kkt_state: Mutex::new(kkt_state), + psq_state: Mutex::new(psq_state), + psk_handle: Mutex::new(None), sending_counter: AtomicU64::new(0), receiving_counter: Mutex::new(ReceivingKeyCounterValidator::default()), + psk_injected: AtomicBool::new(false), + // Ed25519 keys don't impl Clone, so convert to bytes and reconstruct + local_ed25519_private: ed25519::PrivateKey::from_bytes( + &local_ed25519_keypair.0.to_bytes(), + ) + .expect("Valid ed25519 private key"), + local_ed25519_public: ed25519::PublicKey::from_bytes( + &local_ed25519_keypair.1.to_bytes(), + ) + .expect("Valid ed25519 public key"), + remote_ed25519_public: ed25519::PublicKey::from_bytes(&remote_ed25519_key.to_bytes()) + .expect("Valid ed25519 public key"), + local_x25519_private: local_x25519_key.clone(), + remote_x25519_public: remote_x25519_key.clone(), + salt: *salt, }) } @@ -166,20 +370,326 @@ impl LpSession { counter_validator.current_packet_cnt() } + /// Returns the stored PSK handle (ctxt_B) if available. + /// + /// The PSK handle is received from the responder during handshake and can be + /// used for future PSK re-registration without running KEM encapsulation again. + /// + /// # Returns + /// + /// * `Some(Vec)` - The encrypted PSK handle from the responder + /// * `None` - PSK handle not yet received or session is initiator before handshake completion + pub fn get_psk_handle(&self) -> Option> { + self.psk_handle.lock().clone() + } + + /// Prepares a KKT (KEM Key Transfer) request message. + /// + /// This should be called by the initiator before starting the Noise handshake + /// to obtain the responder's KEM public key. The KKT protocol authenticates + /// the exchange using Ed25519 signatures. + /// + /// **Protocol Flow:** + /// 1. Initiator creates KKT request with Ed25519 signature + /// 2. Responder validates signature and responds with KEM public key + signature + /// 3. Initiator validates response and stores KEM key for PSQ + /// + /// # Returns + /// + /// * `Some(Ok(LpMessage::KKTRequest))` - KKT request ready to send + /// * `Some(Err(LpError))` - Error creating KKT request + /// * `None` - KKT not applicable (responder, or already completed) + pub fn prepare_kkt_request(&self) -> Option> { + use nym_kkt::{ + ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}, + kkt::request_kem_key, + }; + + let mut kkt_state = self.kkt_state.lock(); + + // Only initiator creates KKT requests, and only when not started + if !self.is_initiator || !matches!(*kkt_state, KKTState::NotStarted) { + return None; + } + + // Use X25519 as KEM for now (can extend to ML-KEM-768 later) + let ciphersuite = match Ciphersuite::resolve_ciphersuite( + KEM::X25519, + HashFunction::Blake3, + SignatureScheme::Ed25519, + None, + ) { + Ok(cs) => cs, + Err(e) => { + return Some(Err(LpError::Internal(format!( + "KKT ciphersuite error: {:?}", + e + )))); + } + }; + + let mut rng = rand09::rng(); + match request_kem_key(&mut rng, ciphersuite, &self.local_ed25519_private) { + Ok((context, request_frame)) => { + // Store context for response validation + *kkt_state = KKTState::InitiatorWaiting { context }; + + // Serialize KKT frame to bytes + let request_bytes = request_frame.to_bytes(); + Some(Ok(LpMessage::KKTRequest(crate::message::KKTRequestData( + request_bytes, + )))) + } + Err(e) => Some(Err(LpError::Internal(format!( + "KKT request creation failed: {:?}", + e + )))), + } + } + + /// Processes a KKT response from the responder. + /// + /// Validates the responder's signature and stores the authenticated KEM public key + /// for use in PSQ encapsulation. + /// + /// # Arguments + /// + /// * `response_bytes` - Raw KKT response message from responder + /// * `expected_key_hash` - Optional expected hash of responder's KEM key. + /// - `Some(hash)`: Full KKT validation (signature + hash) - use when directory service available + /// - `None`: Signature-only validation (hash computed from received key) - temporary mode + /// + /// # Returns + /// + /// * `Ok(())` - KKT exchange completed, KEM key stored + /// * `Err(LpError)` - Signature verification failed, hash mismatch, or invalid state + /// + /// # Note + /// + /// When None is passed, the function computes the hash from the received key and validates against + /// that (effectively signature-only mode). This allows easy upgrade: just pass Some(directory_hash) + /// when directory service becomes available. The full KKT protocol with hash pinning provides + /// protection against key substitution attacks. + pub fn process_kkt_response( + &self, + response_bytes: &[u8], + expected_key_hash: Option<&[u8]>, + ) -> Result<(), LpError> { + use nym_kkt::key_utils::hash_encapsulation_key; + use nym_kkt::kkt::validate_kem_response; + + let mut kkt_state = self.kkt_state.lock(); + + // Extract context from waiting state + let mut context = match &*kkt_state { + KKTState::InitiatorWaiting { context } => *context, + _ => { + return Err(LpError::Internal( + "KKT response received in invalid state".to_string(), + )); + } + }; + + // Determine hash to validate against + let hash_for_validation: Vec; + let hash_ref = match expected_key_hash { + Some(hash) => hash, + None => { + // Signature-only mode: extract key from response and compute its hash + // This effectively bypasses hash validation while keeping signature validation + use nym_kkt::frame::KKTFrame; + + let (frame, _) = KKTFrame::from_bytes(response_bytes).map_err(|e| { + LpError::Internal(format!("Failed to parse KKT response: {:?}", e)) + })?; + + hash_for_validation = hash_encapsulation_key( + &context.ciphersuite().hash_function(), + context.ciphersuite().hash_len(), + frame.body_ref(), + ); + &hash_for_validation + } + }; + + // Validate response and extract KEM key + let kem_pk = validate_kem_response( + &mut context, + &self.remote_ed25519_public, + hash_ref, + response_bytes, + ) + .map_err(|e| LpError::Internal(format!("KKT response validation failed: {:?}", e)))?; + + // Store the authenticated KEM key + *kkt_state = KKTState::Completed { + kem_pk: Box::new(kem_pk), + }; + + Ok(()) + } + + /// Processes a KKT request from the initiator and prepares a signed response. + /// + /// Validates the initiator's signature and creates a response containing this + /// responder's KEM public key, signed with Ed25519. + /// + /// # Arguments + /// + /// * `request_bytes` - Raw KKT request message from initiator + /// * `responder_kem_pk` - This responder's KEM public key to send + /// + /// # Returns + /// + /// * `Ok(LpMessage::KKTResponse)` - Signed KKT response ready to send + /// * `Err(LpError)` - Signature verification failed or invalid request + pub fn process_kkt_request( + &self, + request_bytes: &[u8], + responder_kem_pk: &EncapsulationKey, + ) -> Result { + use nym_kkt::{frame::KKTFrame, kkt::handle_kem_request}; + + let mut kkt_state = self.kkt_state.lock(); + + // Deserialize request frame + let (request_frame, _) = KKTFrame::from_bytes(request_bytes).map_err(|e| { + LpError::Internal(format!("KKT request deserialization failed: {:?}", e)) + })?; + + // Handle request and create signed response + let response_frame = handle_kem_request( + &request_frame, + Some(&self.remote_ed25519_public), // Verify initiator signature + &self.local_ed25519_private, // Sign response + responder_kem_pk, + ) + .map_err(|e| LpError::Internal(format!("KKT request handling failed: {:?}", e)))?; + + // Mark KKT as processed + // Responder doesn't store the kem_pk since they already have their own KEM keypair + *kkt_state = KKTState::ResponderProcessed; + + // Serialize response frame + let response_bytes = response_frame.to_bytes(); + + Ok(LpMessage::KKTResponse(crate::message::KKTResponseData( + response_bytes, + ))) + } + /// Prepares the next handshake message to be sent, if any. /// /// This should be called by the driver/IO layer to check if the Noise protocol /// state machine requires a message to be sent to the peer. /// + /// For initiators, PSQ always runs on the first message: + /// 1. Converts X25519 keys to DHKEM format + /// 2. Generates PSQ payload and derives PSK + /// 3. Injects PSK into Noise HandshakeState + /// 4. Embeds PSQ payload in first handshake message as: [u16 len][psq_payload][noise_msg] + /// /// # Returns /// /// * `Ok(None)` if no message needs to be sent currently (e.g., waiting for peer, or handshake complete). - /// * `Err(NoiseError)` if there's an error within the Noise protocol state. + /// * `Err(LpError)` if there's an error within the Noise protocol or PSQ. pub fn prepare_handshake_message(&self) -> Option> { let mut noise_state = self.noise_state.lock(); + + // PSQ always runs for initiator on first message + let mut psq_state = self.psq_state.lock(); + + if self.is_initiator && matches!(*psq_state, PSQState::NotStarted) { + // Extract KEM public key from completed KKT exchange + // PSQ requires the authenticated KEM key obtained via KKT protocol + let kkt_state = self.kkt_state.lock(); + let remote_kem = match &*kkt_state { + KKTState::Completed { kem_pk } => kem_pk, + _ => { + return Some(Err(LpError::KKTError( + "PSQ handshake requires completed KKT exchange".to_string(), + ))); + } + }; + + // Generate PSQ payload and PSK using KKT-authenticated KEM key + let session_context = self.id.to_le_bytes(); + + let (psk, psq_payload) = match psq_initiator_create_message( + &self.local_x25519_private, + &self.remote_x25519_public, + remote_kem, + &self.local_ed25519_private, + &self.local_ed25519_public, + &self.salt, + &session_context, + ) { + Ok(result) => result, + Err(e) => { + tracing::error!("PSQ handshake preparation failed, aborting: {:?}", e); + return Some(Err(e)); + } + }; + + // Inject PSK into Noise HandshakeState + if let Err(e) = noise_state.set_psk(3, &psk) { + return Some(Err(LpError::NoiseError(e))); + } + // Mark PSK as injected for safety checks in transport mode + self.psk_injected.store(true, Ordering::Release); + + // Get the Noise handshake message + let noise_msg = match noise_state.get_bytes_to_send() { + Some(Ok(msg)) => msg, + Some(Err(e)) => return Some(Err(LpError::NoiseError(e))), + None => return None, // Should not happen if is_my_turn, but handle gracefully + }; + + // Combine: [u16 psq_len][psq_payload][noise_msg] + let psq_len = psq_payload.len() as u16; + let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg.len()); + combined.extend_from_slice(&psq_len.to_le_bytes()); + combined.extend_from_slice(&psq_payload); + combined.extend_from_slice(&noise_msg); + + // Update PSQ state to InitiatorWaiting + *psq_state = PSQState::InitiatorWaiting { + ciphertext: psq_payload, + }; + + return Some(Ok(LpMessage::Handshake(HandshakeData(combined)))); + } + + // Normal flow (no PSQ, or PSQ already completed) + drop(psq_state); // Release lock + if let Some(message) = noise_state.get_bytes_to_send() { match message { - Ok(message) => Some(Ok(LpMessage::Handshake(HandshakeData(message)))), + Ok(noise_msg) => { + // Check if we have a PSK handle (ctxt_B) to embed (responder message 2 only) + // Only the responder should embed the handle, never the initiator + if !self.is_initiator { + let mut psk_handle_guard = self.psk_handle.lock(); + if let Some(handle_bytes) = psk_handle_guard.take() { + // Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg] + let handle_len = handle_bytes.len() as u16; + let mut combined = + Vec::with_capacity(2 + handle_bytes.len() + noise_msg.len()); + combined.extend_from_slice(&handle_len.to_le_bytes()); + combined.extend_from_slice(&handle_bytes); + combined.extend_from_slice(&noise_msg); + + tracing::debug!( + "Embedding PSK handle ({} bytes) in handshake message 2", + handle_bytes.len() + ); + + return Some(Ok(LpMessage::Handshake(HandshakeData(combined)))); + } + } + // No PSK handle to embed, send noise message as-is + Some(Ok(LpMessage::Handshake(HandshakeData(noise_msg)))) + } Err(e) => Some(Err(LpError::NoiseError(e))), } } else { @@ -192,23 +702,154 @@ impl LpSession { /// This should be called by the driver/IO layer after receiving a potential /// handshake message payload from an LP packet. /// + /// For responders, PSQ always runs on the first message: + /// 1. Extracts PSQ payload from the first handshake message: [u16 len][psq_payload][noise_msg] + /// 2. Converts X25519 keys to DHKEM format + /// 3. Decapsulates PSK from PSQ payload + /// 4. Injects PSK into Noise HandshakeState + /// 5. Processes the remaining Noise handshake message + /// /// # Arguments /// - /// * `noise_payload` - The raw bytes received from the peer, purported to be a Noise handshake message. + /// * `message` - The LP message received from the peer, expected to be a Handshake message. /// /// # Returns /// /// * `Ok(ReadResult)` detailing the outcome (e.g., handshake complete, no-op). - /// * `Err(NoiseError)` if the message is invalid or causes a Noise protocol error. - pub fn process_handshake_message(&self, message: &LpMessage) -> Result { + /// * `Err(LpError)` if the message is invalid or causes a Noise/PSQ protocol error. + pub fn process_handshake_message(&self, message: &LpMessage) -> Result { let mut noise_state = self.noise_state.lock(); + let mut psq_state = self.psq_state.lock(); match message { LpMessage::Handshake(HandshakeData(payload)) => { + // PSQ always runs for responder on first message + if !self.is_initiator && matches!(*psq_state, PSQState::ResponderWaiting) { + // Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg] + if payload.len() < 2 { + return Err(LpError::NoiseError(NoiseError::Other( + "Payload too short for PSQ extraction".to_string(), + ))); + } + + let psq_len = u16::from_le_bytes([payload[0], payload[1]]) as usize; + + if payload.len() < 2 + psq_len { + return Err(LpError::NoiseError(NoiseError::Other( + "Payload length mismatch for PSQ extraction".to_string(), + ))); + } + + let psq_payload = &payload[2..2 + psq_len]; + let noise_payload = &payload[2 + psq_len..]; + + // Convert X25519 local keys to DecapsulationKey/EncapsulationKey (DHKEM) + let local_private_bytes = &self.local_x25519_private.to_bytes(); + let libcrux_private_key = libcrux_kem::PrivateKey::decode( + libcrux_kem::Algorithm::X25519, + local_private_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 local_public_key = self.local_x25519_private.public_key(); + let local_public_bytes = local_public_key.as_bytes(); + let libcrux_public_key = libcrux_kem::PublicKey::decode( + libcrux_kem::Algorithm::X25519, + local_public_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); + + // Decapsulate PSK from PSQ payload using X25519 as DHKEM + let session_context = self.id.to_le_bytes(); + + let (psk, responder_msg_bytes) = match psq_responder_process_message( + &self.local_x25519_private, + &self.remote_x25519_public, + (&dec_key, &enc_key), + &self.remote_ed25519_public, + psq_payload, + &self.salt, + &session_context, + ) { + Ok(result) => result, + Err(e) => { + tracing::error!("PSQ handshake processing failed, aborting: {:?}", e); + return Err(e); + } + }; + + // Store the PSK handle (ctxt_B) for transmission in next message + { + let mut psk_handle = self.psk_handle.lock(); + *psk_handle = Some(responder_msg_bytes); + } + + // Inject PSK into Noise HandshakeState + noise_state.set_psk(3, &psk)?; + // Mark PSK as injected for safety checks in transport mode + self.psk_injected.store(true, Ordering::Release); + + // Update PSQ state to Completed + *psq_state = PSQState::Completed { psk }; + + // Process the Noise handshake message (without PSQ prefix) + drop(psq_state); // Release lock before processing + return noise_state + .read_message(noise_payload) + .map_err(LpError::NoiseError); + } + + // Check if initiator should extract PSK handle from message 2 + if self.is_initiator && matches!(*psq_state, PSQState::InitiatorWaiting { .. }) { + // Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg] + if payload.len() >= 2 { + let handle_len = u16::from_le_bytes([payload[0], payload[1]]) as usize; + + if handle_len > 0 && payload.len() >= 2 + handle_len { + // Extract and store the PSK handle + let handle_bytes = &payload[2..2 + handle_len]; + let noise_payload = &payload[2 + handle_len..]; + + tracing::debug!( + "Extracted PSK handle ({} bytes) from message 2", + handle_len + ); + + { + let mut psk_handle = self.psk_handle.lock(); + *psk_handle = Some(handle_bytes.to_vec()); + } + + // Release psq_state lock before processing + drop(psq_state); + + // Process only the Noise message part + return noise_state + .read_message(noise_payload) + .map_err(LpError::NoiseError); + } + } + // If no valid handle found, fall through to normal processing + } + // The sans-io NoiseProtocol::read_message expects only the payload. - noise_state.read_message(payload) + noise_state + .read_message(payload) + .map_err(LpError::NoiseError) } - _ => Err(NoiseError::IncorrectStateError), + _ => Err(LpError::NoiseError(NoiseError::IncorrectStateError)), } } @@ -231,6 +872,10 @@ impl LpSession { /// * `Err(NoiseError)` if the session is not in transport mode or encryption fails. pub fn encrypt_data(&self, payload: &[u8]) -> Result { let mut noise_state = self.noise_state.lock(); + // Safety: Prevent transport mode with dummy PSK + if !self.psk_injected.load(Ordering::Acquire) { + return Err(NoiseError::PskNotInjected); + } // Explicitly check if handshake is finished before trying to write if !noise_state.is_handshake_finished() { return Err(NoiseError::IncorrectStateError); @@ -254,6 +899,10 @@ impl LpSession { /// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data. pub fn decrypt_data(&self, noise_ciphertext: &LpMessage) -> Result, NoiseError> { let mut noise_state = self.noise_state.lock(); + // Safety: Prevent transport mode with dummy PSK + if !self.psk_injected.load(Ordering::Acquire) { + return Err(NoiseError::PskNotInjected); + } // Explicitly check if handshake is finished before trying to read if !noise_state.is_handshake_finished() { return Err(NoiseError::IncorrectStateError); @@ -266,38 +915,76 @@ impl LpSession { _ => Err(NoiseError::IncorrectStateError), } } + + /// Test-only method to set KKT state to Completed with a mock KEM key. + /// This allows tests to bypass KKT exchange and directly test PSQ handshake. + #[cfg(test)] + pub(crate) fn set_kkt_completed_for_test(&self, remote_x25519_pub: &PublicKey) { + // Convert remote X25519 public key to EncapsulationKey for testing + let remote_kem_bytes = remote_x25519_pub.as_bytes(); + let libcrux_public_key = + libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, remote_kem_bytes) + .expect("Test KEM key conversion failed"); + let kem_pk = EncapsulationKey::X25519(libcrux_public_key); + + let mut kkt_state = self.kkt_state.lock(); + *kkt_state = KKTState::Completed { + kem_pk: Box::new(kem_pk), + }; + } } #[cfg(test)] mod tests { - use snow::{params::NoiseParams, Keypair}; - use super::*; - use crate::{replay::ReplayError, sessions_for_tests, NOISE_PATTERN}; + use crate::{replay::ReplayError, sessions_for_tests}; // Helper function to generate keypairs for tests - fn generate_keypair() -> Keypair { - let params: NoiseParams = NOISE_PATTERN.parse().unwrap(); - snow::Builder::new(params).generate_keypair().unwrap() + fn generate_keypair() -> crate::keypair::Keypair { + crate::keypair::Keypair::default() } // Helper function to create a session with real keys for handshake tests fn create_handshake_test_session( is_initiator: bool, - local_keys: &Keypair, - remote_pub_key: &[u8], - psk: &[u8], + local_keys: &crate::keypair::Keypair, + remote_pub_key: &crate::keypair::PublicKey, ) -> LpSession { - // Use a dummy ID for testing, the important part is is_initiator - let test_id = if is_initiator { 1 } else { 2 }; - LpSession::new( - test_id, + use nym_crypto::asymmetric::ed25519; + + // Compute the shared lp_id from both keypairs (order-independent) + let lp_id = crate::make_lp_id(local_keys.public_key(), remote_pub_key); + + // Create Ed25519 keypairs that correspond to initiator/responder roles + // Initiator uses [1u8], Responder uses [2u8] + let (local_ed25519_seed, remote_ed25519_seed) = if is_initiator { + ([1u8; 32], [2u8; 32]) + } else { + ([2u8; 32], [1u8; 32]) + }; + + let local_ed25519 = ed25519::KeyPair::from_secret(local_ed25519_seed, 0); + let remote_ed25519 = ed25519::KeyPair::from_secret(remote_ed25519_seed, 1); + + let salt = [0u8; 32]; // Test salt + + // PSQ will derive the PSK during handshake using X25519 as DHKEM + let session = LpSession::new( + lp_id, is_initiator, - &local_keys.private, + (local_ed25519.private_key(), local_ed25519.public_key()), + local_keys.private_key(), + remote_ed25519.public_key(), remote_pub_key, - psk, + &salt, ) - .expect("Test session creation failed") + .expect("Test session creation failed"); + + // Initialize KKT state to Completed for tests (bypasses KKT exchange) + // This simulates having already received the remote party's KEM key via KKT + session.set_kkt_completed_for_test(remote_pub_key); + + session } #[test] @@ -313,6 +1000,21 @@ mod tests { 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 session = sessions_for_tests().1; @@ -378,15 +1080,13 @@ mod tests { fn test_prepare_handshake_message_initial_state() { let initiator_keys = generate_keypair(); let responder_keys = generate_keypair(); - let psk = [3u8; 32]; let initiator_session = - create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk); + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); let responder_session = create_handshake_test_session( false, &responder_keys, - &initiator_keys.public, // Responder also needs initiator's key for XK - &psk, + initiator_keys.public_key(), // Responder also needs initiator's key for XK ); // Initiator should have a message to send immediately (-> e) @@ -406,12 +1106,11 @@ mod tests { fn test_process_handshake_message_first_step() { let initiator_keys = generate_keypair(); let responder_keys = generate_keypair(); - let psk = [4u8; 32]; let initiator_session = - create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk); + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); let responder_session = - create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk); + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); // 1. Initiator prepares the first message (-> e) let initiator_msg_result = initiator_session.prepare_handshake_message(); @@ -444,12 +1143,11 @@ mod tests { fn test_handshake_driver_simulation() { let initiator_keys = generate_keypair(); let responder_keys = generate_keypair(); - let psk = [5u8; 32]; let initiator_session = - create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk); + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); let responder_session = - create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk); + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); let mut responder_to_initiator_msg = None; let mut rounds = 0; @@ -532,12 +1230,11 @@ mod tests { // --- Setup Handshake --- let initiator_keys = generate_keypair(); let responder_keys = generate_keypair(); - let psk = [6u8; 32]; let initiator_session = - create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk); + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); let responder_session = - create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk); + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); // Drive handshake to completion (simplified loop from previous test) let mut i_msg = initiator_session @@ -594,10 +1291,9 @@ mod tests { fn test_encrypt_decrypt_before_handshake() { let initiator_keys = generate_keypair(); let responder_keys = generate_keypair(); - let psk = [7u8; 32]; let initiator_session = - create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk); + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); assert!(!initiator_session.is_handshake_complete()); @@ -606,18 +1302,19 @@ mod tests { let result = initiator_session.encrypt_data(plaintext); assert!(result.is_err()); match result.unwrap_err() { - NoiseError::IncorrectStateError => {} // Expected error - e => panic!("Expected IncorrectStateError, got {:?}", e), + NoiseError::PskNotInjected => {} // Expected - PSK check comes before handshake check + e => panic!("Expected PskNotInjected, got {:?}", e), } // Attempt to decrypt before handshake (using dummy ciphertext) let dummy_ciphertext = vec![0u8; 32]; - let result_decrypt = - initiator_session.decrypt_data(&LpMessage::EncryptedData(EncryptedDataPayload(dummy_ciphertext))); + let result_decrypt = initiator_session.decrypt_data(&LpMessage::EncryptedData( + EncryptedDataPayload(dummy_ciphertext), + )); assert!(result_decrypt.is_err()); match result_decrypt.unwrap_err() { - NoiseError::IncorrectStateError => {} // Expected error - e => panic!("Expected IncorrectStateError, got {:?}", e), + NoiseError::PskNotInjected => {} // Expected - PSK check comes before handshake check + e => panic!("Expected PskNotInjected, got {:?}", e), } } @@ -656,4 +1353,546 @@ mod tests { // } } */ + + // ==================================================================== + // PSQ Handshake Integration Tests + // ==================================================================== + + /// Test that PSQ runs during handshake and derives a PSK + #[test] + fn test_psq_handshake_runs_with_psk_injection() { + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + let initiator_session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Drive the handshake + let mut i_msg = initiator_session + .prepare_handshake_message() + .expect("Initiator should have message") + .expect("Message prep should succeed"); + + // The first message should contain PSQ payload embedded + // Verify message is not empty and has reasonable size + assert!(!i_msg.is_empty(), "Initiator message should not be empty"); + assert!( + i_msg.len() > 100, + "Message should contain PSQ payload (actual: {})", + i_msg.len() + ); + + // Responder processes message (which includes PSQ decapsulation) + responder_session + .process_handshake_message(&i_msg) + .expect("Responder should process first message"); + + // Continue handshake + let r_msg = responder_session + .prepare_handshake_message() + .expect("Responder should have message") + .expect("Responder message prep should succeed"); + + initiator_session + .process_handshake_message(&r_msg) + .expect("Initiator should process responder message"); + + i_msg = initiator_session + .prepare_handshake_message() + .expect("Initiator should have final message") + .expect("Final message prep should succeed"); + + responder_session + .process_handshake_message(&i_msg) + .expect("Responder should process final message"); + + // Verify handshake completed + assert!(initiator_session.is_handshake_complete()); + assert!(responder_session.is_handshake_complete()); + + // Verify encryption works (implicitly tests PSK was correctly injected) + let plaintext = b"PSQ test message"; + let encrypted = initiator_session + .encrypt_data(plaintext) + .expect("Encryption should work after handshake"); + + let decrypted = responder_session + .decrypt_data(&encrypted) + .expect("Decryption should work with PSQ-derived PSK"); + + assert_eq!(decrypted, plaintext); + } + + /// 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_keypair(); + let responder_keys = generate_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 that PSQ actually derives a different PSK (not using dummy) + #[test] + fn test_psq_derived_psk_differs_from_dummy() { + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + // Create sessions - they start with dummy PSK [0u8; 32] + let initiator_session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Prepare first message (initiator runs PSQ and injects PSK) + let i_msg = initiator_session + .prepare_handshake_message() + .expect("Initiator should have message") + .expect("Message prep should succeed"); + + // Verify message is not empty (PSQ runs successfully) + assert!( + !i_msg.is_empty(), + "First message should contain PSQ payload" + ); + + // Complete handshake + responder_session + .process_handshake_message(&i_msg) + .expect("Responder should process message"); + + let r_msg = responder_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + + initiator_session.process_handshake_message(&r_msg).unwrap(); + + let final_msg = initiator_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + + responder_session + .process_handshake_message(&final_msg) + .unwrap(); + + // Test that encryption produces non-trivial ciphertext + // (would fail if using dummy PSK incorrectly) + let plaintext = b"test"; + let encrypted = initiator_session.encrypt_data(plaintext).unwrap(); + + // Decrypt should work + let decrypted = responder_session.decrypt_data(&encrypted).unwrap(); + assert_eq!(decrypted, plaintext); + + // Verify ciphertext is not just plaintext (basic encryption sanity) + if let LpMessage::EncryptedData(payload) = encrypted { + assert_ne!( + &payload.0[..plaintext.len()], + plaintext, + "Ciphertext should differ from plaintext" + ); + } else { + panic!("Expected EncryptedData message"); + } + } + + /// Test full end-to-end handshake with PSQ integration + #[test] + fn test_handshake_with_psq_end_to_end() { + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + let initiator_session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Verify initial state + assert!(!initiator_session.is_handshake_complete()); + assert!(!responder_session.is_handshake_complete()); + assert!(initiator_session.is_initiator()); + assert!(!responder_session.is_initiator()); + + // Round 1: Initiator -> Responder (contains PSQ encapsulation) + let msg1 = initiator_session + .prepare_handshake_message() + .expect("Initiator should prepare message") + .expect("Message should succeed"); + + assert!(!msg1.is_empty()); + assert!(!initiator_session.is_handshake_complete()); + + responder_session + .process_handshake_message(&msg1) + .expect("Responder should process PSQ message"); + + assert!(!responder_session.is_handshake_complete()); + + // Round 2: Responder -> Initiator + let msg2 = responder_session + .prepare_handshake_message() + .expect("Responder should prepare message") + .expect("Message should succeed"); + + initiator_session + .process_handshake_message(&msg2) + .expect("Initiator should process message"); + + // Round 3: Initiator -> Responder (final) + let msg3 = initiator_session + .prepare_handshake_message() + .expect("Initiator should prepare final message") + .expect("Message should succeed"); + + responder_session + .process_handshake_message(&msg3) + .expect("Responder should process final message"); + + // Verify both sides completed + assert!(initiator_session.is_handshake_complete()); + assert!(responder_session.is_handshake_complete()); + + // Test bidirectional encrypted communication + let msg_i_to_r = b"Hello from initiator"; + let encrypted_i = initiator_session + .encrypt_data(msg_i_to_r) + .expect("Initiator encryption"); + let decrypted_i = responder_session + .decrypt_data(&encrypted_i) + .expect("Responder decryption"); + assert_eq!(decrypted_i, msg_i_to_r); + + let msg_r_to_i = b"Hello from responder"; + let encrypted_r = responder_session + .encrypt_data(msg_r_to_i) + .expect("Responder encryption"); + let decrypted_r = initiator_session + .decrypt_data(&encrypted_r) + .expect("Initiator decryption"); + assert_eq!(decrypted_r, msg_r_to_i); + + // Successfully completed end-to-end test with PSQ + } + + /// Test that Ed25519 keys are used in PSQ authentication + #[test] + fn test_psq_handshake_uses_ed25519_authentication() { + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + // Create sessions with explicit Ed25519 keys + let initiator_session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Verify sessions store Ed25519 keys + // (Internal verification - keys are used in PSQ calls) + assert_eq!(initiator_session.id(), responder_session.id()); + + // Complete handshake + let msg1 = initiator_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + responder_session.process_handshake_message(&msg1).unwrap(); + + let msg2 = responder_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + initiator_session.process_handshake_message(&msg2).unwrap(); + + let msg3 = initiator_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + responder_session.process_handshake_message(&msg3).unwrap(); + + // If Ed25519 authentication failed, handshake would not complete + assert!(initiator_session.is_handshake_complete()); + assert!(responder_session.is_handshake_complete()); + + // Verify encrypted communication works (proof of successful PSQ with auth) + let test_data = b"Authentication test"; + let encrypted = initiator_session.encrypt_data(test_data).unwrap(); + let decrypted = responder_session.decrypt_data(&encrypted).unwrap(); + assert_eq!(decrypted, test_data); + } + + #[test] + fn test_psq_deserialization_failure() { + // Test that corrupted PSQ payload causes clean abort + let responder_keys = generate_keypair(); + let initiator_keys = generate_keypair(); + + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Create a handshake message with corrupted PSQ payload + let corrupted_psq_data = vec![0xFF; 128]; // Random garbage + let bad_message = LpMessage::Handshake(HandshakeData(corrupted_psq_data)); + + // Attempt to process corrupted message - should fail + let result = responder_session.process_handshake_message(&bad_message); + + // Should return error (PSQ deserialization will fail) + assert!(result.is_err(), "Expected error for corrupted PSQ payload"); + + // Verify session state is unchanged + // PSQ state should still be ResponderWaiting (not modified) + // Noise PSK should still be dummy [0u8; 32] + assert!(!responder_session.is_handshake_complete()); + } + + #[test] + fn test_handshake_abort_on_psq_failure() { + // Test that Ed25519 auth failure causes handshake abort + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + // Create sessions with MISMATCHED Ed25519 keys + // This simulates authentication failure + let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0); + let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! + + let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key()); + let salt = [0u8; 32]; + + let initiator_session = LpSession::new( + lp_id, + true, + ( + initiator_ed25519.private_key(), + initiator_ed25519.public_key(), + ), + initiator_keys.private_key(), + wrong_ed25519.public_key(), // Responder expects THIS key + responder_keys.public_key(), + &salt, + ) + .unwrap(); + // Initialize KKT state for test + initiator_session.set_kkt_completed_for_test(responder_keys.public_key()); + + let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1); + + let responder_session = LpSession::new( + lp_id, + false, + ( + responder_ed25519.private_key(), + responder_ed25519.public_key(), + ), + responder_keys.private_key(), + wrong_ed25519.public_key(), // Expects WRONG key (not initiator's) + initiator_keys.public_key(), + &salt, + ) + .unwrap(); + // Initialize KKT state for test + responder_session.set_kkt_completed_for_test(initiator_keys.public_key()); + + // Initiator prepares message (should succeed - signing works) + let msg1 = initiator_session + .prepare_handshake_message() + .expect("Initiator should prepare message") + .expect("Initiator should have message"); + + // Responder processes message - should FAIL (signature verification fails) + let result = responder_session.process_handshake_message(&msg1); + + // Should return CredError due to Ed25519 signature mismatch + assert!( + result.is_err(), + "Expected error for Ed25519 authentication failure" + ); + + // Verify handshake aborted cleanly + assert!(!initiator_session.is_handshake_complete()); + assert!(!responder_session.is_handshake_complete()); + } + + #[test] + fn test_psq_invalid_signature() { + // Test Ed25519 signature validation specifically + // Setup with matching X25519 keys but mismatched Ed25519 keys + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + // Initiator uses Ed25519 key [1u8] + let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0); + + // Responder expects Ed25519 key [99u8] (wrong!) + let wrong_ed25519_keypair = ed25519::KeyPair::from_secret([99u8; 32], 99); + let wrong_ed25519_public = wrong_ed25519_keypair.public_key(); + + let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key()); + let salt = [0u8; 32]; + + let initiator_session = LpSession::new( + lp_id, + true, + ( + initiator_ed25519.private_key(), + initiator_ed25519.public_key(), + ), + initiator_keys.private_key(), + wrong_ed25519_public, // This doesn't matter for initiator + responder_keys.public_key(), + &salt, + ) + .unwrap(); + // Initialize KKT state for test + initiator_session.set_kkt_completed_for_test(responder_keys.public_key()); + + let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1); + + let responder_session = LpSession::new( + lp_id, + false, + ( + responder_ed25519.private_key(), + responder_ed25519.public_key(), + ), + responder_keys.private_key(), + wrong_ed25519_public, // Responder expects WRONG key + initiator_keys.public_key(), + &salt, + ) + .unwrap(); + // Initialize KKT state for test + responder_session.set_kkt_completed_for_test(initiator_keys.public_key()); + + // Initiator creates message with valid signature (signed with [1u8]) + let msg = initiator_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + + // Responder tries to verify with wrong public key [99u8] + // This should fail Ed25519 signature verification + let result = responder_session.process_handshake_message(&msg); + + assert!(result.is_err(), "Expected signature verification to fail"); + + // Verify error is related to PSQ/authentication + match result.unwrap_err() { + LpError::Internal(msg) if msg.contains("PSQ") => { + // Expected - PSQ v1 responder send failed due to CredError + } + e => panic!("Unexpected error type: {:?}", e), + } + } + + #[test] + fn test_psq_state_unchanged_on_error() { + // Verify that PSQ errors leave session in clean state + let responder_keys = generate_keypair(); + let initiator_keys = generate_keypair(); + + let responder_session = + create_handshake_test_session(false, &responder_keys, initiator_keys.public_key()); + + // Capture initial PSQ state (should be ResponderWaiting) + // (We can't directly access psq_state, but we can verify behavior) + + // Send corrupted data + let corrupted_message = LpMessage::Handshake(HandshakeData(vec![0xFF; 100])); + + // Process should fail + let result = responder_session.process_handshake_message(&corrupted_message); + assert!(result.is_err()); + + // After error, session should still be in handshake mode (not complete) + assert!(!responder_session.is_handshake_complete()); + + // Session should still be functional - can process valid messages + // Create a proper initiator to send valid message + let initiator_session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + + let valid_msg = initiator_session + .prepare_handshake_message() + .unwrap() + .unwrap(); + + // After the error, responder should still be able to process valid messages + let result2 = responder_session.process_handshake_message(&valid_msg); + + // Should succeed (session state was not corrupted by previous error) + assert!( + result2.is_ok(), + "Session should still be functional after PSQ error" + ); + } + + #[test] + fn test_transport_fails_without_psk_injection() { + // This test verifies the safety mechanism that prevents transport mode operations + // from running with the dummy PSK if PSQ injection fails or is skipped. + + let initiator_keys = generate_keypair(); + let responder_keys = generate_keypair(); + + // Create session but don't complete handshake (no PSK injection will occur) + let session = + create_handshake_test_session(true, &initiator_keys, responder_keys.public_key()); + + // Verify session was created successfully + assert!(!session.is_handshake_complete()); + + // Attempt to encrypt data - should fail with PskNotInjected + let plaintext = b"test data"; + let encrypt_result = session.encrypt_data(plaintext); + + assert!( + encrypt_result.is_err(), + "encrypt_data should fail without PSK injection" + ); + match encrypt_result.unwrap_err() { + NoiseError::PskNotInjected => { + // Expected - this is the safety mechanism working + } + e => panic!("Expected PskNotInjected error, got: {:?}", e), + } + + // Create a dummy encrypted message to test decrypt + let dummy_ciphertext = LpMessage::EncryptedData(EncryptedDataPayload(vec![0u8; 48])); + + // Attempt to decrypt data - should also fail with PskNotInjected + let decrypt_result = session.decrypt_data(&dummy_ciphertext); + + assert!( + decrypt_result.is_err(), + "decrypt_data should fail without PSK injection" + ); + match decrypt_result.unwrap_err() { + NoiseError::PskNotInjected => { + // Expected - this is the safety mechanism working + } + e => panic!("Expected PskNotInjected error, got: {:?}", e), + } + } } diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index dc11607660..4e52a6cbd3 100644 --- a/common/nym-lp/src/session_integration/mod.rs +++ b/common/nym-lp/src/session_integration/mod.rs @@ -1,15 +1,16 @@ #[cfg(test)] mod tests { use crate::codec::{parse_lp_packet, serialize_lp_packet}; - use crate::keypair::Keypair; + use crate::keypair::PublicKey; use crate::make_lp_id; use crate::{ + LpError, message::LpMessage, packet::{LpHeader, LpPacket, TRAILER_LEN}, session_manager::SessionManager, - LpError, }; use bytes::BytesMut; + use nym_crypto::asymmetric::ed25519; // Function to create a test packet - similar to how it's done in codec.rs tests fn create_test_packet( @@ -48,25 +49,70 @@ mod tests { // 1. Initialize session manager let session_manager_1 = SessionManager::new(); let session_manager_2 = SessionManager::new(); - // 2. Generate keys and PSK - let peer_a_keys = Keypair::default(); - let peer_b_keys = Keypair::default(); - let lp_id = make_lp_id(peer_a_keys.public_key(), peer_b_keys.public_key()); - let psk = [1u8; 32]; // Define a pre-shared key for the test + + // 2. Generate Ed25519 keypairs for PSQ authentication + let ed25519_keypair_a = ed25519::KeyPair::from_secret([1u8; 32], 0); + let ed25519_keypair_b = ed25519::KeyPair::from_secret([2u8; 32], 1); + + // Derive X25519 keys from Ed25519 (same as state machine does internally) + let x25519_pub_a = ed25519_keypair_a + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + let x25519_pub_b = ed25519_keypair_b + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + + // Convert to LP keypair types + let lp_pub_a = PublicKey::from_bytes(x25519_pub_a.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + + // Calculate lp_id (matches state machine's internal calculation) + let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b); + + // Test salt + let salt = [42u8; 32]; // 4. Create sessions using the pre-built Noise states let peer_a_sm = session_manager_1 - .create_session_state_machine(&peer_a_keys, peer_b_keys.public_key(), true, &psk) + .create_session_state_machine( + ( + ed25519_keypair_a.private_key(), + ed25519_keypair_a.public_key(), + ), + ed25519_keypair_b.public_key(), + true, + &salt, + ) .expect("Failed to create session A"); let peer_b_sm = session_manager_2 - .create_session_state_machine(&peer_b_keys, peer_a_keys.public_key(), false, &psk) + .create_session_state_machine( + ( + ed25519_keypair_b.private_key(), + ed25519_keypair_b.public_key(), + ), + ed25519_keypair_a.public_key(), + false, + &salt, + ) .expect("Failed to create session B"); // Verify session count assert_eq!(session_manager_1.session_count(), 1); assert_eq!(session_manager_2.session_count(), 1); + // Initialize KKT state for both sessions (test bypass) + session_manager_1 + .init_kkt_for_test(peer_a_sm, &lp_pub_b) + .expect("Failed to init KKT for peer A"); + session_manager_2 + .init_kkt_for_test(peer_b_sm, &lp_pub_a) + .expect("Failed to init KKT for peer B"); + // 5. Simulate Noise Handshake (Sans-IO) println!("Starting handshake simulation..."); let mut i_msg_payload; @@ -308,7 +354,9 @@ mod tests { 1, lp_id, counter_b, - LpMessage::EncryptedData(crate::message::EncryptedDataPayload(plaintext_b_to_a.to_vec())), // Using plaintext here, but content doesn't matter for replay check + 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) @@ -450,19 +498,63 @@ mod tests { let session_manager_1 = SessionManager::new(); let session_manager_2 = SessionManager::new(); - // 2. Setup sessions and complete handshake (similar to test_full_session_flow) - let peer_a_keys = Keypair::default(); - let peer_b_keys = Keypair::default(); - let lp_id = make_lp_id(peer_a_keys.public_key(), peer_b_keys.public_key()); - let psk = [2u8; 32]; + // 2. Generate Ed25519 keypairs for PSQ authentication + let ed25519_keypair_a = ed25519::KeyPair::from_secret([3u8; 32], 0); + let ed25519_keypair_b = ed25519::KeyPair::from_secret([4u8; 32], 1); + + // Derive X25519 keys from Ed25519 (same as state machine does internally) + let x25519_pub_a = ed25519_keypair_a + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + let x25519_pub_b = ed25519_keypair_b + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + + // Convert to LP keypair types + let lp_pub_a = PublicKey::from_bytes(x25519_pub_a.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + + // Calculate lp_id (matches state machine's internal calculation) + let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b); + + // Test salt + let salt = [43u8; 32]; let peer_a_sm = session_manager_1 - .create_session_state_machine(&peer_a_keys, peer_b_keys.public_key(), true, &psk) + .create_session_state_machine( + ( + ed25519_keypair_a.private_key(), + ed25519_keypair_a.public_key(), + ), + ed25519_keypair_b.public_key(), + true, + &salt, + ) .unwrap(); let peer_b_sm = session_manager_2 - .create_session_state_machine(&peer_b_keys, peer_a_keys.public_key(), false, &psk) + .create_session_state_machine( + ( + ed25519_keypair_b.private_key(), + ed25519_keypair_b.public_key(), + ), + ed25519_keypair_a.public_key(), + false, + &salt, + ) .unwrap(); + // Initialize KKT state for both sessions (test bypass) + session_manager_1 + .init_kkt_for_test(peer_a_sm, &lp_pub_b) + .expect("Failed to init KKT for peer A"); + session_manager_2 + .init_kkt_for_test(peer_b_sm, &lp_pub_a) + .expect("Failed to init KKT for peer B"); + // Drive handshake to completion (simplified) let mut i_msg = session_manager_1 .prepare_handshake_message(peer_a_sm) @@ -615,15 +707,33 @@ mod tests { // 1. Initialize session manager let session_manager = SessionManager::new(); - // Setup for creating real noise state (keys/psk don't matter for this test) - let keys = Keypair::default(); - let psk = [3u8; 32]; + // Generate Ed25519 keypair for PSQ authentication + let ed25519_keypair = ed25519::KeyPair::from_secret([5u8; 32], 0); - let lp_id = make_lp_id(keys.public_key(), keys.public_key()); + // Derive X25519 key from Ed25519 (same as state machine does internally) + let x25519_pub = ed25519_keypair + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + + // Convert to LP keypair type + let lp_pub = PublicKey::from_bytes(x25519_pub.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + + // Calculate lp_id (self-connection: both sides use same key) + let lp_id = make_lp_id(&lp_pub, &lp_pub); + + // Test salt + let salt = [44u8; 32]; // 2. Create a session (using real noise state) let _session = session_manager - .create_session_state_machine(&keys, keys.public_key(), true, &psk) + .create_session_state_machine( + (ed25519_keypair.private_key(), ed25519_keypair.public_key()), + ed25519_keypair.public_key(), + true, + &salt, + ) .expect("Failed to create session"); // 3. Try to get a non-existent session @@ -639,7 +749,12 @@ mod tests { // 5. Create and immediately remove a session let _temp_session = session_manager - .create_session_state_machine(&keys, keys.public_key(), true, &psk) + .create_session_state_machine( + (ed25519_keypair.private_key(), ed25519_keypair.public_key()), + ed25519_keypair.public_key(), + true, + &salt, + ) .expect("Failed to create temp session"); assert!( @@ -715,19 +830,59 @@ mod tests { let session_manager_1 = SessionManager::new(); let session_manager_2 = SessionManager::new(); - // 2. Generate keys and PSK - let peer_a_keys = Keypair::default(); - let peer_b_keys = Keypair::default(); - let lp_id = make_lp_id(peer_a_keys.public_key(), peer_b_keys.public_key()); - let psk = [1u8; 32]; + // 2. Generate Ed25519 keypairs for PSQ authentication + let ed25519_keypair_a = ed25519::KeyPair::from_secret([6u8; 32], 0); + let ed25519_keypair_b = ed25519::KeyPair::from_secret([7u8; 32], 1); + + // Derive X25519 keys from Ed25519 (same as state machine does internally) + let x25519_pub_a = ed25519_keypair_a + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + let x25519_pub_b = ed25519_keypair_b + .public_key() + .to_x25519() + .expect("Failed to derive X25519 from Ed25519"); + + // Convert to LP keypair types + let lp_pub_a = PublicKey::from_bytes(x25519_pub_a.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes()) + .expect("Failed to create PublicKey from bytes"); + + // Calculate lp_id (matches state machine's internal calculation) + let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b); + + // Test salt + let salt = [45u8; 32]; // 3. Create sessions state machines - assert!(session_manager_1 - .create_session_state_machine(&peer_a_keys, peer_b_keys.public_key(), true, &psk) // Initiator - .is_ok()); - assert!(session_manager_2 - .create_session_state_machine(&peer_b_keys, peer_a_keys.public_key(), false, &psk) // Responder - .is_ok()); + assert!( + session_manager_1 + .create_session_state_machine( + ( + ed25519_keypair_a.private_key(), + ed25519_keypair_a.public_key() + ), + ed25519_keypair_b.public_key(), + true, + &salt, + ) // Initiator + .is_ok() + ); + assert!( + session_manager_2 + .create_session_state_machine( + ( + ed25519_keypair_b.private_key(), + ed25519_keypair_b.public_key() + ), + ed25519_keypair_a.public_key(), + false, + &salt, + ) // Responder + .is_ok() + ); assert_eq!(session_manager_1.session_count(), 1); assert_eq!(session_manager_2.session_count(), 1); @@ -750,7 +905,7 @@ mod tests { let mut packet_a_to_b: Option; let mut packet_b_to_a: Option; let mut rounds = 0; - const MAX_ROUNDS: usize = 5; // XK handshake takes 3 messages + const MAX_ROUNDS: usize = 10; // KKT (2 messages) + XK handshake (3 messages) + PSQ = 6 rounds total // --- Round 1: Initiator Starts --- println!(" Round {}: Initiator starts handshake", rounds); @@ -760,20 +915,21 @@ mod tests { .expect("Initiator StartHandshake failed"); if let LpAction::SendPacket(packet) = action_a1 { - println!(" Initiator produced SendPacket (-> e)"); + println!(" Initiator produced SendPacket (KKT request)"); packet_a_to_b = Some(packet); } else { panic!("Initiator StartHandshake did not produce SendPacket"); } + // After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet) assert_eq!( session_manager_1.get_state(lp_id).unwrap(), - LpStateBare::Handshaking, - "Initiator state wrong after StartHandshake" + LpStateBare::KKTExchange, + "Initiator state wrong after StartHandshake (should be KKTExchange)" ); // *** ADD THIS BLOCK for Responder StartHandshake *** println!( - " Round {}: Responder explicitly enters Handshaking state", + " Round {}: Responder explicitly enters KKTExchange state", rounds ); let action_b_start = session_manager_2.process_input(lp_id, LpInput::StartHandshake); @@ -783,107 +939,209 @@ mod tests { "Responder StartHandshake should produce None action, got {:?}", action_b_start ); - // Verify responder transitions to Handshaking state + // Verify responder transitions to KKTExchange state (not Handshaking yet) assert_eq!( session_manager_2.get_state(lp_id).unwrap(), - LpStateBare::Handshaking, // State should now be Handshaking - "Responder state should be Handshaking after its StartHandshake" + LpStateBare::KKTExchange, // Responder also enters KKTExchange state + "Responder state should be KKTExchange after its StartHandshake" ); // *** END OF ADDED BLOCK *** - // --- Round 2: Responder Receives, Sends Reply --- + // --- Round 2: Responder Receives KKT Request, Sends KKT Response --- rounds += 1; - println!(" Round {}: Responder receives, sends reply", rounds); - let packet_to_process = packet_a_to_b.take().expect("Packet from A was missing"); + println!( + " Round {}: Responder receives KKT request, sends KKT response", + rounds + ); + let packet_to_process = packet_a_to_b + .take() + .expect("KKT request from A was missing"); // Simulate network: serialize -> parse (optional but good practice) let mut buf_a = BytesMut::new(); serialize_lp_packet(&packet_to_process, &mut buf_a).unwrap(); let parsed_packet_a = parse_lp_packet(&buf_a).unwrap(); - // Responder processes (Now starting from Handshaking state) + // Responder processes KKT request let action_b1 = session_manager_2 .process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a)) .expect("Responder ReceivePacket should produce an action") .expect("Responder ReceivePacket failed"); if let LpAction::SendPacket(packet) = action_b1 { - println!(" Responder received, produced SendPacket (<- e, es)"); + println!(" Responder received KKT request, produced KKT response"); packet_b_to_a = Some(packet); } else { - panic!("Responder ReceivePacket did not produce SendPacket"); + panic!("Responder ReceivePacket did not produce SendPacket for KKT response"); } - // State should remain Handshaking until the final message is processed + // Responder transitions to Handshaking after KKT completes assert_eq!( session_manager_2.get_state(lp_id).unwrap(), LpStateBare::Handshaking, - "Responder state should remain Handshaking after processing first packet" // Adjusted assertion + "Responder state should be Handshaking after KKT exchange" ); - // --- Round 3: Initiator Receives, Sends Final, Completes --- + // --- Round 3: Initiator Receives KKT Response, Sends First Noise Message (with PSQ) --- rounds += 1; println!( - " Round {}: Initiator receives, sends final, completes", + " Round {}: Initiator receives KKT response, sends first Noise message (with PSQ)", rounds ); - let packet_to_process = packet_b_to_a.take().expect("Packet from B was missing"); + let packet_to_process = packet_b_to_a + .take() + .expect("KKT response from B was missing"); // Simulate network let mut buf_b = BytesMut::new(); serialize_lp_packet(&packet_to_process, &mut buf_b).unwrap(); let parsed_packet_b = parse_lp_packet(&buf_b).unwrap(); - // Initiator processes + // Initiator processes KKT response let action_a2 = session_manager_1 .process_input(lp_id, LpInput::ReceivePacket(parsed_packet_b)) .expect("Initiator ReceivePacket should produce an action") .expect("Initiator ReceivePacket failed"); - if let LpAction::SendPacket(packet) = action_a2 { - println!(" Initiator received, produced SendPacket (-> s, se)"); - packet_a_to_b = Some(packet); - // Initiator might transition to Transport *after* sending this message - assert_eq!( - session_manager_1.get_state(lp_id).unwrap(), - LpStateBare::Transport, - "Initiator state should be Transport after processing second packet" - ); - // Optional: Check for HandshakeComplete action if process_input returns multiple - } else { - panic!("Initiator ReceivePacket did not produce SendPacket"); + match action_a2 { + LpAction::SendPacket(packet) => { + println!( + " Initiator received KKT response, produced first Noise message (-> e)" + ); + packet_a_to_b = Some(packet); + // Initiator transitions to Handshaking after KKT completes + assert_eq!( + session_manager_1.get_state(lp_id).unwrap(), + LpStateBare::Handshaking, + "Initiator state should be Handshaking after receiving KKT response" + ); + } + LpAction::KKTComplete => { + println!( + " Initiator received KKT response, produced KKTComplete (will send Noise in next step)" + ); + // KKT completed, now need to explicitly trigger handshake message + // This might be the case if KKT completion doesn't automatically send the first Noise message + // Let's try to prepare the handshake message + if let Some(msg_result) = session_manager_1.prepare_handshake_message(lp_id) { + let msg = msg_result.expect("Failed to prepare handshake message after KKT"); + // Create a packet from the message + let packet = create_test_packet(1, lp_id, 0, msg); + packet_a_to_b = Some(packet); + println!(" Prepared first Noise message after KKTComplete"); + } else { + panic!("No handshake message available after KKT complete"); + } + } + other => { + panic!( + "Initiator ReceivePacket produced unexpected action after KKT response: {:?}", + other + ); + } } - // --- Round 4: Responder Receives Final, Completes --- + // --- Round 4: Responder Receives First Noise Message, Sends Second --- rounds += 1; - println!(" Round {}: Responder receives final, completes", rounds); + println!( + " Round {}: Responder receives first Noise message, sends second", + rounds + ); let packet_to_process = packet_a_to_b .take() - .expect("Final packet from A was missing"); + .expect("First Noise packet from A was missing"); // Simulate network let mut buf_a2 = BytesMut::new(); serialize_lp_packet(&packet_to_process, &mut buf_a2).unwrap(); let parsed_packet_a2 = parse_lp_packet(&buf_a2).unwrap(); - // Responder processes + // Responder processes first Noise message and sends second Noise message let action_b2 = session_manager_2 .process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a2)) + .expect("Responder ReceivePacket should produce an action") + .expect("Responder ReceivePacket failed"); + + if let LpAction::SendPacket(packet) = action_b2 { + println!( + " Responder received first Noise message, produced second Noise message (<- e, ee, s, es)" + ); + packet_b_to_a = Some(packet); + } else { + panic!("Responder did not produce SendPacket for second Noise message"); + } + // Responder still in Handshaking, waiting for final message + assert_eq!( + session_manager_2.get_state(lp_id).unwrap(), + LpStateBare::Handshaking, + "Responder state should still be Handshaking after sending second message" + ); + + // --- Round 5: Initiator Receives Second Noise Message, Sends Third, Completes --- + rounds += 1; + println!( + " Round {}: Initiator receives second Noise message, sends third, completes", + rounds + ); + let packet_to_process = packet_b_to_a + .take() + .expect("Second Noise packet from B was missing"); + + let mut buf_b2 = BytesMut::new(); + serialize_lp_packet(&packet_to_process, &mut buf_b2).unwrap(); + let parsed_packet_b2 = parse_lp_packet(&buf_b2).unwrap(); + + let action_a3 = session_manager_1 + .process_input(lp_id, LpInput::ReceivePacket(parsed_packet_b2)) + .expect("Initiator ReceivePacket should produce an action") + .expect("Initiator ReceivePacket failed"); + + if let LpAction::SendPacket(packet) = action_a3 { + println!( + " Initiator received second Noise message, produced third Noise message (-> s, se)" + ); + packet_a_to_b = Some(packet); + } else { + panic!("Initiator did not produce SendPacket for third Noise message"); + } + // Initiator transitions to Transport after sending third message + assert_eq!( + session_manager_1.get_state(lp_id).unwrap(), + LpStateBare::Transport, + "Initiator state should be Transport after sending third message" + ); + + // --- Round 6: Responder Receives Third Noise Message, Completes --- + rounds += 1; + println!( + " Round {}: Responder receives third Noise message, completes", + rounds + ); + let packet_to_process = packet_a_to_b + .take() + .expect("Third Noise packet from A was missing"); + + let mut buf_a3 = BytesMut::new(); + serialize_lp_packet(&packet_to_process, &mut buf_a3).unwrap(); + let parsed_packet_a3 = parse_lp_packet(&buf_a3).unwrap(); + + let action_b3 = session_manager_2 + .process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a3)) .expect("Responder final ReceivePacket should produce an action") .expect("Responder final ReceivePacket failed"); - // Check if the primary action is HandshakeComplete - // The state machine might return HandshakeComplete first, or maybe implicit - if let LpAction::HandshakeComplete = action_b2 { - println!(" Responder received final, produced HandshakeComplete"); + // Responder completes handshake + if let LpAction::HandshakeComplete = action_b3 { + println!(" Responder received third Noise message, produced HandshakeComplete"); } else { - // It might just transition state without an explicit HandshakeComplete action - println!(" Responder received final (Action: {:?})", action_b2); - // Optionally, allow NoOp or other actions if the state transition is the main indicator + println!( + " Responder received third Noise message (Action: {:?})", + action_b3 + ); } assert_eq!( session_manager_2.get_state(lp_id).unwrap(), LpStateBare::Transport, - "Responder state should be Transport after processing final packet" + "Responder state should be Transport after processing third message" ); // --- Verification --- diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index 540dc9cb99..4baa9f5a3f 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -7,8 +7,8 @@ //! creation, retrieval, and storage of sessions. use dashmap::DashMap; +use nym_crypto::asymmetric::ed25519; -use crate::keypair::{Keypair, PublicKey}; use crate::noise_protocol::ReadResult; use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare}; use crate::{LpError, LpMessage, LpSession, LpStateMachine}; @@ -129,9 +129,7 @@ impl SessionManager { lp_id: u32, message: &LpMessage, ) -> Result { - self.with_state_machine(lp_id, |sm| { - Ok(sm.session()?.process_handshake_message(message)?) - })? + self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))? } pub fn session_count(&self) -> usize { @@ -168,12 +166,17 @@ impl SessionManager { pub fn create_session_state_machine( &self, - local_keypair: &Keypair, - remote_public_key: &PublicKey, + local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey), + remote_ed25519_key: &ed25519::PublicKey, is_initiator: bool, - psk: &[u8], + salt: &[u8; 32], ) -> Result { - let sm = LpStateMachine::new(is_initiator, local_keypair, remote_public_key, psk)?; + let sm = LpStateMachine::new( + is_initiator, + local_ed25519_keypair, + remote_ed25519_key, + salt, + )?; let sm_id = sm.id()?; self.state_machines.insert(sm_id, sm); @@ -186,21 +189,39 @@ impl SessionManager { removed.is_some() } + + /// Test-only method to initialize KKT state to Completed for a session. + /// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake. + #[cfg(test)] + pub fn init_kkt_for_test( + &self, + lp_id: u32, + remote_x25519_pub: &crate::keypair::PublicKey, + ) -> Result<(), LpError> { + self.with_state_machine(lp_id, |sm| { + sm.session()?.set_kkt_completed_for_test(remote_x25519_pub); + Ok(()) + })? + } } #[cfg(test)] mod tests { use super::*; + use nym_crypto::asymmetric::ed25519; #[test] fn test_session_manager_get() { let manager = SessionManager::new(); + let ed25519_keypair = ed25519::KeyPair::from_secret([10u8; 32], 0); + let salt = [47u8; 32]; + let sm_1_id = manager .create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + (ed25519_keypair.private_key(), ed25519_keypair.public_key()), + ed25519_keypair.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); @@ -214,12 +235,15 @@ mod tests { #[test] fn test_session_manager_remove() { let manager = SessionManager::new(); + let ed25519_keypair = ed25519::KeyPair::from_secret([11u8; 32], 0); + let salt = [48u8; 32]; + let sm_1_id = manager .create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + (ed25519_keypair.private_key(), ed25519_keypair.public_key()), + ed25519_keypair.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); @@ -234,31 +258,44 @@ mod tests { #[test] fn test_multiple_sessions() { let manager = SessionManager::new(); + let ed25519_keypair_1 = ed25519::KeyPair::from_secret([12u8; 32], 0); + let ed25519_keypair_2 = ed25519::KeyPair::from_secret([13u8; 32], 1); + let ed25519_keypair_3 = ed25519::KeyPair::from_secret([14u8; 32], 2); + let salt = [49u8; 32]; let sm_1 = manager .create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + ( + ed25519_keypair_1.private_key(), + ed25519_keypair_1.public_key(), + ), + ed25519_keypair_1.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); let sm_2 = manager .create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + ( + ed25519_keypair_2.private_key(), + ed25519_keypair_2.public_key(), + ), + ed25519_keypair_2.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); let sm_3 = manager .create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + ( + ed25519_keypair_3.private_key(), + ed25519_keypair_3.public_key(), + ), + ed25519_keypair_3.public_key(), true, - &[2u8; 32], + &salt, ) .unwrap(); @@ -276,12 +313,14 @@ mod tests { #[test] fn test_session_manager_create_session() { let manager = SessionManager::new(); + let ed25519_keypair = ed25519::KeyPair::from_secret([15u8; 32], 0); + let salt = [50u8; 32]; let sm = manager.create_session_state_machine( - &Keypair::default(), - &PublicKey::default(), + (ed25519_keypair.private_key(), ed25519_keypair.public_key()), + ed25519_keypair.public_key(), true, - &[2u8; 32], + &salt, ); assert!(sm.is_ok()); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index 95eafe2ee3..27cc5896ca 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -4,14 +4,15 @@ //! Lewes Protocol State Machine for managing connection lifecycle. use crate::{ - keypair::{Keypair, PublicKey}, + LpError, + keypair::{Keypair, PrivateKey as LpPrivateKey, PublicKey as LpPublicKey}, make_lp_id, noise_protocol::NoiseError, packet::LpPacket, session::LpSession, - LpError, }; use bytes::BytesMut; +use nym_crypto::asymmetric::ed25519; use std::mem; /// Represents the possible states of the Lewes Protocol connection. @@ -21,6 +22,10 @@ pub enum LpState { /// State machine is created with keys, lp_id is derived, session is ready. ReadyToHandshake { session: LpSession }, + /// Performing KKT (KEM Key Transfer) exchange before Noise handshake. + /// Initiator requests responder's KEM public key, responder provides signed key. + KKTExchange { session: LpSession }, + /// Actively performing the Noise handshake. /// (We might be able to merge this with ReadyToHandshake if the first step always happens) Handshaking { session: LpSession }, // Kept for now, logic might merge later @@ -37,6 +42,7 @@ pub enum LpState { #[derive(Debug, Clone, PartialEq, Eq)] pub enum LpStateBare { ReadyToHandshake, + KKTExchange, Handshaking, Transport, Closed, @@ -47,6 +53,7 @@ impl From<&LpState> for LpStateBare { fn from(state: &LpState) -> Self { match state { LpState::ReadyToHandshake { .. } => LpStateBare::ReadyToHandshake, + LpState::KKTExchange { .. } => LpStateBare::KKTExchange, LpState::Handshaking { .. } => LpStateBare::Handshaking, LpState::Transport { .. } => LpStateBare::Transport, LpState::Closed { .. } => LpStateBare::Closed, @@ -75,6 +82,8 @@ pub enum LpAction { SendPacket(LpPacket), /// Deliver decrypted application data received from the peer. DeliverData(BytesMut), + /// Inform the environment that KKT exchange completed successfully. + KKTComplete, /// Inform the environment that the handshake is complete. HandshakeComplete, /// Inform the environment that the connection is closed. @@ -94,6 +103,7 @@ impl LpStateMachine { pub fn session(&self) -> Result<&LpSession, LpError> { match &self.state { LpState::ReadyToHandshake { session } + | LpState::KKTExchange { session } | LpState::Handshaking { session } | LpState::Transport { session } => Ok(session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), @@ -107,6 +117,7 @@ impl LpStateMachine { pub fn into_session(self) -> Result { match self.state { LpState::ReadyToHandshake { session } + | LpState::KKTExchange { session } | LpState::Handshaking { session } | LpState::Transport { session } => Ok(session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), @@ -118,44 +129,75 @@ impl LpStateMachine { Ok(self.session()?.id()) } - /// Creates a new state machine, calculates the lp_id, creates the session, - /// and sets the initial state to ReadyToHandshake. + /// Creates a new state machine from Ed25519 keys, internally deriving X25519 keys. /// - /// Requires the local *full* keypair to get the public key for lp_id calculation. + /// This is the primary constructor that accepts only Ed25519 keys (identity/signing keys) + /// and internally derives the X25519 keys needed for Noise protocol and DHKEM. + /// This simplifies the API by hiding the X25519 derivation as an implementation detail. + /// + /// # Arguments + /// + /// * `is_initiator` - Whether this side initiates the handshake + /// * `local_ed25519_keypair` - Ed25519 keypair for PSQ authentication and X25519 derivation + /// (from client identity key or gateway signing key) + /// * `remote_ed25519_key` - Peer's Ed25519 public key for PSQ authentication and X25519 derivation + /// * `salt` - Fresh salt for PSK derivation (must be unique per session) + /// + /// # Errors + /// + /// Returns `LpError::Ed25519RecoveryError` if Ed25519→X25519 conversion fails for the remote key. + /// Local private key conversion cannot fail. pub fn new( is_initiator: bool, - local_keypair: &Keypair, // Use Keypair - remote_public_key: &PublicKey, - psk: &[u8], - // session_manager: Arc // Optional + local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey), + remote_ed25519_key: &ed25519::PublicKey, + salt: &[u8; 32], ) -> Result { - // Calculate the shared lp_id// Calculate the shared lp_id - let lp_id = make_lp_id(local_keypair.public_key(), remote_public_key); + // We use standard RFC 7748 conversion to derive X25519 keys from Ed25519 identity keys. + // This allows callers to provide only Ed25519 keys (which they already have for signing/identity) + // without needing to manage separate X25519 keypairs. + // + // Security: Ed25519→X25519 conversion is cryptographically sound (RFC 7748). + // The derived X25519 keys are used for: + // - Noise protocol ephemeral DH + // - PSQ ECDH baseline security (pre-quantum) + // - lp_id calculation (session identifier) - let local_private_key = local_keypair.private_key().to_bytes(); - let remote_public_key = remote_public_key.as_bytes(); + // Convert Ed25519 keys to X25519 for Noise protocol + let local_x25519_private = local_ed25519_keypair.0.to_x25519(); + let local_x25519_public = local_ed25519_keypair + .1 + .to_x25519() + .map_err(LpError::Ed25519RecoveryError)?; - // Create the session immediately + let remote_x25519_public = remote_ed25519_key + .to_x25519() + .map_err(LpError::Ed25519RecoveryError)?; + + // Convert nym_crypto X25519 types to nym_lp keypair types + let lp_private = LpPrivateKey::from_bytes(local_x25519_private.as_bytes()); + let lp_public = LpPublicKey::from_bytes(local_x25519_public.as_bytes())?; + let lp_remote_public = LpPublicKey::from_bytes(remote_x25519_public.as_bytes())?; + + // Create X25519 keypair for Noise and lp_id calculation + let local_x25519_keypair = Keypair::from_keys(lp_private, lp_public); + + // Calculate the shared lp_id using derived X25519 keys + let lp_id = make_lp_id(local_x25519_keypair.public_key(), &lp_remote_public); + + // Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise) let session = LpSession::new( lp_id, is_initiator, - &local_private_key, - remote_public_key, - psk, + local_ed25519_keypair, + local_x25519_keypair.private_key(), + remote_ed25519_key, + &lp_remote_public, + salt, )?; - // TODO: Register the session with the SessionManager if applicable - // if let Some(manager) = session_manager { - // manager.insert_session(lp_id, session.clone())?; // Assuming insert_session exists - // } - Ok(LpStateMachine { state: LpState::ReadyToHandshake { session }, - // Store necessary info if needed for recreation, otherwise remove - // is_initiator, - // local_private_key: local_private_key.to_vec(), - // remote_public_key: remote_public_key.to_vec(), - // psk: psk.to_vec(), }) } /// Processes an input event and returns a list of actions to perform. @@ -170,22 +212,30 @@ impl LpStateMachine { // --- ReadyToHandshake State --- (LpState::ReadyToHandshake { session }, LpInput::StartHandshake) => { if session.is_initiator() { - // Initiator sends the first message - match self.start_handshake(&session) { - Some(Ok(action)) => { - result_action = Some(Ok(action)); - LpState::Handshaking { session } // Transition state + // Initiator starts by requesting KEM key via KKT + match session.prepare_kkt_request() { + Some(Ok(kkt_message)) => { + match session.next_packet(kkt_message) { + Ok(kkt_packet) => { + result_action = Some(Ok(LpAction::SendPacket(kkt_packet))); + LpState::KKTExchange { session } // Transition to KKTExchange + } + Err(e) => { + let reason = e.to_string(); + result_action = Some(Err(e)); + LpState::Closed { reason } + } + } } Some(Err(e)) => { - // Error occurred, move to Closed state let reason = e.to_string(); result_action = Some(Err(e)); LpState::Closed { reason } } None => { - // Should not happen, treat as internal error + // Should not happen for initiator let err = LpError::Internal( - "start_handshake returned None unexpectedly".to_string(), + "prepare_kkt_request returned None for initiator".to_string(), ); let reason = err.to_string(); result_action = Some(Err(err)); @@ -193,12 +243,116 @@ impl LpStateMachine { } } } else { - // Responder waits for the first message, transition to Handshaking to wait. - LpState::Handshaking { session } + // Responder waits for KKT request + LpState::KKTExchange { session } // No action needed yet, result_action remains None. } } + // --- KKTExchange State --- + (LpState::KKTExchange { session }, LpInput::ReceivePacket(packet)) => { + // Check if packet lp_id matches our session + if packet.header.session_id() != session.id() { + result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id()))); + LpState::KKTExchange { session } + } else { + use crate::message::LpMessage; + + // Packet message is already parsed, match on it directly + match &packet.message { + LpMessage::KKTRequest(kkt_request) if !session.is_initiator() => { + // Responder processes KKT request + // Convert X25519 public key to KEM format for KKT response + use nym_kkt::ciphersuite::EncapsulationKey; + + // Get local X25519 public key by deriving from private key + let local_x25519_public = session.local_x25519_public(); + + // Convert to libcrux KEM public key + match libcrux_kem::PublicKey::decode( + libcrux_kem::Algorithm::X25519, + local_x25519_public.as_bytes(), + ) { + Ok(libcrux_public_key) => { + let responder_kem_pk = EncapsulationKey::X25519(libcrux_public_key); + + match session.process_kkt_request(&kkt_request.0, &responder_kem_pk) { + Ok(kkt_response_message) => { + match session.next_packet(kkt_response_message) { + Ok(response_packet) => { + result_action = Some(Ok(LpAction::SendPacket(response_packet))); + // After KKT exchange, move to Handshaking + LpState::Handshaking { 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)); + LpState::Closed { reason } + } + } + } + Err(e) => { + let reason = format!("Failed to convert X25519 to KEM: {:?}", e); + let err = LpError::Internal(reason.clone()); + result_action = Some(Err(err)); + LpState::Closed { reason } + } + } + } + LpMessage::KKTResponse(kkt_response) if session.is_initiator() => { + // Initiator processes KKT response (signature-only mode with None) + match session.process_kkt_response(&kkt_response.0, None) { + Ok(()) => { + result_action = Some(Ok(LpAction::KKTComplete)); + // After successful KKT, move to Handshaking + LpState::Handshaking { session } + } + Err(e) => { + let reason = e.to_string(); + result_action = Some(Err(e)); + LpState::Closed { reason } + } + } + } + _ => { + // Wrong message type for KKT state + let err = LpError::InvalidStateTransition { + state: "KKTExchange".to_string(), + input: format!("Unexpected message type: {:?}", packet.message), + }; + let reason = err.to_string(); + result_action = Some(Err(err)); + LpState::Closed { reason } + } + } + } + } + + // Reject SendData during KKT exchange + (LpState::KKTExchange { session }, LpInput::SendData(_)) => { + result_action = Some(Err(LpError::InvalidStateTransition { + state: "KKTExchange".to_string(), + input: "SendData".to_string(), + })); + LpState::KKTExchange { session } + } + + // Reject StartHandshake if already in KKT exchange + (LpState::KKTExchange { session }, LpInput::StartHandshake) => { + result_action = Some(Err(LpError::InvalidStateTransition { + state: "KKTExchange".to_string(), + input: "StartHandshake".to_string(), + })); + LpState::KKTExchange { session } + } + // --- Handshaking State --- (LpState::Handshaking { session }, LpInput::ReceivePacket(packet)) => { // Check if packet lp_id matches our session @@ -270,7 +424,7 @@ impl LpStateMachine { } Err(e) => { // Error from process_handshake_message let reason = e.to_string(); - result_action = Some(Err(e.into())); + result_action = Some(Err(e)); LpState::Closed { reason } } } @@ -360,9 +514,10 @@ impl LpStateMachine { LpState::Transport { session } } - // --- Close Transition (applies to ReadyToHandshake, Handshaking, Transport) --- + // --- Close Transition (applies to ReadyToHandshake, KKTExchange, Handshaking, Transport) --- ( LpState::ReadyToHandshake { .. } // We consume the session here + | LpState::KKTExchange { .. } | LpState::Handshaking { .. } | LpState::Transport { .. }, LpInput::Close, @@ -421,20 +576,6 @@ impl LpStateMachine { result_action // Return the determined action (or None) } - // Helper to start the handshake (sends first message if initiator) - // Kept as it doesn't mutate self.state - fn start_handshake(&self, session: &LpSession) -> Option> { - session - .prepare_handshake_message() - .map(|result| match result { - Ok(message) => match session.next_packet(message) { - Ok(packet) => Ok(LpAction::SendPacket(packet)), - Err(e) => Err(e), - }, - Err(e) => Err(e), - }) - } - // Helper to prepare an outgoing data packet // Kept as it doesn't mutate self.state fn prepare_data_packet( @@ -452,17 +593,27 @@ impl LpStateMachine { #[cfg(test)] mod tests { use super::*; - use crate::keypair::Keypair; use bytes::Bytes; + use nym_crypto::asymmetric::ed25519; #[test] fn test_state_machine_init() { - let init_key = Keypair::new(); - let resp_key = Keypair::new(); - let psk = vec![0u8; 32]; - let remote_pub_key = resp_key.public_key(); + // Ed25519 keypairs for PSQ authentication and X25519 derivation + let ed25519_keypair_init = ed25519::KeyPair::from_secret([16u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([17u8; 32], 1); - let initiator_sm = LpStateMachine::new(true, &init_key, remote_pub_key, &psk); + // Test salt + let salt = [51u8; 32]; + + let initiator_sm = LpStateMachine::new( + true, + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, + ); assert!(initiator_sm.is_ok()); let initiator_sm = initiator_sm.unwrap(); assert!(matches!( @@ -472,7 +623,15 @@ mod tests { let init_session = initiator_sm.session().unwrap(); assert!(init_session.is_initiator()); - let responder_sm = LpStateMachine::new(false, &resp_key, init_key.public_key(), &psk); + let responder_sm = LpStateMachine::new( + false, + ( + ed25519_keypair_resp.private_key(), + ed25519_keypair_resp.public_key(), + ), + ed25519_keypair_init.public_key(), + &salt, + ); assert!(responder_sm.is_ok()); let responder_sm = responder_sm.unwrap(); assert!(matches!( @@ -482,73 +641,119 @@ mod tests { let resp_session = responder_sm.session().unwrap(); assert!(!resp_session.is_initiator()); - // Check lp_id is the same - let expected_lp_id = make_lp_id(init_key.public_key(), remote_pub_key); - assert_eq!(init_session.id(), expected_lp_id); - assert_eq!(resp_session.id(), expected_lp_id); + // Check lp_id is the same (derived internally from Ed25519 keys) + // Both state machines should have the same lp_id + assert_eq!(init_session.id(), resp_session.id()); } #[test] fn test_state_machine_simplified_flow() { - // Create test keys - let init_key = Keypair::new(); - let resp_key = Keypair::new(); - let psk = vec![0u8; 32]; + // Ed25519 keypairs for PSQ authentication and X25519 derivation + let ed25519_keypair_init = ed25519::KeyPair::from_secret([18u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([19u8; 32], 1); + + // Test salt + let salt = [52u8; 32]; // Create state machines (already in ReadyToHandshake) let mut initiator = LpStateMachine::new( true, // is_initiator - &init_key, - resp_key.public_key(), - &psk.clone(), + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, ) .unwrap(); let mut responder = LpStateMachine::new( false, // is_initiator - &resp_key, - init_key.public_key(), - &psk, + ( + ed25519_keypair_resp.private_key(), + ed25519_keypair_resp.public_key(), + ), + ed25519_keypair_init.public_key(), + &salt, ) .unwrap(); let lp_id = initiator.id().unwrap(); assert_eq!(lp_id, responder.id().unwrap()); - // --- Start Handshake --- (No index exchange needed) - println!("--- Step 1: Initiator starts handshake ---"); + // --- KKT Exchange --- + println!("--- Step 1: Initiator starts handshake (sends KKT request) ---"); let init_actions_1 = initiator.process_input(LpInput::StartHandshake); - let init_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_1 { + let kkt_request_packet = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_1 { packet.clone() } else { - panic!("Initiator should produce 1 action"); + panic!("Initiator should send KKT request"); }; assert!( - matches!(initiator.state, LpState::Handshaking { .. }), - "Initiator should be Handshaking" + matches!(initiator.state, LpState::KKTExchange { .. }), + "Initiator should be in KKTExchange" ); assert_eq!( - init_packet_1.header.session_id(), + kkt_request_packet.header.session_id(), lp_id, - "Packet 1 has wrong lp_id" + "KKT request packet has wrong lp_id" ); - println!("--- Step 2: Responder starts handshake (waits) ---"); + println!("--- Step 2: Responder starts handshake (waits for KKT) ---"); let resp_actions_1 = responder.process_input(LpInput::StartHandshake); assert!( resp_actions_1.is_none(), "Responder should produce 0 actions initially" ); assert!( - matches!(responder.state, LpState::Handshaking { .. }), - "Responder should be Handshaking" + matches!(responder.state, LpState::KKTExchange { .. }), + "Responder should be in KKTExchange" ); - // --- Handshake Message Exchange --- - println!("--- Step 3: Responder receives packet 1, sends packet 2 ---"); - let resp_actions_2 = responder.process_input(LpInput::ReceivePacket(init_packet_1)); - let resp_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_2 { + println!("--- Step 3: Responder receives KKT request, sends KKT response ---"); + let resp_actions_2 = responder.process_input(LpInput::ReceivePacket(kkt_request_packet)); + let kkt_response_packet = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_2 { + packet.clone() + } else { + panic!("Responder should send KKT response"); + }; + assert!( + matches!(responder.state, LpState::Handshaking { .. }), + "Responder should be Handshaking after KKT" + ); + + println!("--- Step 4: Initiator receives KKT response (KKT complete) ---"); + let init_actions_2 = initiator.process_input(LpInput::ReceivePacket(kkt_response_packet)); + assert!( + matches!(init_actions_2, Some(Ok(LpAction::KKTComplete))), + "Initiator should signal KKT complete" + ); + assert!( + matches!(initiator.state, LpState::Handshaking { .. }), + "Initiator should be Handshaking after KKT" + ); + + // --- Noise Handshake Message Exchange --- + println!("--- Step 5: Responder receives Noise msg 1, sends Noise msg 2 ---"); + // Now both sides are in Handshaking, continue with Noise handshake + // Initiator needs to send first Noise message + // (In real flow, this might happen automatically or via another process_input call) + // For this test, we'll simulate the responder receiving the first Noise message + // Actually, let me check if initiator automatically sends the first Noise message... + // Looking at the old test, it seems packet 1 was the first Noise message. + // With KKT, we need the initiator to send the first Noise message now. + + // Initiator prepares and sends first Noise handshake message + let init_noise_msg = initiator.session().unwrap().prepare_handshake_message(); + let init_packet_1 = if let Some(Ok(msg)) = init_noise_msg { + initiator.session().unwrap().next_packet(msg).unwrap() + } else { + panic!("Initiator should have first Noise message"); + }; + + let resp_actions_3 = responder.process_input(LpInput::ReceivePacket(init_packet_1)); + let resp_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_3 { packet.clone() } else { panic!("Responder should send packet 2"); @@ -563,12 +768,12 @@ mod tests { "Packet 2 has wrong lp_id" ); - println!("--- Step 4: Initiator receives packet 2, sends packet 3 ---"); - let init_actions_2 = initiator.process_input(LpInput::ReceivePacket(resp_packet_2)); - let init_packet_3 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_2 { + println!("--- Step 6: Initiator receives Noise msg 2, sends Noise msg 3 ---"); + let init_actions_3 = initiator.process_input(LpInput::ReceivePacket(resp_packet_2)); + let init_packet_3 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_3 { packet.clone() } else { - panic!("Initiator should send packet 3"); + panic!("Initiator should send Noise packet 3"); }; assert!( matches!(initiator.state, LpState::Transport { .. }), @@ -577,13 +782,13 @@ mod tests { assert_eq!( init_packet_3.header.session_id(), lp_id, - "Packet 3 has wrong lp_id" + "Noise packet 3 has wrong lp_id" ); - println!("--- Step 5: Responder receives packet 3, completes handshake ---"); - let resp_actions_3 = responder.process_input(LpInput::ReceivePacket(init_packet_3)); + println!("--- Step 7: Responder receives Noise msg 3, completes handshake ---"); + let resp_actions_4 = responder.process_input(LpInput::ReceivePacket(init_packet_3)); assert!( - matches!(resp_actions_3, Some(Ok(LpAction::HandshakeComplete))), + matches!(resp_actions_4, Some(Ok(LpAction::HandshakeComplete))), "Responder should complete handshake" ); assert!( @@ -592,58 +797,249 @@ mod tests { ); // --- Transport Phase --- - println!("--- Step 6: Initiator sends data ---"); + println!("--- Step 8: Initiator sends data ---"); let data_to_send_1 = b"hello responder"; - let init_actions_3 = initiator.process_input(LpInput::SendData(data_to_send_1.to_vec())); - let data_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_3 { + let init_actions_4 = initiator.process_input(LpInput::SendData(data_to_send_1.to_vec())); + 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.session_id(), lp_id); - println!("--- Step 7: Responder receives data ---"); - let resp_actions_4 = responder.process_input(LpInput::ReceivePacket(data_packet_1)); - let resp_data_1 = if let Some(Ok(LpAction::DeliverData(data))) = resp_actions_4 { + println!("--- Step 9: 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, Bytes::copy_from_slice(data_to_send_1)); - println!("--- Step 8: Responder sends data ---"); + println!("--- Step 10: Responder sends data ---"); let data_to_send_2 = b"hello initiator"; - let resp_actions_5 = responder.process_input(LpInput::SendData(data_to_send_2.to_vec())); - let data_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_5 { + let resp_actions_6 = responder.process_input(LpInput::SendData(data_to_send_2.to_vec())); + 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.session_id(), lp_id); - println!("--- Step 9: Initiator receives data ---"); - let init_actions_4 = initiator.process_input(LpInput::ReceivePacket(data_packet_2)); - if let Some(Ok(LpAction::DeliverData(data))) = init_actions_4 { + println!("--- Step 11: 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, Bytes::copy_from_slice(data_to_send_2)); } else { panic!("Initiator should deliver data"); } // --- Close --- - println!("--- Step 10: Initiator closes ---"); - let init_actions_5 = initiator.process_input(LpInput::Close); + println!("--- Step 12: Initiator closes ---"); + let init_actions_6 = initiator.process_input(LpInput::Close); assert!(matches!( - init_actions_5, + init_actions_6, Some(Ok(LpAction::ConnectionClosed)) )); assert!(matches!(initiator.state, LpState::Closed { .. })); - println!("--- Step 11: Responder closes ---"); - let resp_actions_6 = responder.process_input(LpInput::Close); + println!("--- Step 13: Responder closes ---"); + let resp_actions_7 = responder.process_input(LpInput::Close); assert!(matches!( - resp_actions_6, + resp_actions_7, Some(Ok(LpAction::ConnectionClosed)) )); assert!(matches!(responder.state, LpState::Closed { .. })); } + + #[test] + fn test_kkt_exchange_initiator_flow() { + // Ed25519 keypairs for PSQ authentication and X25519 derivation + let ed25519_keypair_init = ed25519::KeyPair::from_secret([20u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([21u8; 32], 1); + + let salt = [53u8; 32]; + + // Create initiator state machine + let mut initiator = LpStateMachine::new( + true, + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, + ) + .unwrap(); + + // Verify initial state + assert!(matches!(initiator.state, LpState::ReadyToHandshake { .. })); + + // Step 1: Initiator starts handshake (should send KKT request) + let init_action = initiator.process_input(LpInput::StartHandshake); + assert!(matches!(init_action, Some(Ok(LpAction::SendPacket(_))))); + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); + } + + #[test] + fn test_kkt_exchange_responder_flow() { + // Ed25519 keypairs for PSQ authentication and X25519 derivation + let ed25519_keypair_init = ed25519::KeyPair::from_secret([22u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([23u8; 32], 1); + + let salt = [54u8; 32]; + + // Create responder state machine + let mut responder = LpStateMachine::new( + false, + ( + ed25519_keypair_resp.private_key(), + ed25519_keypair_resp.public_key(), + ), + ed25519_keypair_init.public_key(), + &salt, + ) + .unwrap(); + + // Verify initial state + assert!(matches!(responder.state, LpState::ReadyToHandshake { .. })); + + // Step 1: Responder starts handshake (should transition to KKTExchange without sending) + let resp_action = responder.process_input(LpInput::StartHandshake); + assert!(resp_action.is_none()); + assert!(matches!(responder.state, LpState::KKTExchange { .. })); + } + + #[test] + fn test_kkt_exchange_full_roundtrip() { + // Ed25519 keypairs for PSQ authentication and X25519 derivation + let ed25519_keypair_init = ed25519::KeyPair::from_secret([24u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([25u8; 32], 1); + + let salt = [55u8; 32]; + + // Create both state machines + let mut initiator = LpStateMachine::new( + true, + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, + ) + .unwrap(); + + let mut responder = LpStateMachine::new( + false, + ( + ed25519_keypair_resp.private_key(), + ed25519_keypair_resp.public_key(), + ), + ed25519_keypair_init.public_key(), + &salt, + ) + .unwrap(); + + // Step 1: Initiator starts handshake, sends KKT request + let init_action = initiator.process_input(LpInput::StartHandshake); + let kkt_request_packet = if let Some(Ok(LpAction::SendPacket(packet))) = init_action { + packet.clone() + } else { + panic!("Initiator should send KKT request"); + }; + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); + + // Step 2: Responder transitions to KKTExchange + let resp_action = responder.process_input(LpInput::StartHandshake); + assert!(resp_action.is_none()); + assert!(matches!(responder.state, LpState::KKTExchange { .. })); + + // Step 3: Responder receives KKT request, sends KKT response + let resp_action = responder.process_input(LpInput::ReceivePacket(kkt_request_packet)); + let kkt_response_packet = if let Some(Ok(LpAction::SendPacket(packet))) = resp_action { + packet.clone() + } else { + panic!("Responder should send KKT response"); + }; + // After sending KKT response, responder moves to Handshaking + assert!(matches!(responder.state, LpState::Handshaking { .. })); + + // Step 4: Initiator receives KKT response, completes KKT + let init_action = initiator.process_input(LpInput::ReceivePacket(kkt_response_packet)); + assert!(matches!(init_action, Some(Ok(LpAction::KKTComplete)))); + // After KKT complete, initiator moves to Handshaking + assert!(matches!(initiator.state, LpState::Handshaking { .. })); + } + + #[test] + fn test_kkt_exchange_close() { + // Ed25519 keypairs for KKT authentication + let ed25519_keypair_init = ed25519::KeyPair::from_secret([26u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([27u8; 32], 1); + + let salt = [56u8; 32]; + + // Create initiator state machine + let mut initiator = LpStateMachine::new( + true, + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, + ) + .unwrap(); + + // Start handshake to enter KKTExchange state + initiator.process_input(LpInput::StartHandshake); + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); + + // Close during KKT exchange + let close_action = initiator.process_input(LpInput::Close); + assert!(matches!(close_action, Some(Ok(LpAction::ConnectionClosed)))); + assert!(matches!(initiator.state, LpState::Closed { .. })); + } + + #[test] + fn test_kkt_exchange_rejects_invalid_inputs() { + // Ed25519 keypairs for KKT authentication + let ed25519_keypair_init = ed25519::KeyPair::from_secret([28u8; 32], 0); + let ed25519_keypair_resp = ed25519::KeyPair::from_secret([29u8; 32], 1); + + let salt = [57u8; 32]; + + // Create initiator state machine + let mut initiator = LpStateMachine::new( + true, + ( + ed25519_keypair_init.private_key(), + ed25519_keypair_init.public_key(), + ), + ed25519_keypair_resp.public_key(), + &salt, + ) + .unwrap(); + + // Start handshake to enter KKTExchange state + initiator.process_input(LpInput::StartHandshake); + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); + + // Try SendData during KKT exchange (should be rejected) + let send_action = initiator.process_input(LpInput::SendData(vec![1, 2, 3])); + assert!(matches!( + send_action, + Some(Err(LpError::InvalidStateTransition { .. })) + )); + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); // Still in KKTExchange + + // Try StartHandshake again during KKT exchange (should be rejected) + let start_action = initiator.process_input(LpInput::StartHandshake); + assert!(matches!( + start_action, + Some(Err(LpError::InvalidStateTransition { .. })) + )); + assert!(matches!(initiator.state, LpState::KKTExchange { .. })); // Still in KKTExchange + } } diff --git a/common/types/bindings/ts-packages/types/src/types/rust/NymNodeBond.ts b/common/types/bindings/ts-packages/types/src/types/rust/NymNodeBond.ts index 0d50d01f55..dcd95efae6 100644 --- a/common/types/bindings/ts-packages/types/src/types/rust/NymNodeBond.ts +++ b/common/types/bindings/ts-packages/types/src/types/rust/NymNodeBond.ts @@ -36,4 +36,9 @@ custom_http_port: number | null, /** * Base58-encoded ed25519 EdDSA public key. */ -identity_key: string, }; +identity_key: string, +/** + * Optional LP (Lewes Protocol) listener address for direct gateway connections. + * Format: "host:port", for example "1.1.1.1:41264" or "gateway.example.com:41264" + */ +lp_address: string | null, }; diff --git a/common/wireguard-types/src/lib.rs b/common/wireguard-types/src/lib.rs index 8f73b40419..455eea38dd 100644 --- a/common/wireguard-types/src/lib.rs +++ b/common/wireguard-types/src/lib.rs @@ -12,3 +12,5 @@ pub use error::Error; pub use public_key::PeerPublicKey; pub const DEFAULT_PEER_TIMEOUT_CHECK: Duration = Duration::from_secs(5); // 5 seconds +pub const DEFAULT_IP_CLEANUP_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes +pub const DEFAULT_IP_STALE_AGE: Duration = Duration::from_secs(3600); // 1 hour diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index 44a15c0a3c..7d887b7394 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -15,6 +15,9 @@ base64 = { workspace = true } defguard_wireguard_rs = { workspace = true } futures = { workspace = true } ip_network = { workspace = true } +ipnetwork = { workspace = true } +log.workspace = true +rand = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-stream = { workspace = true } @@ -25,6 +28,7 @@ nym-credential-verification = { path = "../credential-verification" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-gateway-storage = { path = "../gateway-storage" } nym-gateway-requests = { path = "../gateway-requests" } +nym-ip-packet-requests = { path = "../ip-packet-requests" } nym-metrics = { path = "../nym-metrics" } nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } diff --git a/common/wireguard/src/error.rs b/common/wireguard/src/error.rs index d240889d4a..7f5437d630 100644 --- a/common/wireguard/src/error.rs +++ b/common/wireguard/src/error.rs @@ -20,6 +20,9 @@ pub enum Error { #[error("{0}")] SystemTime(#[from] std::time::SystemTimeError), + + #[error("IP pool error: {0}")] + IpPool(String), } pub type Result = std::result::Result; diff --git a/common/wireguard/src/ip_pool.rs b/common/wireguard/src/ip_pool.rs new file mode 100644 index 0000000000..e1c2b0453f --- /dev/null +++ b/common/wireguard/src/ip_pool.rs @@ -0,0 +1,202 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use ipnetwork::IpNetwork; +use nym_ip_packet_requests::IpPair; +use rand::seq::IteratorRandom; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; + +/// Represents the state of an IP allocation +#[derive(Debug, Clone, Copy)] +pub enum AllocationState { + /// IP is available for allocation + Free, + /// IP is allocated and in use, with timestamp of allocation + Allocated(SystemTime), +} + +/// Thread-safe IP address pool manager +/// +/// Manages allocation of IPv4/IPv6 address pairs from configured CIDR ranges. +/// Ensures collision-free allocation and supports stale cleanup. +#[derive(Clone)] +pub struct IpPool { + allocations: Arc>>, +} + +impl IpPool { + /// Create a new IP pool from IPv4 and IPv6 CIDR ranges + /// + /// # Arguments + /// * `ipv4_network` - Base IPv4 address for the pool + /// * `ipv4_prefix` - CIDR prefix length for IPv4 (e.g., 16 for /16) + /// * `ipv6_network` - Base IPv6 address for the pool + /// * `ipv6_prefix` - CIDR prefix length for IPv6 (e.g., 112 for /112) + /// + /// # Errors + /// Returns error if CIDR ranges are invalid + pub fn new( + ipv4_network: Ipv4Addr, + ipv4_prefix: u8, + ipv6_network: Ipv6Addr, + ipv6_prefix: u8, + ) -> Result { + let ipv4_net = IpNetwork::new(ipv4_network.into(), ipv4_prefix)?; + let ipv6_net = IpNetwork::new(ipv6_network.into(), ipv6_prefix)?; + + // Build initial pool with all IPs marked as free + let mut allocations = HashMap::new(); + + // Collect IPv4 and IPv6 addresses into vectors for pairing + let ipv4_addrs: Vec = ipv4_net + .iter() + .filter_map(|ip| { + if let IpAddr::V4(v4) = ip { + Some(v4) + } else { + None + } + }) + .collect(); + + let ipv6_addrs: Vec = ipv6_net + .iter() + .filter_map(|ip| { + if let IpAddr::V6(v6) = ip { + Some(v6) + } else { + None + } + }) + .collect(); + + // Create IpPairs by matching IPv4 and IPv6 addresses + // Use the minimum length to avoid index out of bounds + let pair_count = ipv4_addrs.len().min(ipv6_addrs.len()); + for i in 0..pair_count { + let pair = IpPair::new(ipv4_addrs[i], ipv6_addrs[i]); + allocations.insert(pair, AllocationState::Free); + } + + tracing::info!( + "Initialized IP pool with {} address pairs from {}/{} and {}/{}", + allocations.len(), + ipv4_network, + ipv4_prefix, + ipv6_network, + ipv6_prefix + ); + + Ok(IpPool { + allocations: Arc::new(RwLock::new(allocations)), + }) + } + + /// Allocate a free IP pair from the pool + /// + /// Randomly selects an available IP pair and marks it as allocated. + /// + /// # Errors + /// Returns `IpPoolError::NoFreeIp` if no IPs are available + pub async fn allocate(&self) -> Result { + let mut pool = self.allocations.write().await; + + // Find a free IP and allocate it + let free_ip = pool + .iter_mut() + .filter(|(_, state)| matches!(state, AllocationState::Free)) + .choose(&mut rand::thread_rng()) + .ok_or(IpPoolError::NoFreeIp)?; + + let ip_pair = *free_ip.0; + *free_ip.1 = AllocationState::Allocated(SystemTime::now()); + + tracing::debug!("Allocated IP pair: {}", ip_pair); + Ok(ip_pair) + } + + /// Release an IP pair back to the pool + /// + /// Marks the IP as free for future allocations. + pub async fn release(&self, ip_pair: IpPair) { + let mut pool = self.allocations.write().await; + if let Some(state) = pool.get_mut(&ip_pair) { + *state = AllocationState::Free; + tracing::debug!("Released IP pair: {}", ip_pair); + } + } + + /// Mark an IP pair as allocated (used during initialization from database) + /// + /// This is used when restoring state from the database on gateway startup. + pub async fn mark_used(&self, ip_pair: IpPair) { + let mut pool = self.allocations.write().await; + if let Some(state) = pool.get_mut(&ip_pair) { + *state = AllocationState::Allocated(SystemTime::now()); + tracing::debug!("Marked IP pair as used: {}", ip_pair); + } else { + tracing::warn!("Attempted to mark unknown IP pair as used: {}", ip_pair); + } + } + + /// Get the number of free IPs in the pool + pub async fn free_count(&self) -> usize { + let pool = self.allocations.read().await; + pool.iter() + .filter(|(_, state)| matches!(state, AllocationState::Free)) + .count() + } + + /// Get the number of allocated IPs in the pool + pub async fn allocated_count(&self) -> usize { + let pool = self.allocations.read().await; + pool.iter() + .filter(|(_, state)| matches!(state, AllocationState::Allocated(_))) + .count() + } + + /// Get the total pool size + pub async fn total_count(&self) -> usize { + let pool = self.allocations.read().await; + pool.len() + } + + /// Clean up stale allocations older than the specified duration + /// + /// Returns the number of IPs that were freed + pub async fn cleanup_stale(&self, max_age: std::time::Duration) -> usize { + let mut pool = self.allocations.write().await; + let now = SystemTime::now(); + let mut freed = 0; + + for (_ip, state) in pool.iter_mut() { + if let AllocationState::Allocated(allocated_at) = state + && let Ok(age) = now.duration_since(*allocated_at) + && age > max_age + { + *state = AllocationState::Free; + freed += 1; + } + } + + if freed > 0 { + tracing::info!("Cleaned up {} stale IP allocations", freed); + } + + freed + } +} + +/// Errors that can occur during IP pool operations +#[derive(Debug, thiserror::Error)] +pub enum IpPoolError { + #[error("No free IP addresses available in pool")] + NoFreeIp, + + #[error("Invalid IP network configuration: {0}")] + InvalidNetwork(#[from] ipnetwork::IpNetworkError), +} diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 455f16c4f6..7fda2e11c9 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -16,16 +16,23 @@ use tracing::error; #[cfg(target_os = "linux")] use nym_credential_verification::ecash::EcashManager; +#[cfg(target_os = "linux")] +use nym_ip_packet_requests::IpPair; +#[cfg(target_os = "linux")] +use std::net::IpAddr; + #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; pub mod error; +pub mod ip_pool; pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; pub use error::Error; -pub use peer_controller::PeerControlRequest; +pub use ip_pool::{IpPool, IpPoolError}; +pub use peer_controller::{PeerControlRequest, PeerRegistrationData}; pub const CONTROL_CHANNEL_SIZE: usize = 256; @@ -176,14 +183,16 @@ pub async fn start_wireguard( use base64::{Engine, prelude::BASE64_STANDARD}; use defguard_wireguard_rs::{InterfaceConfiguration, WireguardInterfaceApi}; use ip_network::IpNetwork; - use nym_credential_verification::ecash::traits::EcashManager; use peer_controller::PeerController; use std::collections::HashMap; use tokio::sync::RwLock; use tracing::info; let ifname = String::from(WG_TUN_BASE_NAME); - info!("Initializing WireGuard interface '{}' with use_userspace={}", ifname, use_userspace); + info!( + "Initializing WireGuard interface '{}' with use_userspace={}", + ifname, use_userspace + ); let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), use_userspace)?; let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len()); @@ -207,7 +216,7 @@ pub async fn start_wireguard( prvkey: BASE64_STANDARD.encode(wireguard_data.inner.keypair().private_key().to_bytes()), address: wireguard_data.inner.config().private_ipv4.to_string(), port: wireguard_data.inner.config().announced_tunnel_port as u32, - peers, + peers: peers.clone(), // Clone since we need to use peers later to mark IPs as used mtu: None, }; info!( @@ -260,9 +269,38 @@ pub async fn start_wireguard( let host = wg_api.read_interface_data()?; let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api)); + // Initialize IP pool from configuration + info!("Initializing IP pool for WireGuard peer allocation"); + let ip_pool = IpPool::new( + wireguard_data.inner.config().private_ipv4, + wireguard_data.inner.config().private_network_prefix_v4, + wireguard_data.inner.config().private_ipv6, + wireguard_data.inner.config().private_network_prefix_v6, + )?; + + // Mark existing peer IPs as used in the pool + for peer in &peers { + for allowed_ip in &peer.allowed_ips { + // Extract IPv4 and IPv6 from peer's allowed_ips + if let IpAddr::V4(ipv4) = allowed_ip.ip { + // Find corresponding IPv6 + if let Some(ipv6_mask) = peer + .allowed_ips + .iter() + .find(|ip| matches!(ip.ip, IpAddr::V6(_))) + { + if let IpAddr::V6(ipv6) = ipv6_mask.ip { + ip_pool.mark_used(IpPair::new(ipv4, ipv6)).await; + } + } + } + } + } + let mut controller = PeerController::new( ecash_manager, metrics, + ip_pool, wg_api.clone(), host, peer_bandwidth_managers, diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index aa1fd5b2ed..d4fff10042 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -20,22 +20,73 @@ use nym_credential_verification::{ use nym_credentials_interface::CredentialSpendingData; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::traits::BandwidthGatewayStorage; +use nym_ip_packet_requests::IpPair; use nym_node_metrics::NymNodeMetrics; -use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; +use nym_wireguard_types::{ + DEFAULT_IP_CLEANUP_INTERVAL, DEFAULT_IP_STALE_AGE, DEFAULT_PEER_TIMEOUT_CHECK, +}; use std::{collections::HashMap, sync::Arc}; use std::{ - net::IpAddr, + net::{IpAddr, SocketAddr}, time::{Duration, SystemTime}, }; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; use tracing::{debug, error, info, trace}; +use crate::{ + error::{Error, Result}, + ip_pool::IpPool, + peer_handle::SharedBandwidthStorageManager, +}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; + +/// Registration data for a new peer (without pre-allocated IPs) +#[derive(Debug, Clone)] +pub struct PeerRegistrationData { + pub public_key: Key, + pub preshared_key: Option, + pub endpoint: Option, + pub persistent_keepalive_interval: Option, +} + +impl PeerRegistrationData { + pub fn new(public_key: Key) -> Self { + Self { + public_key, + preshared_key: None, + endpoint: None, + persistent_keepalive_interval: None, + } + } + + pub fn with_preshared_key(mut self, key: Key) -> Self { + self.preshared_key = Some(key); + self + } + + pub fn with_endpoint(mut self, endpoint: SocketAddr) -> Self { + self.endpoint = Some(endpoint); + self + } + + pub fn with_keepalive(mut self, interval: u16) -> Self { + self.persistent_keepalive_interval = Some(interval); + self + } +} + pub enum PeerControlRequest { + /// Add a peer with pre-allocated IPs (for backwards compatibility) AddPeer { peer: Peer, response_tx: oneshot::Sender, }, + /// Register a new peer and allocate IPs from the pool + RegisterPeer { + registration_data: PeerRegistrationData, + response_tx: oneshot::Sender, + }, RemovePeer { key: Key, response_tx: oneshot::Sender, @@ -65,6 +116,7 @@ pub enum PeerControlRequest { } pub type AddPeerControlResponse = Result<()>; +pub type RegisterPeerControlResponse = Result; pub type RemovePeerControlResponse = Result<()>; pub type QueryPeerControlResponse = Result>; pub type GetClientBandwidthControlResponse = Result; @@ -77,6 +129,9 @@ pub struct PeerController { // so the overhead is minimal metrics: NymNodeMetrics, + // IP address pool for peer allocation + ip_pool: IpPool, + // used to receive commands from individual handles too request_tx: mpsc::Sender, request_rx: mpsc::Receiver, @@ -84,6 +139,7 @@ pub struct PeerController { host_information: Arc>, bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, + ip_cleanup_interval: IntervalStream, /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting /// their bandwidth metered. @@ -96,6 +152,7 @@ impl PeerController { pub(crate) fn new( ecash_verifier: Arc, metrics: NymNodeMetrics, + ip_pool: IpPool, wg_api: Arc, initial_host_information: Host, bw_storage_managers: HashMap, @@ -106,6 +163,8 @@ impl PeerController { ) -> Self { let timeout_check_interval = IntervalStream::new(tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK)); + let ip_cleanup_interval = + IntervalStream::new(tokio::time::interval(DEFAULT_IP_CLEANUP_INTERVAL)); let host_information = Arc::new(RwLock::new(initial_host_information)); for (public_key, (bandwidth_storage_manager, peer)) in bw_storage_managers.iter() { let cached_peer_manager = CachedPeerManager::new(peer); @@ -131,15 +190,17 @@ impl PeerController { PeerController { ecash_verifier, + metrics, + ip_pool, wg_api, host_information, bw_storage_managers, request_tx, request_rx, timeout_check_interval, + ip_cleanup_interval, upgrade_mode, shutdown_token, - metrics, } } @@ -231,6 +292,29 @@ impl PeerController { Ok(()) } + /// Allocate IP pair from pool for a new peer registration + /// + /// This only allocates IPs - the caller must handle database storage and + /// then call AddPeer with a complete Peer struct. + async fn handle_register_request( + &mut self, + _registration_data: PeerRegistrationData, + ) -> Result { + nym_metrics::inc!("wg_ip_allocation_attempts"); + + // Allocate IP pair from pool + let ip_pair = self + .ip_pool + .allocate() + .await + .map_err(|e| Error::IpPool(e.to_string()))?; + + nym_metrics::inc!("wg_ip_allocation_success"); + tracing::debug!("Allocated IP pair: {}", ip_pair); + + Ok(ip_pair) + } + async fn ip_to_key(&self, ip: IpAddr) -> Result> { Ok(self .bw_storage_managers @@ -408,6 +492,14 @@ impl PeerController { *self.host_information.write().await = host; } + _ = self.ip_cleanup_interval.next() => { + // Periodically cleanup stale IP allocations + let freed = self.ip_pool.cleanup_stale(DEFAULT_IP_STALE_AGE).await; + if freed > 0 { + nym_metrics::inc_by!("wg_stale_ips_cleaned", freed as u64); + log::info!("Cleaned up {} stale IP allocations", freed); + } + } _ = self.shutdown_token.cancelled() => { trace!("PeerController handler: Received shutdown"); break; @@ -417,6 +509,9 @@ impl PeerController { Some(PeerControlRequest::AddPeer { peer, response_tx }) => { response_tx.send(self.handle_add_request(&peer).await).ok(); } + Some(PeerControlRequest::RegisterPeer { registration_data, response_tx }) => { + response_tx.send(self.handle_register_request(registration_data).await).ok(); + } Some(PeerControlRequest::RemovePeer { key, response_tx }) => { response_tx.send(self.remove_peer(&key).await).ok(); } @@ -543,6 +638,7 @@ pub fn start_controller( Arc>, nym_task::ShutdownManager, ) { + use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::Arc; let storage = Arc::new(RwLock::new( @@ -552,10 +648,21 @@ pub fn start_controller( Box::new(storage.clone()), )); let wg_api = Arc::new(MockWgApi::default()); + + // Create IP pool for testing + let ip_pool = IpPool::new( + Ipv4Addr::new(10, 0, 0, 0), + 24, + Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0), + 112, + ) + .expect("Failed to create IP pool for testing"); + let shutdown_manager = nym_task::ShutdownManager::empty_mock(); let mut peer_controller = PeerController::new( ecash_manager, Default::default(), + ip_pool, wg_api, Default::default(), Default::default(), @@ -577,8 +684,7 @@ pub async fn stop_controller(mut shutdown_manager: nym_task::ShutdownManager) { shutdown_manager.run_until_shutdown().await; } -#[cfg(test)] -#[cfg(feature = "mock")] +#[cfg(all(test, feature = "mock"))] mod tests { use super::*; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 42e5e1352a..85f258b9c1 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1158,10 +1158,12 @@ version = "0.4.0" dependencies = [ "base64 0.22.1", "bs58", + "curve25519-dalek", "ed25519-dalek", "nym-pemstore", "nym-sphinx-types", "rand", + "sha2", "subtle-encoding", "thiserror 2.0.12", "x25519-dalek", @@ -1795,9 +1797,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 1508ac1548..ed5af774a0 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -967,6 +967,7 @@ pub mod test_helpers { host: "1.2.3.4".to_string(), custom_http_port: None, identity_key, + lp_address: None, }; let msg = nymnode_bonding_sign_payload(self.deps(), sender, node.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 33e90beaf3..4f943eca75 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -446,6 +446,7 @@ pub(crate) trait PerformanceContractTesterExt: host: "1.2.3.4".to_string(), custom_http_port: None, identity_key, + lp_address: None, }; let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) diff --git a/docs/LP_PROTOCOL.md b/docs/LP_PROTOCOL.md new file mode 100644 index 0000000000..de3e5f50be --- /dev/null +++ b/docs/LP_PROTOCOL.md @@ -0,0 +1,990 @@ +# Lewes Protocol (LP) - Technical Specification + +## Overview + +The Lewes Protocol (LP) is a direct TCP-based registration protocol for Nym gateways. It provides an alternative to mixnet-based registration with different trade-offs: lower latency at the cost of revealing client IP to the gateway. + +**Design Goals:** +- **Low latency**: Direct TCP connection vs multi-hop mixnet routing +- **High reliability**: KCP protocol provides ordered, reliable delivery with ARQ +- **Strong security**: Noise XKpsk3 provides mutual authentication and forward secrecy +- **Replay protection**: Bitmap-based counter validation prevents replay attacks +- **Observability**: Prometheus metrics for production monitoring + +**Non-Goals:** +- Network-level anonymity (use mixnet registration for that) +- Persistent connections (LP is registration-only, single-use) +- Backward compatibility with legacy protocols + +## Architecture + +### Protocol Stack + +``` +┌─────────────────────────────────────────┐ +│ Application Layer │ +│ - Registration Requests │ +│ - E-cash Credential Verification │ +│ - WireGuard Peer Management │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ LP Layer (Lewes Protocol) │ +│ - Noise XKpsk3 Handshake │ +│ - Replay Protection (1024-pkt window) │ +│ - Counter-based Sequencing │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ KCP Layer (Reliability) │ +│ - Ordered Delivery │ +│ - ARQ with Selective ACK │ +│ - Congestion Control │ +│ - RTT Estimation │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ TCP Layer │ +│ - Connection Establishment │ +│ - Byte Stream Delivery │ +└─────────────────────────────────────────┘ +``` + +### Why This Layering? + +**TCP**: Provides connection-oriented byte stream and handles network-level retransmission. + +**KCP**: Adds application-level reliability optimized for low latency: +- **Fast retransmit**: Triggered after 2 duplicate ACKs (vs TCP's 3) +- **Selective ACK**: Acknowledges specific packets, not just cumulative +- **Configurable RTO**: Minimum RTO of 100ms (configurable) +- **No Nagle**: Immediate sending for low-latency applications + +**LP**: Provides cryptographic security and session management: +- **Noise XKpsk3**: Mutual authentication with pre-shared key +- **Replay protection**: Prevents duplicate packet acceptance +- **Session isolation**: Each session has unique cryptographic state + +**Application**: Business logic for registration and credential verification. + +## Protocol Flow + +### 1. Connection Establishment + +``` +Client Gateway + | | + |--- TCP SYN ---------------------------> | + |<-- TCP SYN-ACK ------------------------ | + |--- TCP ACK ----------------------------> | + | | +``` + +- **Control Port**: 41264 (default, configurable) +- **Data Port**: 51264 (reserved for future use, not currently used) + +### 2. Session Initialization + +Client generates session parameters: + +```rust +// Client-side session setup +let client_lp_keypair = Keypair::generate(); // X25519 keypair +let gateway_lp_public = gateway.lp_public_key; // From gateway descriptor +let salt = [timestamp (8 bytes) || nonce (24 bytes)]; // 32-byte salt + +// Derive PSK using ECDH + Blake3 KDF +let shared_secret = ECDH(client_private, gateway_public); +let psk = Blake3_derive_key( + context = "nym-lp-psk-v1", + input = shared_secret, + salt = salt +); + +// Calculate session IDs (deterministic from keys) +let lp_id = hash(client_lp_public || 0xCC || gateway_lp_public) & 0xFFFFFFFF; +let kcp_conv_id = hash(client_lp_public || 0xFF || gateway_lp_public) & 0xFFFFFFFF; +``` + +**Session ID Properties:** +- **Deterministic**: Same key pair always produces same ID +- **Order-independent**: `ID(A, B) == ID(B, A)` due to sorted hashing +- **Collision-resistant**: Uses full hash, truncated to u32 +- **Unique per protocol**: Different delimiters (0xCC for LP, 0xFF for KCP) + +### 3. Noise Handshake (XKpsk3 Pattern) + +``` +Client (Initiator) Gateway (Responder) + | | + |--- e ----------------------------------> | [1] Client ephemeral + | | + |<-- e, ee, s, es --------------------- | [2] Gateway ephemeral + static + | | + |--- s, se, psk -------------------------> | [3] Client static + PSK mix + | | + [Transport mode established] +``` + +**Message Contents:** + +**[1] Initiator → Responder: `e`** +- Payload: Client ephemeral public key (32 bytes) +- Encrypted: No (initial message) + +**[2] Responder → Initiator: `e, ee, s, es`** +- `e`: Responder ephemeral public key +- `ee`: Mix ephemeral-ephemeral DH +- `s`: Responder static public key (encrypted) +- `es`: Mix ephemeral-static DH +- Encrypted: Yes (with keys from `ee`) + +**[3] Initiator → Responder: `s, se, psk`** +- `s`: Initiator static public key (encrypted) +- `se`: Mix static-ephemeral DH +- `psk`: Mix pre-shared key (at position 3) +- Encrypted: Yes (with keys from `ee`, `es`) + +**Security Properties:** +- ✅ **Mutual authentication**: Both sides prove identity via static keys +- ✅ **Forward secrecy**: Ephemeral keys provide PFS +- ✅ **PSK authentication**: Binds session to out-of-band PSK +- ✅ **Identity hiding**: Static keys encrypted after first message + +**Handshake Characteristics:** +- **Messages**: 3 (1.5 round trips) +- **Minimum network RTTs**: 1.5 +- **Cryptographic operations**: ECDH, ChaCha20-Poly1305, SHA-256 + +### 4. PSK Derivation Details + +**Formula:** +``` +shared_secret = X25519(client_private_lp, gateway_public_lp) +psk = Blake3_derive_key( + context = "nym-lp-psk-v1", + key_material = shared_secret (32 bytes), + salt = timestamp || nonce (32 bytes) +) +``` + +**Implementation** (from `common/nym-lp/src/psk.rs:48`): +```rust +pub fn derive_psk( + local_private: &PrivateKey, + remote_public: &PublicKey, + salt: &[u8; 32], +) -> [u8; 32] { + let shared_secret = local_private.diffie_hellman(remote_public); + nym_crypto::kdf::derive_key_blake3(PSK_CONTEXT, shared_secret.as_bytes(), salt) +} +``` + +**Why This Design:** + +1. **Identity-bound**: PSK tied to LP keypairs, not ephemeral + - Prevents MITM without LP private key + - Links session to long-term identities + +2. **Session-specific via salt**: Different registrations use different PSKs + - `timestamp`: 8-byte Unix timestamp (milliseconds) + - `nonce`: 24-byte random value + - Prevents PSK reuse across sessions + +3. **Symmetric derivation**: Both sides derive same PSK + - Client: `ECDH(client_priv, gateway_pub)` + - Gateway: `ECDH(gateway_priv, client_pub)` + - Mathematical property: `ECDH(a, B) == ECDH(b, A)` + +4. **Blake3 KDF with domain separation**: + - Context string prevents cross-protocol attacks + - Generates uniform 32-byte output suitable for Noise + +**Salt Transmission:** +- Included in `ClientHello` message (cleartext) +- Gateway extracts salt before deriving PSK +- Timestamp validation rejects stale salts + +### 5. Replay Protection + +**Mechanism: Sliding Window with Bitmap** (from `common/nym-lp/src/replay/validator.rs:32`): + +```rust +const WORD_SIZE: usize = 64; +const N_WORDS: usize = 16; // 1024 bits total +const N_BITS: usize = WORD_SIZE * N_WORDS; // 1024 + +pub struct ReceivingKeyCounterValidator { + next: u64, // Next expected counter + receive_cnt: u64, // Total packets received + bitmap: [u64; 16], // 1024-bit bitmap +} +``` + +**Algorithm:** +``` +For each incoming packet with counter C: + 1. Quick check (branchless): + - If C >= next: Accept (growing) + - If C + 1024 < next: Reject (too old, outside window) + - If bitmap[C % 1024] is set: Reject (duplicate) + - Else: Accept (out-of-order within window) + + 2. After successful processing, mark: + - Set bitmap[C % 1024] = 1 + - If C >= next: Update next = C + 1 + - Increment receive_cnt +``` + +**Performance Optimizations:** + +1. **SIMD-accelerated bitmap operations** (from `common/nym-lp/src/replay/simd/`): + - AVX2 support (x86_64) + - SSE2 support (x86_64) + - NEON support (ARM) + - Scalar fallback (portable) + +2. **Branchless execution** (constant-time): + ```rust + // No early returns - prevents timing attacks + let result = if is_growing { + Some(Ok(())) + } else if too_far_back { + Some(Err(ReplayError::OutOfWindow)) + } else if duplicate { + Some(Err(ReplayError::DuplicateCounter)) + } else { + Some(Ok(())) + }; + result.unwrap() + ``` + +3. **Overflow-safe arithmetic**: + ```rust + let too_far_back = if counter > u64::MAX - 1024 { + false // Can't overflow, so not too far back + } else { + counter + 1024 < self.next + }; + ``` + +**Memory Usage** (verified from `common/nym-lp/src/replay/validator.rs:738`): +```rust +// test_memory_usage() +size = size_of::() * 2 + // next + receive_cnt = 16 bytes + size_of::() * N_WORDS; // bitmap = 128 bytes +// Total: 144 bytes +``` + +### 6. Registration Request + +After handshake completes, client sends encrypted registration request: + +```rust +pub struct RegistrationRequest { + pub mode: RegistrationMode, + pub credential: EcashCredential, + pub gateway_identity: String, +} + +pub enum RegistrationMode { + Dvpn { + wg_public_key: [u8; 32], + }, + Mixnet { + client_id: String, + mix_address: Option, + }, +} +``` + +**Encryption:** +- Encrypted using Noise transport mode +- Includes 16-byte Poly1305 authentication tag +- Replay protection via LP counter + +### 7. Credential Verification + +Gateway verifies the e-cash credential: + +```rust +// Gateway-side verification +pub async fn verify_credential( + &self, + credential: &EcashCredential, +) -> Result { + // 1. Check credential signature (BLS12-381) + verify_blinded_signature(&credential.signature)?; + + // 2. Check credential not already spent (nullifier check) + if self.storage.is_nullifier_spent(&credential.nullifier).await? { + return Err(CredentialError::AlreadySpent); + } + + // 3. Extract bandwidth allocation + let bandwidth_bytes = credential.bandwidth_value; + + // 4. Mark nullifier as spent + self.storage.mark_nullifier_spent(&credential.nullifier).await?; + + Ok(VerifiedCredential { + bandwidth_bytes, + expiry: credential.expiry, + }) +} +``` + +**For dVPN Mode:** +```rust +let peer_config = WireguardPeerConfig { + public_key: request.wg_public_key, + allowed_ips: vec!["10.0.0.0/8"], + bandwidth_limit: verified.bandwidth_bytes, +}; +self.wg_controller.add_peer(peer_config).await?; +``` + +### 8. Registration Response + +```rust +pub enum RegistrationResponse { + Success { + bandwidth_allocated: u64, + expiry: u64, + gateway_data: GatewayData, + }, + Error { + code: ErrorCode, + message: String, + }, +} + +pub enum ErrorCode { + InvalidCredential = 1, + CredentialExpired = 2, + CredentialAlreadyUsed = 3, + InsufficientBandwidth = 4, + WireguardPeerRegistrationFailed = 5, + InternalError = 99, +} +``` + +## State Machine and Security Protocol + +### Protocol Components + +The Lewes Protocol combines three cryptographic protocols for secure, post-quantum resistant communication: + +1. **KKT (KEM Key Transfer)** - Dynamically fetches responder's KEM public key with Ed25519 authentication +2. **PSQ (Post-Quantum Secure PSK)** - Derives PSK using KEM-based protocol for HNDL resistance +3. **Noise XKpsk3** - Provides encrypted transport with mutual authentication and forward secrecy + +### State Machine + +The LP state machine orchestrates the complete protocol flow from connection to encrypted transport: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ LEWES PROTOCOL STATE MACHINE │ +└─────────────────────────────────────────────────────────────────────┘ + + ┌──────────────────┐ + │ ReadyToHandshake │ + │ │ + │ • Keys loaded │ + │ • Session ID set │ + └────────┬─────────┘ + │ + StartHandshake input + │ + ▼ + ┌───────────────────────────────────────┐ + │ KKTExchange │ + │ │ + │ Initiator: │ + │ 1. Send KKT request (signed) │ + │ 2. Receive KKT response │ + │ 3. Validate Ed25519 signature │ + │ 4. Extract KEM public key │ + │ │ + │ Responder: │ + │ 1. Wait for KKT request │ + │ 2. Validate signature │ + │ 3. Send signed KEM key │ + └───────────────┬───────────────────────┘ + │ + KKT Complete + │ + ▼ + ┌───────────────────────────────────────┐ + │ Handshaking │ + │ │ + │ PSQ Protocol: │ + │ 1. Initiator encapsulates PSK │ + │ (embedded in Noise msg 1) │ + │ 2. Responder decapsulates PSK │ + │ (sends ctxt_B in Noise msg 2) │ + │ 3. Both derive final PSK: │ + │ KDF(ECDH || KEM_shared) │ + │ │ + │ Noise XKpsk3 Handshake: │ + │ → msg 1: e, es, ss + PSQ payload │ + │ ← msg 2: e, ee, se + ctxt_B │ + │ → msg 3: s, se (handshake complete) │ + └───────────────┬───────────────────────┘ + │ + Handshake Complete + │ + ▼ + ┌───────────────────────────────────────┐ + │ Transport │ + │ │ + │ • Encrypted data transfer │ + │ • AEAD with ChaCha20-Poly1305 │ + │ • Replay protection (counters) │ + │ • Bidirectional communication │ + └───────────────┬───────────────────────┘ + │ + Close input + │ + ▼ + ┌──────────┐ + │ Closed │ + │ │ + │ • Reason │ + └──────────┘ +``` + +### Message Sequence + +Complete protocol flow from connection to encrypted transport: + +``` +Initiator Responder + │ │ + │ ════════════════ KKT EXCHANGE ════════════════ │ + │ │ + │ KKTRequest (signed with Ed25519) │ + ├──────────────────────────────────────────────────────────>│ + │ │ Validate + │ │ signature + │ KKTResponse (signed KEM key + hash) │ + │<──────────────────────────────────────────────────────────┤ + │ │ + │ Validate signature │ + │ Extract kem_pk │ + │ │ + │ ══════════════ PSQ + NOISE HANDSHAKE ══════════════ │ + │ │ + │ Noise msg 1: e, es, ss │ + │ + PSQ InitiatorMsg (KEM encapsulation) │ + ├──────────────────────────────────────────────────────────>│ + │ │ + │ │ PSQ: Decapsulate + │ │ Derive PSK + │ │ Inject into Noise + │ Noise msg 2: e, ee, se │ + │ + ctxt_B (encrypted PSK) │ + │<──────────────────────────────────────────────────────────┤ + │ │ + │ Extract ctxt_B │ + │ Store for re-registration │ + │ Inject PSK into Noise │ + │ │ + │ Noise msg 3: s, se │ + ├──────────────────────────────────────────────────────────>│ + │ │ + │ Handshake Complete ✓ │ Handshake Complete ✓ + │ Transport mode active │ Transport mode active + │ │ + │ ═══════════════ TRANSPORT MODE ═══════════════ │ + │ │ + │ EncryptedData (AEAD, counter N) │ + ├──────────────────────────────────────────────────────────>│ + │ │ + │ EncryptedData (counter M) │ + │<──────────────────────────────────────────────────────────┤ + │ │ + │ (bidirectional encrypted communication) │ + │◄──────────────────────────────────────────────────────────► + │ │ +``` + +### KKT (KEM Key Transfer) Protocol + +**Purpose**: Securely obtain responder's KEM public key before PSQ can begin. + +**Key Features**: +- Ed25519 signatures for authentication (both request and response signed) +- Optional hash validation for key pinning (future directory service integration) +- Currently signature-only mode (deployable without infrastructure) +- Easy upgrade path to hash-based key pinning + +**Initiator Flow**: +```rust +1. Generate KKT request with Ed25519 signature +2. Send KKTRequest to responder +3. Receive KKTResponse with signed KEM key +4. Validate Ed25519 signature +5. (Optional) Validate key hash against directory +6. Store KEM key for PSQ encapsulation +``` + +**Responder Flow**: +```rust +1. Receive KKTRequest from initiator +2. Validate initiator's Ed25519 signature +3. Generate KKTResponse with: + - Responder's KEM public key + - Ed25519 signature over (key || timestamp) + - Blake3 hash of KEM key +4. Send KKTResponse to initiator +``` + +### PSQ (Post-Quantum Secure PSK) Protocol + +**Purpose**: Derive a post-quantum secure PSK for Noise protocol. + +**Security Properties**: +- **HNDL resistance**: PSK derived from KEM-based protocol +- **Forward secrecy**: Ephemeral KEM keypair per session +- **Authentication**: Ed25519 signatures prevent MitM +- **Algorithm agility**: Easy upgrade from X25519 to ML-KEM + +**PSK Derivation**: +``` +Classical ECDH: + ecdh_secret = X25519_DH(local_private, remote_public) + +KEM Encapsulation (Initiator): + (kem_shared_secret, ciphertext) = KEM.Encap(responder_kem_pk) + +KEM Decapsulation (Responder): + kem_shared_secret = KEM.Decap(kem_private, ciphertext) + +Final PSK: + combined = ecdh_secret || kem_shared_secret || salt + psk = Blake3_KDF("nym-lp-psk-psq-v1", combined) +``` + +**Integration with Noise**: +- PSQ payload embedded in first Noise message (no extra round-trip) +- Responder sends encrypted PSK handle (ctxt_B) in second Noise message +- Both sides inject derived PSK before completing Noise handshake +- Noise validates PSK correctness during handshake + +**PSK Handle (ctxt_B)**: +The responder's encrypted PSK handle allows future re-registration without repeating PSQ: +- Encrypted with responder's long-term key +- Can be presented in future sessions +- Enables fast re-registration for returning clients + +### Security Guarantees + +**Achieved Properties**: +- ✅ **Mutual authentication**: Ed25519 signatures in KKT and PSQ +- ✅ **Forward secrecy**: Ephemeral keys in Noise handshake +- ✅ **Post-quantum PSK**: KEM-based PSK derivation +- ✅ **HNDL resistance**: PSK safe even if private keys compromised later +- ✅ **Replay protection**: Monotonic counters with sliding window +- ✅ **Key confirmation**: Noise handshake validates PSK correctness + +**Implementation Status**: +- 🔄 **Key pinning**: Hash validation via directory service (signature-only for now) +- 🔄 **ML-KEM support**: Easy config upgrade from X25519 to ML-KEM-768 +- 🔄 **PSK re-use**: ctxt_B handle stored for future re-registration + +### Algorithm Choices + +**Current (Testing/Development)**: +- KEM: X25519 (DHKEM) - Classical ECDH, widely tested +- Hash: Blake3 - Fast, secure, parallel +- Signature: Ed25519 - Fast verification, compact +- AEAD: ChaCha20-Poly1305 - Fast, constant-time + +**Future (Production)**: +- KEM: ML-KEM-768 - NIST-approved post-quantum KEM +- Hash: Blake3 - No change needed +- Signature: Ed25519 - No change needed (or upgrade to ML-DSA) +- AEAD: ChaCha20-Poly1305 - No change needed + +**Migration Path**: +```toml +# Current deployment +[lp.crypto] +kem_algorithm = "x25519" + +# Future upgrade (config change only) +[lp.crypto] +kem_algorithm = "ml-kem-768" +``` + +### Message Types + +**KKT Messages**: +```rust +// Message Type 0x0004 +struct KKTRequest { + timestamp: u64, // Unix timestamp (replay protection) + initiator_ed25519_pk: [u8; 32], // Initiator's public key + signature: [u8; 64], // Ed25519 signature +} + +// Message Type 0x0005 +struct KKTResponse { + kem_pk: Vec, // Responder's KEM public key + key_hash: [u8; 32], // Blake3 hash of KEM key + timestamp: u64, // Unix timestamp + signature: [u8; 64], // Ed25519 signature +} +``` + +**PSQ Embedding**: +- PSQ InitiatorMsg embedded in Noise message 1 payload (after 'e, es, ss') +- PSQ ResponderMsg (ctxt_B) embedded in Noise message 2 payload (after 'e, ee, se') +- No additional round-trips beyond standard 3-message Noise handshake + +## KCP Protocol Details + +### KCP Configuration + +From `common/nym-kcp/src/session.rs`: + +```rust +pub struct KcpSession { + conv: u32, // Conversation ID + mtu: usize, // Default: 1400 bytes + snd_wnd: u16, // Send window: 128 segments + rcv_wnd: u16, // Receive window: 128 segments + rx_minrto: u32, // Minimum RTO: 100ms (configurable) +} +``` + +### KCP Packet Format + +``` +┌────────────────────────────────────────────────┐ +│ Conv ID (4 bytes) - Conversation identifier │ +├────────────────────────────────────────────────┤ +│ Cmd (1 byte) - PSH/ACK/WND/ERR │ +├────────────────────────────────────────────────┤ +│ Frg (1 byte) - Fragment number (reverse order) │ +├────────────────────────────────────────────────┤ +│ Wnd (2 bytes) - Receive window size │ +├────────────────────────────────────────────────┤ +│ Timestamp (4 bytes) - Send timestamp │ +├────────────────────────────────────────────────┤ +│ Sequence Number (4 bytes) - Packet sequence │ +├────────────────────────────────────────────────┤ +│ UNA (4 bytes) - Unacknowledged sequence │ +├────────────────────────────────────────────────┤ +│ Length (4 bytes) - Data length │ +├────────────────────────────────────────────────┤ +│ Data (variable) - Payload │ +└────────────────────────────────────────────────┘ +``` + +**Total header**: 24 bytes + +### KCP Features + +**Reliability Mechanisms:** +- **Sequence Numbers (sn)**: Track packet ordering +- **Fragment Numbers (frg)**: Handle message fragmentation +- **UNA (Unacknowledged)**: Cumulative ACK up to this sequence +- **Selective ACK**: Via individual ACK packets +- **Fast Retransmit**: Triggered by duplicate ACKs (configurable threshold) +- **RTO Calculation**: Smoothed RTT with variance + +## LP Packet Format + +### LP Header + +``` +┌────────────────────────────────────────────────┐ +│ Protocol Version (1 byte) - Currently: 1 │ +├────────────────────────────────────────────────┤ +│ Session ID (4 bytes) - LP session identifier │ +├────────────────────────────────────────────────┤ +│ Counter (8 bytes) - Replay protection counter │ +└────────────────────────────────────────────────┘ +``` + +**Total header**: 13 bytes + +### LP Message Types + +```rust +pub enum LpMessage { + Handshake(Vec), + EncryptedData(Vec), + ClientHello { + client_lp_public: [u8; 32], + salt: [u8; 32], + timestamp: u64, + }, + Busy, +} +``` + +### Complete Packet Structure + +``` +┌─────────────────────────────────────┐ +│ LP Header (13 bytes) │ +│ - Version, Session ID, Counter │ +├─────────────────────────────────────┤ +│ LP Message (variable) │ +│ - Type tag (1 byte) │ +│ - Message data │ +├─────────────────────────────────────┤ +│ Trailer (16 bytes) │ +│ - Reserved for future MAC/tag │ +└─────────────────────────────────────┘ +``` + +## Security Properties + +### Threat Model + +**Protected Against:** +- ✅ **Passive eavesdropping**: Noise encryption (ChaCha20-Poly1305) +- ✅ **Active MITM**: Mutual authentication via static keys + PSK +- ✅ **Replay attacks**: Counter-based validation with 1024-packet window +- ✅ **Packet injection**: Poly1305 authentication tags +- ✅ **Timestamp replay**: 30-second window for ClientHello timestamps (configurable) +- ✅ **DoS (connection flood)**: Connection limit (default: 10,000, configurable) +- ✅ **Credential reuse**: Nullifier tracking in database + +**Not Protected Against:** +- ❌ **Network-level traffic analysis**: LP is not anonymous (use mixnet for that) +- ❌ **Gateway compromise**: Gateway sees client registration data +- ⚠️ **Per-IP DoS**: No per-IP rate limiting (global limit only) + +### Cryptographic Primitives + +| Component | Algorithm | Key Size | Source | +|-----------|-----------|----------|--------| +| Key Exchange | X25519 | 256 bits | RustCrypto | +| Encryption | ChaCha20 | 256 bits | RustCrypto | +| Authentication | Poly1305 | 256 bits | RustCrypto | +| KDF | Blake3 | 256 bits | nym_crypto | +| Hash (Noise) | SHA-256 | 256 bits | snow crate | +| Signature (E-cash) | BLS12-381 | 381 bits | E-cash contract | + +### Forward Secrecy + +Noise XKpsk3 provides forward secrecy through ephemeral keys: + +1. **Initial handshake**: Uses ephemeral + static keys +2. **Key compromise scenario**: + - Compromise of **static key**: Past sessions remain secure (ephemeral keys destroyed) + - Compromise of **PSK**: Attacker needs static key too (two-factor security) + - Compromise of **both**: Only future sessions affected, not past + +3. **Session key lifetime**: Destroyed after single registration completes + +### Timing Attack Resistance + +**Constant-time operations:** +- ✅ Replay protection check (branchless) +- ✅ Bitmap bit operations (branchless) +- ✅ Noise crypto operations (via snow/RustCrypto) + +**Variable-time operations:** +- ⚠️ Credential verification (database lookup time varies) +- ⚠️ WireGuard peer registration (filesystem operations) + +## Configuration + +### Gateway Configuration + +From `gateway/src/node/lp_listener/mod.rs:78`: + +```toml +[lp] +# Enable/disable LP listener +enabled = true + +# Bind address +bind_address = "0.0.0.0" + +# Control port (for LP handshake and registration) +control_port = 41264 + +# Data port (reserved for future use) +data_port = 51264 + +# Maximum concurrent connections +max_connections = 10000 + +# Timestamp validation window (seconds) +# ClientHello messages older than this are rejected +timestamp_tolerance_secs = 30 + +# Use mock e-cash verifier (TESTING ONLY!) +use_mock_ecash = false +``` + +### Firewall Rules + +**Required inbound rules:** +```bash +# Allow TCP connections to LP control port +iptables -A INPUT -p tcp --dport 41264 -j ACCEPT + +# Optional: Rate limiting +iptables -A INPUT -p tcp --dport 41264 -m state --state NEW \ + -m recent --set --name LP_LIMIT +iptables -A INPUT -p tcp --dport 41264 -m state --state NEW \ + -m recent --update --seconds 60 --hitcount 100 --name LP_LIMIT \ + -j DROP +``` + +## Metrics + +From `gateway/src/node/lp_listener/mod.rs:4`: + +**Connection Metrics:** +- `active_lp_connections`: Gauge tracking current active LP connections +- `lp_connections_total`: Counter for total LP connections handled +- `lp_connection_duration_seconds`: Histogram of connection durations +- `lp_connections_completed_gracefully`: Counter for successful completions +- `lp_connections_completed_with_error`: Counter for error terminations + +**Handshake Metrics:** +- `lp_handshakes_success`: Counter for successful handshakes +- `lp_handshakes_failed`: Counter for failed handshakes +- `lp_handshake_duration_seconds`: Histogram of handshake durations +- `lp_client_hello_failed`: Counter for ClientHello failures + +**Registration Metrics:** +- `lp_registration_attempts_total`: Counter for all registration attempts +- `lp_registration_success_total`: Counter for successful registrations +- `lp_registration_failed_total`: Counter for failed registrations +- `lp_registration_duration_seconds`: Histogram of registration durations + +**Mode-Specific:** +- `lp_registration_dvpn_attempts/success/failed`: dVPN mode counters +- `lp_registration_mixnet_attempts/success/failed`: Mixnet mode counters + +**Credential Metrics:** +- `lp_credential_verification_attempts/success/failed`: Verification counters +- `lp_bandwidth_allocated_bytes_total`: Total bandwidth allocated + +**Error Metrics:** +- `lp_errors_handshake`: Handshake errors +- `lp_errors_timestamp_too_old/too_far_future`: Timestamp validation errors +- `lp_errors_wg_peer_registration`: WireGuard peer registration failures + +## Error Codes + +### Handshake Errors + +| Error | Description | +|-------|-------------| +| `NOISE_DECRYPT_ERROR` | Invalid ciphertext or wrong keys | +| `NOISE_PROTOCOL_ERROR` | Unexpected message or state | +| `REPLAY_DUPLICATE` | Counter already seen | +| `REPLAY_OUT_OF_WINDOW` | Counter outside 1024-packet window | +| `TIMESTAMP_TOO_OLD` | ClientHello > configured tolerance | +| `TIMESTAMP_FUTURE` | ClientHello from future | + +### Registration Errors + +| Code | Name | Description | +|------|------|-------------| +| `CREDENTIAL_INVALID` | Invalid credential | Signature verification failed | +| `CREDENTIAL_EXPIRED` | Credential expired | Past expiry timestamp | +| `CREDENTIAL_SPENT` | Already used | Nullifier already in database | +| `INSUFFICIENT_BANDWIDTH` | Not enough bandwidth | Requested > credential value | +| `WIREGUARD_FAILED` | Peer registration failed | Kernel error adding WireGuard peer | + +## Limitations + +### Current Limitations + +1. **No persistent sessions**: Each registration is independent +2. **Single registration per session**: Connection closes after registration +3. **No streaming**: Protocol is request-response only +4. **No gateway discovery**: Client must know gateway's LP public key beforehand +5. **No version negotiation**: Protocol version fixed at 1 +6. **No per-IP rate limiting**: Only global connection limit + +### Testing Gaps + +1. **No end-to-end integration tests**: Unit tests exist, integration tests pending +2. **No performance benchmarks**: Latency/throughput not measured +3. **No load testing**: Concurrent connection limits not stress-tested +4. **No security audit**: Cryptographic implementation not externally reviewed + +## References + +### Specifications + +- **Noise Protocol Framework**: https://noiseprotocol.org/noise.html +- **XKpsk3 Pattern**: https://noiseexplorer.com/patterns/XKpsk3/ +- **KCP Protocol**: https://github.com/skywind3000/kcp +- **Blake3**: https://github.com/BLAKE3-team/BLAKE3-specs + +### Implementations + +- **snow**: Rust Noise protocol implementation +- **RustCrypto**: Cryptographic primitives (ChaCha20-Poly1305, X25519) +- **tokio**: Async runtime for network I/O + +### Security Audits + +- [ ] Noise implementation audit (pending) +- [ ] Replay protection audit (pending) +- [ ] E-cash integration audit (pending) +- [ ] Penetration testing (pending) + +## Changelog + +### Version 1.1 (Post-Quantum PSK with KKT) + +**Implemented:** +- KKTExchange state in state machine for pre-handshake KEM key transfer +- PSQ (Post-Quantum Secure PSK) protocol integration +- KKT (KEM Key Transfer) protocol with Ed25519 authentication +- Optional hash validation for KEM key pinning (signature-only mode active) +- PSK handle (ctxt_B) storage for future re-registration +- X25519 DHKEM support (ready for ML-KEM upgrade) +- Comprehensive state machine tests (7 test cases) +- generate_fresh_salt() utility for session creation + +**Security Improvements:** +- Post-quantum PSK derivation (KEM-based) +- HNDL (Harvest Now, Decrypt Later) resistance +- Mutual authentication via Ed25519 signatures +- Easy migration path to ML-KEM-768 + +**Architecture:** +- State flow: ReadyToHandshake → KKTExchange → Handshaking → Transport +- PSQ embedded in Noise handshake (no extra round-trip) +- Automatic KKT on StartHandshake (no manual key distribution) + +**Related Issues:** +- nym-4za: Add KKTExchange state to LpStateMachine + +### Version 1.0 (Initial Implementation) + +**Implemented:** +- Noise XKpsk3 handshake +- KCP reliability layer +- Replay protection (1024-packet window with SIMD) +- PSK derivation (ECDH + Blake3) +- dVPN and Mixnet registration modes +- E-cash credential verification +- WireGuard peer management +- Prometheus metrics +- DoS protection (connection limits, timestamp validation) + +**Pending:** +- End-to-end integration tests +- Performance benchmarks +- Security audit +- Client implementation +- Gateway probe support +- Per-IP rate limiting diff --git a/gateway/src/error.rs b/gateway/src/error.rs index dd62de82c7..0e6d4f73bc 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -146,6 +146,15 @@ pub enum GatewayError { address: String, source: Box, }, + + #[error("Failed to parse ip address: {source}")] + IpAddrParseError { + #[from] + source: defguard_wireguard_rs::net::IpAddrParseError, + }, + + #[error("Invalid SystemTime: {0}")] + InvalidSystemTime(#[from] std::time::SystemTimeError), } impl From for GatewayError { diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index 36f57be1f6..3fd487e76a 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -6,10 +6,7 @@ use super::messages::{LpRegistrationRequest, LpRegistrationResponse}; use super::registration::process_registration; use super::LpHandlerState; use crate::error::GatewayError; -use nym_lp::{ - keypair::{Keypair, PrivateKey as LpPrivateKey, PublicKey}, - LpMessage, LpPacket, LpSession, -}; +use nym_lp::{keypair::PublicKey, LpMessage, LpPacket, LpSession}; use nym_metrics::{add_histogram_obs, inc}; use std::net::SocketAddr; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -104,36 +101,14 @@ impl LpConnectionHandler { // Track total LP connections handled inc!("lp_connections_total"); - // For LP, we need: - // 1. Gateway's keypair (from local_identity) - // 2. Client's public key (will be received during handshake) - // 3. PSK (pre-shared key) - for now use a placeholder - - // Derive LP keypair from gateway's ed25519 identity using proper conversion - // This creates a valid x25519 keypair for ECDH operations in Noise protocol - let x25519_private = self.state.local_identity.private_key().to_x25519(); - let x25519_public = self - .state - .local_identity - .public_key() - .to_x25519() - .map_err(|e| { - GatewayError::LpHandshakeError(format!( - "Failed to convert ed25519 public key to x25519: {}", - e - )) - })?; - - let lp_private = LpPrivateKey::from_bytes(x25519_private.as_bytes()); - let lp_public = PublicKey::from_bytes(x25519_public.as_bytes()).map_err(|e| { - GatewayError::LpHandshakeError(format!("Failed to create LP public key: {}", e)) - })?; - - let gateway_keypair = Keypair::from_keys(lp_private, lp_public); + // The state machine now accepts only Ed25519 keys and internally derives X25519 keys. + // This simplifies the API by removing manual key conversion from the caller. + // Gateway's Ed25519 identity is used for both PSQ authentication and X25519 derivation. // Receive client's public key and salt via ClientHello message // The client initiates by sending ClientHello as first packet - let (client_pubkey, salt) = match self.receive_client_hello().await { + let (_client_pubkey, client_ed25519_pubkey, salt) = match self.receive_client_hello().await + { Ok(result) => result, Err(e) => { // Track ClientHello failures (timestamp validation, protocol errors, etc.) @@ -144,13 +119,16 @@ impl LpConnectionHandler { } }; - // Derive PSK using ECDH + Blake3 KDF (nym-109) - // Both client and gateway derive the same PSK from their respective keys - let psk = nym_lp::derive_psk(gateway_keypair.private_key(), &client_pubkey, &salt); - tracing::trace!("Derived PSK from LP keys and ClientHello salt"); - // Create LP handshake as responder - let handshake = LpGatewayHandshake::new_responder(&gateway_keypair, &client_pubkey, &psk)?; + // Pass Ed25519 keys directly - X25519 derivation and PSK generation happen internally + let handshake = LpGatewayHandshake::new_responder( + ( + self.state.local_identity.private_key(), + self.state.local_identity.public_key(), + ), + &client_ed25519_pubkey, + &salt, + )?; // Complete the LP handshake with duration tracking let handshake_start = std::time::Instant::now(); @@ -239,18 +217,9 @@ impl LpConnectionHandler { fn validate_timestamp(client_timestamp: u64, tolerance_secs: u64) -> Result<(), GatewayError> { use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - let age = if now >= client_timestamp { - now - client_timestamp - } else { - // Client timestamp is in the future - client_timestamp - now - }; + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let age = now.abs_diff(client_timestamp); if age > tolerance_secs { let direction = if now >= client_timestamp { "old" @@ -278,7 +247,16 @@ impl LpConnectionHandler { } /// Receive client's public key and salt via ClientHello message - async fn receive_client_hello(&mut self) -> Result<(PublicKey, [u8; 32]), GatewayError> { + async fn receive_client_hello( + &mut self, + ) -> Result< + ( + PublicKey, + nym_crypto::asymmetric::ed25519::PublicKey, + [u8; 32], + ), + GatewayError, + > { // Receive first packet which should be ClientHello let packet = self.receive_lp_packet().await?; @@ -294,25 +272,33 @@ impl LpConnectionHandler { timestamp, { use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); now.abs_diff(timestamp) }, self.state.lp_config.timestamp_tolerance_secs ); - // Convert bytes to PublicKey + // Convert bytes to X25519 PublicKey (for Noise protocol) let client_pubkey = PublicKey::from_bytes(&hello_data.client_lp_public_key) .map_err(|e| { GatewayError::LpProtocolError(format!("Invalid client public key: {}", e)) })?; + // Convert bytes to Ed25519 PublicKey (for PSQ authentication) + let client_ed25519_pubkey = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes( + &hello_data.client_ed25519_public_key, + ) + .map_err(|e| { + GatewayError::LpProtocolError(format!( + "Invalid client Ed25519 public key: {}", + e + )) + })?; + // Extract salt for PSK derivation let salt = hello_data.salt; - Ok((client_pubkey, salt)) + Ok((client_pubkey, client_ed25519_pubkey, salt)) } other => Err(GatewayError::LpProtocolError(format!( "Expected ClientHello, got {}", @@ -484,7 +470,6 @@ mod tests { use crate::node::ActiveClientsStore; use bytes::BytesMut; use nym_lp::codec::{parse_lp_packet, serialize_lp_packet}; - use nym_lp::keypair::Keypair; use nym_lp::message::{ClientHelloData, EncryptedDataPayload, HandshakeData, LpMessage}; use nym_lp::packet::{LpHeader, LpPacket}; use std::sync::Arc; @@ -530,7 +515,7 @@ mod tests { ) -> Result<(), std::io::Error> { let mut packet_buf = BytesMut::new(); serialize_lp_packet(packet, &mut packet_buf) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| std::io::Error::other(e.to_string()))?; // Write length prefix let len = packet_buf.len() as u32; @@ -557,8 +542,7 @@ mod tests { stream.read_exact(&mut packet_buf).await?; // Parse packet - parse_lp_packet(&packet_buf) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + parse_lp_packet(&packet_buf).map_err(|e| std::io::Error::other(e.to_string())) } // ==================== Existing Tests ==================== @@ -831,7 +815,9 @@ mod tests { assert_eq!(received.header().session_id, 200); assert_eq!(received.header().counter, 20); match received.message() { - LpMessage::EncryptedData(data) => assert_eq!(data, &EncryptedDataPayload(expected_payload)), + LpMessage::EncryptedData(data) => { + assert_eq!(data, &EncryptedDataPayload(expected_payload)) + } _ => panic!("Expected EncryptedData message"), } } @@ -845,7 +831,8 @@ mod tests { let addr = listener.local_addr().unwrap(); let client_key = [7u8; 32]; - let hello_data = ClientHelloData::new_with_fresh_salt(client_key); + let client_ed25519_key = [8u8; 32]; + let hello_data = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key); let expected_salt = hello_data.salt; // Clone salt before moving hello_data let server_task = tokio::spawn(async move { @@ -901,9 +888,17 @@ mod tests { let mut client_stream = TcpStream::connect(addr).await.unwrap(); // Create and send valid ClientHello - let client_keypair = Keypair::default(); - let hello_data = - ClientHelloData::new_with_fresh_salt(client_keypair.public_key().to_bytes()); + // Create separate Ed25519 keypair and derive X25519 from it (like production code) + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; + + let client_ed25519_keypair = ed25519::KeyPair::new(&mut OsRng); + let client_x25519_public = client_ed25519_keypair.public_key().to_x25519().unwrap(); + + let hello_data = ClientHelloData::new_with_fresh_salt( + client_x25519_public.to_bytes(), + client_ed25519_keypair.public_key().to_bytes(), + ); let packet = LpPacket::new( LpHeader { protocol_version: 1, @@ -919,10 +914,14 @@ mod tests { // Handler should receive and parse it let result = server_task.await.unwrap(); - assert!(result.is_ok()); + assert!(result.is_ok(), "Expected Ok, got: {:?}", result); - let (pubkey, salt) = result.unwrap(); - assert_eq!(pubkey.as_bytes(), &client_keypair.public_key().to_bytes()); + let (x25519_pubkey, ed25519_pubkey, salt) = result.unwrap(); + assert_eq!(x25519_pubkey.as_bytes(), &client_x25519_public.to_bytes()); + assert_eq!( + ed25519_pubkey.to_bytes(), + client_ed25519_keypair.public_key().to_bytes() + ); assert_eq!(salt, hello_data.salt); } @@ -944,9 +943,17 @@ mod tests { let mut client_stream = TcpStream::connect(addr).await.unwrap(); // Create ClientHello with old timestamp - let client_keypair = Keypair::default(); - let mut hello_data = - ClientHelloData::new_with_fresh_salt(client_keypair.public_key().to_bytes()); + // Use proper separate Ed25519 and X25519 keys (like production code) + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; + + let client_ed25519_keypair = ed25519::KeyPair::new(&mut OsRng); + let client_x25519_public = client_ed25519_keypair.public_key().to_x25519().unwrap(); + + let mut hello_data = ClientHelloData::new_with_fresh_salt( + client_x25519_public.to_bytes(), + client_ed25519_keypair.public_key().to_bytes(), + ); // Manually set timestamp to be very old (100 seconds ago) let old_timestamp = SystemTime::now() diff --git a/gateway/src/node/lp_listener/handshake.rs b/gateway/src/node/lp_listener/handshake.rs index 935b501303..2e79e83687 100644 --- a/gateway/src/node/lp_listener/handshake.rs +++ b/gateway/src/node/lp_listener/handshake.rs @@ -3,7 +3,6 @@ use crate::error::GatewayError; use nym_lp::{ - keypair::{Keypair, PublicKey}, state_machine::{LpAction, LpInput, LpStateMachine}, LpPacket, LpSession, }; @@ -18,16 +17,24 @@ pub struct LpGatewayHandshake { impl LpGatewayHandshake { /// Create a new responder (gateway side) handshake + /// + /// # Arguments + /// * `gateway_ed25519_keypair` - Gateway's Ed25519 identity keypair (for PSQ auth and X25519 derivation) + /// * `client_ed25519_public_key` - Client's Ed25519 public key (from ClientHello) + /// * `salt` - Salt from ClientHello (for PSK derivation) pub fn new_responder( - local_keypair: &Keypair, - remote_public_key: &PublicKey, - psk: &[u8; 32], + gateway_ed25519_keypair: ( + &nym_crypto::asymmetric::ed25519::PrivateKey, + &nym_crypto::asymmetric::ed25519::PublicKey, + ), + client_ed25519_public_key: &nym_crypto::asymmetric::ed25519::PublicKey, + salt: &[u8; 32], ) -> Result { let state_machine = LpStateMachine::new( false, // responder - local_keypair, - remote_public_key, - psk, + gateway_ed25519_keypair, + client_ed25519_public_key, + salt, ) .map_err(|e| { GatewayError::LpHandshakeError(format!("Failed to create state machine: {}", e)) diff --git a/gateway/src/node/lp_listener/registration.rs b/gateway/src/node/lp_listener/registration.rs index d1a8a80c23..2439721c8b 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/gateway/src/node/lp_listener/registration.rs @@ -19,9 +19,6 @@ use nym_gateway_storage::traits::BandwidthGatewayStorage; use nym_metrics::{add_histogram_obs, inc, inc_by}; use nym_registration_common::GatewayData; use nym_wireguard::PeerControlRequest; -use rand::RngCore; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::str::FromStr; use std::sync::Arc; use tracing::*; @@ -77,9 +74,7 @@ async fn credential_storage_preparation( .storage() .get_available_bandwidth(client_id) .await? - .ok_or_else(|| { - GatewayError::InternalError("bandwidth entry should exist".to_string()) - })?; + .ok_or_else(|| GatewayError::InternalError("bandwidth entry should exist".to_string()))?; Ok(bandwidth) } @@ -158,7 +153,6 @@ pub async fn process_registration( // Register as WireGuard peer first to get client_id let (gateway_data, client_id) = match register_wg_peer( request.wg_public_key.inner().as_ref(), - request.client_ip, request.ticket_type, state, ) @@ -223,7 +217,12 @@ pub async fn process_registration( inc!("lp_registration_mixnet_attempts"); // Generate i64 client_id from the [u8; 32] in the request - let client_id = i64::from_be_bytes(client_id_bytes[0..8].try_into().unwrap()); + #[allow(clippy::expect_used)] + let client_id = i64::from_be_bytes( + client_id_bytes[0..8] + .try_into() + .expect("This cannot fail, since the id is 32 bytes long"), + ); info!( "LP Mixnet registration for client_id {}, session {}", @@ -290,7 +289,6 @@ pub async fn process_registration( /// Register a WireGuard peer and return gateway data along with the client_id async fn register_wg_peer( public_key_bytes: &[u8], - client_ip: IpAddr, ticket_type: nym_credentials_interface::TicketType, state: &LpHandlerState, ) -> Result<(GatewayData, i64), GatewayError> { @@ -316,29 +314,47 @@ async fn register_wg_peer( key_bytes.copy_from_slice(public_key_bytes); let peer_key = Key::new(key_bytes); - // Allocate IP addresses for the client - // TODO: Proper IP pool management - for now use random in private range - inc!("wg_ip_allocation_attempts"); - let last_octet = { - let mut rng = rand::thread_rng(); - (rng.next_u32() % 254 + 1) as u8 - }; + // Allocate IPs from centralized pool managed by PeerController + let registration_data = nym_wireguard::PeerRegistrationData::new(peer_key.clone()); - let client_ipv4 = Ipv4Addr::new(10, 1, 0, last_octet); - let client_ipv6 = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, last_octet as u16); - inc!("wg_ip_allocation_success"); + // Request IP allocation from PeerController + let (tx, rx) = oneshot::channel(); + wg_controller + .send(PeerControlRequest::RegisterPeer { + registration_data, + response_tx: tx, + }) + .await + .map_err(|e| { + GatewayError::InternalError(format!("Failed to send IP allocation request: {}", e)) + })?; - // Create WireGuard peer + // Wait for IP allocation from pool + let ip_pair = rx + .await + .map_err(|e| { + GatewayError::InternalError(format!("Failed to receive IP allocation: {}", e)) + })? + .map_err(|e| { + error!("Failed to allocate IPs from pool: {}", e); + GatewayError::InternalError(format!("Failed to allocate IPs: {:?}", e)) + })?; + + let client_ipv4 = ip_pair.ipv4; + let client_ipv6 = ip_pair.ipv6; + + info!( + "Allocated IPs for peer {}: {} / {}", + peer_key, client_ipv4, client_ipv6 + ); + + // Create WireGuard peer with allocated IPs let mut peer = Peer::new(peer_key.clone()); peer.preshared_key = Some(Key::new(state.local_identity.public_key().to_bytes())); - peer.endpoint = Some( - format!("{}:51820", client_ip) - .parse() - .unwrap_or_else(|_| SocketAddr::from_str("0.0.0.0:51820").unwrap()), - ); + peer.endpoint = None; peer.allowed_ips = vec![ - format!("{client_ipv4}/32").parse().unwrap(), - format!("{client_ipv6}/128").parse().unwrap(), + format!("{client_ipv4}/32").parse()?, + format!("{client_ipv6}/128").parse()?, ]; peer.persistent_keepalive_interval = Some(25); @@ -357,7 +373,7 @@ async fn register_wg_peer( // This must happen BEFORE AddPeer because generate_bandwidth_manager() expects it to exist credential_storage_preparation(state.ecash_verifier.clone(), client_id).await?; - // Now send to WireGuard peer controller and track latency + // Now send peer to WireGuard controller and track latency let controller_start = std::time::Instant::now(); let (tx, rx) = oneshot::channel(); wg_controller diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 8e8fffcfcb..1edbdd3e24 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -225,7 +225,9 @@ impl GatewayTasksBuilder { info!("Using MockEcashManager for LP testing (credentials NOT verified)"); let mock_manager = MockEcashManager::new(Box::new(self.storage.clone())); return Ok(Arc::new(mock_manager) - as Arc); + as Arc< + dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync, + >); } // Production path: use real EcashManager with blockchain verification @@ -262,7 +264,9 @@ impl GatewayTasksBuilder { ); Ok(Arc::new(ecash_manager) - as Arc) + as Arc< + dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync, + >) } async fn ecash_manager( @@ -319,7 +323,10 @@ impl GatewayTasksBuilder { active_clients_store: ActiveClientsStore, ) -> Result { // Get WireGuard peer controller if available - let wg_peer_controller = self.wireguard_data.as_ref().map(|wg_data| wg_data.inner.peer_tx().clone()); + let wg_peer_controller = self + .wireguard_data + .as_ref() + .map(|wg_data| wg_data.inner.peer_tx().clone()); let handler_state = lp_listener::LpHandlerState { ecash_verifier: self.ecash_manager().await?, diff --git a/nym-gateway-probe/src/bandwidth_helpers.rs b/nym-gateway-probe/src/bandwidth_helpers.rs index ffb2becf63..1f0048d722 100644 --- a/nym-gateway-probe/src/bandwidth_helpers.rs +++ b/nym-gateway-probe/src/bandwidth_helpers.rs @@ -167,6 +167,7 @@ pub(crate) async fn acquire_bandwidth( /// /// This uses a pre-serialized test credential from the wireguard tests - since MockEcashManager /// doesn't verify anything, any valid CredentialSpendingData structure will work. +#[allow(clippy::expect_used)] // Test helper with hardcoded valid data pub(crate) fn create_dummy_credential( _gateway_identity: &[u8; 32], _ticket_type: TicketType, @@ -233,8 +234,8 @@ pub(crate) fn create_dummy_credential( 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, 0, 0, 0, 0, 0, 1, ]; diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 6349f03c90..5424386557 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -394,8 +394,10 @@ impl Probe { &NymNetworkDetails::new_from_env(), )?; let client = nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?; - let bw_controller = - nym_bandwidth_controller::BandwidthController::new(storage.credential_store().clone(), client); + let bw_controller = nym_bandwidth_controller::BandwidthController::new( + storage.credential_store().clone(), + client, + ); // Run LP registration probe let lp_outcome = lp_registration_probe( @@ -836,41 +838,24 @@ where St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static, ::StorageError: Send + Sync, { - use nym_lp::keypair::{Keypair as LpKeypair, PublicKey as LpPublicKey}; + use nym_crypto::asymmetric::ed25519; use nym_registration_client::LpRegistrationClient; - info!("Starting LP registration probe for gateway at {}", gateway_lp_address); + info!( + "Starting LP registration probe for gateway at {}", + gateway_lp_address + ); let mut lp_outcome = types::LpProbeResults::default(); - // Generate LP keypair for this connection - let client_lp_keypair = std::sync::Arc::new(LpKeypair::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)); - // Derive gateway LP public key from gateway identity using proper ed25519→x25519 conversion - let gateway_x25519_pub = match gateway_identity.to_x25519() { - Ok(key) => key, - Err(e) => { - let error_msg = format!("Failed to convert gateway ed25519 key to x25519: {}", e); - error!("{}", error_msg); - lp_outcome.error = Some(error_msg); - return Ok(lp_outcome); - } - }; - - let gateway_lp_key = match LpPublicKey::from_bytes(gateway_x25519_pub.as_bytes()) { - Ok(key) => key, - Err(e) => { - let error_msg = format!("Failed to create LP key from x25519 bytes: {}", e); - error!("{}", error_msg); - lp_outcome.error = Some(error_msg); - return Ok(lp_outcome); - } - }; - - // Create LP registration client + // Create LP registration client (uses Ed25519 keys directly, derives X25519 internally) let mut client = LpRegistrationClient::new_with_default_psk( - client_lp_keypair, - gateway_lp_key, + client_ed25519_keypair, + gateway_identity, gateway_lp_address, gateway_ip, ); @@ -913,7 +898,9 @@ where 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()) { + 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); @@ -932,12 +919,15 @@ where ticket_type, ); - match client.send_registration_request_with_credential( - &wg_keypair, - &gateway_ed25519_pubkey, - credential, - ticket_type, - ).await { + match client + .send_registration_request_with_credential( + &wg_keypair, + &gateway_ed25519_pubkey, + credential, + ticket_type, + ) + .await + { Ok(_) => { info!("LP registration request sent successfully with mock ecash"); } @@ -950,12 +940,15 @@ where } } else { info!("Using real bandwidth controller for LP registration"); - match client.send_registration_request( - &wg_keypair, - &gateway_ed25519_pubkey, - bandwidth_controller, - ticket_type, - ).await { + match client + .send_registration_request( + &wg_keypair, + &gateway_ed25519_pubkey, + bandwidth_controller, + ticket_type, + ) + .await + { Ok(_) => { info!("LP registration request sent successfully with real ecash"); } diff --git a/nym-gateway-probe/src/main.rs b/nym-gateway-probe/src/main.rs index d2237d4328..d5ebc4cdd5 100644 --- a/nym-gateway-probe/src/main.rs +++ b/nym-gateway-probe/src/main.rs @@ -15,6 +15,7 @@ client_defaults!( #[cfg(unix)] #[tokio::main] +#[allow(clippy::exit)] // Intentional exit on error for CLI tool async fn main() -> anyhow::Result<()> { match run::run().await { Ok(ref result) => { @@ -31,6 +32,7 @@ async fn main() -> anyhow::Result<()> { #[cfg(not(unix))] #[tokio::main] +#[allow(clippy::exit)] // Intentional exit for unsupported platform async fn main() -> anyhow::Result<()> { eprintln!("This tool is only supported on Unix systems"); std::process::exit(1) diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index cd67c724bb..84128b15b3 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -4,9 +4,9 @@ use crate::TestedNodeDetails; use anyhow::{Context, anyhow, bail}; use nym_api_requests::models::{ - AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, - IpPacketRouterDetails, NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, - WebSockets, WireguardDetails, + AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, IpPacketRouterDetails, + NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, WebSockets, + WireguardDetails, }; use nym_authenticator_requests::AuthenticatorVersion; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -166,8 +166,8 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result anyhow::Result = wireguard_result.map(|wg| WireguardDetails { - port: wg.port, - tunnel_port: wg.tunnel_port, - metadata_port: wg.metadata_port, - public_key: wg.public_key, - }); + #[allow(deprecated)] + let wireguard: Option = + wireguard_result.map(|wg| WireguardDetails { + port: wg.tunnel_port, // Use tunnel_port for deprecated port field + tunnel_port: wg.tunnel_port, + metadata_port: wg.metadata_port, + public_key: wg.public_key, + }); // Construct NymNodeData let node_data = NymNodeData { diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index c76b45c96c..f293733d2e 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -4,7 +4,7 @@ use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; -use nym_gateway_probe::nodes::{query_gateway_by_ip, NymApiDirectory}; +use nym_gateway_probe::nodes::{NymApiDirectory, query_gateway_by_ip}; use nym_gateway_probe::{CredentialArgs, NetstackArgs, ProbeResult, TestedNode}; use nym_sdk::mixnet::NodeIdentity; use std::path::Path; @@ -136,10 +136,7 @@ pub(crate) async fn run() -> anyhow::Result { // Still create the directory for potential secondary lookups, // but only if API URL is available - let directory = if let Some(api_url) = network - .endpoints - .first() - .and_then(|ep| ep.api_url()) + let directory = if let Some(api_url) = network.endpoints.first().and_then(|ep| ep.api_url()) { Some(NymApiDirectory::new(api_url).await?) } else { diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index af73e92b06..6816c1b1e2 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -481,8 +481,8 @@ mod tests { let builder = BuilderConfig::builder(); // Verify the builder returns itself for chaining - let builder = builder.two_hops(true); - let builder = builder.two_hops(false); + let builder = builder.data_path(None); + let builder = builder.data_path(Some("/tmp/test".into())); let builder = builder.data_path(None); // Builder should still fail because required fields are missing diff --git a/nym-registration-client/src/builder/mod.rs b/nym-registration-client/src/builder/mod.rs index 7b90ee8ead..b40afc90fc 100644 --- a/nym-registration-client/src/builder/mod.rs +++ b/nym-registration-client/src/builder/mod.rs @@ -12,10 +12,14 @@ use nym_validator_client::{ QueryHttpRpcNyxdClient, nyxd::{Config as NyxdClientConfig, NyxdClient}, }; +use std::time::Duration; use crate::{RegistrationClient, config::RegistrationClientConfig, error::RegistrationClientError}; use config::BuilderConfig; +/// Timeout for mixnet client startup and connection +const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); + pub(crate) mod config; pub struct RegistrationClientBuilder { @@ -46,7 +50,7 @@ impl RegistrationClientBuilder { let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage) .event_tx(EventSender(event_tx)); let mixnet_client = tokio::time::timeout( - self.config.mixnet_client_startup_timeout, + MIXNET_CLIENT_STARTUP_TIMEOUT, self.config.build_and_connect_mixnet_client(builder), ) .await??; @@ -56,7 +60,7 @@ impl RegistrationClientBuilder { } else { let builder = MixnetClientBuilder::new_ephemeral().event_tx(EventSender(event_tx)); let mixnet_client = tokio::time::timeout( - self.config.mixnet_client_startup_timeout, + MIXNET_CLIENT_STARTUP_TIMEOUT, self.config.build_and_connect_mixnet_client(builder), ) .await??; diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index ab4c79e3de..05d5ceeb7d 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -7,7 +7,6 @@ use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_bandwidth_controller::BandwidthTicketProvider; use nym_credentials_interface::TicketType; use nym_ip_packet_client::IprClientConnect; -use nym_lp::keypair::{Keypair as LpKeypair, PublicKey as LpPublicKey}; use nym_registration_common::AssignedAddresses; use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient}; use std::sync::Arc; @@ -53,8 +52,7 @@ impl RegistrationClient { node_id: self.config.exit.node.identity.to_base58_string(), }, )?; - let mut ipr_client = - IprClientConnect::new(self.mixnet_client, self.cancel_token.clone()).await; + let mut ipr_client = IprClientConnect::new(self.mixnet_client, self.cancel_token.clone()); let interface_addresses = ipr_client .connect(ipr_address) .await @@ -125,22 +123,22 @@ impl RegistrationClient { let (entry, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await; - let entry = - entry.map_err( - |source| RegistrationClientError::EntryGatewayRegisterWireguard { - gateway_id: self.config.entry.node.identity.to_base58_string(), - authenticator_address: Box::new(entry_auth_address), - source: Box::new(source), - }, - )?; - let exit = - exit.map_err( - |source| RegistrationClientError::ExitGatewayRegisterWireguard { - gateway_id: self.config.exit.node.identity.to_base58_string(), - authenticator_address: Box::new(exit_auth_address), - source: Box::new(source), - }, - )?; + let entry = entry.map_err(|source| { + RegistrationClientError::from_authenticator_error( + source, + self.config.entry.node.identity.to_base58_string(), + entry_auth_address, + true, // is entry + ) + })?; + let exit = exit.map_err(|source| { + RegistrationClientError::from_authenticator_error( + source, + self.config.exit.node.identity.to_base58_string(), + exit_auth_address, + false, // is exit (not entry) + ) + })?; Ok(RegistrationResult::Wireguard(Box::new( WireguardRegistrationResult { @@ -171,49 +169,12 @@ impl RegistrationClient { tracing::debug!("Entry gateway LP address: {}", entry_lp_address); tracing::debug!("Exit gateway LP address: {}", exit_lp_address); - // Convert gateway ed25519 identities to x25519 LP public keys using proper conversion - let entry_x25519_pub = self.config.entry.node.identity.to_x25519().map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: failed to convert ed25519 to x25519: {}", - self.config.entry.node.identity.to_base58_string(), - e - ), - } - })?; - - let entry_gateway_lp_key = LpPublicKey::from_bytes(entry_x25519_pub.as_bytes()).map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: invalid LP key: {}", - self.config.entry.node.identity.to_base58_string(), - e - ), - } - })?; - - let exit_x25519_pub = self.config.exit.node.identity.to_x25519().map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: failed to convert ed25519 to x25519: {}", - self.config.exit.node.identity.to_base58_string(), - e - ), - } - })?; - - let exit_gateway_lp_key = LpPublicKey::from_bytes(exit_x25519_pub.as_bytes()).map_err(|e| { - RegistrationClientError::LpRegistrationNotPossible { - node_id: format!( - "{}: invalid LP key: {}", - self.config.exit.node.identity.to_base58_string(), - e - ), - } - })?; - - // Generate LP keypairs for this connection - let client_lp_keypair = Arc::new(LpKeypair::default()); + // Generate fresh Ed25519 keypairs for LP registration + // These are ephemeral and used only for the LP handshake protocol + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; + let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); + let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); // Register entry gateway via LP let entry_fut = { @@ -221,12 +182,12 @@ impl RegistrationClient { let entry_keys = self.config.entry.keys.clone(); let entry_identity = self.config.entry.node.identity; let entry_ip = self.config.entry.node.ip_address; - let lp_keypair = client_lp_keypair.clone(); + let entry_lp_keys = entry_lp_keypair.clone(); async move { let mut client = LpRegistrationClient::new_with_default_psk( - lp_keypair, - entry_gateway_lp_key, + entry_lp_keys, + entry_identity, entry_lp_address, entry_ip, ); @@ -263,12 +224,12 @@ impl RegistrationClient { let exit_keys = self.config.exit.keys.clone(); let exit_identity = self.config.exit.node.identity; let exit_ip = self.config.exit.node.ip_address; - let lp_keypair = client_lp_keypair; + let exit_lp_keys = exit_lp_keypair; async move { let mut client = LpRegistrationClient::new_with_default_psk( - lp_keypair, - exit_gateway_lp_key, + exit_lp_keys, + exit_identity, exit_lp_address, exit_ip, ); diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index 6f1e665d8e..e541a58766 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -12,7 +12,6 @@ use nym_credentials_interface::{CredentialSpendingData, TicketType}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::LpPacket; use nym_lp::codec::{parse_lp_packet, serialize_lp_packet}; -use nym_lp::keypair::{Keypair, PublicKey}; use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine}; use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse}; use nym_wireguard_types::PeerPublicKey; @@ -41,11 +40,11 @@ pub struct LpRegistrationClient { /// Created during `connect()`, None before connection is established. tcp_stream: Option, - /// Client's LP keypair for Noise protocol. - local_keypair: Arc, + /// Client's Ed25519 identity keypair (used for PSQ authentication and X25519 derivation). + local_ed25519_keypair: Arc, - /// Gateway's public key for Noise protocol. - gateway_public_key: PublicKey, + /// Gateway's Ed25519 public key (from directory/discovery). + gateway_ed25519_public_key: ed25519::PublicKey, /// Gateway LP listener address (host:port, e.g., "1.1.1.1:41264"). gateway_lp_address: SocketAddr, @@ -65,8 +64,8 @@ impl LpRegistrationClient { /// Creates a new LP registration client. /// /// # Arguments - /// * `local_keypair` - Client's LP keypair for Noise protocol - /// * `gateway_public_key` - Gateway's public key + /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair (for PSQ auth and X25519 derivation) + /// * `gateway_ed25519_public_key` - Gateway's Ed25519 public key (from directory/discovery) /// * `gateway_lp_address` - Gateway's LP listener socket address /// * `client_ip` - Client IP address for registration /// * `config` - Configuration for timeouts and TCP parameters (use `LpConfig::default()`) @@ -74,18 +73,18 @@ impl LpRegistrationClient { /// # Note /// This creates the client but does not establish the connection. /// Call `connect()` to establish the TCP connection. - /// PSK is derived automatically during handshake using ECDH + Blake3 KDF (nym-109). + /// PSK is derived automatically during handshake inside the state machine. pub fn new( - local_keypair: Arc, - gateway_public_key: PublicKey, + local_ed25519_keypair: Arc, + gateway_ed25519_public_key: ed25519::PublicKey, gateway_lp_address: SocketAddr, client_ip: IpAddr, config: LpConfig, ) -> Self { Self { tcp_stream: None, - local_keypair, - gateway_public_key, + local_ed25519_keypair, + gateway_ed25519_public_key, gateway_lp_address, state_machine: None, client_ip, @@ -96,23 +95,23 @@ impl LpRegistrationClient { /// Creates a new LP registration client with default configuration. /// /// # Arguments - /// * `local_keypair` - Client's LP keypair for Noise protocol - /// * `gateway_public_key` - Gateway's public key + /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair + /// * `gateway_ed25519_public_key` - Gateway's Ed25519 public key /// * `gateway_lp_address` - Gateway's LP listener socket address /// * `client_ip` - Client IP address for registration /// /// Uses default config (LpConfig::default()) with sane timeout and TCP parameters. - /// PSK is derived automatically during handshake using ECDH + Blake3 KDF (nym-109). + /// PSK is derived automatically during handshake inside the state machine. /// For custom config, use `new()` directly. pub fn new_with_default_psk( - local_keypair: Arc, - gateway_public_key: PublicKey, + local_ed25519_keypair: Arc, + gateway_ed25519_public_key: ed25519::PublicKey, gateway_lp_address: SocketAddr, client_ip: IpAddr, ) -> Self { Self::new( - local_keypair, - gateway_public_key, + local_ed25519_keypair, + gateway_ed25519_public_key, gateway_lp_address, client_ip, LpConfig::default(), @@ -232,9 +231,20 @@ impl LpRegistrationClient { tracing::debug!("Starting LP handshake as initiator"); - // Step 1: Generate ClientHelloData with fresh salt (timestamp + nonce) + // Step 1: Derive X25519 keys from Ed25519 for Noise protocol (internal to ClientHello) + // The Ed25519 keys are used for PSQ authentication and also converted to X25519 + let client_x25519_public = self + .local_ed25519_keypair + .public_key() + .to_x25519() + .map_err(|e| { + LpClientError::Crypto(format!("Failed to derive X25519 public key: {}", e)) + })?; + + // Step 2: Generate ClientHelloData with fresh salt and both public keys let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt( - self.local_keypair.public_key().to_bytes(), + client_x25519_public.to_bytes(), + self.local_ed25519_keypair.public_key().to_bytes(), ); let salt = client_hello_data.salt; @@ -243,7 +253,7 @@ impl LpRegistrationClient { client_hello_data.extract_timestamp() ); - // Step 2: Send ClientHello as first packet (before Noise handshake) + // Step 3: Send ClientHello as first packet (before Noise handshake) let client_hello_header = nym_lp::packet::LpHeader::new( 0, // session_id not yet established 0, // counter starts at 0 @@ -255,20 +265,16 @@ impl LpRegistrationClient { Self::send_packet(stream, &client_hello_packet).await?; tracing::debug!("Sent ClientHello packet"); - // Step 3: Derive PSK using ECDH + Blake3 KDF - let psk = nym_lp::derive_psk( - self.local_keypair.private_key(), - &self.gateway_public_key, - &salt, - ); - tracing::trace!("Derived PSK from identity keys and salt"); - - // Step 4: Create state machine as initiator with derived PSK + // Step 4: Create state machine as initiator with Ed25519 keys + // PSK derivation happens internally in the state machine constructor let mut state_machine = LpStateMachine::new( true, // is_initiator - &self.local_keypair, - &self.gateway_public_key, - &psk, + ( + self.local_ed25519_keypair.private_key(), + self.local_ed25519_keypair.public_key(), + ), + &self.gateway_ed25519_public_key, + &salt, )?; // Start handshake - client (initiator) sends first @@ -311,6 +317,21 @@ impl LpRegistrationClient { tracing::info!("LP handshake completed successfully"); break; } + LpAction::KKTComplete => { + tracing::info!("KKT exchange completed, starting Noise handshake"); + // After KKT completes, initiator must send first Noise handshake message + let noise_msg = state_machine + .session()? + .prepare_handshake_message() + .ok_or_else(|| { + LpClientError::Transport( + "No handshake message available after KKT".to_string(), + ) + })??; + let noise_packet = state_machine.session()?.next_packet(noise_msg)?; + tracing::trace!("Sending first Noise handshake message"); + Self::send_packet(stream, &noise_packet).await?; + } other => { tracing::trace!("Received action during handshake: {:?}", other); } @@ -764,8 +785,9 @@ mod tests { #[test] fn test_client_creation() { - let keypair = Arc::new(Keypair::default()); - let gateway_key = PublicKey::default(); + let mut rng = rand::thread_rng(); + let keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); + let gateway_key = *ed25519::KeyPair::new(&mut rng).public_key(); let address = "127.0.0.1:41264".parse().unwrap(); let client_ip = "192.168.1.100".parse().unwrap(); diff --git a/nym-registration-client/src/lp_client/config.rs b/nym-registration-client/src/lp_client/config.rs index 2ac695be8f..0d18a0299a 100644 --- a/nym-registration-client/src/lp_client/config.rs +++ b/nym-registration-client/src/lp_client/config.rs @@ -87,7 +87,7 @@ mod tests { assert_eq!(config.connect_timeout, Duration::from_secs(10)); assert_eq!(config.handshake_timeout, Duration::from_secs(15)); assert_eq!(config.registration_timeout, Duration::from_secs(30)); - assert_eq!(config.tcp_nodelay, true); + assert!(config.tcp_nodelay); assert_eq!(config.tcp_keepalive, None); } diff --git a/nym-registration-client/src/lp_client/error.rs b/nym-registration-client/src/lp_client/error.rs index 7b18b9eee1..20633a6dbc 100644 --- a/nym-registration-client/src/lp_client/error.rs +++ b/nym-registration-client/src/lp_client/error.rs @@ -53,6 +53,10 @@ pub enum LpClientError { /// Timeout waiting for response #[error("Timeout waiting for {operation}")] Timeout { operation: String }, + + /// Cryptographic operation failed + #[error("Cryptographic error: {0}")] + Crypto(String), } pub type Result = std::result::Result; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4f910e1a15..014cf185f9 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4289,6 +4289,7 @@ version = "0.4.0" dependencies = [ "base64 0.22.1", "bs58", + "curve25519-dalek", "ed25519-dalek", "jwt-simple", "nym-pemstore", @@ -4296,6 +4297,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_bytes", + "sha2 0.10.9", "subtle-encoding", "thiserror 2.0.12", "x25519-dalek",