Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b10e8db21b | |||
| d28f54d13a | |||
| fb29b6fde3 | |||
| 17894e5f8b | |||
| 410dd6f725 | |||
| 130f2d690b | |||
| d078ca88d1 | |||
| 5827937040 | |||
| 3f82cf6d5f | |||
| 7f43f38879 |
@@ -1,5 +1,2 @@
|
||||
nym-validator-rewarder/.sqlx/** diff=nodiff
|
||||
nym-node-status-api/nym-node-status-api/.sqlx/** diff=nodiff
|
||||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/beads.jsonl merge=beads
|
||||
|
||||
@@ -8,13 +8,10 @@ on:
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
- 'nym-authenticator-client/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- 'nym-ip-packet-client/**'
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
- 'nym-node-status-api/**'
|
||||
- 'nym-registration-client/**'
|
||||
- 'nym-statistics-api/**'
|
||||
- 'nym-outfox/**'
|
||||
- 'nym-validator-rewarder/**'
|
||||
|
||||
@@ -6,14 +6,16 @@ on:
|
||||
paths:
|
||||
- "ts-packages/**"
|
||||
- "sdk/typescript/**"
|
||||
- "nym-connect/desktop/src/**"
|
||||
- "nym-connect/desktop/package.json"
|
||||
- "nym-wallet/src/**"
|
||||
- "nym-wallet/package.json"
|
||||
- "explorer-v2/**"
|
||||
- "explorer/**"
|
||||
- ".github/workflows/ci-lint-typescript.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-linux-latest
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
@@ -23,7 +25,6 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
@@ -36,12 +37,14 @@ jobs:
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '116'
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
go-version: "1.23.7"
|
||||
|
||||
- name: Install
|
||||
run: yarn
|
||||
@@ -49,11 +52,7 @@ jobs:
|
||||
- name: Build packages
|
||||
run: yarn build:ci
|
||||
|
||||
- name: Install again
|
||||
run: yarn
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Typecheck with tsc
|
||||
run: yarn tsc
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-linux-latest
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: arc-ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -17,13 +17,10 @@ jobs:
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install rust toolchain
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
@@ -32,9 +29,9 @@ jobs:
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
go-version: "1.23.7"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
@@ -63,6 +63,3 @@ nym-api/redocly/formatted-openapi.json
|
||||
|
||||
**/settings.sql
|
||||
**/enter_db.sh
|
||||
.beads
|
||||
CLAUDE.md
|
||||
docs
|
||||
@@ -4,38 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.20-leerdammer] (2025-11-12)
|
||||
|
||||
- Max/tweak ts sdk actions ([#6185])
|
||||
- chore: resolve clippy 1.91 warnings ([#6168])
|
||||
- [chore] Remove unused dependencies ([#6151])
|
||||
- Use typed-builder for registration client builder config ([#6150])
|
||||
- tommy is too quick ([#6149])
|
||||
- configurable mixnet client startup timeout ([#6148])
|
||||
- [Feature/operators]: QUIC bridge deployment script v2 ([#6145])
|
||||
- Bugfix: Add circuit breaker ([#6143])
|
||||
- bugfix: update internal owner address in transferred share ([#6139])
|
||||
- Update quic_bridge_deployment.sh for IPv4 and .deb package ([#6138])
|
||||
- feat: expose more explicit new_with_fronted_urls builder for http API client ([#6136])
|
||||
- bugfix: update stored epoch share when changing ownership ([#6135])
|
||||
- Domain fronting ([#6134])
|
||||
- bugfix: update stored epoch share when changing announce address ([#6131])
|
||||
|
||||
[#6185]: https://github.com/nymtech/nym/pull/6185
|
||||
[#6168]: https://github.com/nymtech/nym/pull/6168
|
||||
[#6151]: https://github.com/nymtech/nym/pull/6151
|
||||
[#6150]: https://github.com/nymtech/nym/pull/6150
|
||||
[#6149]: https://github.com/nymtech/nym/pull/6149
|
||||
[#6148]: https://github.com/nymtech/nym/pull/6148
|
||||
[#6145]: https://github.com/nymtech/nym/pull/6145
|
||||
[#6143]: https://github.com/nymtech/nym/pull/6143
|
||||
[#6139]: https://github.com/nymtech/nym/pull/6139
|
||||
[#6138]: https://github.com/nymtech/nym/pull/6138
|
||||
[#6136]: https://github.com/nymtech/nym/pull/6136
|
||||
[#6135]: https://github.com/nymtech/nym/pull/6135
|
||||
[#6134]: https://github.com/nymtech/nym/pull/6134
|
||||
[#6131]: https://github.com/nymtech/nym/pull/6131
|
||||
|
||||
## [2025.19-kase] (2025-10-30)
|
||||
|
||||
- update ns agent workflow ([#6154])
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
# 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
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Rust Components
|
||||
|
||||
```bash
|
||||
# Default build (debug)
|
||||
cargo build
|
||||
|
||||
# Release build
|
||||
cargo build --release
|
||||
|
||||
# Build a specific package
|
||||
cargo build -p <package-name>
|
||||
|
||||
# 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 <package-name>
|
||||
|
||||
# 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 <package-name>
|
||||
|
||||
# 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:
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
- `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 <migration_name>
|
||||
|
||||
# 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
|
||||
Generated
+14
-478
@@ -165,15 +165,6 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
@@ -1000,12 +991,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "byte_string"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed"
|
||||
|
||||
[[package]]
|
||||
name = "bytecodec"
|
||||
version = "0.4.15"
|
||||
@@ -1274,16 +1259,6 @@ 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"
|
||||
@@ -1457,16 +1432,6 @@ 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"
|
||||
@@ -1894,7 +1859,6 @@ 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",
|
||||
@@ -2615,7 +2579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "extension-storage"
|
||||
version = "1.4.1"
|
||||
version = "1.4.0-rc.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"console_error_panic_hook",
|
||||
@@ -3195,43 +3159,6 @@ 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"
|
||||
@@ -4180,15 +4107,6 @@ 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"
|
||||
@@ -4267,213 +4185,6 @@ 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"
|
||||
@@ -4767,7 +4478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.1"
|
||||
version = "1.4.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -4777,7 +4488,6 @@ dependencies = [
|
||||
"nym-ordered-buffer",
|
||||
"nym-service-providers-common",
|
||||
"nym-socks5-requests",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
@@ -5103,28 +4813,6 @@ 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"
|
||||
@@ -5136,7 +4824,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.69"
|
||||
version = "1.1.68"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5359,7 +5047,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.66"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5442,7 +5130,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.66"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5592,7 +5280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client-wasm"
|
||||
version = "1.4.1"
|
||||
version = "1.4.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -5878,7 +5566,6 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5914,11 +5601,9 @@ 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",
|
||||
@@ -5984,7 +5669,6 @@ dependencies = [
|
||||
"bs58",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"ed25519-dalek",
|
||||
"generic-array 0.14.7",
|
||||
@@ -6117,7 +5801,6 @@ dependencies = [
|
||||
"bincode",
|
||||
"bip39",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"defguard_wireguard_rs",
|
||||
"fastrand 2.3.0",
|
||||
@@ -6135,14 +5818,10 @@ dependencies = [
|
||||
"nym-gateway-storage",
|
||||
"nym-id",
|
||||
"nym-ip-packet-router",
|
||||
"nym-kcp",
|
||||
"nym-lp",
|
||||
"nym-metrics",
|
||||
"nym-mixnet-client",
|
||||
"nym-network-defaults",
|
||||
"nym-network-requester",
|
||||
"nym-node-metrics",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-service-provider-requests-common",
|
||||
"nym-sphinx",
|
||||
@@ -6216,7 +5895,6 @@ dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
"hex",
|
||||
"nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
"nym-authenticator-requests",
|
||||
"nym-bandwidth-controller",
|
||||
@@ -6232,13 +5910,7 @@ dependencies = [
|
||||
"nym-http-api-client-macro",
|
||||
"nym-ip-packet-client",
|
||||
"nym-ip-packet-requests",
|
||||
"nym-lp",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-requests",
|
||||
"nym-node-status-client",
|
||||
"nym-registration-client",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-topology",
|
||||
"nym-validator-client",
|
||||
@@ -6247,7 +5919,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -6379,7 +6050,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"wasmtimer",
|
||||
]
|
||||
@@ -6530,48 +6200,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-kcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"byte_string",
|
||||
"bytes",
|
||||
"env_logger",
|
||||
"log",
|
||||
"thiserror 2.0.12",
|
||||
"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"
|
||||
@@ -6583,43 +6211,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"bincode",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"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",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "nym-metrics"
|
||||
version = "0.1.0"
|
||||
@@ -6763,7 +6354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.67"
|
||||
version = "1.1.66"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6813,7 +6404,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.21.0"
|
||||
version = "1.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6844,7 +6435,6 @@ dependencies = [
|
||||
"nym-bin-common",
|
||||
"nym-client-core-config-types",
|
||||
"nym-config",
|
||||
"nym-credential-verification",
|
||||
"nym-crypto",
|
||||
"nym-gateway",
|
||||
"nym-gateway-stats-storage",
|
||||
@@ -6917,13 +6507,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",
|
||||
@@ -6934,7 +6524,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
@@ -7053,7 +6642,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-tester-wasm"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0-rc.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
@@ -7196,21 +6785,15 @@ dependencies = [
|
||||
name = "nym-registration-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bytes",
|
||||
"futures",
|
||||
"nym-authenticator-client",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-credential-storage",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ip-packet-client",
|
||||
"nym-lp",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-validator-client",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -7223,15 +6806,10 @@ dependencies = [
|
||||
name = "nym-registration-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"nym-authenticator-requests",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ip-packet-requests",
|
||||
"nym-sphinx",
|
||||
"nym-wireguard-types",
|
||||
"serde",
|
||||
"time",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
@@ -7353,7 +6931,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.66"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7991,20 +7569,15 @@ dependencies = [
|
||||
"defguard_wireguard_rs",
|
||||
"futures",
|
||||
"ip_network",
|
||||
"ipnetwork",
|
||||
"log",
|
||||
"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",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -8092,7 +7665,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.31"
|
||||
version = "0.1.30"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -8246,9 +7819,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.110"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8446,12 +8019,6 @@ 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"
|
||||
@@ -10197,16 +9764,6 @@ 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"
|
||||
@@ -11193,27 +10750,6 @@ 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"
|
||||
|
||||
+3
-13
@@ -72,10 +72,6 @@ members = [
|
||||
"common/nym-cache",
|
||||
"common/nym-connection-monitor",
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-lp-common",
|
||||
"common/nym-kkt",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -154,7 +150,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",
|
||||
@@ -169,7 +165,7 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"nym-gateway-probe"
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -208,7 +204,6 @@ aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
ammonia = "4"
|
||||
ansi_term = "0.12"
|
||||
anyhow = "1.0.98"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
@@ -248,7 +243,6 @@ criterion = "0.5"
|
||||
csv = "1.3.1"
|
||||
ctr = "0.9.1"
|
||||
cupid = "0.6.1"
|
||||
curve25519-dalek = "4.1.3"
|
||||
dashmap = "5.5.3"
|
||||
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
|
||||
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
|
||||
@@ -288,9 +282,7 @@ 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"
|
||||
@@ -300,7 +292,6 @@ 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"
|
||||
@@ -347,7 +338,6 @@ 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"
|
||||
|
||||
@@ -107,16 +107,16 @@ sdk-wasm-build:
|
||||
$(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/node-tester
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
# $(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/full-nym-wasm
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
sdk-typescript-build:
|
||||
npx lerna run --scope @nymproject/sdk build --stream
|
||||
npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
npx lerna run --scope @nymproject/node-tester build --stream
|
||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
# npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
# npx lerna run --scope @nymproject/node-tester build --stream
|
||||
# yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.66"
|
||||
version = "1.1.65"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.66"
|
||||
version = "1.1.65"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -6,10 +6,7 @@ use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
use crate::{
|
||||
AuthenticatorVersion, Error,
|
||||
traits::{
|
||||
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
|
||||
Versionable,
|
||||
},
|
||||
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
|
||||
v2, v3, v4, v5, v6,
|
||||
};
|
||||
|
||||
@@ -21,7 +18,6 @@ pub enum ClientMessage {
|
||||
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
|
||||
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
|
||||
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
|
||||
UpgradeModeCheck(Box<dyn UpgradeModeMessage + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
pub struct SerialisedRequest {
|
||||
@@ -135,7 +131,6 @@ impl ClientMessage {
|
||||
);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +189,6 @@ impl ClientMessage {
|
||||
);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +237,6 @@ impl ClientMessage {
|
||||
});
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +245,6 @@ impl ClientMessage {
|
||||
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
|
||||
request::AuthenticatorRequest,
|
||||
topup::TopUpMessage,
|
||||
upgrade_mode_check::UpgradeModeCheckRequest,
|
||||
};
|
||||
match self {
|
||||
ClientMessage::Initial(init_message) => {
|
||||
@@ -290,22 +282,6 @@ impl ClientMessage {
|
||||
});
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
ClientMessage::UpgradeModeCheck(upgrade_mode_check) => {
|
||||
// currently JWT is the only emergency credential option
|
||||
let Some(upgrade_mode_jwt) =
|
||||
upgrade_mode_check.upgrade_mode_global_attestation_jwt()
|
||||
else {
|
||||
return Err(Error::conversion(
|
||||
"no valid known upgrade mode check variants",
|
||||
));
|
||||
};
|
||||
let msg = UpgradeModeCheckRequest::UpgradeModeJwt {
|
||||
token: upgrade_mode_jwt,
|
||||
};
|
||||
|
||||
let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +292,7 @@ impl ClientMessage {
|
||||
match self {
|
||||
Self::Final(msg) => msg.credential().is_some(),
|
||||
Self::TopUp(_) => true,
|
||||
Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false,
|
||||
Self::Initial(_) | Self::Query(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +302,6 @@ impl ClientMessage {
|
||||
ClientMessage::Final(msg) => msg.version(),
|
||||
ClientMessage::Query(msg) => msg.version(),
|
||||
ClientMessage::TopUp(msg) => msg.version(),
|
||||
ClientMessage::UpgradeModeCheck(msg) => msg.version(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod client_message;
|
||||
pub mod models;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod traits;
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_credentials_interface::{
|
||||
BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CurrentUpgradeModeStatus {
|
||||
Enabled,
|
||||
Disabled,
|
||||
// everything pre-v6
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CurrentUpgradeModeStatus {
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, CurrentUpgradeModeStatus::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for CurrentUpgradeModeStatus {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
CurrentUpgradeModeStatus::Enabled
|
||||
} else {
|
||||
CurrentUpgradeModeStatus::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CurrentUpgradeModeStatus> for Option<bool> {
|
||||
fn from(value: CurrentUpgradeModeStatus) -> Self {
|
||||
match value {
|
||||
CurrentUpgradeModeStatus::Enabled => Some(true),
|
||||
CurrentUpgradeModeStatus::Disabled => Some(false),
|
||||
CurrentUpgradeModeStatus::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct BandwidthClaim {
|
||||
pub credential: BandwidthCredential,
|
||||
pub kind: TicketType,
|
||||
}
|
||||
|
||||
impl TryFrom<CredentialSpendingData> for BandwidthClaim {
|
||||
type Error = UnknownTicketType;
|
||||
|
||||
fn try_from(credential: CredentialSpendingData) -> Result<Self, Self::Error> {
|
||||
Ok(BandwidthClaim {
|
||||
kind: TicketType::try_from_encoded(credential.payment.t_type)?,
|
||||
credential: BandwidthCredential::from(credential),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
|
||||
use crate::traits::{
|
||||
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
|
||||
};
|
||||
use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage};
|
||||
use crate::{v1, v2, v3, v4, v5, v6};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -35,11 +33,6 @@ pub enum AuthenticatorRequest {
|
||||
reply_to: Option<Recipient>,
|
||||
request_id: u64,
|
||||
},
|
||||
CheckUpgradeMode {
|
||||
msg: Box<dyn UpgradeModeMessage + Send + Sync + 'static>,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
|
||||
@@ -241,13 +234,6 @@ impl From<v6::request::AuthenticatorRequest> for AuthenticatorRequest {
|
||||
request_id: value.request_id,
|
||||
}
|
||||
}
|
||||
v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => {
|
||||
Self::CheckUpgradeMode {
|
||||
msg: Box::new(upgrade_mode_check_msg),
|
||||
protocol: value.protocol,
|
||||
request_id: value.request_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::CurrentUpgradeModeStatus;
|
||||
use crate::traits::{
|
||||
Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse,
|
||||
TopUpBandwidthResponse, UpgradeModeStatus,
|
||||
CurrentUpgradeModeStatus, Id, PendingRegistrationResponse, RegisteredResponse,
|
||||
RemainingBandwidthResponse, TopUpBandwidthResponse, UpgradeModeStatus,
|
||||
};
|
||||
use crate::{v2, v3, v4, v5, v6};
|
||||
|
||||
@@ -14,7 +13,6 @@ pub enum AuthenticatorResponse {
|
||||
Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>),
|
||||
RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>),
|
||||
TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>),
|
||||
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl UpgradeModeStatus for AuthenticatorResponse {
|
||||
@@ -32,9 +30,6 @@ impl UpgradeModeStatus for AuthenticatorResponse {
|
||||
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
|
||||
top_up_bandwidth_response.upgrade_mode_status()
|
||||
}
|
||||
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => {
|
||||
upgrade_mode_response.upgrade_mode_status()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +47,6 @@ impl Id for AuthenticatorResponse {
|
||||
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
|
||||
top_up_bandwidth_response.id()
|
||||
}
|
||||
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,9 +139,6 @@ impl From<v6::response::AuthenticatorResponse> for AuthenticatorResponse {
|
||||
v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
|
||||
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
|
||||
}
|
||||
v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => {
|
||||
Self::UpgradeMode(Box::new(upgrade_mode_check_response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,70 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::latest::registration::IpPair;
|
||||
use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus};
|
||||
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::fmt;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::latest::registration::IpPair;
|
||||
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6};
|
||||
use nym_credentials_interface::{
|
||||
BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType,
|
||||
};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CurrentUpgradeModeStatus {
|
||||
Enabled,
|
||||
Disabled,
|
||||
// everything pre-v6
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CurrentUpgradeModeStatus {
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, CurrentUpgradeModeStatus::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for CurrentUpgradeModeStatus {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
CurrentUpgradeModeStatus::Enabled
|
||||
} else {
|
||||
CurrentUpgradeModeStatus::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CurrentUpgradeModeStatus> for Option<bool> {
|
||||
fn from(value: CurrentUpgradeModeStatus) -> Self {
|
||||
match value {
|
||||
CurrentUpgradeModeStatus::Enabled => Some(true),
|
||||
CurrentUpgradeModeStatus::Disabled => Some(false),
|
||||
CurrentUpgradeModeStatus::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct BandwidthClaim {
|
||||
pub credential: BandwidthCredential,
|
||||
pub kind: TicketType,
|
||||
}
|
||||
|
||||
impl TryFrom<CredentialSpendingData> for BandwidthClaim {
|
||||
type Error = UnknownTicketType;
|
||||
|
||||
fn try_from(credential: CredentialSpendingData) -> Result<Self, Self::Error> {
|
||||
Ok(BandwidthClaim {
|
||||
kind: TicketType::try_from_encoded(credential.payment.t_type)?,
|
||||
credential: BandwidthCredential::from(credential),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Versionable {
|
||||
fn version(&self) -> AuthenticatorVersion;
|
||||
}
|
||||
@@ -116,13 +170,7 @@ impl Versionable for v6::topup::TopUpMessage {
|
||||
}
|
||||
}
|
||||
|
||||
impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest {
|
||||
fn version(&self) -> AuthenticatorVersion {
|
||||
AuthenticatorVersion::V6
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UpgradeModeStatus: Id + fmt::Debug {
|
||||
pub trait UpgradeModeStatus {
|
||||
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus;
|
||||
}
|
||||
|
||||
@@ -257,12 +305,6 @@ impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeModeStatus for v6::response::UpgradeModeResponse {
|
||||
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
|
||||
self.upgrade_mode_enabled.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InitMessage: Versionable + fmt::Debug {
|
||||
fn pub_key(&self) -> PeerPublicKey;
|
||||
}
|
||||
@@ -574,32 +616,10 @@ impl TopUpMessage for v6::topup::TopUpMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UpgradeModeMessage: Versionable + fmt::Debug {
|
||||
// the idea is to expose different types of emergency credentials here,
|
||||
// like upgrade mode JWT, emergency threshold credential issued by signers, etc.
|
||||
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest {
|
||||
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String> {
|
||||
use v6::upgrade_mode_check::UpgradeModeCheckRequest;
|
||||
|
||||
match self {
|
||||
UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Id {
|
||||
fn id(&self) -> u64;
|
||||
}
|
||||
|
||||
impl Id for v1::response::PendingRegistrationResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v2::response::PendingRegistrationResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
@@ -630,12 +650,6 @@ impl Id for v6::response::PendingRegistrationResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v1::response::RegisteredResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v2::response::RegisteredResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
@@ -666,12 +680,6 @@ impl Id for v6::response::RegisteredResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v1::response::RemainingBandwidthResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v2::response::RemainingBandwidthResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
@@ -726,12 +734,6 @@ impl Id for v6::response::TopUpBandwidthResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for v6::response::UpgradeModeResponse {
|
||||
fn id(&self) -> u64 {
|
||||
self.request_id
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug {
|
||||
fn nonce(&self) -> u64;
|
||||
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>;
|
||||
|
||||
@@ -245,7 +245,7 @@ mod tests {
|
||||
use x25519_dalek::PublicKey;
|
||||
|
||||
use super::*;
|
||||
use crate::models::BandwidthClaim;
|
||||
use crate::traits::BandwidthClaim;
|
||||
use crate::{util::tests::CREDENTIAL_BYTES, v5};
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,6 @@ pub mod registration;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod topup;
|
||||
pub mod upgrade_mode_check;
|
||||
|
||||
pub const VERSION: u8 = 6;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::BandwidthClaim;
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
@@ -12,6 +11,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::time::SystemTime;
|
||||
use std::{fmt, ops::Deref, str::FromStr};
|
||||
|
||||
use crate::traits::BandwidthClaim;
|
||||
#[cfg(feature = "verify")]
|
||||
use hmac::{Hmac, Mac};
|
||||
#[cfg(feature = "verify")]
|
||||
|
||||
@@ -5,7 +5,6 @@ use super::{
|
||||
PROTOCOL,
|
||||
registration::{FinalMessage, InitMessage},
|
||||
topup::TopUpMessage,
|
||||
upgrade_mode_check::UpgradeModeCheckRequest,
|
||||
};
|
||||
use nym_service_provider_requests_common::Protocol;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
@@ -82,18 +81,6 @@ impl AuthenticatorRequest {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
(
|
||||
Self {
|
||||
protocol: PROTOCOL,
|
||||
data: AuthenticatorRequestData::CheckUpgradeMode(message),
|
||||
request_id,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
@@ -106,7 +93,6 @@ pub enum AuthenticatorRequestData {
|
||||
Final(Box<FinalMessage>),
|
||||
QueryBandwidth(PeerPublicKey),
|
||||
TopUpBandwidth(Box<TopUpMessage>),
|
||||
CheckUpgradeMode(UpgradeModeCheckRequest),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -76,16 +76,6 @@ impl AuthenticatorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self {
|
||||
Self {
|
||||
protocol: PROTOCOL,
|
||||
data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse {
|
||||
request_id,
|
||||
upgrade_mode_enabled,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
@@ -104,7 +94,6 @@ impl AuthenticatorResponse {
|
||||
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
|
||||
AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id),
|
||||
AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id),
|
||||
AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +104,6 @@ pub enum AuthenticatorResponseData {
|
||||
Registered(RegisteredResponse),
|
||||
RemainingBandwidth(RemainingBandwidthResponse),
|
||||
TopUpBandwidth(TopUpBandwidthResponse),
|
||||
UpgradeMode(UpgradeModeResponse),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -145,9 +133,3 @@ pub struct TopUpBandwidthResponse {
|
||||
pub reply: RemainingBandwidthData,
|
||||
pub upgrade_mode_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct UpgradeModeResponse {
|
||||
pub request_id: u64,
|
||||
pub upgrade_mode_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum UpgradeModeCheckRequest {
|
||||
/// Attempt to request upgrade mode recheck via the JWT issued as the result of
|
||||
/// global attestation.json being published
|
||||
UpgradeModeJwt { token: String },
|
||||
}
|
||||
@@ -22,7 +22,7 @@ pub enum AuthenticatorVersion {
|
||||
/// introduced in dorina-patched release (1.6.1)
|
||||
V5,
|
||||
|
||||
/// introduced in niolo release (1.23.0)
|
||||
/// introduced in yet to be named release, currently aiming for Leerdammer (1.22.0)
|
||||
V6,
|
||||
|
||||
/// an unknown, future, variant that can be present if running outdated software
|
||||
@@ -32,6 +32,9 @@ pub enum AuthenticatorVersion {
|
||||
impl AuthenticatorVersion {
|
||||
pub const LATEST: Self = Self::V6;
|
||||
|
||||
#[deprecated(
|
||||
note = "the final version of V6 won't be known until appropriate release is scheduled. after that happens From<semver::Version> trait will have to be adjusted"
|
||||
)]
|
||||
pub const fn release_version(&self) -> semver::Version {
|
||||
match self {
|
||||
AuthenticatorVersion::V1 => semver::Version::new(1, 1, 5),
|
||||
@@ -39,7 +42,7 @@ impl AuthenticatorVersion {
|
||||
AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10),
|
||||
AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0),
|
||||
AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1),
|
||||
AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0),
|
||||
AuthenticatorVersion::V6 => semver::Version::new(1, 22, 0),
|
||||
AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0),
|
||||
}
|
||||
}
|
||||
@@ -203,9 +206,8 @@ mod tests {
|
||||
assert_eq!(AuthenticatorVersion::V5, "1.7.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V5, "1.16.11".into());
|
||||
assert_eq!(AuthenticatorVersion::V5, "1.17.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V5, "1.22.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V6, "1.23.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V6, "1.23.1".into());
|
||||
assert_eq!(AuthenticatorVersion::V6, "1.24.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V5, "1.20.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V6, "1.21.0".into());
|
||||
assert_eq!(AuthenticatorVersion::V6, "1.22.0".into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ pub enum BandwidthControllerError {
|
||||
#[error("There was a credential storage error - {0}")]
|
||||
CredentialStorageError(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
#[error("retrieved upgrade mode token is not a valid String")]
|
||||
MalformedUpgradeModeToken,
|
||||
|
||||
#[error("the credential storage does not contain any usable credentials")]
|
||||
NoCredentialsAvailable,
|
||||
|
||||
|
||||
@@ -228,10 +228,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
where
|
||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
self.storage
|
||||
.get_emergency_credential(typ)
|
||||
.await
|
||||
.map_err(BandwidthControllerError::credential_storage_error)
|
||||
self.storage.get_emergency_credential(typ).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,11 @@ where
|
||||
return Ok(None);
|
||||
};
|
||||
// upgrade mode credential is just a simple stringified JWT
|
||||
let token = String::from_utf8(emergency_credential.data.content)
|
||||
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
|
||||
let token = String::from_utf8(emergency_credential.content).map_err(|err| {
|
||||
BandwidthControllerError::CredentialStorageError(Box::new(format!(
|
||||
"malformed upgrade mode token: {err}"
|
||||
)))
|
||||
})?;
|
||||
Ok(Some(token))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +81,6 @@ pub struct CommonClientInitArgs {
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
pub enabled_credentials_mode: Option<bool>,
|
||||
|
||||
/// Change the default minimum node performance used during initial node selection filtering.
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
pub minimum_gateway_performance: Option<u8>,
|
||||
|
||||
/// Mostly debug-related option to increase default traffic rate so that you would not need to
|
||||
/// modify config post init
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
@@ -177,14 +173,10 @@ where
|
||||
})?;
|
||||
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
||||
} else {
|
||||
let minimum_performance = common_args
|
||||
.minimum_gateway_performance
|
||||
.unwrap_or(core.debug.topology.minimum_gateway_performance);
|
||||
|
||||
crate::init::helpers::gateways_for_init(
|
||||
&core.client.nym_api_urls,
|
||||
user_agent,
|
||||
minimum_performance,
|
||||
core.debug.topology.minimum_gateway_performance,
|
||||
core.debug.topology.ignore_ingress_epoch_role,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ pub enum ClientCoreError {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
|
||||
|
||||
#[error("no gateways on network")]
|
||||
NoGatewaysOnNetwork,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(crate) async fn connect_async(
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::GatewayClientError;
|
||||
use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
|
||||
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
|
||||
use si_scale::helpers::bibytes2;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -103,7 +103,7 @@ impl BandwidthTickets {
|
||||
|
||||
// 20% of entry ticket value
|
||||
pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 =
|
||||
DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
|
||||
(V1MixnetEntry.bandwidth_value() / 5) as i64;
|
||||
|
||||
pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option<i64> = None;
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ pub(crate) async fn connect_async(
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ pub enum GatewayClientError {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
|
||||
|
||||
#[error("No shared key was provided or obtained")]
|
||||
NoSharedKeyAvailable,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::bandwidth::ClientBandwidth;
|
||||
use crate::client::config::BandwidthTickets;
|
||||
use crate::error::GatewayClientError;
|
||||
use crate::packet_router::PacketRouter;
|
||||
use crate::traits::GatewayPacketRouter;
|
||||
@@ -178,20 +177,7 @@ impl PartiallyDelegatedRouter {
|
||||
let available_bi2 = bibytes2(available as f64);
|
||||
let required_bi2 = bibytes2(required as f64);
|
||||
warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message");
|
||||
// if we run out of bandwidth (and tried to send reasonable amount of data),
|
||||
// the upgrade mode is implicitly disabled, as otherwise we would have been
|
||||
// to proceed
|
||||
let upgrade_mode = if available
|
||||
< BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD
|
||||
{
|
||||
Some(false)
|
||||
} else {
|
||||
// we were attempting to send a lot of data at once
|
||||
// - we have no certainty about upgrade mode at this point
|
||||
None
|
||||
};
|
||||
self.client_bandwidth
|
||||
.update_and_log(available, upgrade_mode);
|
||||
self.client_bandwidth.update_and_log(available, None);
|
||||
// UNIMPLEMENTED: we should stop sending messages until we recover bandwidth
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub identity_key: String,
|
||||
|
||||
#[clap(long, help = "LP (Lewes Protocol) listener port (default: 41264)")]
|
||||
pub lp_port: Option<u16>,
|
||||
|
||||
#[clap(long)]
|
||||
pub profit_margin_percent: Option<u64>,
|
||||
|
||||
@@ -60,13 +57,10 @@ pub async fn bond_nymnode(args: Args, client: SigningClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lp_address = args.lp_port.map(|port| format!("{}:{}", args.host, port));
|
||||
|
||||
let nymnode = nym_mixnet_contract_common::NymNode {
|
||||
host: args.host,
|
||||
custom_http_port: args.http_api_port,
|
||||
identity_key: args.identity_key,
|
||||
lp_address,
|
||||
};
|
||||
|
||||
let coin = Coin::new(args.amount, denom);
|
||||
|
||||
@@ -25,9 +25,6 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub custom_http_api_port: Option<u16>,
|
||||
|
||||
#[clap(long, help = "LP (Lewes Protocol) listener port (default: 41264)")]
|
||||
pub lp_port: Option<u16>,
|
||||
|
||||
#[clap(long)]
|
||||
pub profit_margin_percent: Option<u64>,
|
||||
|
||||
@@ -50,13 +47,10 @@ pub struct Args {
|
||||
pub async fn create_payload(args: Args, client: SigningClient) {
|
||||
let denom = client.current_chain_details().mix_denom.base.as_str();
|
||||
|
||||
let lp_address = args.lp_port.map(|port| format!("{}:{}", args.host, port));
|
||||
|
||||
let mixnode = nym_mixnet_contract_common::NymNode {
|
||||
host: args.host,
|
||||
custom_http_port: args.custom_http_api_port,
|
||||
identity_key: args.identity_key,
|
||||
lp_address,
|
||||
};
|
||||
|
||||
let coin = Coin::new(args.amount, denom);
|
||||
|
||||
@@ -19,16 +19,6 @@ pub struct Args {
|
||||
// equivalent to setting `custom_http_port` to `None`
|
||||
#[clap(long)]
|
||||
pub restore_default_http_port: bool,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "LP (Lewes Protocol) listener address (format: host:port)"
|
||||
)]
|
||||
pub lp_address: Option<String>,
|
||||
|
||||
// equivalent to setting `lp_address` to `None`
|
||||
#[clap(long)]
|
||||
pub restore_default_lp_address: bool,
|
||||
}
|
||||
|
||||
pub async fn update_config(args: Args, client: SigningClient) {
|
||||
@@ -49,8 +39,6 @@ pub async fn update_config(args: Args, client: SigningClient) {
|
||||
host: args.host,
|
||||
custom_http_port: args.custom_http_port,
|
||||
restore_default_http_port: args.restore_default_http_port,
|
||||
lp_address: args.lp_address,
|
||||
restore_default_lp_address: args.restore_default_lp_address,
|
||||
};
|
||||
|
||||
let res = client
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
// 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,
|
||||
/**
|
||||
* LP listener address for direct gateway connections (format: "host:port")
|
||||
*/
|
||||
lp_address: string | null, restore_default_lp_address: boolean, };
|
||||
export type NodeConfigUpdate = { host: string | null, custom_http_port: number | null, restore_default_http_port: boolean, };
|
||||
|
||||
+1
-6
@@ -17,9 +17,4 @@ custom_http_port: number | null,
|
||||
/**
|
||||
* Base58-encoded ed25519 EdDSA public key.
|
||||
*/
|
||||
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, };
|
||||
identity_key: string, };
|
||||
|
||||
@@ -373,11 +373,6 @@ pub struct NymNode {
|
||||
/// Base58-encoded ed25519 EdDSA public key.
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
|
||||
pub identity_key: IdentityKey,
|
||||
|
||||
/// 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"
|
||||
#[serde(default)]
|
||||
pub lp_address: Option<String>,
|
||||
// TODO: I don't think we want to include sphinx keys here,
|
||||
// given we want to rotate them and keeping that in sync with contract will be a PITA
|
||||
}
|
||||
@@ -410,7 +405,6 @@ impl From<MixNode> for NymNode {
|
||||
host: value.host,
|
||||
custom_http_port: Some(value.http_api_port),
|
||||
identity_key: value.identity_key,
|
||||
lp_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +415,6 @@ impl From<Gateway> for NymNode {
|
||||
host: value.host,
|
||||
custom_http_port: None,
|
||||
identity_key: value.identity_key,
|
||||
lp_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,13 +437,6 @@ pub struct NodeConfigUpdate {
|
||||
// equivalent to setting `custom_http_port` to `None`
|
||||
#[serde(default)]
|
||||
pub restore_default_http_port: bool,
|
||||
|
||||
/// LP listener address for direct gateway connections (format: "host:port")
|
||||
pub lp_address: Option<String>,
|
||||
|
||||
// equivalent to setting `lp_address` to `None`
|
||||
#[serde(default)]
|
||||
pub restore_default_lp_address: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_ecash_signer_check::SignerCheckError;
|
||||
use nym_validator_client::coconut::EcashApiError;
|
||||
use nym_validator_client::nym_api::{EpochId, error::NymAPIError};
|
||||
@@ -169,24 +168,6 @@ pub enum CredentialProxyError {
|
||||
device_id: String,
|
||||
credential_id: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"the attestation check url has not been provided through either the CLI nor the default .env config"
|
||||
)]
|
||||
AttestationCheckUrlNotSet,
|
||||
|
||||
#[error("the provided attester public key is malformed: {source}")]
|
||||
MalformedAttestationCheckUrl { source: url::ParseError },
|
||||
|
||||
#[error(
|
||||
"the attester public key has not been provided through either the CLI nor the default .env config"
|
||||
)]
|
||||
AttesterPublicKeyNotSet,
|
||||
|
||||
#[error("the provided attester public key is malformed: {source}")]
|
||||
MalformedAttesterPublicKey {
|
||||
source: ed25519::Ed25519RecoveryError,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<NymAPIError> for CredentialProxyError {
|
||||
|
||||
@@ -14,7 +14,6 @@ bincode = { workspace = true, optional = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
|
||||
CREATE TABLE emergency_credential
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
-- don't define any strict schema on the content as it might be implementation-dependant
|
||||
content BLOB NOT NULL,
|
||||
expiration TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
type TEXT NOT NULL,
|
||||
|
||||
-- no point in allowing duplicate data
|
||||
CREATE UNIQUE INDEX emergency_credential_unique_type_content
|
||||
ON emergency_credential (type, content);
|
||||
-- don't define any strict schema on the content as it might be implementation-dependant
|
||||
content BLOB NOT NULL,
|
||||
|
||||
expiration TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
BasicTicketbookInformation, EmergencyCredential, RetrievedPendingTicketbook,
|
||||
RetrievedTicketbook,
|
||||
};
|
||||
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
|
||||
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
|
||||
@@ -25,12 +25,6 @@ pub struct MemoryEcachTicketbookManager {
|
||||
inner: Arc<RwLock<EcashCredentialManagerInner>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InternalIdCounters {
|
||||
next_ticketbook_id: i64,
|
||||
next_emergency_credential_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EcashCredentialManagerInner {
|
||||
ticketbooks: HashMap<i64, RetrievedTicketbook>,
|
||||
@@ -39,21 +33,13 @@ struct EcashCredentialManagerInner {
|
||||
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
|
||||
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
|
||||
emergency_credentials: HashMap<String, Vec<EmergencyCredential>>,
|
||||
|
||||
// internal counters emulating assignment of an increasing id to new inserted database entries
|
||||
internal_counters: InternalIdCounters,
|
||||
_next_id: i64,
|
||||
}
|
||||
|
||||
impl EcashCredentialManagerInner {
|
||||
fn next_ticketbook_id(&mut self) -> i64 {
|
||||
let next = self.internal_counters.next_ticketbook_id;
|
||||
self.internal_counters.next_ticketbook_id += 1;
|
||||
next
|
||||
}
|
||||
|
||||
fn next_emergency_credential_id(&mut self) -> i64 {
|
||||
let next = self.internal_counters.next_emergency_credential_id;
|
||||
self.internal_counters.next_emergency_credential_id += 1;
|
||||
fn next_id(&mut self) -> i64 {
|
||||
let next = self._next_id;
|
||||
self._next_id += 1;
|
||||
next
|
||||
}
|
||||
}
|
||||
@@ -188,7 +174,7 @@ impl MemoryEcachTicketbookManager {
|
||||
used_tickets: u32,
|
||||
) {
|
||||
let mut guard = self.inner.write().await;
|
||||
let id = guard.next_ticketbook_id();
|
||||
let id = guard.next_id();
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
|
||||
@@ -302,34 +288,13 @@ impl MemoryEcachTicketbookManager {
|
||||
guard.emergency_credentials.get(typ)?.first().cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) {
|
||||
pub(crate) async fn insert_emergency_credential(&self, credential: &EmergencyCredential) {
|
||||
let mut guard = self.inner.write().await;
|
||||
let id = guard.next_emergency_credential_id();
|
||||
|
||||
guard
|
||||
.emergency_credentials
|
||||
.entry(credential.typ.clone())
|
||||
.or_default()
|
||||
.push(EmergencyCredential {
|
||||
id,
|
||||
data: credential.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credential(&self, id: i64) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.emergency_credentials.retain(|_, credentials| {
|
||||
credentials.retain(|c| c.id != id);
|
||||
!credentials.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) {
|
||||
let mut guard = self.inner.write().await;
|
||||
guard.emergency_credentials.remove(typ);
|
||||
.push(credential.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey,
|
||||
StoredIssuedTicketbook, StoredPendingTicketbook,
|
||||
BasicTicketbookInformation, EmergencyCredential, RawCoinIndexSignatures,
|
||||
RawExpirationDateSignatures, RawVerificationKey, StoredIssuedTicketbook,
|
||||
StoredPendingTicketbook,
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
@@ -317,7 +317,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM emergency_credential
|
||||
WHERE type = ?
|
||||
AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP)
|
||||
ORDER BY expiration DESC NULLS LAST
|
||||
ORDER BY expiration ASC NULLS LAST
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
@@ -328,14 +328,13 @@ impl SqliteEcashTicketbookManager {
|
||||
|
||||
pub(crate) async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
credential: &EmergencyCredential,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO emergency_credential
|
||||
(type, content, expiration)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(type, content) DO NOTHING;
|
||||
"#,
|
||||
credential.typ,
|
||||
credential.content,
|
||||
@@ -345,35 +344,6 @@ impl SqliteEcashTicketbookManager {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM emergency_credential
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM emergency_credential
|
||||
WHERE type = ?
|
||||
"#,
|
||||
typ
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_next_unspent_ticketbook<'a, E>(
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
use crate::backends::memory::MemoryEcachTicketbookManager;
|
||||
use crate::error::StorageError;
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
BasicTicketbookInformation, EmergencyCredential, RetrievedPendingTicketbook,
|
||||
RetrievedTicketbook,
|
||||
};
|
||||
use crate::storage::Storage;
|
||||
use async_trait::async_trait;
|
||||
@@ -231,28 +231,13 @@ impl Storage for EphemeralStorage {
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
credential: &EmergencyCredential,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.insert_emergency_credential(credential)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager.remove_emergency_credential(id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.remove_emergency_credentials_of_type(typ)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
|
||||
use nym_ecash_time::Date;
|
||||
use time::OffsetDateTime;
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub struct RetrievedTicketbook {
|
||||
@@ -83,14 +83,6 @@ pub struct RawVerificationKey {
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
pub struct EmergencyCredential {
|
||||
pub id: i64,
|
||||
#[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))]
|
||||
pub data: EmergencyCredentialContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
pub struct EmergencyCredentialContent {
|
||||
#[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))]
|
||||
pub typ: String,
|
||||
pub content: Vec<u8>,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
mod legacy_helpers;
|
||||
|
||||
use crate::models::{EmergencyCredential, EmergencyCredentialContent};
|
||||
use crate::models::EmergencyCredential;
|
||||
use crate::{
|
||||
backends::sqlite::{
|
||||
get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager,
|
||||
@@ -412,26 +412,11 @@ impl Storage for PersistentStorage {
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
credential: &EmergencyCredential,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.insert_emergency_credential(credential)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager.remove_emergency_credential(id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.remove_emergency_credentials_of_type(typ)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
BasicTicketbookInformation, EmergencyCredential, RetrievedPendingTicketbook,
|
||||
RetrievedTicketbook,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use nym_compact_ecash::VerificationKeyAuth;
|
||||
@@ -119,13 +119,6 @@ pub trait Storage: Clone + Send + Sync {
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
credential: &EmergencyCredential,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,9 @@ tracing = { workspace = true }
|
||||
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
||||
nym-credentials = { path = "../credentials" }
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
|
||||
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-gateway-requests = { path = "../gateway-requests" }
|
||||
nym-gateway-storage = { path = "../gateway-storage" }
|
||||
nym-metrics = { path = "../nym-metrics" }
|
||||
nym-task = { path = "../task" }
|
||||
nym-validator-client = { path = "../client-libs/validator-client" }
|
||||
nym-upgrade-mode-check = { path = "../upgrade-mode-check" }
|
||||
|
||||
@@ -59,13 +59,9 @@ impl traits::EcashManager for EcashManager {
|
||||
.verify(aggregated_verification_key)
|
||||
.map_err(|err| match err {
|
||||
CompactEcashError::ExpirationDateSignatureValidity => {
|
||||
nym_metrics::inc!("ecash_verification_failures_invalid_date_signature");
|
||||
EcashTicketError::MalformedTicketInvalidDateSignatures
|
||||
}
|
||||
_ => {
|
||||
nym_metrics::inc!("ecash_verification_failures_signature");
|
||||
EcashTicketError::MalformedTicket
|
||||
}
|
||||
_ => EcashTicketError::MalformedTicket,
|
||||
})?;
|
||||
|
||||
self.insert_pay_info(credential.pay_info.into(), insert_index)
|
||||
@@ -253,8 +249,4 @@ impl traits::EcashManager for MockEcashManager {
|
||||
}
|
||||
|
||||
fn async_verify(&self, _ticket: ClientTicket) {}
|
||||
|
||||
fn is_mock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,13 +222,9 @@ impl SharedState {
|
||||
RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.master_key))
|
||||
{
|
||||
trace!("we already had cached api clients for epoch {epoch_id}");
|
||||
nym_metrics::inc!("ecash_verification_key_cache_hits");
|
||||
return Ok(mapped);
|
||||
}
|
||||
|
||||
// Cache miss - need to fetch and set epoch data
|
||||
nym_metrics::inc!("ecash_verification_key_cache_misses");
|
||||
|
||||
let write_guard = self.set_epoch_data(epoch_id).await?;
|
||||
let guard = write_guard.downgrade();
|
||||
|
||||
|
||||
@@ -20,10 +20,4 @@ pub trait EcashManager {
|
||||
aggregated_verification_key: &VerificationKeyAuth,
|
||||
) -> Result<(), EcashTicketError>;
|
||||
fn async_verify(&self, ticket: ClientTicket);
|
||||
|
||||
/// Returns true if this is a mock ecash manager (for local testing).
|
||||
/// Default implementation returns false.
|
||||
fn is_mock(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ use nym_credentials::ecash::utils::{EcashTime, cred_exp_date, ecash_today};
|
||||
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
|
||||
use nym_gateway_requests::models::CredentialSpendingRequest;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use time::{Date, OffsetDateTime};
|
||||
use tracing::*;
|
||||
|
||||
pub use client_bandwidth::*;
|
||||
pub use error::*;
|
||||
pub use upgrade_mode::UpgradeModeState;
|
||||
|
||||
pub mod bandwidth_storage_manager;
|
||||
mod client_bandwidth;
|
||||
@@ -22,10 +20,6 @@ pub mod ecash;
|
||||
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];
|
||||
|
||||
pub struct CredentialVerifier {
|
||||
credential: CredentialSpendingRequest,
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
@@ -69,7 +63,6 @@ impl CredentialVerifier {
|
||||
.await?;
|
||||
if spent {
|
||||
trace!("the credential has already been spent before at this gateway");
|
||||
nym_metrics::inc!("ecash_verification_failures_double_spending");
|
||||
return Err(Error::BandwidthCredentialAlreadySpent);
|
||||
}
|
||||
Ok(())
|
||||
@@ -111,9 +104,6 @@ impl CredentialVerifier {
|
||||
}
|
||||
|
||||
pub async fn verify(&mut self) -> Result<i64> {
|
||||
let start = Instant::now();
|
||||
nym_metrics::inc!("ecash_verification_attempts");
|
||||
|
||||
let received_at = OffsetDateTime::now_utc();
|
||||
let spend_date = ecash_today();
|
||||
|
||||
@@ -122,39 +112,15 @@ impl CredentialVerifier {
|
||||
let credential_type = TicketType::try_from_encoded(self.credential.data.payment.t_type)?;
|
||||
|
||||
if self.credential.data.payment.spend_value != 1 {
|
||||
nym_metrics::inc!("ecash_verification_failures_multiple_tickets");
|
||||
return Err(Error::MultipleTickets);
|
||||
}
|
||||
|
||||
if let Err(e) = self.check_credential_spending_date(spend_date.ecash_date()) {
|
||||
nym_metrics::inc!("ecash_verification_failures_invalid_spend_date");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
self.check_credential_spending_date(spend_date.ecash_date())?;
|
||||
self.check_local_db_for_double_spending(&serial_number)
|
||||
.await?;
|
||||
|
||||
// TODO: do we HAVE TO do it?
|
||||
let verify_result = self.cryptographically_verify_ticket().await;
|
||||
|
||||
// Track verification duration
|
||||
let duration = start.elapsed().as_secs_f64();
|
||||
nym_metrics::add_histogram_obs!(
|
||||
"ecash_verification_duration_seconds",
|
||||
duration,
|
||||
ECASH_VERIFICATION_DURATION_BUCKETS
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
nym_metrics::metrics_registry().maybe_register_and_inc(&epoch_metric, None);
|
||||
|
||||
// Check verification result after timing
|
||||
verify_result?;
|
||||
self.cryptographically_verify_ticket().await?;
|
||||
|
||||
let ticket_id = self.store_received_ticket(received_at).await?;
|
||||
self.async_verify_ticket(ticket_id);
|
||||
@@ -168,8 +134,6 @@ impl CredentialVerifier {
|
||||
.increase_bandwidth(bandwidth, cred_exp_date())
|
||||
.await?;
|
||||
|
||||
nym_metrics::inc!("ecash_verification_success");
|
||||
|
||||
Ok(self
|
||||
.bandwidth_storage_manager
|
||||
.client_bandwidth
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_upgrade_mode_check::{
|
||||
CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt,
|
||||
};
|
||||
@@ -12,7 +11,7 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UpgradeModeEnableError {
|
||||
@@ -101,10 +100,6 @@ impl UpgradeModeDetails {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &UpgradeModeState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.state.upgrade_mode_enabled()
|
||||
}
|
||||
@@ -142,11 +137,11 @@ impl UpgradeModeDetails {
|
||||
}
|
||||
|
||||
// first validate whether the received JWT is even valid
|
||||
// note: we expect the token has been signed by our credential proxy
|
||||
// (in the future, we won't care about it, and we'll have proper key discovery endpoint. 2026™️)
|
||||
let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?;
|
||||
|
||||
// send request to revalidate internal state
|
||||
// this will, among other things, pull fresh attestation from the configured endpoint
|
||||
// and also verify required signatures (and pubkeys)
|
||||
self.request_recheck().await;
|
||||
|
||||
// not strictly necessary, but check if provided attestation actually matches the one retrieved
|
||||
@@ -160,7 +155,6 @@ impl UpgradeModeDetails {
|
||||
|
||||
// note: if attestation has been returned, it means we're definitely in upgrade mode
|
||||
// (otherwise it wouldn't have existed in the state)
|
||||
info!("managed to initialise upgrade mode through received JWT");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -192,10 +186,9 @@ impl UpgradeModeStatus {
|
||||
}
|
||||
|
||||
impl UpgradeModeState {
|
||||
pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState {
|
||||
pub fn new_empty() -> UpgradeModeState {
|
||||
UpgradeModeState {
|
||||
inner: Arc::new(UpgradeModeStateInner {
|
||||
expected_attester_public_key: attester_public_key,
|
||||
expected_attestation: RwLock::new(None),
|
||||
last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()),
|
||||
status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))),
|
||||
@@ -203,34 +196,22 @@ impl UpgradeModeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attester_pubkey(&self) -> ed25519::PublicKey {
|
||||
self.inner.expected_attester_public_key
|
||||
}
|
||||
|
||||
pub async fn attestation(&self) -> Option<UpgradeModeAttestation> {
|
||||
self.inner.expected_attestation.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn try_set_expected_attestation(
|
||||
pub async fn set_expected_attestation(
|
||||
&self,
|
||||
expected_attestation: Option<UpgradeModeAttestation>,
|
||||
) {
|
||||
let mut guard = self.inner.expected_attestation.write().await;
|
||||
// make sure to only enable upgrade mode flag AFTER we have written the expected value
|
||||
// (or still hold the exclusive lock as in this instance)
|
||||
let mut guard = self.inner.expected_attestation.write().await;
|
||||
|
||||
// ensure that the attestation had been signed with the expected key
|
||||
if let Some(attestation) = expected_attestation.as_ref() {
|
||||
if attestation.content.attester_public_key != self.inner.expected_attester_public_key {
|
||||
self.update_last_queried(OffsetDateTime::now_utc());
|
||||
return;
|
||||
}
|
||||
|
||||
if expected_attestation.is_some() {
|
||||
self.enable_upgrade_mode()
|
||||
} else {
|
||||
self.disable_upgrade_mode()
|
||||
}
|
||||
|
||||
self.update_last_queried(OffsetDateTime::now_utc());
|
||||
*guard = expected_attestation;
|
||||
}
|
||||
@@ -275,9 +256,6 @@ impl UpgradeModeState {
|
||||
}
|
||||
|
||||
struct UpgradeModeStateInner {
|
||||
/// Expected public key of the entity issuing upgrade mode attestations.
|
||||
expected_attester_public_key: ed25519::PublicKey,
|
||||
|
||||
/// Contents of the published upgrade mode attestation, as queried by this node
|
||||
expected_attestation: RwLock<Option<UpgradeModeAttestation>>,
|
||||
|
||||
|
||||
@@ -30,14 +30,7 @@ pub use nym_compact_ecash::{
|
||||
};
|
||||
pub use nym_ecash_time::{EcashTime, ecash_today};
|
||||
pub use nym_network_defaults::TicketTypeRepr;
|
||||
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
|
||||
|
||||
/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms
|
||||
/// to increase their allowance.
|
||||
// currently defined as 20% of entry ticket value
|
||||
// clients are, of course, free to override this value
|
||||
pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 =
|
||||
(V1MixnetEntry.bandwidth_value() / 5) as i64;
|
||||
pub use nym_upgrade_mode_check::UpgradeModeAttestation;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum BandwidthCredential {
|
||||
|
||||
@@ -15,7 +15,6 @@ base64.workspace = true
|
||||
bs58 = { workspace = true }
|
||||
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
|
||||
ctr = { workspace = true, optional = true }
|
||||
curve25519-dalek = { workspace = true, optional = true }
|
||||
digest = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
@@ -48,7 +47,7 @@ default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
|
||||
@@ -213,37 +213,6 @@ impl PublicKey {
|
||||
) -> Result<(), SignatureError> {
|
||||
self.0.verify(message.as_ref(), &signature.0)
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 public key to an X25519 public key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion by converting the Edwards point
|
||||
/// to Montgomery form. This is the same approach as libsodium's
|
||||
/// `crypto_sign_ed25519_pk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(x25519::PublicKey)` - The converted X25519 public key
|
||||
/// * `Err(Ed25519RecoveryError)` - If the conversion fails (e.g., low-order point)
|
||||
pub fn to_x25519(&self) -> Result<crate::asymmetric::x25519::PublicKey, Ed25519RecoveryError> {
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
|
||||
// Decompress the Ed25519 point
|
||||
let compressed = CompressedEdwardsY((*self).to_bytes());
|
||||
let edwards_point = compressed.decompress().ok_or_else(|| {
|
||||
Ed25519RecoveryError::MalformedBytes(SignatureError::from_source(
|
||||
"Failed to decompress Ed25519 point".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Convert to Montgomery form
|
||||
let montgomery = edwards_point.to_montgomery();
|
||||
|
||||
// Create X25519 public key
|
||||
crate::asymmetric::x25519::PublicKey::from_bytes(montgomery.as_bytes()).map_err(|_| {
|
||||
Ed25519RecoveryError::MalformedBytes(SignatureError::from_source(
|
||||
"Failed to convert to X25519".to_string(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
@@ -365,28 +334,6 @@ impl PrivateKey {
|
||||
let signature_bytes = self.sign(text).to_bytes();
|
||||
bs58::encode(signature_bytes).into_string()
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 private key to an X25519 private key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion via SHA-512 hash and clamping.
|
||||
/// This is the same approach as libsodium's `crypto_sign_ed25519_sk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// The converted X25519 private key
|
||||
pub fn to_x25519(&self) -> crate::asymmetric::x25519::PrivateKey {
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
// Hash the Ed25519 secret key with SHA-512
|
||||
let hash = Sha512::digest(self.0);
|
||||
|
||||
// Take first 32 bytes (clamping is done automatically by x25519_dalek::StaticSecret)
|
||||
let mut x25519_bytes = [0u8; 32];
|
||||
x25519_bytes.copy_from_slice(&hash[..32]);
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
crate::asymmetric::x25519::PrivateKey::from_bytes(&x25519_bytes)
|
||||
.expect("x25519 key conversion should never fail")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -570,27 +517,4 @@ mod tests {
|
||||
|
||||
assert_eq!(sig1.to_vec(), sig2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "rand")]
|
||||
fn test_ed25519_to_x25519_ecdh() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Create two ed25519 keypairs
|
||||
let alice_ed = KeyPair::new(&mut rng);
|
||||
let bob_ed = KeyPair::new(&mut rng);
|
||||
|
||||
// Convert to x25519
|
||||
let alice_x25519_private = alice_ed.private_key().to_x25519();
|
||||
let alice_x25519_public = alice_ed.public_key().to_x25519().unwrap();
|
||||
let bob_x25519_private = bob_ed.private_key().to_x25519();
|
||||
let bob_x25519_public = bob_ed.public_key().to_x25519().unwrap();
|
||||
|
||||
// Perform ECDH both ways
|
||||
let alice_shared = alice_x25519_private.diffie_hellman(&bob_x25519_public);
|
||||
let bob_shared = bob_x25519_private.diffie_hellman(&alice_x25519_public);
|
||||
|
||||
// Both should produce the same shared secret
|
||||
assert_eq!(alice_shared, bob_shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -13,7 +13,7 @@ use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// specific errors (that should not be nested!!) for clients to match on
|
||||
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SimpleGatewayRequestsError {
|
||||
#[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")]
|
||||
|
||||
@@ -46,7 +46,8 @@ impl SensitiveServerResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct BandwidthResponse {
|
||||
pub available_total: i64,
|
||||
|
||||
@@ -56,7 +57,8 @@ pub struct BandwidthResponse {
|
||||
pub upgrade_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct SendResponse {
|
||||
pub remaining_bandwidth: i64,
|
||||
|
||||
@@ -66,7 +68,7 @@ pub struct SendResponse {
|
||||
pub upgrade_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[non_exhaustive]
|
||||
pub enum ServerResponse {
|
||||
@@ -164,79 +166,3 @@ impl TryFrom<String> for ServerResponse {
|
||||
serde_json::from_str(&msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn server_response_serde_compat() {
|
||||
// make sure new serialisation is identical and compatible
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[non_exhaustive]
|
||||
pub enum OldServerResponse {
|
||||
Bandwidth { available_total: i64 },
|
||||
Send { remaining_bandwidth: i64 },
|
||||
}
|
||||
|
||||
// OLD => NEW
|
||||
let old_bandwidth = OldServerResponse::Bandwidth {
|
||||
available_total: 100,
|
||||
};
|
||||
let old_send = OldServerResponse::Send {
|
||||
remaining_bandwidth: 100,
|
||||
};
|
||||
|
||||
let old_bandwidth_str = serde_json::to_string(&old_bandwidth).unwrap();
|
||||
let old_send_str = serde_json::to_string(&old_send).unwrap();
|
||||
|
||||
let recovered_bandwidth = ServerResponse::try_from(old_bandwidth_str).unwrap();
|
||||
assert_eq!(
|
||||
recovered_bandwidth,
|
||||
ServerResponse::Bandwidth(BandwidthResponse {
|
||||
available_total: 100,
|
||||
upgrade_mode: false
|
||||
})
|
||||
);
|
||||
|
||||
let recovered_send = ServerResponse::try_from(old_send_str).unwrap();
|
||||
assert_eq!(
|
||||
recovered_send,
|
||||
ServerResponse::Send(SendResponse {
|
||||
remaining_bandwidth: 100,
|
||||
upgrade_mode: false
|
||||
})
|
||||
);
|
||||
|
||||
// NEW => OLD
|
||||
let new_bandwidth = ServerResponse::Bandwidth(BandwidthResponse {
|
||||
available_total: 100,
|
||||
upgrade_mode: false,
|
||||
});
|
||||
let new_send = ServerResponse::Send(SendResponse {
|
||||
remaining_bandwidth: 100,
|
||||
upgrade_mode: false,
|
||||
});
|
||||
|
||||
let new_bandwidth_str = serde_json::to_string(&new_bandwidth).unwrap();
|
||||
let new_send_str = serde_json::to_string(&new_send).unwrap();
|
||||
|
||||
let recovered_bandwidth: OldServerResponse =
|
||||
serde_json::from_str(&new_bandwidth_str).unwrap();
|
||||
assert_eq!(
|
||||
recovered_bandwidth,
|
||||
OldServerResponse::Bandwidth {
|
||||
available_total: 100
|
||||
}
|
||||
);
|
||||
|
||||
let recovered_send: OldServerResponse = serde_json::from_str(&new_send_str).unwrap();
|
||||
assert_eq!(
|
||||
recovered_send,
|
||||
OldServerResponse::Send {
|
||||
remaining_bandwidth: 100
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
bytes = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
@@ -52,4 +52,5 @@ workspace = true
|
||||
features = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
|
||||
@@ -30,26 +30,19 @@
|
||||
use crate::ClientBuilder;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
ResolveError, TokioResolver,
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy},
|
||||
lookup_ip::{LookupIp, LookupIpIntoIter},
|
||||
name_server::TokioConnectionProvider,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use tracing::*;
|
||||
|
||||
mod constants;
|
||||
mod static_resolver;
|
||||
pub use static_resolver::*;
|
||||
use tracing::warn;
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
@@ -66,6 +59,10 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
|
||||
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
|
||||
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
|
||||
@@ -75,24 +72,11 @@ static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
#[error("hickory-dns resolver error: {hickory_error}")]
|
||||
/// Error occurring while resolving a hostname into an IP address.
|
||||
pub enum ResolveError {
|
||||
#[error("invalid name: {0}")]
|
||||
InvalidNameError(String),
|
||||
#[error("hickory-dns resolver error: {0}")]
|
||||
ResolveError(#[from] hickory_resolver::ResolveError),
|
||||
#[error("high level lookup timed out")]
|
||||
Timeout,
|
||||
#[error("hostname not found in static lookup table")]
|
||||
StaticLookupMiss,
|
||||
}
|
||||
|
||||
impl ResolveError {
|
||||
/// Returns true if the error is a timeout.
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ResolveError::Timeout)
|
||||
}
|
||||
pub struct HickoryDnsError {
|
||||
#[from]
|
||||
hickory_error: ResolveError,
|
||||
}
|
||||
|
||||
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
|
||||
@@ -103,118 +87,71 @@ impl ResolveError {
|
||||
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
|
||||
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
|
||||
/// internal `AsyncResolver`.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct HickoryDnsResolver {
|
||||
// Since we might not have been called in the context of a
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
dont_use_shared: bool,
|
||||
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
||||
/// use of retries, server_ordering_strategy, etc. ends absolutely if this timeout is reached.
|
||||
overall_dns_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for HickoryDnsResolver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
fallback: Default::default(),
|
||||
static_base: Default::default(),
|
||||
dont_use_shared: Default::default(),
|
||||
overall_dns_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let resolver = self.state.clone();
|
||||
let maybe_fallback = self.fallback.clone();
|
||||
let maybe_static = self.static_base.clone();
|
||||
let independent = self.dont_use_shared;
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
resolve(
|
||||
name,
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
independent,
|
||||
overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
let resolver = resolver.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
})?;
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name.as_str()).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
if let Some(ref fallback) = maybe_fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.ok_or(e)? // if the shared resolver has no fallback return the original error
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
})?;
|
||||
|
||||
resolver.lookup_ip(name.as_str()).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
Ok(addrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
name: Name,
|
||||
resolver: Arc<OnceCell<TokioResolver>>,
|
||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
independent: bool,
|
||||
overall_dns_timeout: Duration,
|
||||
) -> Result<Addrs, ResolveError> {
|
||||
let resolver = resolver.get_or_try_init(|| HickoryDnsResolver::new_resolver(independent))?;
|
||||
|
||||
// Attempt a lookup using the primary resolver
|
||||
let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
|
||||
let primary_err = match resolve_fut.await {
|
||||
Err(_) => ResolveError::Timeout,
|
||||
Ok(Ok(lookup)) => {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
return Ok(addrs);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error: {e}");
|
||||
}
|
||||
e.into()
|
||||
}
|
||||
};
|
||||
|
||||
// If the primary resolver encountered an error, attempt a lookup using the fallback
|
||||
// resolver if one is configured.
|
||||
if let Some(ref fallback) = maybe_fallback {
|
||||
let resolver =
|
||||
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
|
||||
|
||||
let resolve_fut =
|
||||
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
|
||||
if let Ok(Ok(lookup)) = resolve_fut.await {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
|
||||
// If no record has been found and a static map of fallback addresses is configured
|
||||
// check the table for our entry
|
||||
if let Some(ref static_resolver) = maybe_static {
|
||||
debug!("checking static");
|
||||
let resolver =
|
||||
static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
|
||||
|
||||
if let Ok(addrs) = resolver.resolve(name).await {
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
|
||||
Err(primary_err)
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for SocketAddrs {
|
||||
type Item = SocketAddr;
|
||||
|
||||
@@ -225,22 +162,28 @@ impl Iterator for SocketAddrs {
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||
pub async fn resolve_str(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
||||
let n =
|
||||
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
||||
resolve(
|
||||
n,
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
self.static_base.clone(),
|
||||
self.dont_use_shared,
|
||||
self.overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
.map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
|
||||
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
|
||||
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
if let Some(ref fallback) = self.fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
|
||||
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
|
||||
resolver.lookup_ip(name).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(lookup)
|
||||
}
|
||||
|
||||
/// Create a (lazy-initialized) resolver that is not shared across threads.
|
||||
@@ -251,20 +194,16 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if dont_use_shared {
|
||||
fn new_resolver(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver_system(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
@@ -276,18 +215,8 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_static_fallback(dont_use_shared: bool) -> StaticResolver {
|
||||
if !dont_use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
|
||||
shared_resolver
|
||||
.get_or_init(new_default_static_fallback)
|
||||
.clone()
|
||||
} else {
|
||||
new_default_static_fallback()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
|
||||
self.fallback = Some(Default::default());
|
||||
let _ = self
|
||||
.fallback
|
||||
@@ -302,51 +231,22 @@ impl HickoryDnsResolver {
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
}
|
||||
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
/// exists.
|
||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_addrs())
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
|
||||
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
let cell = OnceCell::new();
|
||||
cell.set(StaticResolver::new(addrs))
|
||||
.expect("infallible assign");
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
|
||||
/// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm.
|
||||
///
|
||||
/// Timeout Defaults to 5 seconds
|
||||
/// Number of retries after lookup failure before giving up Defaults to 2
|
||||
///
|
||||
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
|
||||
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
|
||||
fn new_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new configured resolver");
|
||||
|
||||
fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
|
||||
let mut name_servers = NameServerConfigGroup::quad9_tls();
|
||||
name_servers.merge(NameServerConfigGroup::quad9_https());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_https());
|
||||
|
||||
configure_and_build_resolver(name_servers)
|
||||
}
|
||||
|
||||
fn configure_and_build_resolver(
|
||||
name_servers: NameServerConfigGroup,
|
||||
) -> Result<TokioResolver, ResolveError> {
|
||||
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
|
||||
let mut resolver_builder =
|
||||
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
|
||||
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
// Cache successful responses for queries received by this resolver for 30 min minimum.
|
||||
resolver_builder.options_mut().positive_min_ttl = Some(Duration::from_secs(1800));
|
||||
resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
@@ -354,27 +254,20 @@ fn configure_and_build_resolver(
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
/// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
|
||||
/// addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
fn new_resolver_system() -> Result<TokioResolver, HickoryDnsError> {
|
||||
let mut resolver_builder = TokioResolver::builder_tokio()?;
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reqwest_with_custom_dns() {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
async fn reqwest_hickory_doh() {
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver.into())
|
||||
.build()
|
||||
@@ -393,7 +286,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dns_lookup() -> Result<(), ResolveError> {
|
||||
async fn dns_lookup() -> Result<(), HickoryDnsError> {
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
|
||||
let domain = "ifconfig.me";
|
||||
@@ -403,124 +296,4 @@ mod test {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
|
||||
let example_domain = "non-existent.nymvpn.com";
|
||||
let mut resolver = HickoryDnsResolver {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = resolver.resolve_str(example_domain).await;
|
||||
assert!(result.is_err()); // should be NXDomain
|
||||
|
||||
resolver.static_base = Some(Default::default());
|
||||
|
||||
let mut addr_map = HashMap::new();
|
||||
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
|
||||
|
||||
resolver.set_static_fallbacks(addr_map);
|
||||
|
||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||
assert!(addrs.contains(&example_ip4));
|
||||
assert!(addrs.contains(&example_ip6));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod failure_test {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
/// Addresses drawn from blocks set off by RFC5737 (ipv4) and RFC3849 (ipv6)
|
||||
const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
|
||||
];
|
||||
|
||||
// Create a resolver that behaves the same as the custom configured router, except for the fact
|
||||
// that it is guaranteed to fail.
|
||||
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new faulty resolver");
|
||||
|
||||
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
853,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
broken_ns_group.merge(broken_ns_https);
|
||||
|
||||
configure_and_build_resolver(broken_ns_group)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dns_lookup_failures() -> Result<(), ResolveError> {
|
||||
let time_start = std::time::Instant::now();
|
||||
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
dont_use_shared: true,
|
||||
state: Arc::new(r),
|
||||
overall_dns_timeout: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
};
|
||||
build_broken_resolver()?;
|
||||
let domain = "ifconfig.me";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
|
||||
let duration = time_start.elapsed();
|
||||
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_to_static() -> Result<(), ResolveError> {
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
dont_use_shared: true,
|
||||
state: Arc::new(r),
|
||||
static_base: Some(Default::default()),
|
||||
overall_dns_timeout: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
};
|
||||
build_broken_resolver()?;
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "nymvpn.com";
|
||||
let _ = resolver
|
||||
.resolve_str(domain)
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub const NYM_API_DOMAIN: &str = "validator.nymtech.net";
|
||||
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(212, 71, 233, 232))];
|
||||
|
||||
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
|
||||
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
|
||||
|
||||
pub const NYM_FRONTDOOR_VERCEL_DOMAIN: &str = "nym-frontdoor.vercel.app";
|
||||
pub const NYM_FRONTDOOR_VERCEL_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
|
||||
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
|
||||
];
|
||||
|
||||
pub const NYM_FRONTDOOR_FASTLY_DOMAIN: &str = "nym-frontdoor.global.ssl.fastly.net";
|
||||
pub const NYM_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const NYMVPN_FRONTDOOR_FASTLY_DOMAIN: &str = "nymvpn-frontdoor.global.ssl.fastly.net";
|
||||
pub const NYMVPN_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const YELP_FASTLY_DOMAIN: &str = "yelp.global.ssl.fastly.net";
|
||||
pub const YELP_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const VERCEL_APP_DOMAIN: &str = "vercel.app";
|
||||
pub const VERCEL_APP_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
|
||||
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
|
||||
];
|
||||
|
||||
pub const VERCEL_COM_DOMAIN: &str = "vercel.com";
|
||||
pub const VERCEL_COM_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(198, 169, 2, 129)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
|
||||
];
|
||||
|
||||
pub const NYM_COM_DOMAIN: &str = "nym.com";
|
||||
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
|
||||
|
||||
pub const NYM_STATS_API_DOMAIN: &str = "nym-statistics-api.nymtech.cc";
|
||||
pub const NYM_STATS_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(91, 92, 153, 96))];
|
||||
|
||||
pub const NYM_RPC_DOMAIN: &str = "rpc.nymtech.net";
|
||||
pub const NYM_RPC_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(194, 182, 169, 49)),
|
||||
IpAddr::V4(Ipv4Addr::new(91, 92, 200, 116)),
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
0x2a04, 0xc43, 0xe00, 0x6f28, 0x400, 0xd8ff, 0xfe00, 0x1483,
|
||||
)),
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
0x2a04, 0xc46, 0xe00, 0x6f28, 0x4b3, 0x68ff, 0xfe00, 0x460,
|
||||
)),
|
||||
];
|
||||
|
||||
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
|
||||
m.insert(NYM_VPN_API_DOMAIN.to_string(), NYM_VPN_API_IPS.to_vec());
|
||||
m.insert(
|
||||
NYM_FRONTDOOR_VERCEL_DOMAIN.to_string(),
|
||||
NYM_FRONTDOOR_VERCEL_IPS.to_vec(),
|
||||
);
|
||||
m.insert(
|
||||
NYM_FRONTDOOR_FASTLY_DOMAIN.to_string(),
|
||||
NYM_FRONTDOOR_FASTLY_IPS.to_vec(),
|
||||
);
|
||||
m.insert(
|
||||
NYMVPN_FRONTDOOR_FASTLY_DOMAIN.to_string(),
|
||||
NYMVPN_FRONTDOOR_FASTLY_IPS.to_vec(),
|
||||
);
|
||||
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
|
||||
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
|
||||
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
|
||||
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
|
||||
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
|
||||
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
|
||||
m
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
use crate::dns::ResolveError;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use tracing::*;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StaticResolver {
|
||||
static_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||
}
|
||||
|
||||
impl StaticResolver {
|
||||
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
|
||||
debug!("building static resolver");
|
||||
Self {
|
||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
self.static_addr_map.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for StaticResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
debug!("looking up {name:?} in static resolver");
|
||||
let addr_map = self.static_addr_map.clone();
|
||||
Box::pin(async move {
|
||||
let addr_map = addr_map.lock().unwrap();
|
||||
let lookup = match addr_map.get(name.as_str()) {
|
||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||
Some(addrs) => addrs,
|
||||
};
|
||||
let addrs: Addrs = Box::new(
|
||||
lookup
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|ip_addr| SocketAddr::new(ip_addr, 0)),
|
||||
);
|
||||
|
||||
Ok(addrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_using_static_resolver() -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
|
||||
// lookup for domain for which there is no entry
|
||||
let resolver = StaticResolver::new(HashMap::new());
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let result = resolver.resolve(url).await;
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Ok(_) => panic!("lookup with empty map should fail"),
|
||||
Err(e) => assert_eq!(e.to_string(), ResolveError::StaticLookupMiss.to_string()),
|
||||
}
|
||||
|
||||
// Successful lookup
|
||||
let mut addr_map = HashMap::new();
|
||||
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let resolver = StaticResolver::new(addr_map);
|
||||
let mut addrs = resolver.resolve(url).await?;
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -121,89 +121,4 @@ mod tests {
|
||||
// println!("{response:?}");
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_on_failure() {
|
||||
let url1 = Url::new(
|
||||
"https://fake-domain.nymtech.net",
|
||||
Some(vec![
|
||||
"https://fake-front-1.nymtech.net",
|
||||
"https://fake-front-2.nymtech.net",
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
let url2 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap(); // fastly
|
||||
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
// Check that the initial configuration has the broken domain and front.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-1.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-2.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the domain and front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://validator.global.ssl.fastly.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("yelp.global.ssl.fastly.net"),
|
||||
);
|
||||
|
||||
let response = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed get request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ mod dns;
|
||||
mod path;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use dns::{HickoryDnsResolver, ResolveError};
|
||||
pub use dns::{HickoryDnsError, HickoryDnsResolver};
|
||||
|
||||
// helper for generating user agent based on binary information
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -395,13 +395,6 @@ pub enum HttpClientError {
|
||||
#[error("failed to resolve request to {url} due to data inconsistency: {details}")]
|
||||
InternalResponseInconsistency { url: ::url::Url, details: String },
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("encountered dns failure: {inner}")]
|
||||
DnsLookupFailure {
|
||||
#[from]
|
||||
inner: ResolveError,
|
||||
},
|
||||
|
||||
#[error("Failed to encode bincode: {0}")]
|
||||
Bincode(#[from] bincode::Error),
|
||||
|
||||
@@ -428,8 +421,6 @@ impl HttpClientError {
|
||||
HttpClientError::ReqwestClientError { source } => source.is_timeout(),
|
||||
HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
|
||||
HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
HttpClientError::RequestTimeout => true,
|
||||
_ => false,
|
||||
@@ -784,7 +775,6 @@ impl ClientBuilder {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
using_secure_dns: self.use_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
@@ -805,7 +795,6 @@ pub struct Client {
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: reqwest::Client,
|
||||
using_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
@@ -863,7 +852,6 @@ impl Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
using_secure_dns: self.using_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front.clone(),
|
||||
@@ -895,37 +883,8 @@ impl Client {
|
||||
self.retry_limit = limit;
|
||||
}
|
||||
|
||||
fn matches_current_host(&self, url: &Url) -> bool {
|
||||
#[cfg(feature = "tunneling")]
|
||||
{
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
url.host_str() == self.current_url().front_str()
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "tunneling"))]
|
||||
{
|
||||
url.host_str() == self.current_url().host_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
|
||||
///
|
||||
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
|
||||
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
|
||||
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
|
||||
/// multiple times.
|
||||
fn update_host(&self, maybe_url: Option<Url>) {
|
||||
// If a causal url is provided and it doesn't match the hostname currently in use, skip update.
|
||||
if let Some(err_url) = maybe_url
|
||||
&& !self.matches_current_host(&err_url)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fn update_host(&self) {
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
@@ -1089,28 +1048,8 @@ impl ApiClientCore for Client {
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)?;
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
let url: Url = req.url().clone().into();
|
||||
|
||||
// try an explicit DNS resolution - if successful then it will be in cache when reqwest
|
||||
// goes to execute the request. If failure then we get to handle the DNS lookup error.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.using_secure_dns
|
||||
&& let Some(hostname) = req.url().domain()
|
||||
// Default here will use a shared resolver instance
|
||||
&& let Err(err) = HickoryDnsResolver::default().resolve_str(hostname).await
|
||||
{
|
||||
// on failure update host, but don't trigger fronting enable.
|
||||
self.update_host(Some(url.clone()));
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
attempts += 1;
|
||||
warn!(
|
||||
"Retrying request due to dns error on attempt ({attempts}/{}): {err}",
|
||||
self.retry_limit
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let url = req.url().clone();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
@@ -1128,39 +1067,25 @@ impl ApiClientCore for Client {
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(err) => {
|
||||
// only if there was a network issue should we consider updating the host info
|
||||
//
|
||||
// note: for now this includes DNS resolution failure, I am not sure how I would go about
|
||||
// segregating that based on the interface provided by request for errors.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let is_network_err = err.is_timeout() || err.is_connect();
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host();
|
||||
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host(Some(url.clone()));
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
warn!("Retrying request due to http error: {err}");
|
||||
attempts += 1;
|
||||
warn!(
|
||||
"Retrying request due to http error on attempt ({attempts}/{}): {err}",
|
||||
self.retry_limit
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1169,7 +1094,7 @@ impl ApiClientCore for Client {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
return Err(err);
|
||||
} else {
|
||||
return Err(HttpClientError::request_send_error(url.into(), err));
|
||||
return Err(HttpClientError::request_send_error(url, err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,15 +91,14 @@ fn sanitizing_urls() {
|
||||
#[tokio::test]
|
||||
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = ClientBuilder::new_with_urls(vec![
|
||||
"http://broken.nym.test".parse()?, // This will fail because of DNS (rotate)
|
||||
"http://127.0.0.1:9".parse()?, // This will fail because of TCP refused (rotate)
|
||||
"https://httpbin.org/status/200".parse()?, // This should succeed
|
||||
"http://broken.nym.test".parse()?, // This will fail
|
||||
"https://httpbin.org/status/200".parse()?, // This will succeed
|
||||
])?
|
||||
.with_retries(3)
|
||||
.build()?;
|
||||
|
||||
let req = client.create_get_request(&[], NO_PARAMS).unwrap();
|
||||
let _resp = client.send(req).await?;
|
||||
let resp = client.send(req).await?;
|
||||
|
||||
// The main test is that we successfully retried and switched to the working URL
|
||||
// We accept any response from the working endpoint since external services can be unreliable
|
||||
@@ -108,9 +107,7 @@ async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"https://httpbin.org/status/200"
|
||||
);
|
||||
|
||||
// // This assert can be unreliable due to factors beyond our control and beyond the scope of
|
||||
// // this test
|
||||
// assert_eq!(_resp.status(), StatusCode::OK);
|
||||
println!("Response status: {}", resp.status());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -126,7 +123,7 @@ fn host_updating() {
|
||||
assert_eq!(current_url.front_str(), None);
|
||||
|
||||
// update the url
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
// check that the url is still the same since there is one URL
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
@@ -141,13 +138,13 @@ fn host_updating() {
|
||||
client.change_base_urls(new_urls);
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
// check that the url got updated now that there are multiple URLs
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
|
||||
assert_eq!(client.current_url().front_str(), None);
|
||||
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
// =======================================
|
||||
@@ -165,33 +162,12 @@ fn host_updating() {
|
||||
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
// check that the url got updated now that there are multiple URLs
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_updating_url_conditioned() {
|
||||
let url1 = Url::new("http://nym-api1.test", None).unwrap();
|
||||
let url2 = Url::new("http://nym-api2.test", None).unwrap();
|
||||
let urls = vec![url1.clone(), url2.clone()];
|
||||
let client = ClientBuilder::new_with_urls(urls).unwrap().build().unwrap();
|
||||
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
// Try to update with a URL that does NOT match current - should result in no change
|
||||
client.update_host(Some(Url::parse("http://example.com").unwrap()));
|
||||
|
||||
// check that the url did NOT get updated
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
assert_eq!(client.current_url().front_str(), None);
|
||||
|
||||
// Try to update with a URL that DOES match current - should result in no change
|
||||
client.update_host(Some(url1));
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn fronted_host_updating() {
|
||||
@@ -208,7 +184,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn1.test"));
|
||||
|
||||
// update the url
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
// check that the url is still the same since there is one URL and one front
|
||||
let current_url = client.current_url();
|
||||
@@ -233,7 +209,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn1.test"));
|
||||
|
||||
// update the url - this should keep the same host but change the front
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
let current_url = client.current_url();
|
||||
// check that the url is still the same since there is one URL
|
||||
@@ -241,7 +217,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn2.test"));
|
||||
|
||||
// update the url - this should wrap around to the first front as the second url is not fronted
|
||||
client.update_host(None);
|
||||
client.update_host();
|
||||
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://nym-api.test/");
|
||||
|
||||
@@ -55,10 +55,7 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
|
||||
pub const UPGRADE_MODE_ATTESTATION_URL: &str =
|
||||
"https://nymtech.net/.wellknown/upgrade-mode/attestation.json";
|
||||
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
"3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd";
|
||||
pub const UPGRADE_MOST_ATTESTATION_URL: &str = "https://nym.com/upgrade-mode/attestation.json";
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
@@ -166,12 +163,8 @@ pub fn export_to_env() {
|
||||
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
set_var_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTATION_URL,
|
||||
UPGRADE_MODE_ATTESTATION_URL,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
var_names::UPGRADE_MOST_ATTESTATION_URL,
|
||||
UPGRADE_MOST_ATTESTATION_URL,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,11 +207,7 @@ pub fn export_to_env_if_not_set() {
|
||||
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_conditionally_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTATION_URL,
|
||||
UPGRADE_MODE_ATTESTATION_URL,
|
||||
);
|
||||
set_var_conditionally_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
var_names::UPGRADE_MOST_ATTESTATION_URL,
|
||||
UPGRADE_MOST_ATTESTATION_URL,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
|
||||
pub const UPGRADE_MODE_ATTESTATION_URL: &str = "UPGRADE_MODE_ATTESTATION_URL";
|
||||
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str = "UPGRADE_MODE_ATTESTER_ED25519_PUBKEY";
|
||||
pub const UPGRADE_MOST_ATTESTATION_URL: &str = "UPGRADE_MOST_ATTESTATION_URL";
|
||||
|
||||
pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION";
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "nym-kcp"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wire_format"
|
||||
path = "bin/wire_format/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "session"
|
||||
path = "bin/session/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
byte_string = "1.0"
|
||||
bytes = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
ansi_term = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
@@ -1,80 +0,0 @@
|
||||
use bytes::BytesMut;
|
||||
use log::info;
|
||||
use nym_kcp::{packet::KcpPacket, session::KcpSession};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create two KcpSessions, simulating two endpoints
|
||||
let mut local_sess = KcpSession::new(42);
|
||||
let mut remote_sess = KcpSession::new(42);
|
||||
|
||||
// Set an MSS (max segment size) smaller than our data to force fragmentation
|
||||
local_sess.set_mtu(40);
|
||||
remote_sess.set_mtu(40);
|
||||
|
||||
// Some data larger than 30 bytes to demonstrate multi-fragment
|
||||
let big_data = b"The quick brown fox jumps over the lazy dog. This is a test.";
|
||||
|
||||
// --- LOCAL sends data ---
|
||||
info!(
|
||||
"Local: sending data: {:?}",
|
||||
String::from_utf8_lossy(big_data)
|
||||
);
|
||||
local_sess.send(big_data);
|
||||
|
||||
// Update local session's logic at time=0
|
||||
local_sess.update(100);
|
||||
|
||||
// LOCAL fetches outgoing (to be sent across the network)
|
||||
let outgoing_pkts = local_sess.fetch_outgoing();
|
||||
info!("Local: outgoing pkts: {:?}", outgoing_pkts);
|
||||
// Here you'd normally encrypt and send them. We’ll just encode them into a buffer.
|
||||
// Then that buffer is "transferred" to the remote side.
|
||||
let mut wire_buf = BytesMut::new();
|
||||
for pkt in &outgoing_pkts {
|
||||
pkt.encode(&mut wire_buf);
|
||||
}
|
||||
|
||||
// --- REMOTE receives data ---
|
||||
// The remote side "decrypts" (here we just clone) and decodes
|
||||
let mut remote_in = wire_buf.clone();
|
||||
|
||||
// Decode zero or more KcpPackets from remote_in
|
||||
while let Some(decoded_pkt) = KcpPacket::decode(&mut remote_in)? {
|
||||
info!(
|
||||
"Decoded packet, sn: {}, frg: {}",
|
||||
decoded_pkt.sn(),
|
||||
decoded_pkt.frg()
|
||||
);
|
||||
remote_sess.input(&decoded_pkt);
|
||||
}
|
||||
|
||||
// Update remote session to process newly received data
|
||||
remote_sess.update(100);
|
||||
|
||||
// The remote session likely generated ACK packets
|
||||
let ack_pkts = remote_sess.fetch_outgoing();
|
||||
|
||||
// --- LOCAL receives ACKs ---
|
||||
// The local side decodes them
|
||||
let mut ack_buf = BytesMut::new();
|
||||
for pkt in &ack_pkts {
|
||||
pkt.encode(&mut ack_buf);
|
||||
}
|
||||
|
||||
while let Some(decoded_pkt) = KcpPacket::decode(&mut ack_buf)? {
|
||||
local_sess.input(&decoded_pkt);
|
||||
}
|
||||
|
||||
// Update local again with some arbitrary time, e.g. 50 ms later
|
||||
local_sess.update(100);
|
||||
|
||||
// Just for completeness, local might produce more packets, though typically it's just empty now
|
||||
let _ = local_sess.fetch_outgoing();
|
||||
|
||||
// --- REMOTE reads reassembled data ---
|
||||
|
||||
let incoming = remote_sess.fetch_incoming();
|
||||
info!("Remote: incoming pkts: {:?}", incoming);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead as _, BufReader},
|
||||
};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use log::info;
|
||||
use nym_kcp::{
|
||||
codec::KcpCodec,
|
||||
packet::{KcpCommand, KcpPacket},
|
||||
};
|
||||
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1) Open a file and read lines
|
||||
let file = File::open("bin/wire_format/packets.txt")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
// 2) Create our KcpCodec
|
||||
let mut codec = KcpCodec {};
|
||||
|
||||
// We'll use out_buf for encoded data from *all* lines
|
||||
let mut out_buf = BytesMut::new();
|
||||
|
||||
let mut input_lines = vec![];
|
||||
|
||||
// Read lines & encode them all
|
||||
for (i, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
info!("Original line #{}: {}", i + 1, line);
|
||||
|
||||
// Construct a KcpPacket
|
||||
let pkt = KcpPacket::new(
|
||||
42,
|
||||
KcpCommand::Push,
|
||||
0,
|
||||
128,
|
||||
0,
|
||||
i as u32,
|
||||
0,
|
||||
line.as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
input_lines.push(pkt.clone_data());
|
||||
|
||||
// Encode (serialize) the packet into out_buf
|
||||
codec.encode(pkt, &mut out_buf)?;
|
||||
}
|
||||
|
||||
// === Simulate encryption & transmission ===
|
||||
// In reality, you might do `encrypt(&out_buf)` and then
|
||||
// send it over the network. We'll just clone here:
|
||||
let mut received_buf = out_buf.clone();
|
||||
|
||||
// 3) Now decode (deserialize) all packets at once
|
||||
// For demonstration, read them back out
|
||||
let mut count = 0;
|
||||
|
||||
let mut decoded_lines = vec![];
|
||||
|
||||
#[allow(clippy::while_let_loop)]
|
||||
loop {
|
||||
match codec.decode(&mut received_buf)? {
|
||||
Some(decoded_pkt) => {
|
||||
count += 1;
|
||||
// Convert packet data back to a string
|
||||
let decoded_str = String::from_utf8_lossy(decoded_pkt.data());
|
||||
info!("Decoded line #{}: {}", decoded_pkt.sn() + 1, decoded_str);
|
||||
|
||||
decoded_lines.push(decoded_pkt.clone_data());
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
for (i, j) in input_lines.iter().zip(decoded_lines.iter()) {
|
||||
assert_eq!(i, j);
|
||||
}
|
||||
|
||||
info!("Decoded {} lines total.", count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
packet 1
|
||||
packet 2
|
||||
packet 3
|
||||
packet 4
|
||||
packet 5
|
||||
packet 6
|
||||
packet 7
|
||||
packet 8
|
||||
packet 9
|
||||
packet 10
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::packet::KcpPacket;
|
||||
|
||||
/// Our codec for encoding/decoding KCP packets
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KcpCodec;
|
||||
|
||||
impl Decoder for KcpCodec {
|
||||
type Item = KcpPacket;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
// We simply delegate to `KcpPacket::decode`
|
||||
KcpPacket::decode(src).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<KcpPacket> for KcpCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, item: KcpPacket, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
// We just call `item.encode` to append the bytes
|
||||
item.encode(dst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use bytes::BytesMut;
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{error::KcpError, packet::KcpPacket, session::KcpSession};
|
||||
|
||||
pub struct KcpDriver {
|
||||
session: KcpSession,
|
||||
buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl KcpDriver {
|
||||
pub fn conv_id(&self) -> Result<u32, KcpError> {
|
||||
Ok(self.session.conv)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, data: &[u8]) {
|
||||
self.session.send(data);
|
||||
}
|
||||
|
||||
pub fn input(&mut self, data: &[u8]) -> Result<Vec<KcpPacket>, KcpError> {
|
||||
self.buffer.extend_from_slice(data);
|
||||
let mut pkts = Vec::new();
|
||||
while let Ok(Some(pkt)) = KcpPacket::decode(&mut self.buffer) {
|
||||
debug!(
|
||||
"Decoded packet, cmd: {}, sn: {}, frg: {}",
|
||||
pkt.command(),
|
||||
pkt.sn(),
|
||||
pkt.frg()
|
||||
);
|
||||
self._input(&pkt)?;
|
||||
pkts.push(pkt);
|
||||
}
|
||||
Ok(pkts)
|
||||
}
|
||||
|
||||
fn _input(&mut self, pkt: &KcpPacket) -> Result<(), KcpError> {
|
||||
self.session.input(pkt);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_outgoing(&mut self) -> Vec<KcpPacket> {
|
||||
trace!(
|
||||
"ts_flush: {}, ts_current: {}",
|
||||
self.session.ts_flush(),
|
||||
self.session.ts_current()
|
||||
);
|
||||
self.session.fetch_outgoing()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tick: u64) {
|
||||
self.session.update(tick as u32);
|
||||
}
|
||||
|
||||
pub fn new(session: KcpSession) -> Self {
|
||||
KcpDriver {
|
||||
session,
|
||||
buffer: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KcpError {
|
||||
#[error("Invalid KCP command value: {0}")]
|
||||
InvalidCommand(u8),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod codec;
|
||||
pub mod driver;
|
||||
pub mod error;
|
||||
pub mod packet;
|
||||
pub mod session;
|
||||
@@ -1,219 +0,0 @@
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::{debug, trace};
|
||||
|
||||
use super::error::KcpError;
|
||||
|
||||
pub const KCP_HEADER: usize = 24;
|
||||
|
||||
/// Typed enumeration for KCP commands.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum KcpCommand {
|
||||
Push = 81, // cmd: push data
|
||||
Ack = 82, // cmd: ack
|
||||
Wask = 83, // cmd: window probe (ask)
|
||||
Wins = 84, // cmd: window size (tell)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KcpCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KcpCommand::Push => write!(f, "Push"),
|
||||
KcpCommand::Ack => write!(f, "Ack"),
|
||||
KcpCommand::Wask => write!(f, "Window Probe (ask)"),
|
||||
KcpCommand::Wins => write!(f, "Window Size (tell)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KcpCommand {
|
||||
type Error = KcpError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
81 => Ok(KcpCommand::Push),
|
||||
82 => Ok(KcpCommand::Ack),
|
||||
83 => Ok(KcpCommand::Wask),
|
||||
84 => Ok(KcpCommand::Wins),
|
||||
_ => Err(KcpError::InvalidCommand(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<u8> for KcpCommand {
|
||||
fn into(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// A single KCP packet (on-wire format).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KcpPacket {
|
||||
conv: u32,
|
||||
cmd: KcpCommand,
|
||||
frg: u8,
|
||||
wnd: u16,
|
||||
ts: u32,
|
||||
sn: u32,
|
||||
una: u32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl KcpPacket {
|
||||
pub fn new(
|
||||
conv: u32,
|
||||
cmd: KcpCommand,
|
||||
frg: u8,
|
||||
wnd: u16,
|
||||
ts: u32,
|
||||
sn: u32,
|
||||
una: u32,
|
||||
data: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
conv,
|
||||
cmd,
|
||||
frg,
|
||||
wnd,
|
||||
ts,
|
||||
sn,
|
||||
una,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command(&self) -> KcpCommand {
|
||||
self.cmd
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn clone_data(&self) -> Vec<u8> {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
pub fn conv(&self) -> u32 {
|
||||
self.conv
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> KcpCommand {
|
||||
self.cmd
|
||||
}
|
||||
|
||||
pub fn frg(&self) -> u8 {
|
||||
self.frg
|
||||
}
|
||||
|
||||
pub fn wnd(&self) -> u16 {
|
||||
self.wnd
|
||||
}
|
||||
|
||||
pub fn ts(&self) -> u32 {
|
||||
self.ts
|
||||
}
|
||||
|
||||
pub fn sn(&self) -> u32 {
|
||||
self.sn
|
||||
}
|
||||
|
||||
pub fn una(&self) -> u32 {
|
||||
self.una
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KcpPacket {
|
||||
fn default() -> Self {
|
||||
// We must pick some default command, e.g. `Push`.
|
||||
// Or omit `Default` if you don't need it.
|
||||
KcpPacket {
|
||||
conv: 0,
|
||||
cmd: KcpCommand::Push,
|
||||
frg: 0,
|
||||
wnd: 0,
|
||||
ts: 0,
|
||||
sn: 0,
|
||||
una: 0,
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KcpPacket {
|
||||
/// Attempt to decode a `KcpPacket` from `src`.
|
||||
/// Returns Ok(Some(pkt)) if fully available, Ok(None) if not enough data,
|
||||
/// or Err(...) if there's an invalid command or other error.
|
||||
pub fn decode(src: &mut BytesMut) -> Result<Option<Self>, KcpError> {
|
||||
trace!("Decoding buffer with len: {}", src.len());
|
||||
if src.len() < KCP_HEADER {
|
||||
// Not enough for even the header, this is usually fine, more data will arrive
|
||||
debug!("Not enough data for header");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Peek into the first 28 bytes
|
||||
let mut header = &src[..KCP_HEADER];
|
||||
|
||||
let conv = header.get_u32_le();
|
||||
let cmd_byte = header.get_u8();
|
||||
let frg = header.get_u8();
|
||||
let wnd = header.get_u16_le();
|
||||
let ts = header.get_u32_le();
|
||||
let sn = header.get_u32_le();
|
||||
let una = header.get_u32_le();
|
||||
let len = header.get_u32_le() as usize;
|
||||
|
||||
let total_needed = KCP_HEADER + len;
|
||||
if src.len() < total_needed {
|
||||
// We don't have the full packet yet
|
||||
debug!(
|
||||
"Not enough data for packet, want {}, have {}",
|
||||
total_needed,
|
||||
src.len()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Convert the raw u8 into our KcpCommand enum
|
||||
let cmd = KcpCommand::try_from(cmd_byte)?;
|
||||
|
||||
// Now we can read out the data portion
|
||||
let data = src[KCP_HEADER..KCP_HEADER + len].to_vec();
|
||||
|
||||
// Advance the buffer so it no longer contains this packet
|
||||
src.advance(total_needed);
|
||||
|
||||
Ok(Some(Self {
|
||||
conv,
|
||||
cmd,
|
||||
frg,
|
||||
wnd,
|
||||
ts,
|
||||
sn,
|
||||
una,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encode this packet into `dst`.
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
let total_len = KCP_HEADER + self.data.len();
|
||||
trace!("Encoding packet: {:?}, len: {}", self, total_len);
|
||||
dst.reserve(total_len);
|
||||
|
||||
dst.put_u32_le(self.conv);
|
||||
dst.put_u8(self.cmd.into()); // Convert enum -> u8
|
||||
dst.put_u8(self.frg);
|
||||
dst.put_u16_le(self.wnd);
|
||||
dst.put_u32_le(self.ts);
|
||||
dst.put_u32_le(self.sn);
|
||||
dst.put_u32_le(self.una);
|
||||
dst.put_u32_le(self.data.len() as u32);
|
||||
dst.extend_from_slice(&self.data);
|
||||
|
||||
trace!("Encoded packet: {:?}, len: {}", dst, dst.len());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
version = "0.1.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
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"]}
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
|
||||
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"]}
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
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
|
||||
@@ -1,518 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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);
|
||||
@@ -1,301 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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: usize = 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<Self, KKTError> {
|
||||
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<u8> {
|
||||
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<u8>,
|
||||
) -> Result<Self, KKTError> {
|
||||
let hash_len = match custom_hash_length {
|
||||
Some(l) => {
|
||||
if l < 16 {
|
||||
return Err(KKTError::InsecureHashLen);
|
||||
} else {
|
||||
l
|
||||
}
|
||||
}
|
||||
None => HASH_LEN_256 as u8,
|
||||
};
|
||||
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 as usize {
|
||||
HASH_LEN_256 => 0u8,
|
||||
_ => self.hash_length,
|
||||
},
|
||||
match self.signature_scheme {
|
||||
SignatureScheme::Ed25519 => 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
pub fn decode(encoding: &[u8]) -> Result<Self, KKTError> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Self, KKTError> {
|
||||
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<Self, KKTError> {
|
||||
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<Vec<u8>, KKTError> {
|
||||
let mut header_bytes: Vec<u8> = 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<Self, KKTError> {
|
||||
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
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
use blake3::Hasher;
|
||||
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
|
||||
use nym_sphinx::{PrivateKey, PublicKey};
|
||||
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{CURVE25519_KEY_LEN, HASH_LEN_256},
|
||||
context::KKTContext,
|
||||
error::KKTError,
|
||||
frame::KKTFrame,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
pub struct KKTSessionSecret([u8; 32]);
|
||||
|
||||
impl KKTSessionSecret {
|
||||
pub fn new(remote_public_key: &PublicKey) -> (Self, PublicKey) {
|
||||
// this doesn't use the newer rand crate
|
||||
let ephemeral_private_key = PrivateKey::random();
|
||||
let ephemeral_public_key = PublicKey::from(&ephemeral_private_key);
|
||||
|
||||
(
|
||||
Self::derive(&ephemeral_private_key, &remote_public_key),
|
||||
ephemeral_public_key,
|
||||
)
|
||||
}
|
||||
pub fn from_bytes(secret: [u8; 32]) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
pub fn try_derive(private_key: &PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
|
||||
let mut pub_key: [u8; 32] = [0u8; 32];
|
||||
pub_key.copy_from_slice(&public_key[0..CURVE25519_KEY_LEN]);
|
||||
|
||||
// Todo: check validity of pk...
|
||||
let pk = PublicKey::from(pub_key);
|
||||
Ok(Self::derive(private_key, &pk))
|
||||
}
|
||||
|
||||
pub fn derive(private_key: &PrivateKey, public_key: &PublicKey) -> Self {
|
||||
let mut shared_secret = private_key.diffie_hellman(&public_key);
|
||||
|
||||
let mut hasher = Hasher::new();
|
||||
|
||||
hasher.update(shared_secret.as_bytes());
|
||||
shared_secret.zeroize();
|
||||
|
||||
Self(hasher.finalize().as_bytes().to_owned())
|
||||
}
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_initial_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &PublicKey,
|
||||
kkt_frame: &KKTFrame,
|
||||
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(remote_public_key);
|
||||
|
||||
let mut encrypted_frame =
|
||||
encrypt_kkt_frame(rng, &session_secret_key, &kkt_frame, b"KKT_INITIAL_FRAME")?;
|
||||
|
||||
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + CURVE25519_KEY_LEN);
|
||||
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
|
||||
output_buffer.append(&mut encrypted_frame);
|
||||
|
||||
// [ 32 | 12 | ciphertext | 16];
|
||||
// [eph_pub_key | nonce | ciphertext | tag];
|
||||
Ok((session_secret_key, output_buffer))
|
||||
}
|
||||
|
||||
pub fn decrypt_initial_kkt_frame(
|
||||
responder_private_key: &PrivateKey,
|
||||
encrypted_frame_bytes: &[u8],
|
||||
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
|
||||
if encrypted_frame_bytes.len() < CURVE25519_KEY_LEN + TAG_LEN + NONCE_LEN {
|
||||
return Err(KKTError::AEADError {
|
||||
info: "Encrypted KKT Frame is too short.",
|
||||
});
|
||||
} else {
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_private_key,
|
||||
&encrypted_frame_bytes[0..CURVE25519_KEY_LEN],
|
||||
)?;
|
||||
|
||||
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
|
||||
&shared_secret,
|
||||
&encrypted_frame_bytes[CURVE25519_KEY_LEN..],
|
||||
b"KKT_INITIAL_FRAME",
|
||||
)?;
|
||||
Ok((shared_secret, kkt_frame, kkt_context))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame: &KKTFrame,
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let kkt_frame_bytes = kkt_frame.to_bytes();
|
||||
|
||||
// generate nonce
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut ciphertext = encrypt(&secret_key.as_bytes(), &kkt_frame_bytes, &aad, &nonce)?;
|
||||
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
let mut output_buffer: Vec<u8> =
|
||||
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);
|
||||
|
||||
output_buffer.extend_from_slice(&nonce);
|
||||
output_buffer.append(&mut ciphertext);
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
// kkt_frame_bytes should look like this
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
pub fn decrypt_kkt_frame(
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame_bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<(KKTFrame, KKTContext), KKTError> {
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);
|
||||
|
||||
let plaintext = decrypt(
|
||||
secret_key.as_bytes(),
|
||||
&kkt_frame_bytes[NONCE_LEN..],
|
||||
aad,
|
||||
&nonce,
|
||||
)?;
|
||||
|
||||
KKTFrame::from_bytes(&plaintext)
|
||||
}
|
||||
|
||||
fn encrypt(
|
||||
secret_key: &[u8; 32],
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
|
||||
libcrux_chacha20poly1305::encrypt(&secret_key, &plaintext, &mut output_buffer, &aad, &nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
fn decrypt(
|
||||
secret_key: &[u8; 32],
|
||||
ciphertext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
|
||||
libcrux_chacha20poly1305::decrypt(&secret_key, &mut output_buffer, &ciphertext, &aad, &nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rand::{RngCore, rng};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::HASH_LEN_256,
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
let responder_x25519_keypair = generate_keypair_x25519();
|
||||
|
||||
let (session_secret_key, ephemeral_public_key) =
|
||||
KKTSessionSecret::new(&responder_x25519_keypair.1);
|
||||
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
&responder_x25519_keypair.0,
|
||||
&ephemeral_public_key.as_bytes().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption() {
|
||||
let mut rng = rng();
|
||||
|
||||
let mut secret_key = [0u8; HASH_LEN_256];
|
||||
rng.fill_bytes(&mut secret_key);
|
||||
|
||||
let mut plaintext = vec![0; 100];
|
||||
rng.fill_bytes(&mut plaintext);
|
||||
|
||||
let mut nonce = [0; 12];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut aad = vec![0; 124];
|
||||
rng.fill_bytes(&mut aad);
|
||||
|
||||
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
|
||||
|
||||
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
|
||||
|
||||
assert_eq!(o_plaintext, plaintext)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use nym_crypto::asymmetric::x25519::KeyRecoveryError;
|
||||
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("{}", info)]
|
||||
AEADError { info: &'static str },
|
||||
|
||||
#[error("Generic libcrux error")]
|
||||
LibcruxError,
|
||||
}
|
||||
|
||||
impl From<KeyRecoveryError> for KKTError {
|
||||
fn from(err: KeyRecoveryError) -> Self {
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> 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<libcrux_ecdh::Error> for KKTError {
|
||||
fn from(err: libcrux_ecdh::Error) -> Self {
|
||||
match err {
|
||||
libcrux_ecdh::Error::InvalidPoint => KKTError::KEMError {
|
||||
info: "Invalid Remote Public Key",
|
||||
},
|
||||
_ => KKTError::LibcruxError,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<libcrux_chacha20poly1305::AeadError> for KKTError {
|
||||
fn from(err: libcrux_chacha20poly1305::AeadError) -> Self {
|
||||
KKTError::KEMError {
|
||||
info: match err {
|
||||
libcrux_chacha20poly1305::AeadError::PlaintextTooLarge => {
|
||||
"Plaintext is longer than u32::MAX"
|
||||
}
|
||||
libcrux_chacha20poly1305::AeadError::CiphertextTooLarge => {
|
||||
"Ciphertext is longer than u32::MAX"
|
||||
}
|
||||
libcrux_chacha20poly1305::AeadError::AadTooLarge => "Aad is longer than u32::MAX",
|
||||
libcrux_chacha20poly1305::AeadError::CiphertextTooShort => {
|
||||
"The provided destination ciphertext does not fit the ciphertext and tag"
|
||||
}
|
||||
libcrux_chacha20poly1305::AeadError::PlaintextTooShort => {
|
||||
"The provided destination plaintext is too short to fit the decrypted plaintext"
|
||||
}
|
||||
libcrux_chacha20poly1305::AeadError::InvalidCiphertext => {
|
||||
"The ciphertext is not a valid encryption under the given key and nonce."
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<u8>,
|
||||
session_id: Vec<u8>,
|
||||
body: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
// 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<u8> {
|
||||
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<u8>, Vec<u8>, Vec<u8>) =
|
||||
(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()
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use crate::{
|
||||
ciphersuite::{HashFunction, KEM},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
use libcrux_kem::{Algorithm, key_gen};
|
||||
|
||||
use libcrux_sha3;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
pub fn generate_keypair_ed25519<R>(rng: &mut R, index: Option<u32>) -> ed25519::KeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub fn generate_keypair_x25519() -> (nym_sphinx::PrivateKey, nym_sphinx::PublicKey) {
|
||||
let private_key = nym_sphinx::PrivateKey::random();
|
||||
let public_key = nym_sphinx::PublicKey::from(&private_key);
|
||||
(private_key, public_key)
|
||||
}
|
||||
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_libcrux<R>(
|
||||
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<u8> {
|
||||
let mut hashed_key: Vec<u8> = 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<u8> {
|
||||
hash_key_bytes(hash_function, hash_length, encapsulation_key)
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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},
|
||||
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
// Re-export core session functions for advanced use cases
|
||||
pub use crate::session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
};
|
||||
|
||||
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
|
||||
|
||||
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
|
||||
///
|
||||
/// This is the client-side operation that initiates a KKT exchange.
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - Random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
|
||||
/// * `KKTContext` - Context to use when validating the response
|
||||
/// * `Vec<u8>` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let (session_secret, context, request_frame) = request_kem_key(
|
||||
/// &mut rng,
|
||||
/// ciphersuite,
|
||||
/// client_signing_key,
|
||||
/// responder_dh_public_key,
|
||||
/// )?;
|
||||
/// // Send request_frame to gateway
|
||||
/// ```
|
||||
pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_public_key: &nym_sphinx::PublicKey,
|
||||
) -> Result<(KKTSessionSecret, KKTContext, Vec<u8>), KKTError> {
|
||||
// OneWay mode: client only wants responder's KEM key
|
||||
// None: client doesn't send their own KEM key
|
||||
let (initiator_context, initiator_frame) =
|
||||
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?;
|
||||
|
||||
// Generate the session's shared secret and encrypt the Intitiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?;
|
||||
|
||||
Ok((session_secret, initiator_context, encrypted_request_bytes))
|
||||
}
|
||||
|
||||
/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key.
|
||||
///
|
||||
/// This is the client-side operation that processes the gateway's response.
|
||||
/// It verifies the signature and validates the key hash against the expected value
|
||||
/// (typically retrieved from a directory service).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context from the initial request
|
||||
/// * `session_secret` - Session Secret Key (generated with request)
|
||||
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
|
||||
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
|
||||
/// * `response_bytes` - Serialized response frame from responder
|
||||
///
|
||||
/// # Returns
|
||||
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
/// &response_bytes,
|
||||
/// )?;
|
||||
/// // Use gateway_kem_key for PSQ
|
||||
/// ```
|
||||
pub fn validate_kem_response<'a>(
|
||||
context: &mut KKTContext,
|
||||
session_secret: &KKTSessionSecret,
|
||||
responder_vk: &ed25519::PublicKey,
|
||||
expected_key_hash: &[u8],
|
||||
encrypted_response_bytes: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
let (responder_frame, responder_context) =
|
||||
decrypt_kkt_frame(&session_secret, &encrypted_response_bytes, b"KKT_Response")?;
|
||||
|
||||
initiator_ingest_response(
|
||||
context,
|
||||
&responder_frame,
|
||||
&responder_context,
|
||||
responder_vk,
|
||||
&expected_key_hash,
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key.
|
||||
///
|
||||
/// This is the gateway-side operation that processes a client's KKT request.
|
||||
/// It validates the request signature (if authenticated) and responds with
|
||||
/// the gateway's KEM public key, signed for authenticity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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, R>(
|
||||
rng: &mut R,
|
||||
encrypted_request_bytes: &[u8],
|
||||
initiator_vk: Option<&ed25519::PublicKey>,
|
||||
responder_signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_private_key: &nym_sphinx::PrivateKey,
|
||||
responder_kem_key: &EncapsulationKey<'a>,
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Compute the session's shared secret, decrypt and parse context from the request frame
|
||||
|
||||
let (session_secret, request_frame, initiator_context) =
|
||||
decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?;
|
||||
|
||||
// Validate the request (verifies signature if initiator_vk provided)
|
||||
let (mut response_context, _) = responder_ingest_message(
|
||||
&initiator_context,
|
||||
initiator_vk,
|
||||
None, // Not checking initiator's KEM key in OneWay mode
|
||||
&request_frame,
|
||||
)?;
|
||||
|
||||
// Generate signed response with our KEM public key
|
||||
let responder_frame = responder_process(
|
||||
&mut response_context,
|
||||
request_frame.session_id_ref(),
|
||||
responder_signing_key,
|
||||
responder_kem_key,
|
||||
)?;
|
||||
|
||||
// Encrypt the responder's response with the session's shared secret
|
||||
encrypt_kkt_frame(rng, &session_secret, &responder_frame, b"KKT_Response")
|
||||
}
|
||||
|
||||
// #[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());
|
||||
// }
|
||||
// }
|
||||
@@ -1,487 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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;
|
||||
|
||||
// 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},
|
||||
encryption::{
|
||||
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
|
||||
encrypt_kkt_frame,
|
||||
},
|
||||
frame::KKTFrame,
|
||||
key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
|
||||
generate_keypair_x25519, hash_encapsulation_key,
|
||||
},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (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 (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (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_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (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 (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_keypair_x25519();
|
||||
|
||||
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();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&i_context_r, 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();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, b"KKT_Response")
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, b"KKT_Response").unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, r_context) =
|
||||
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_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();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, b"KKT_Response")
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, b"KKT_Response").unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (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();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, b"KKT_Response")
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, b"KKT_Response").unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
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<R>(
|
||||
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_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
check_compatibility(own_context, &remote_context)?;
|
||||
match remote_context.status() {
|
||||
KKTStatus::Ok => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
remote_context.full_message_len() - remote_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
let received_encapsulation_key = EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
remote_frame.body_ref(),
|
||||
)?;
|
||||
|
||||
match validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => Ok(received_encapsulation_key),
|
||||
|
||||
// The key does not match the hash obtained from the directory
|
||||
false => Err(KKTError::KEMError {
|
||||
info: "Hash of received encapsulation key does not match the value stored on the directory.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
_ => Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// todo: figure out how to handle errors using status codes
|
||||
|
||||
pub fn responder_ingest_message<'a>(
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: Option<&ed25519::PublicKey>,
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: &KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), 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<u8> = 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<KKTFrame, KKTError> {
|
||||
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(())
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
@@ -1,28 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Write;
|
||||
|
||||
pub fn format_debug_bytes(bytes: &[u8]) -> Result<String, fmt::Error> {
|
||||
let mut out = String::new();
|
||||
const LINE_LEN: usize = 16;
|
||||
for (i, chunk) in bytes.chunks(LINE_LEN).enumerate() {
|
||||
let line_prefix = format!("[{}:{}]", 1 + i * LINE_LEN, i * LINE_LEN + chunk.len());
|
||||
write!(out, "{line_prefix:12}")?;
|
||||
let mut line = String::new();
|
||||
for b in chunk {
|
||||
line.push_str(format!("{:02x} ", b).as_str());
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{line:48} {}",
|
||||
chunk
|
||||
.iter()
|
||||
.map(|&b| b as char)
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '.' })
|
||||
.collect::<String>()
|
||||
)?;
|
||||
|
||||
writeln!(out)?;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
bincode = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
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 }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
harness = false
|
||||
@@ -1,365 +0,0 @@
|
||||
# LP Protocol Design
|
||||
|
||||
## Overview
|
||||
|
||||
The Lewes Protocol (LP) provides authenticated, encrypted sessions with replay protection. Key design principles:
|
||||
|
||||
1. **Unified packet structure** - Same format for all packet types
|
||||
2. **Receiver index** - Client-proposed session identifier (replaces computed session_id)
|
||||
3. **Opportunistic encryption** - Header authentication and payload encryption as soon as PSK is available
|
||||
4. **WireGuard-inspired simplicity** - Minimal header, clear security model
|
||||
|
||||
## Packet Structure
|
||||
|
||||
### Unified Format (v2)
|
||||
|
||||
All packets share the same outer structure - cleartext fields are always first:
|
||||
|
||||
```
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ trailer │
|
||||
│ 4B │ 8B │ 1B │ 3B │ variable │ 16B │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer header ────┤│←── inner (cleartext or encrypted) ──────┤│─ 16B ──┤
|
||||
```
|
||||
|
||||
**Total overhead:** 32 bytes (12B outer + 4B inner prefix + 16B trailer)
|
||||
|
||||
Key properties:
|
||||
- **Outer header** (12 bytes): Always cleartext, used for routing before session lookup
|
||||
- **Inner content**: Cleartext before PSK, encrypted after PSK
|
||||
- **No disambiguation needed**: Format is identical for both modes
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
**Outer Header** (always cleartext, 12 bytes):
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|------|-------------|
|
||||
| receiver_index | 4 bytes | Session identifier, proposed by client (routing key) |
|
||||
| counter | 8 bytes | Monotonic counter, used as AEAD nonce and for replay protection |
|
||||
|
||||
**Inner Content** (cleartext or encrypted):
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|------|-------------|
|
||||
| version | 1 byte | Protocol version |
|
||||
| reserved | 3 bytes | Reserved for future use |
|
||||
| payload | variable | Message type (2B) + content |
|
||||
| trailer | 16 bytes | Zeros (no PSK) or AEAD Poly1305 tag (with PSK) |
|
||||
|
||||
### Wire Format
|
||||
|
||||
Length-prefixed over TCP:
|
||||
|
||||
```
|
||||
┌────────────────────┬─────────────────────────────────────────────────────┐
|
||||
│ length (4B BE u32) │ LpPacket │
|
||||
└────────────────────┴─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Message Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| Busy | 0x0000 | Server congestion signal |
|
||||
| Handshake | 0x0001 | Noise protocol messages |
|
||||
| EncryptedData | 0x0002 | Encrypted application data |
|
||||
| ClientHello | 0x0003 | Initial session setup |
|
||||
| KKTRequest | 0x0004 | KEM key transfer request |
|
||||
| KKTResponse | 0x0005 | KEM key transfer response |
|
||||
| ForwardPacket | 0x0006 | Nested session forwarding |
|
||||
| Collision | 0x0007 | Receiver index collision |
|
||||
| Ack | 0x0008 | Gateway confirms receipt of message |
|
||||
|
||||
### Planned Message Types (not yet implemented)
|
||||
|
||||
| Type | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| SubsessionRequest | 0x0009 | Client requests new subsession |
|
||||
| SubsessionKK1 | 0x000A | KK handshake msg 1 (responder → initiator) |
|
||||
| SubsessionKK2 | 0x000B | KK handshake msg 2 (initiator → responder) |
|
||||
| SubsessionReady | 0x000C | Subsession established confirmation |
|
||||
|
||||
## Receiver Index
|
||||
|
||||
### Assignment
|
||||
|
||||
The client generates a random 4-byte receiver_index and includes it in ClientHello. The gateway uses this as the session lookup key. This replaces the previous approach of computing a deterministic session_id from both parties' keys.
|
||||
|
||||
### Collision Handling
|
||||
|
||||
With 4 bytes (2^32 values), collision probability is negligible:
|
||||
|
||||
| Active Sessions | Collision Probability |
|
||||
|-----------------|----------------------|
|
||||
| 10,000 | ~0.001% |
|
||||
| 100,000 | ~0.1% |
|
||||
|
||||
If collision detected, gateway rejects ClientHello and client retries with new index.
|
||||
|
||||
## Opportunistic Encryption
|
||||
|
||||
### Principle
|
||||
|
||||
As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequent packets use outer AEAD encryption:
|
||||
|
||||
- **Header**: Authenticated as associated data (AD)
|
||||
- **Payload**: Encrypted (message type + content)
|
||||
- **Trailer**: AEAD tag
|
||||
|
||||
### Timeline
|
||||
|
||||
| Packet | PSK Available | Header | Payload | Trailer |
|
||||
|--------|---------------|--------|---------|---------|
|
||||
| ClientHello | No | Clear | Clear | Zeros |
|
||||
| Ack | No | Clear | Clear | Zeros |
|
||||
| KKTRequest | No | Clear | Clear | Zeros |
|
||||
| KKTResponse | No | Clear | Clear | Zeros |
|
||||
| Noise msg 1 | No | Clear | Clear | Zeros |
|
||||
| | | **PSK derived** | | |
|
||||
| Noise msg 2 | Yes | Authenticated | Encrypted | Tag |
|
||||
| Noise msg 3 | Yes | Authenticated | Encrypted | Tag |
|
||||
| Data | Yes | Authenticated | Encrypted | Tag |
|
||||
|
||||
### Encryption Scheme
|
||||
|
||||
- **AEAD**: ChaCha20-Poly1305
|
||||
- **Key**: outer_key = KDF(PSK, "lp-outer-aead") - derived from PSK, not PSK itself
|
||||
- **Nonce**: counter (8 bytes, zero-padded to 12 bytes)
|
||||
- **AAD**: receiver_index ‖ counter (12 bytes) - the outer header
|
||||
- **Encrypted**: version ‖ reserved ‖ message_type ‖ content
|
||||
|
||||
Note: PSK is used as-is for Noise (which does internal key derivation). The outer_key derivation avoids key reuse between the two encryption layers.
|
||||
|
||||
### Before PSK
|
||||
|
||||
```
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ 00...00 │
|
||||
│ │ │ │ │ (plaintext) │ │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer ──────────┤│←────────────── cleartext inner ──────────┤│─zeros──┤
|
||||
```
|
||||
|
||||
### After PSK
|
||||
|
||||
```
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ tag │
|
||||
│ │ │ (enc) │ (enc) │ (encrypted) │ │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer (AAD) ────┤│←────────── encrypted inner ──────────────┤│─ tag ──┤
|
||||
```
|
||||
|
||||
## Handshake Flow
|
||||
|
||||
Each arrow represents a separate TCP connection (packet-per-connection model).
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
│ │
|
||||
│ [hdr][ClientHello][zeros] │
|
||||
│──────────────────────────────────────►│ store state[receiver_index]
|
||||
│ │
|
||||
│ [hdr][Ack][zeros] │
|
||||
│◄──────────────────────────────────────│ confirm ClientHello
|
||||
│ │
|
||||
│ [hdr][KKTRequest][zeros] │
|
||||
│──────────────────────────────────────►│
|
||||
│ │
|
||||
│ [hdr][KKTResponse][zeros] │
|
||||
│◄──────────────────────────────────────│
|
||||
│ │
|
||||
│ [hdr][Noise1+PSQ][zeros] │
|
||||
│──────────────────────────────────────►│ derive PSK
|
||||
│ │
|
||||
│ [hdr][encrypted Noise2][tag] │ ← authenticated
|
||||
│◄──────────────────────────────────────│
|
||||
│ │
|
||||
│ [hdr][encrypted Noise3][tag] │ ← authenticated
|
||||
│──────────────────────────────────────►│
|
||||
│ │
|
||||
│ ════════ Session Established ═════════│
|
||||
│ │
|
||||
│ [hdr][encrypted Data][tag] │
|
||||
│◄─────────────────────────────────────►│
|
||||
```
|
||||
|
||||
## Data Packet Encryption
|
||||
|
||||
Data packets have two encryption layers:
|
||||
|
||||
```
|
||||
Application Data
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Noise encrypt │ Inner layer (forward secrecy, ratcheting)
|
||||
│ (session keys) │
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ PSK AEAD │ Outer layer (header auth, payload encryption)
|
||||
│ (pre-shared key) │
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
Wire: [header][encrypted payload][tag]
|
||||
```
|
||||
|
||||
### What Outer AEAD Encrypts
|
||||
|
||||
The outer AEAD encrypts: message_type (2B) + message content
|
||||
|
||||
This hides the message type from observers after PSK is available.
|
||||
|
||||
## Subsessions and Rekeying
|
||||
|
||||
Subsessions enable **forward secrecy** through periodic rekeying and **channel multiplexing** for independent encrypted streams.
|
||||
|
||||
### Design Principles
|
||||
|
||||
| Aspect | Decision | Rationale |
|
||||
|--------|----------|-----------|
|
||||
| Key derivation | Noise KK handshake | Clean crypto, both parties already authenticated |
|
||||
| Initiation channel | Tunneled through parent | Already authenticated, no proof-of-ownership needed |
|
||||
| Hierarchy | Promotion model (chain) | Simpler than tree, natural for rekeying |
|
||||
| Old session after promotion | Read-only until TTL | Drains in-flight packets, provides grace period |
|
||||
|
||||
### Noise KK Pattern
|
||||
|
||||
Subsessions use `Noise_KK_25519_ChaChaPoly_SHA256`:
|
||||
|
||||
- **KK** = Both parties already know each other's static keys
|
||||
- **2 messages** to complete (vs 3 for XKpsk3)
|
||||
- **No PSK needed** - already authenticated via parent session
|
||||
|
||||
### Promotion Model
|
||||
|
||||
When a subsession is created, it becomes the new "master" and the old session becomes read-only:
|
||||
|
||||
```
|
||||
Session A (master) → Session B created → A demoted, B is master
|
||||
A: read-only until TTL
|
||||
```
|
||||
|
||||
This creates a chain (A → B → C) but maintains only one level of nesting conceptually. Each promotion replaces the previous master.
|
||||
|
||||
### Protocol Flow
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
│ │
|
||||
│═══════ Parent Session (A) ════════│ Transport mode
|
||||
│ │
|
||||
│──[SubsessionRequest{idx=B}]──────►│ Encrypted in parent
|
||||
│ │ Gateway creates KK responder
|
||||
│◄──[SubsessionKK1{idx=B, e}]───────│ KK handshake msg 1
|
||||
│──[SubsessionKK2{idx=B, e,ee,se}]─►│ KK handshake msg 2
|
||||
│◄──[SubsessionReady{idx=B}]────────│ Subsession established
|
||||
│ │
|
||||
│ Session A: read-only (receive) │
|
||||
│═══════ Session B (new master) ════│ New Transport mode
|
||||
```
|
||||
|
||||
### Session State Transitions
|
||||
|
||||
```
|
||||
Parent Session (A):
|
||||
Transport → ReadOnlyTransport (on subsession creation)
|
||||
ReadOnlyTransport → (expires via TTL cleanup)
|
||||
|
||||
Subsession (B):
|
||||
(created) → KKHandshaking → Transport (becomes new master)
|
||||
```
|
||||
|
||||
### Read-Only Session Semantics
|
||||
|
||||
After demotion:
|
||||
- **Can receive**: Decrypt and process incoming packets (drain in-flight)
|
||||
- **Cannot send**: Encryption blocked, returns error
|
||||
- **Cleaned up**: Via normal TTL expiration
|
||||
|
||||
### Message Formats
|
||||
|
||||
```rust
|
||||
SubsessionRequestData {
|
||||
new_receiver_index: u32, // Client-proposed index for subsession
|
||||
}
|
||||
|
||||
SubsessionKK1Data {
|
||||
new_receiver_index: u32,
|
||||
kk_message: Vec<u8>, // Noise KK message 1
|
||||
}
|
||||
|
||||
SubsessionKK2Data {
|
||||
new_receiver_index: u32,
|
||||
kk_message: Vec<u8>, // Noise KK message 2
|
||||
}
|
||||
|
||||
SubsessionReadyData {
|
||||
new_receiver_index: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### Counter Independence
|
||||
|
||||
- Each session has independent counters
|
||||
- Subsession starts at counter 0
|
||||
- No counter coordination needed between parent and subsession
|
||||
|
||||
### Failure Handling
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| KK handshake fails | Discard attempt, keep using parent |
|
||||
| Receiver index collision | Retry with new receiver_index |
|
||||
| Parent session not found | Return error, client reconnects |
|
||||
|
||||
### Security Benefits
|
||||
|
||||
1. **Forward secrecy**: Compromise of current keys doesn't expose past traffic
|
||||
2. **Key rotation**: Periodic rekeying limits exposure window
|
||||
3. **Channel isolation**: Independent streams can't cross-decrypt
|
||||
|
||||
## Security Properties
|
||||
|
||||
### Always Visible to Observer
|
||||
|
||||
Only the outer header (12 bytes) is visible after PSK establishment:
|
||||
|
||||
- Receiver index (4 bytes) - opaque, unlinkable to identity
|
||||
- Counter (8 bytes) - reveals packet ordering
|
||||
- Packet size
|
||||
|
||||
Note: Before PSK, version, reserved, and message type are also visible.
|
||||
|
||||
### Protected After PSK
|
||||
|
||||
- Outer header integrity (authenticated via AEAD AAD)
|
||||
- Inner content confidentiality (encrypted):
|
||||
- Protocol version
|
||||
- Reserved field
|
||||
- Message type
|
||||
- Payload
|
||||
- Application data (double encrypted: outer AEAD + inner Noise)
|
||||
|
||||
### Cryptographic Guarantees
|
||||
|
||||
| Property | Mechanism |
|
||||
|----------|-----------|
|
||||
| Confidentiality | ChaCha20 (outer) + Noise ChaCha20 (inner) |
|
||||
| Integrity | Poly1305 (outer) + Noise Poly1305 (inner) |
|
||||
| Replay protection | Counter validation (before decryption) |
|
||||
| Forward secrecy | Noise session keys (inner) + subsession rekeying |
|
||||
| Header authentication | AEAD associated data |
|
||||
| Key rotation | Periodic subsession creation (Noise KK) |
|
||||
|
||||
## References
|
||||
|
||||
- WireGuard Protocol - Inspiration for receiver_index and packet simplicity
|
||||
- Noise Protocol Framework - Inner encryption layer, KK pattern for subsessions
|
||||
- RFC 8439 ChaCha20-Poly1305 - AEAD cipher
|
||||
- Noise Explorer KK - https://noiseexplorer.com/patterns/KK/
|
||||
@@ -1,309 +0,0 @@
|
||||
# Nym Lewes Protocol
|
||||
|
||||
The Lewes Protocol (LP) is a secure network communication protocol implemented in Rust. It provides authenticated, encrypted sessions with replay protection and supports nested session forwarding for privacy-preserving multi-hop connections.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌────────────────┐ ┌───────────────┐
|
||||
│ Transport Layer │◄───►│ LP Session │◄───►│ LP Codec │
|
||||
│ (TCP) │ │ - State machine│ │ - Serialize │
|
||||
└─────────────────┘ │ - Noise crypto │ │ - Deserialize │
|
||||
│ - Replay prot. │ └───────────────┘
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
## Packet Structure
|
||||
|
||||
The protocol uses a length-prefixed packet format over TCP:
|
||||
|
||||
```
|
||||
Wire Format:
|
||||
┌────────────────────┬─────────────────────────────────────────┐
|
||||
│ Length (4B BE u32) │ LpPacket │
|
||||
└────────────────────┴─────────────────────────────────────────┘
|
||||
|
||||
LpPacket:
|
||||
┌──────────────────┬───────────────────┬──────────────────┐
|
||||
│ Header (16B) │ Message │ Trailer (16B) │
|
||||
├──────────────────┼───────────────────┼──────────────────┤
|
||||
│ Version (1B) │ Type (2B LE u16) │ Reserved │
|
||||
│ Reserved (3B) │ Content (var) │ (16 bytes) │
|
||||
│ SessionID (4B LE)│ │ │
|
||||
│ Counter (8B LE) │ │ │
|
||||
└──────────────────┴───────────────────┴──────────────────┘
|
||||
```
|
||||
|
||||
- **Header**: Protocol version (1), session identifier, monotonic counter
|
||||
- **Message**: Type discriminant + variable-length content
|
||||
- **Trailer**: Reserved for future use (16 bytes)
|
||||
|
||||
## Message Types
|
||||
|
||||
| Type | Value | Purpose |
|
||||
|------|-------|---------|
|
||||
| `Busy` | 0x0000 | Server congestion signal |
|
||||
| `Handshake` | 0x0001 | Noise protocol handshake messages |
|
||||
| `EncryptedData` | 0x0002 | Encrypted application data |
|
||||
| `ClientHello` | 0x0003 | Initial session negotiation |
|
||||
| `KKTRequest` | 0x0004 | KEM Key Transfer request |
|
||||
| `KKTResponse` | 0x0005 | KEM Key Transfer response |
|
||||
| `ForwardPacket` | 0x0006 | Nested session forwarding |
|
||||
|
||||
## Session Establishment
|
||||
|
||||
### Session ID
|
||||
|
||||
Sessions are identified by a deterministic 32-bit ID computed from both parties' X25519 public keys:
|
||||
|
||||
```
|
||||
session_id = make_lp_id(client_x25519_pub, gateway_x25519_pub)
|
||||
```
|
||||
|
||||
The computation is order-independent, allowing both sides to derive the same ID independently.
|
||||
|
||||
**BOOTSTRAP_SESSION_ID (0)**: A special session ID used only for the initial `ClientHello` packet, since neither side can compute the final ID until both X25519 keys are known.
|
||||
|
||||
### Handshake Flow
|
||||
|
||||
```
|
||||
┌────────┐ ┌─────────┐
|
||||
│ Client │ │ Gateway │
|
||||
└───┬────┘ └────┬────┘
|
||||
│ │
|
||||
│ 1. ClientHello (session_id=0) │
|
||||
│ [client_x25519, client_ed25519, salt]│
|
||||
│───────────────────────────────────────►│
|
||||
│ │ (computes session_id)
|
||||
│ │ (stores state machine)
|
||||
│ │
|
||||
│ 2. KKTRequest (session_id=N) │
|
||||
│ [signed request for KEM key] │
|
||||
│───────────────────────────────────────►│
|
||||
│ │
|
||||
│ 3. KKTResponse │
|
||||
│ [gateway KEM key + signature] │
|
||||
│◄───────────────────────────────────────│
|
||||
│ │
|
||||
│ 4. Noise Handshake Msg 1 │
|
||||
│ [PSQ payload + noise message] │
|
||||
│───────────────────────────────────────►│
|
||||
│ │ (derives PSK from PSQ)
|
||||
│ 5. Noise Handshake Msg 2 │
|
||||
│ [PSK handle + noise message] │
|
||||
│◄───────────────────────────────────────│
|
||||
│ │
|
||||
│ 6. Noise Handshake Msg 3 │
|
||||
│───────────────────────────────────────►│
|
||||
│ │
|
||||
│ ═══════ Session Established ═══════ │
|
||||
│ │
|
||||
│ 7. EncryptedData │
|
||||
│ [encrypted application data] │
|
||||
│◄──────────────────────────────────────►│
|
||||
│ │
|
||||
```
|
||||
|
||||
### ClientHello Data
|
||||
|
||||
```rust
|
||||
struct ClientHelloData {
|
||||
client_lp_public_key: [u8; 32], // X25519 (derived from Ed25519)
|
||||
client_ed25519_public_key: [u8; 32], // For authentication
|
||||
salt: [u8; 32], // timestamp (8B) + nonce (24B)
|
||||
}
|
||||
```
|
||||
|
||||
## Packet-Per-Connection Model
|
||||
|
||||
The gateway processes **exactly one packet per TCP connection**, then closes. State persists between connections via in-memory maps:
|
||||
|
||||
```
|
||||
TCP Connect → Receive Packet → Process → Send Response → TCP Close
|
||||
```
|
||||
|
||||
**State Storage:**
|
||||
- `handshake_states`: Maps `session_id → LpStateMachine` (during handshake)
|
||||
- `session_states`: Maps `session_id → LpSession` (after handshake complete)
|
||||
|
||||
Both maps use TTL-based cleanup to remove stale entries (default: 5 min handshake, 1 hour session).
|
||||
|
||||
### Gateway Packet Routing
|
||||
|
||||
```
|
||||
Packet Received
|
||||
│
|
||||
├─► session_id == 0 (BOOTSTRAP)
|
||||
│ └─► handle_client_hello()
|
||||
│ └─► Create state machine, store in handshake_states
|
||||
│
|
||||
├─► session_id in handshake_states
|
||||
│ └─► handle_handshake_packet()
|
||||
│ └─► Process KKT/Noise, move to session_states when complete
|
||||
│
|
||||
└─► session_id in session_states
|
||||
└─► handle_transport_packet()
|
||||
└─► Decrypt, process registration or forwarding
|
||||
```
|
||||
|
||||
## Session Forwarding
|
||||
|
||||
Forwarding enables a client to establish an independent session with an exit gateway through an entry gateway, providing network-level privacy.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Client │
|
||||
└────┬─────┘
|
||||
│ Outer LP Session (established, encrypted)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Entry Gateway │ Sees: Client IP
|
||||
│ │ Doesn't see: Exit destination
|
||||
└────────┬───────┘
|
||||
│ Forwards inner packets (TCP)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Exit Gateway │ Sees: Entry Gateway IP
|
||||
│ │ Doesn't see: Client IP
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
### ForwardPacket Message
|
||||
|
||||
```rust
|
||||
struct ForwardPacketData {
|
||||
target_gateway_identity: [u8; 32], // Exit gateway's Ed25519 key
|
||||
target_lp_address: String, // e.g., "2.2.2.2:41264"
|
||||
inner_packet_bytes: Vec<u8>, // Complete LP packet for exit
|
||||
}
|
||||
```
|
||||
|
||||
### Forwarding Flow
|
||||
|
||||
1. **Client** establishes outer LP session with entry gateway
|
||||
2. **Client** creates `ClientHello` packet for exit gateway
|
||||
3. **Client** wraps inner packet in `ForwardPacketData`:
|
||||
- Sets `target_gateway_identity` to exit's Ed25519 key
|
||||
- Sets `target_lp_address` to exit's LP listener address
|
||||
- Serializes complete LP packet as `inner_packet_bytes`
|
||||
4. **Client** encrypts `ForwardPacketData` using outer session
|
||||
5. **Client** sends as `EncryptedData` to entry gateway
|
||||
|
||||
6. **Entry Gateway** decrypts, sees `ForwardPacketData`
|
||||
7. **Entry Gateway** connects to exit gateway (new TCP)
|
||||
8. **Entry Gateway** sends `inner_packet_bytes` directly
|
||||
9. **Entry Gateway** receives exit's response
|
||||
10. **Entry Gateway** encrypts response using outer session
|
||||
11. **Entry Gateway** sends encrypted response to client
|
||||
|
||||
12. **Client** decrypts response, processes in inner session state
|
||||
|
||||
### NestedLpSession
|
||||
|
||||
The `NestedLpSession` struct manages the inner session from the client's perspective:
|
||||
|
||||
```rust
|
||||
struct NestedLpSession {
|
||||
exit_identity: [u8; 32], // Exit gateway Ed25519
|
||||
exit_address: String, // Exit LP address
|
||||
client_keypair: Arc<ed25519::KeyPair>,
|
||||
exit_public_key: ed25519::PublicKey,
|
||||
state_machine: Option<LpStateMachine>,
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```rust
|
||||
// Create nested session targeting exit gateway
|
||||
let nested = NestedLpSession::new(exit_identity, exit_address, keypair, exit_pubkey);
|
||||
|
||||
// Perform handshake through outer session
|
||||
nested.handshake_and_register(&mut outer_client).await?;
|
||||
|
||||
// Inner session now established with exit gateway
|
||||
```
|
||||
|
||||
## State Machine States
|
||||
|
||||
```
|
||||
ReadyToHandshake
|
||||
│
|
||||
▼
|
||||
KKTExchange ◄─── KKTRequest/KKTResponse
|
||||
│
|
||||
▼
|
||||
Handshaking ◄─── Noise messages + PSQ
|
||||
│
|
||||
▼
|
||||
Transport ◄─── EncryptedData
|
||||
│
|
||||
▼
|
||||
Closed
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Key Types
|
||||
- **Ed25519**: Identity keys, signing
|
||||
- **X25519**: Key exchange (derived from Ed25519 via RFC 7748)
|
||||
|
||||
### Noise Protocol
|
||||
- Pattern: `Noise_XKpsk3_25519_ChaChaPoly_SHA256`
|
||||
- Provides: Forward secrecy, mutual authentication, PSK binding
|
||||
|
||||
### PSK Derivation (PSQ)
|
||||
The Pre-Shared Key is derived via Post-Quantum Secure Key Exchange:
|
||||
1. Client encapsulates using authenticated KEM key from KKT
|
||||
2. Produces 32-byte PSK + ciphertext
|
||||
3. Gateway decapsulates to derive same PSK
|
||||
4. PSK injected into Noise at position 3
|
||||
|
||||
### Replay Protection
|
||||
|
||||
- **Monotonic counter**: Each packet has incrementing 64-bit counter
|
||||
- **Sliding window**: Bitmap tracks received counters (1024 packet window)
|
||||
- **SIMD optimized**: Branchless validation for constant-time operation
|
||||
|
||||
```rust
|
||||
// Validation flow
|
||||
validator.will_accept_branchless(counter) // Check before decrypt
|
||||
validator.mark_did_receive_branchless(counter) // Mark after decrypt
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
### LpSession Fields
|
||||
```rust
|
||||
struct LpSession {
|
||||
id: u32, // Session identifier
|
||||
is_initiator: bool, // Client or gateway role
|
||||
noise_state: NoiseState, // Noise transport state
|
||||
kkt_state: KktState, // KKT exchange progress
|
||||
psq_state: PsqState, // PSQ handshake progress
|
||||
psk_handle: Option<Vec<u8>>,// PSK handle from responder
|
||||
sending_counter: AtomicU64, // Outgoing packet counter
|
||||
receiving_counter: Validator, // Replay protection
|
||||
psk_injected: AtomicBool, // Safety: real PSK injected?
|
||||
}
|
||||
```
|
||||
|
||||
### PSK Safety
|
||||
Sessions initialize with a dummy PSK. The `psk_injected` flag must be `true` before `encrypt_data()` or `decrypt_data()` will operate, preventing accidental use of the insecure dummy.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
common/nym-lp/src/
|
||||
├── lib.rs # Module exports
|
||||
├── message.rs # LpMessage enum, ClientHelloData, ForwardPacketData
|
||||
├── packet.rs # LpPacket, LpHeader, BOOTSTRAP_SESSION_ID
|
||||
├── codec.rs # Serialization/deserialization
|
||||
├── session.rs # LpSession, cryptographic operations
|
||||
├── state_machine.rs # LpStateMachine, state transitions
|
||||
├── psk.rs # PSK derivation utilities
|
||||
└── error.rs # Error types
|
||||
```
|
||||
@@ -1,238 +0,0 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_sequential");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("sequential_counters", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
let counters: Vec<u64> = (0..size).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut validator = validator.clone();
|
||||
for &counter in &counters {
|
||||
let _ = black_box(validator.will_accept_branchless(counter));
|
||||
let _ = black_box(validator.mark_did_receive_branchless(counter));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_out_of_order");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("out_of_order_counters", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut validator = validator.clone();
|
||||
for &counter in &counters {
|
||||
let _ = black_box(validator.will_accept_branchless(counter));
|
||||
let _ = black_box(validator.mark_did_receive_branchless(counter));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_thread_safety(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_thread_safety");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("thread_safe_validator", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default()));
|
||||
let counters: Vec<u64> = (0..size).collect();
|
||||
|
||||
b.iter(|| {
|
||||
for &counter in &counters {
|
||||
let result = {
|
||||
let guard = validator.lock();
|
||||
black_box(guard.will_accept_branchless(counter))
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
let mut guard = validator.lock();
|
||||
let _ = black_box(guard.mark_did_receive_branchless(counter));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_window_sliding(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_window_sliding");
|
||||
group.sample_size(100);
|
||||
|
||||
for window_size in [128, 512, 1024] {
|
||||
group.throughput(Throughput::Elements(window_size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("window_sliding", window_size),
|
||||
&window_size,
|
||||
|b, &window_size| {
|
||||
b.iter(|| {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// First fill the window with sequential packets
|
||||
for i in 0..window_size {
|
||||
let _ = black_box(validator.mark_did_receive_branchless(i));
|
||||
}
|
||||
|
||||
// Then jump ahead to force window sliding
|
||||
let _ = black_box(validator.mark_did_receive_branchless(window_size * 3));
|
||||
|
||||
// Try some packets in the new window
|
||||
for i in (window_size * 2 + 1)..(window_size * 3) {
|
||||
let _ = black_box(validator.will_accept_branchless(i));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark operations that would benefit from SIMD optimization
|
||||
fn bench_core_operations(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_core_operations");
|
||||
group.sample_size(1000);
|
||||
|
||||
// Create validators with different states
|
||||
let empty_validator = ReceivingKeyCounterValidator::default();
|
||||
let mut half_full_validator = ReceivingKeyCounterValidator::default();
|
||||
let mut full_validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Fill validators with different patterns
|
||||
for i in 0..512 {
|
||||
half_full_validator.mark_did_receive_branchless(i).unwrap();
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
full_validator.mark_did_receive_branchless(i).unwrap();
|
||||
}
|
||||
|
||||
// Benchmark clearing operations
|
||||
group.bench_function("clear_empty_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = empty_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("clear_half_full_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = half_full_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("clear_full_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = full_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark thread safety with different thread counts
|
||||
fn bench_concurrency_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_concurrency_scaling");
|
||||
group.sample_size(50);
|
||||
|
||||
for thread_count in [1, 2, 4, 8] {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("mutex_threads", thread_count),
|
||||
&thread_count,
|
||||
|b, &thread_count| {
|
||||
b.iter(|| {
|
||||
let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default()));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for t in 0..thread_count {
|
||||
let validator_clone = Arc::clone(&validator);
|
||||
let handle = std::thread::spawn(move || {
|
||||
let mut success_count = 0;
|
||||
for i in 0..100 {
|
||||
let counter = t * 1000 + i;
|
||||
let mut guard = validator_clone.lock();
|
||||
if guard.mark_did_receive_branchless(counter as u64).is_ok() {
|
||||
success_count += 1;
|
||||
}
|
||||
}
|
||||
success_count
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
let mut total = 0;
|
||||
for handle in handles {
|
||||
total += handle.join().unwrap();
|
||||
}
|
||||
|
||||
black_box(total)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
replay_benches,
|
||||
bench_sequential_counters,
|
||||
bench_out_of_order_counters,
|
||||
bench_thread_safety,
|
||||
bench_window_sliding,
|
||||
bench_core_operations,
|
||||
bench_concurrency_scaling
|
||||
);
|
||||
criterion_main!(replay_benches);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user