Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bd6a1006b | |||
| 1678fb6b94 | |||
| 94a3599b4d | |||
| a6bc54461a | |||
| 4f0c40dab7 | |||
| 3eff6e5e3b | |||
| a519f4ccb8 | |||
| a3ba3bfc5a | |||
| 988df7cff7 | |||
| 260f8e9714 | |||
| d28d0ac39e | |||
| dce4d6b34b | |||
| bc47e9a1b2 | |||
| 3b693741b2 | |||
| 5d7f3402c7 | |||
| 2d73ea5c82 | |||
| b8d8ee6109 | |||
| a779b7a266 | |||
| cb277fe487 | |||
| b2d7b54f34 | |||
| 8bb29f4d07 | |||
| e753f24ed1 | |||
| c7cd962627 | |||
| 00467e4440 | |||
| f3d1000472 | |||
| 597aae1a20 | |||
| 40a3cd28b7 | |||
| d93d25ebae | |||
| ae0ab69bd2 | |||
| 4897cb0ce4 | |||
| 46b9d5374b | |||
| e7fcaa980f | |||
| 5fc2936d3f | |||
| 3d59a72ee8 | |||
| bb694855d5 | |||
| 9cb2655e7d | |||
| 0c3efe67fb | |||
| ca5ad94420 | |||
| 220c64100d | |||
| 505a19e32f | |||
| b75839461b | |||
| 4e4e0df721 | |||
| c3520b575f | |||
| c7a466860e | |||
| 956df22d86 | |||
| 0ca122c56b | |||
| 492eb22d74 | |||
| 9513eb458b | |||
| 8bca0698ee | |||
| 8e278866c7 | |||
| ff93657609 | |||
| d46e967b5b | |||
| 1219dcf874 | |||
| a7068ea421 | |||
| 5dc6546f1c | |||
| 5f2bc60c2c | |||
| 195c75d293 | |||
| f9827f5dd4 | |||
| b92dd2f264 | |||
| 8e792b7b93 | |||
| 061840c47c | |||
| 93834bcf28 | |||
| 89ab2630cd | |||
| fd47ebfad0 | |||
| b0d01ec12a | |||
| 470282612b | |||
| bb24b5e91d | |||
| 4222a7b684 | |||
| 73bc746cd6 | |||
| 32402d64e8 | |||
| 681b0d17b5 | |||
| 068ee7d2b7 | |||
| 39cfd532a8 | |||
| 613d496133 | |||
| 1ecb457c66 | |||
| 49faa13855 | |||
| 51e5e7825d | |||
| ded23a6271 | |||
| 801dcdda1e | |||
| e2d29f184d | |||
| a151a03181 | |||
| b19e82d4f7 | |||
| 88a4633bc4 | |||
| 660eff45dc | |||
| d4882ca276 | |||
| cfcf804b47 | |||
| b6d22abc01 | |||
| bd755385ed | |||
| 940fb09ae4 | |||
| 47af0b24f0 | |||
| 52edfdcc2f | |||
| af04afbe5e | |||
| 63f158cccb | |||
| b4aee7a1d9 | |||
| c55b215b65 | |||
| 7e8faf0ec6 | |||
| 0082b9fc50 | |||
| e16a337354 | |||
| cd0881462b | |||
| 8916b021a9 | |||
| dccdde108c | |||
| 9d661e7a7b | |||
| 76ce1bc0f9 | |||
| d3648f13c5 | |||
| 9a931b9251 | |||
| f4ba8ac2b3 | |||
| c274cc588d | |||
| 7dd1dd1a6c | |||
| 982786b678 | |||
| 561182ce6b | |||
| f4b59158df | |||
| 8e4cae2f57 | |||
| 00e4caec08 | |||
| 944b4f5aad | |||
| d99eff9178 | |||
| 0d290b6028 | |||
| 83bf9dc7cc | |||
| 8af759fb1d | |||
| 3597682b33 | |||
| 2024163be6 | |||
| a4638b8d2f | |||
| dbf571cb0a | |||
| d6ae10304d | |||
| 727d39ad72 | |||
| b513a99498 | |||
| b5d1e6a93f | |||
| e2be2b0b34 | |||
| a63a1e745e | |||
| 43d1c61b70 | |||
| 9c81a87173 | |||
| 751929fa04 | |||
| 441b46d2cc | |||
| 74b05d9066 | |||
| c1adf41643 | |||
| c1ddcc75cf | |||
| 3b20e22aa1 | |||
| b949d0fb01 | |||
| 52c47a950e | |||
| 377c22f283 | |||
| 036ae5c6dc | |||
| 7462926bcf | |||
| 7b78740327 | |||
| 9cca73bc3a | |||
| 00e8528fed | |||
| 4795fa89a9 | |||
| fb85de9ab6 | |||
| 6e62e34ac8 | |||
| 18e72c90df | |||
| fd051540aa |
@@ -3,4 +3,5 @@
|
||||
.gitignore
|
||||
**/node_modules
|
||||
**/target
|
||||
target-otel
|
||||
dist
|
||||
|
||||
@@ -3,13 +3,28 @@ name: ci-build-upload-binaries
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
feature_profile:
|
||||
description: "Select a predefined cargo feature profile"
|
||||
required: false
|
||||
default: "none"
|
||||
type: choice
|
||||
options:
|
||||
- none
|
||||
- tokio-console
|
||||
- otel
|
||||
- otel,tokio-console
|
||||
extra_features:
|
||||
description: "Additional comma-separated cargo features (e.g. feat1,feat2)"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
add_tokio_unstable:
|
||||
description: 'True to add RUSTFLAGS="--cfg tokio_unstable"'
|
||||
required: true
|
||||
description: 'Force RUSTFLAGS="--cfg tokio_unstable" (auto-set when tokio-console is selected)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
enable_deb:
|
||||
description: "True to enable cargo-deb installation and .deb package building"
|
||||
description: "Enable cargo-deb installation and .deb package building"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -21,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ arc-linux-latest ]
|
||||
platform: [arc-linux-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
@@ -36,38 +51,62 @@ jobs:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p $OUTPUT_DIR
|
||||
echo $OUTPUT_DIR
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
echo "$OUTPUT_DIR"
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
||||
- name: Resolve cargo features and RUSTFLAGS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
|
||||
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
|
||||
FEATURES=""
|
||||
PROFILE="${{ inputs.feature_profile }}"
|
||||
EXTRA="${{ inputs.extra_features }}"
|
||||
|
||||
if [[ "$PROFILE" != "none" && -n "$PROFILE" ]]; then
|
||||
FEATURES="$PROFILE"
|
||||
fi
|
||||
|
||||
if [[ -n "$EXTRA" ]]; then
|
||||
if [[ -n "$FEATURES" ]]; then
|
||||
FEATURES="${FEATURES},${EXTRA}"
|
||||
else
|
||||
FEATURES="$EXTRA"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$FEATURES" ]]; then
|
||||
echo "CARGO_FEATURES=--features ${FEATURES}" >> "$GITHUB_ENV"
|
||||
echo "::notice::Selected cargo features: $FEATURES"
|
||||
else
|
||||
echo "::notice::No additional cargo features selected"
|
||||
fi
|
||||
|
||||
if [[ "$FEATURES" == *"tokio-console"* ]] || [[ "${{ inputs.add_tokio_unstable }}" == "true" ]]; then
|
||||
echo "RUSTFLAGS=--cfg tokio_unstable" >> "$GITHUB_ENV"
|
||||
echo "::notice::Enabled RUSTFLAGS --cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
shell: bash
|
||||
run: cargo build --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
|
||||
- name: Install cargo-deb
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-deb
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
||||
shell: bash
|
||||
run: cargo install cargo-deb
|
||||
|
||||
- name: Build deb packages
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
||||
shell: bash
|
||||
run: make deb
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
@@ -84,24 +123,22 @@ jobs:
|
||||
target/release/nym-node
|
||||
retention-days: 30
|
||||
|
||||
# If this was a pull_request or nightly, upload to build server
|
||||
|
||||
- name: Prepare build output
|
||||
# if: github.event_name == 'schedule' || github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
cp target/release/nym-client $OUTPUT_DIR
|
||||
cp target/release/nym-socks5-client $OUTPUT_DIR
|
||||
cp target/release/nym-api $OUTPUT_DIR
|
||||
cp target/release/nym-network-requester $OUTPUT_DIR
|
||||
cp target/release/nymvisor $OUTPUT_DIR
|
||||
cp target/release/nym-node $OUTPUT_DIR
|
||||
cp target/release/nym-cli $OUTPUT_DIR
|
||||
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
|
||||
cp target/debian/*.deb $OUTPUT_DIR
|
||||
cp target/release/nym-client "$OUTPUT_DIR"
|
||||
cp target/release/nym-socks5-client "$OUTPUT_DIR"
|
||||
cp target/release/nym-api "$OUTPUT_DIR"
|
||||
cp target/release/nym-network-requester "$OUTPUT_DIR"
|
||||
cp target/release/nymvisor "$OUTPUT_DIR"
|
||||
cp target/release/nym-node "$OUTPUT_DIR"
|
||||
cp target/release/nym-cli "$OUTPUT_DIR"
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.enable_deb }}" == "true" ]]; then
|
||||
cp target/debian/*.deb "$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- 'nym-api/**'
|
||||
- 'nym-authenticator-client/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- 'nym-gateway-probe/**'
|
||||
- 'nym-ip-packet-client/**'
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
@@ -89,7 +90,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
|
||||
|
||||
- name: Clippy (non-macos)
|
||||
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
|
||||
@@ -104,6 +105,14 @@ jobs:
|
||||
with:
|
||||
command: build
|
||||
|
||||
# only build on linux because of wg FFI bindings of its dependency (network probe)
|
||||
- name: Build nym-node-status-api (linux only)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p nym-node-status-api
|
||||
|
||||
- name: Build all examples
|
||||
if: contains(matrix.os, 'linux')
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "nym-node-status-api/**"
|
||||
- "nym-node-status-api/nym-node-status-api/**"
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--allow-branch ${{ github.ref_name }} \
|
||||
--no-git-commit \
|
||||
|
||||
# Dry run may show cascading dependency errors because packages aren't
|
||||
# actually uploaded - these are expected and ignored. We check for real
|
||||
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
|
||||
- name: Publish (dry run)
|
||||
run: |
|
||||
output=$(cargo workspaces publish --dry-run --allow-dirty 2>&1) || true
|
||||
echo "$output"
|
||||
|
||||
# Check for real errors (not cascading dependency errors)
|
||||
# Cascading errors mention "crates.io index", real errors mention "Cargo.toml"
|
||||
echo "$output" | grep -i "Cargo.toml" && exit 1 || true
|
||||
|
||||
# Show the list of packages published
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,59 @@
|
||||
# This is in case, for whatever reason, a publication run fails, and we need to restart halfway down the list, of unbumped/unpublished crates.
|
||||
name: Resume crates.io publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
CRATES=$(cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2)
|
||||
|
||||
if [ -z "$CRATES" ]; then
|
||||
echo "Error: No crates found after '${{ inputs.resume_after }}'"
|
||||
echo "Check the crate name matches exactly from 'cargo workspaces plan'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Will publish the following crates:"
|
||||
echo "$CRATES"
|
||||
echo ""
|
||||
|
||||
echo "$CRATES" | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting ${{ inputs.publish_interval }}s before next publish..."
|
||||
sleep ${{ inputs.publish_interval }}
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Publish crates to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes (600 for first publish, 60 after)"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
backup_author:
|
||||
description: "Second team member added as owner of the crate"
|
||||
required: false
|
||||
default: "jstuczyn"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
cargo workspaces publish \
|
||||
--publish-as-is \
|
||||
--publish-interval ${{ inputs.publish_interval }}
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
|
||||
- name: Add team as crate owners
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM="github:nymtech:core"
|
||||
echo "Checking and adding $TEAM as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM"; then
|
||||
echo " $TEAM already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM as owner of $crate..."
|
||||
cargo owner --add "$TEAM" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
|
||||
- name: Add secondary member as crate owner
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM_MEMBER="${{ inputs.backup_author }}"
|
||||
echo "Checking and adding $TEAM_MEMBER as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM_MEMBER"; then
|
||||
echo " $TEAM_MEMBER already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM_MEMBER as owner of $crate..."
|
||||
cargo owner --add "$TEAM_MEMBER" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Bump crate versions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to set (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
version-bump:
|
||||
runs-on: arc-linux-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--yes
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git add -A
|
||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
||||
git push
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,21 @@
|
||||
name: ci-docs-linkcheck
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "documentation/docs/**"
|
||||
- ".github/workflows/ci-docs-linkcheck.yml"
|
||||
- "lychee.toml"
|
||||
|
||||
jobs:
|
||||
linkcheck:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check links
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: ${{ github.workspace }}/documentation/docs/ --config ${{ github.workspace }}/lychee.toml --root-dir ${{ github.workspace }}/documentation/docs/pages/
|
||||
fail: true
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--no-git-tag \
|
||||
--no-git-push \
|
||||
--yes
|
||||
|
||||
# Note: Dry run may show cascading dependency errors because packages
|
||||
# aren't actually uploaded. Check if the missing dependency has an
|
||||
# "aborting upload due to dry run" message earlier in the output - if so,
|
||||
# it would succeed in a real publish since cargo-workspaces publishes in
|
||||
# dependency order. cargo-workspaces doesn't fail on err, so there isn't
|
||||
# a good way to check this at the moment.
|
||||
- name: Publish (dry run)
|
||||
run: cargo workspaces publish --from-git --dry-run --allow-dirty
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# - name: Configure git
|
||||
# run: |
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-push \
|
||||
--no-git-tag \
|
||||
--yes
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo workspaces publish --from-git --no-git-commit
|
||||
|
||||
# - name: Push version commit
|
||||
# run: |
|
||||
# git push origin HEAD
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Resume publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2 | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting 600s before next publish..."
|
||||
sleep 600
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
+1
-1
@@ -67,7 +67,6 @@ nym-api/redocly/formatted-openapi.json
|
||||
*.profraw
|
||||
.beads
|
||||
CLAUDE.md
|
||||
docs
|
||||
.claude
|
||||
.superego
|
||||
|
||||
@@ -77,3 +76,4 @@ docs
|
||||
.claude/settings.json
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
|
||||
+162
@@ -4,6 +4,168 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.3-parmigiano] (2026-02-10)
|
||||
|
||||
- chore: disable LP on parmigiano branch ([#6422])
|
||||
- revert mixnet-based client fautly changes from LP ([#6420])
|
||||
- [LP fix] Registration client with fallback ([#6419])
|
||||
- Lp/ip pool fixes ([#6412])
|
||||
- [LP-fix] expose wg psk for the vpn-client ([#6411])
|
||||
- LP-fix : configurable LP timeouts ([#6409])
|
||||
- LP-fix : add LP x25519 key to the description ([#6408])
|
||||
- use rng that is Send ([#6404])
|
||||
- use local kem key instead of local x25519 ([#6402])
|
||||
- [LP Gateway Probe] CLI and behavior improvements ([#6400])
|
||||
- lp: attempt to negotiate (and use) protocol version ([#6399])
|
||||
- bugfix: use correct reserved bytes when parsing LpHeader ([#6398])
|
||||
- Lp/bugfix/share ip allocation ([#6395])
|
||||
- feat: use hex-encoding for lp key digests ([#6394])
|
||||
- Add socks5 test to gateway-probe ([#6393])
|
||||
- [LP Gateway probe] Improve file structure ([#6391])
|
||||
- Reduce the size of `HttpClientError` ([#6390])
|
||||
- Lp/two step dvpn reg ([#6386])
|
||||
- Add extra configured nym api url to env ([#6382])
|
||||
- Lp/dvpn psk injection ([#6378])
|
||||
- LP: include signing key digests to LP responses ([#6373])
|
||||
- Lp/use noise x25519 ([#6372])
|
||||
- Topology fallback ([#6363])
|
||||
- NS API socks5 support ([#6361])
|
||||
- LP: modified LPRemotePeer to dynamically choose required KEM key hash ([#6358])
|
||||
- Fix KKT Integration into LP ([#6357])
|
||||
- LP: mixnet reg fixes ([#6356])
|
||||
- LP: announced KEM key hashes ([#6349])
|
||||
- revert faulty drop changes ([#6346])
|
||||
- small qol changes ([#6340])
|
||||
- Apply configured api urls via env ([#6337])
|
||||
- lp chore: make sure to take reserved bytes straight from the header ([#6336])
|
||||
- LP: x25519/ed22519 cleanup round ([#6335])
|
||||
- Lp/encrypted kkt ([#6331])
|
||||
- ensure packets with incompatible versions are rejected ([#6326])
|
||||
- standarise lp serialisation: ([#6324])
|
||||
- Upgrade to def_guard_wireguard v0.8.0 ([#6315])
|
||||
- Max/crates io prep v2 ([#6270])
|
||||
|
||||
[#6422]: https://github.com/nymtech/nym/pull/6422
|
||||
[#6420]: https://github.com/nymtech/nym/pull/6420
|
||||
[#6419]: https://github.com/nymtech/nym/pull/6419
|
||||
[#6412]: https://github.com/nymtech/nym/pull/6412
|
||||
[#6411]: https://github.com/nymtech/nym/pull/6411
|
||||
[#6409]: https://github.com/nymtech/nym/pull/6409
|
||||
[#6408]: https://github.com/nymtech/nym/pull/6408
|
||||
[#6404]: https://github.com/nymtech/nym/pull/6404
|
||||
[#6402]: https://github.com/nymtech/nym/pull/6402
|
||||
[#6400]: https://github.com/nymtech/nym/pull/6400
|
||||
[#6399]: https://github.com/nymtech/nym/pull/6399
|
||||
[#6398]: https://github.com/nymtech/nym/pull/6398
|
||||
[#6395]: https://github.com/nymtech/nym/pull/6395
|
||||
[#6394]: https://github.com/nymtech/nym/pull/6394
|
||||
[#6393]: https://github.com/nymtech/nym/pull/6393
|
||||
[#6391]: https://github.com/nymtech/nym/pull/6391
|
||||
[#6390]: https://github.com/nymtech/nym/pull/6390
|
||||
[#6386]: https://github.com/nymtech/nym/pull/6386
|
||||
[#6382]: https://github.com/nymtech/nym/pull/6382
|
||||
[#6378]: https://github.com/nymtech/nym/pull/6378
|
||||
[#6373]: https://github.com/nymtech/nym/pull/6373
|
||||
[#6372]: https://github.com/nymtech/nym/pull/6372
|
||||
[#6363]: https://github.com/nymtech/nym/pull/6363
|
||||
[#6361]: https://github.com/nymtech/nym/pull/6361
|
||||
[#6358]: https://github.com/nymtech/nym/pull/6358
|
||||
[#6357]: https://github.com/nymtech/nym/pull/6357
|
||||
[#6356]: https://github.com/nymtech/nym/pull/6356
|
||||
[#6349]: https://github.com/nymtech/nym/pull/6349
|
||||
[#6346]: https://github.com/nymtech/nym/pull/6346
|
||||
[#6340]: https://github.com/nymtech/nym/pull/6340
|
||||
[#6337]: https://github.com/nymtech/nym/pull/6337
|
||||
[#6336]: https://github.com/nymtech/nym/pull/6336
|
||||
[#6335]: https://github.com/nymtech/nym/pull/6335
|
||||
[#6331]: https://github.com/nymtech/nym/pull/6331
|
||||
[#6326]: https://github.com/nymtech/nym/pull/6326
|
||||
[#6324]: https://github.com/nymtech/nym/pull/6324
|
||||
[#6315]: https://github.com/nymtech/nym/pull/6315
|
||||
[#6270]: https://github.com/nymtech/nym/pull/6270
|
||||
|
||||
## [2026.2-oscypek] (2026-01-27)
|
||||
|
||||
- bugfix: downgrade gateway protocol to clients proposed version ([#6377])
|
||||
- bugfix: ack fix ([#6364])
|
||||
- Cherry pick/api urls oscypek ([#6348])
|
||||
- Update nix to v0.30.1 ([#6316])
|
||||
- Deriving Serialize for GatewayData ([#6314])
|
||||
- chore: remove repetitive words in comment ([#6313])
|
||||
- [bugfix] Sqlite transaction escalation was causing errors ([#6299])
|
||||
- DNS static table pre-resolve ([#6297])
|
||||
- Add Copy+Clone to nym_api_provider::Config ([#6296])
|
||||
- [chore] clippy fixes and use fixed rust version from REQUIRED_RUSTC_VERSION ([#6295])
|
||||
- build(deps): bump SonarSource/sonarqube-scan-action from 6 to 7 ([#6294])
|
||||
- build(deps): bump mikefarah/yq from 4.49.2 to 4.50.1 ([#6293])
|
||||
- build(deps): bump actions/upload-artifact from 5 to 6 ([#6292])
|
||||
- build(deps): bump actions/download-artifact from 6 to 7 ([#6291])
|
||||
- build(deps): bump js-yaml from 3.14.1 to 3.14.2 in /documentation/docs ([#6290])
|
||||
- build(deps): bump next from 15.4.9 to 15.4.10 in /nym-node-status-api/nym-node-status-ui ([#6289])
|
||||
- build(deps): bump next from 14.2.33 to 14.2.35 ([#6288])
|
||||
- LP Registration + Telescoping + Gateway Probe Localnet Mode ([#6286])
|
||||
- build(deps): bump next from 15.5.7 to 15.5.9 in /documentation/docs ([#6285])
|
||||
- build(deps): bump next from 15.4.7 to 15.4.9 in /nym-node-status-api/nym-node-status-ui ([#6284])
|
||||
- Minor DNS improvements ([#6283])
|
||||
- HTTP client without default features ([#6281])
|
||||
- DNS: reduce number of attempts ([#6278])
|
||||
- [bugfix] use proper mixing delay instead of poisson delay in cover traffic ([#6269])
|
||||
- build(deps): bump node-forge from 1.3.1 to 1.3.3 in /wasm/zknym-lib/internal-dev ([#6261])
|
||||
- build(deps-dev): bump node-forge from 1.3.1 to 1.3.3 in /wasm/mix-fetch/internal-dev ([#6260])
|
||||
- build(deps-dev): bump node-forge from 1.3.1 to 1.3.2 in /wasm/client/internal-dev ([#6251])
|
||||
- build(deps): bump node-forge from 1.3.1 to 1.3.2 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6250])
|
||||
- [Feature] Fallback gateway listener and remove legacy key support ([#6249])
|
||||
- build(deps-dev): bump node-forge from 1.3.0 to 1.3.2 in /clients/native/examples/js-examples/websocket ([#6248])
|
||||
- build(deps): bump node-forge from 1.3.1 to 1.3.2 ([#6246])
|
||||
- build(deps): bump pnpm/action-setup from 4.1.0 to 4.2.0 ([#6245])
|
||||
- build(deps): bump actions/download-artifact from 5 to 6 ([#6244])
|
||||
- build(deps): bump actions/checkout from 4 to 6 ([#6243])
|
||||
- build(deps): bump mikefarah/yq from 4.48.1 to 4.49.2 ([#6242])
|
||||
- build(deps): bump actions/upload-artifact from 4 to 5 ([#6241])
|
||||
- fix: fix assertion ([#6238])
|
||||
- Initial changes to support extra configurable parameters and to print… ([#6237])
|
||||
- Data Observatory ([#6172])
|
||||
|
||||
[#6377]: https://github.com/nymtech/nym/pull/6377
|
||||
[#6364]: https://github.com/nymtech/nym/pull/6364
|
||||
[#6348]: https://github.com/nymtech/nym/pull/6348
|
||||
[#6316]: https://github.com/nymtech/nym/pull/6316
|
||||
[#6314]: https://github.com/nymtech/nym/pull/6314
|
||||
[#6313]: https://github.com/nymtech/nym/pull/6313
|
||||
[#6299]: https://github.com/nymtech/nym/pull/6299
|
||||
[#6297]: https://github.com/nymtech/nym/pull/6297
|
||||
[#6296]: https://github.com/nymtech/nym/pull/6296
|
||||
[#6295]: https://github.com/nymtech/nym/pull/6295
|
||||
[#6294]: https://github.com/nymtech/nym/pull/6294
|
||||
[#6293]: https://github.com/nymtech/nym/pull/6293
|
||||
[#6292]: https://github.com/nymtech/nym/pull/6292
|
||||
[#6291]: https://github.com/nymtech/nym/pull/6291
|
||||
[#6290]: https://github.com/nymtech/nym/pull/6290
|
||||
[#6289]: https://github.com/nymtech/nym/pull/6289
|
||||
[#6288]: https://github.com/nymtech/nym/pull/6288
|
||||
[#6286]: https://github.com/nymtech/nym/pull/6286
|
||||
[#6285]: https://github.com/nymtech/nym/pull/6285
|
||||
[#6284]: https://github.com/nymtech/nym/pull/6284
|
||||
[#6283]: https://github.com/nymtech/nym/pull/6283
|
||||
[#6281]: https://github.com/nymtech/nym/pull/6281
|
||||
[#6278]: https://github.com/nymtech/nym/pull/6278
|
||||
[#6269]: https://github.com/nymtech/nym/pull/6269
|
||||
[#6261]: https://github.com/nymtech/nym/pull/6261
|
||||
[#6260]: https://github.com/nymtech/nym/pull/6260
|
||||
[#6251]: https://github.com/nymtech/nym/pull/6251
|
||||
[#6250]: https://github.com/nymtech/nym/pull/6250
|
||||
[#6249]: https://github.com/nymtech/nym/pull/6249
|
||||
[#6248]: https://github.com/nymtech/nym/pull/6248
|
||||
[#6246]: https://github.com/nymtech/nym/pull/6246
|
||||
[#6245]: https://github.com/nymtech/nym/pull/6245
|
||||
[#6244]: https://github.com/nymtech/nym/pull/6244
|
||||
[#6243]: https://github.com/nymtech/nym/pull/6243
|
||||
[#6242]: https://github.com/nymtech/nym/pull/6242
|
||||
[#6241]: https://github.com/nymtech/nym/pull/6241
|
||||
[#6238]: https://github.com/nymtech/nym/pull/6238
|
||||
[#6237]: https://github.com/nymtech/nym/pull/6237
|
||||
[#6172]: https://github.com/nymtech/nym/pull/6172
|
||||
|
||||
## [2026.1-niolo] (2026-01-13)
|
||||
|
||||
- bugfix: mozzarella -> niolo config migration ([#6259])
|
||||
|
||||
Generated
+1976
-1781
File diff suppressed because it is too large
Load Diff
+111
-108
@@ -174,7 +174,7 @@ members = [
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests", "common/nym-lp-transport",
|
||||
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -185,7 +185,6 @@ default-members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-node",
|
||||
"nym-node-status-api/nym-node-status-agent",
|
||||
"nym-node-status-api/nym-node-status-api",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -206,7 +205,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.85"
|
||||
readme = "README.md"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -233,7 +232,7 @@ blake3 = "1.7.0"
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
bytes = "1.11.1"
|
||||
cargo_metadata = "0.19.2"
|
||||
celes = "2.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
@@ -304,13 +303,16 @@ ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
log = "0.4"
|
||||
mime = "0.3.17"
|
||||
mock_instant = "0.6.0"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
nix = "0.30.1"
|
||||
notify = "5.1.0"
|
||||
num_enum = "0.7.5"
|
||||
once_cell = "1.21.3"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
opentelemetry = "0.31.0"
|
||||
opentelemetry_sdk = "0.31.0"
|
||||
opentelemetry-otlp = "0.31.0"
|
||||
tonic = "0.14.4"
|
||||
parking_lot = "0.12.3"
|
||||
pem = "0.8"
|
||||
petgraph = "0.6.5"
|
||||
@@ -320,12 +322,13 @@ publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
@@ -367,9 +370,8 @@ tower = "0.5.2"
|
||||
tower-http = "0.6.6"
|
||||
tracing = "0.1.41"
|
||||
tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-subscriber = "0.3.20"
|
||||
tracing-tree = "0.2.2"
|
||||
tracing-indicatif = "0.3.9"
|
||||
tracing-test = "0.2.5"
|
||||
ts-rs = "10.1.0"
|
||||
@@ -381,7 +383,7 @@ url = "2.5"
|
||||
utoipa = "5.2"
|
||||
utoipa-swagger-ui = "8.1"
|
||||
utoipauto = "0.2"
|
||||
uuid = "*"
|
||||
uuid = "1.19.0"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
@@ -391,105 +393,106 @@ zeroize = "1.7.0"
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.1", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
|
||||
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.1", path = "common/task" }
|
||||
nym-tun = { version = "1.20.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.1", path = "common/topology" }
|
||||
nym-types = { version = "1.20.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.4", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.4", path = "common/task" }
|
||||
nym-tun = { version = "1.20.4", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.4", path = "common/topology" }
|
||||
nym-types = { version = "1.20.4", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.20.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.68"
|
||||
version = "1.1.70"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
+1004
-974
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack": "^5.105.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.68"
|
||||
version = "1.1.70"
|
||||
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"
|
||||
|
||||
@@ -18,6 +18,7 @@ mod util;
|
||||
mod version;
|
||||
|
||||
pub use error::Error;
|
||||
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
pub use v6 as latest;
|
||||
pub use version::AuthenticatorVersion;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::traits::{
|
||||
TopUpBandwidthResponse, UpgradeModeStatus,
|
||||
};
|
||||
use crate::{v2, v3, v4, v5, v6};
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthenticatorResponse {
|
||||
@@ -17,6 +18,17 @@ pub enum AuthenticatorResponse {
|
||||
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
pub struct SerialisedResponse {
|
||||
pub bytes: Vec<u8>,
|
||||
pub reply_to: Option<Recipient>,
|
||||
}
|
||||
|
||||
impl SerialisedResponse {
|
||||
pub fn new(bytes: Vec<u8>, reply_to: Option<Recipient>) -> Self {
|
||||
Self { bytes, reply_to }
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeModeStatus for AuthenticatorResponse {
|
||||
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
|
||||
match self {
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_network_defaults::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub fn authenticator_ipv6_to_ipv4(addr: Ipv6Addr) -> Ipv4Addr {
|
||||
let before_last_byte = addr.octets()[14];
|
||||
let last_byte = addr.octets()[15];
|
||||
|
||||
Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticator_ipv4_to_ipv6(addr: Ipv4Addr) -> Ipv6Addr {
|
||||
let before_last_byte = addr.octets()[2];
|
||||
let last_byte = addr.octets()[3];
|
||||
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -56,27 +56,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -54,27 +54,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::BandwidthClaim;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
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;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::time::SystemTime;
|
||||
use std::{fmt, ops::Deref, str::FromStr};
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
@@ -20,13 +19,11 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
|
||||
use sha2::Sha256;
|
||||
|
||||
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
|
||||
pub type PrivateIPs = HashMap<IpPair, Taken>;
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
pub type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
pub type Nonce = u64;
|
||||
pub type Taken = Option<SystemTime>;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct IpPair {
|
||||
@@ -54,27 +51,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
|
||||
impl BandwidthTicketProvider for MockBandwidthController {
|
||||
async fn get_ecash_ticket(
|
||||
&self,
|
||||
_ticket_type: TicketType,
|
||||
ticket_type: TicketType,
|
||||
_gateway_id: PublicKey,
|
||||
tickets_to_spend: u32,
|
||||
) -> Result<PreparedCredential, BandwidthControllerError> {
|
||||
@@ -100,6 +100,10 @@ impl BandwidthTicketProvider for MockBandwidthController {
|
||||
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
|
||||
.expect("Failed to deserialize test credential - this is a bug in the test harness");
|
||||
|
||||
// change the ticket type to the requested ticket
|
||||
// note that verification outside mocks is going to fail
|
||||
credential.payment.t_type = ticket_type.to_repr() as u8;
|
||||
|
||||
// Update spend_date to today to pass validation
|
||||
credential.spend_date = OffsetDateTime::now_utc().date();
|
||||
|
||||
|
||||
@@ -57,3 +57,22 @@ where
|
||||
Ok(Some(token))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box<T> {
|
||||
async fn get_ecash_ticket(
|
||||
&self,
|
||||
ticket_type: TicketType,
|
||||
gateway_id: ed25519::PublicKey,
|
||||
tickets_to_spend: u32,
|
||||
) -> Result<PreparedCredential, BandwidthControllerError> {
|
||||
(**self)
|
||||
.get_ecash_ticket(ticket_type, gateway_id, tickets_to_spend)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
|
||||
(**self).get_upgrade_mode_token().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,15 @@ serde_json = { workspace = true, optional = true }
|
||||
|
||||
## tracing
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||
tracing-tree = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
|
||||
opentelemetry = { workspace = true, features = ["trace"], optional = true }
|
||||
|
||||
## otel-otlp (modern OTLP export to SigNoz/any OTLP collector)
|
||||
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
|
||||
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "trace", "tls-roots"], optional = true }
|
||||
tonic = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
@@ -35,13 +38,14 @@ default = []
|
||||
openapi = ["utoipa"]
|
||||
output_format = ["serde_json", "dep:clap"]
|
||||
bin_info_schema = ["schemars"]
|
||||
basic_tracing = ["dep:tracing", "tracing-subscriber"]
|
||||
tracing = [
|
||||
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
|
||||
otel-otlp = [
|
||||
"basic_tracing",
|
||||
"tracing-tree",
|
||||
"opentelemetry-jaeger",
|
||||
"tracing-opentelemetry",
|
||||
"opentelemetry",
|
||||
"dep:opentelemetry",
|
||||
"dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry-otlp",
|
||||
"dep:tracing-opentelemetry",
|
||||
"dep:tonic",
|
||||
]
|
||||
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
|
||||
models = []
|
||||
|
||||
@@ -124,6 +124,10 @@ impl BinaryBuildInformation {
|
||||
}
|
||||
}
|
||||
|
||||
// to whoever is thinking of modifying this struct.
|
||||
// you MUST NOT change its structure in any way - adding, removing or changing fields
|
||||
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
|
||||
// even if you put `#[serde(default)]` all over the place
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[cfg_attr(feature = "bin_info_schema", derive(schemars::JsonSchema))]
|
||||
|
||||
@@ -4,16 +4,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
// Re-export tracing_subscriber for consumers that need to compose layers
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub use tracing_subscriber;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_tree;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -69,40 +62,106 @@ pub fn setup_tracing_logger() {
|
||||
build_tracing_logger().init()
|
||||
}
|
||||
|
||||
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason
|
||||
#[cfg(feature = "tracing")]
|
||||
#[macro_export]
|
||||
macro_rules! setup_tracing {
|
||||
($service_name: expr) => {
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
/// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
|
||||
///
|
||||
/// This produces a layer compatible with `tracing_subscriber::registry()` that
|
||||
/// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
|
||||
///
|
||||
/// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
|
||||
/// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
|
||||
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
|
||||
/// or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
|
||||
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
|
||||
/// sent as the `signoz-ingestion-key` gRPC metadata header on every export.
|
||||
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
|
||||
/// Attached as the `deployment.environment` OTel resource attribute.
|
||||
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
|
||||
/// Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
|
||||
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
|
||||
/// unbounded blocking if the collector is slow or unreachable.
|
||||
#[cfg(feature = "otel-otlp")]
|
||||
pub fn init_otel_layer<S>(
|
||||
service_name: &str,
|
||||
endpoint: &str,
|
||||
ingestion_key: Option<&str>,
|
||||
environment: &str,
|
||||
sample_ratio: f64,
|
||||
export_timeout_secs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
|
||||
opentelemetry_sdk::trace::SdkTracerProvider,
|
||||
),
|
||||
Box<dyn std::error::Error + Send + Sync>,
|
||||
>
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
{
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_otlp::WithTonicConfig;
|
||||
use opentelemetry_sdk::trace::Sampler;
|
||||
use std::time::Duration;
|
||||
|
||||
let registry = nym_bin_common::logging::tracing_subscriber::Registry::default()
|
||||
.with(nym_bin_common::logging::tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(
|
||||
nym_bin_common::logging::tracing_tree::HierarchicalLayer::new(4)
|
||||
.with_targets(true)
|
||||
.with_bracketed_fields(true),
|
||||
);
|
||||
// Validate endpoint URI early to fail with a clear message
|
||||
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
|
||||
return Err(format!(
|
||||
"invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let tracer = nym_bin_common::logging::opentelemetry_jaeger::new_collector_pipeline()
|
||||
.with_endpoint("http://44.199.230.10:14268/api/traces")
|
||||
.with_service_name($service_name)
|
||||
.with_isahc()
|
||||
.with_trace_config(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::config().with_sampler(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::Sampler::TraceIdRatioBased(
|
||||
0.1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.install_batch(nym_bin_common::logging::opentelemetry::runtime::Tokio)
|
||||
.expect("Could not init tracer");
|
||||
let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
|
||||
|
||||
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
let mut builder = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(export_timeout_secs));
|
||||
|
||||
registry.with(telemetry).init();
|
||||
};
|
||||
// Explicitly configure TLS when the endpoint uses HTTPS
|
||||
if endpoint.starts_with("https://") {
|
||||
builder =
|
||||
builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
|
||||
}
|
||||
|
||||
if let Some(key) = ingestion_key {
|
||||
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||
metadata.insert(
|
||||
"signoz-ingestion-key",
|
||||
key.parse()
|
||||
.map_err(|_| "invalid ingestion key format (value redacted)")?,
|
||||
);
|
||||
builder = builder.with_metadata(metadata);
|
||||
}
|
||||
|
||||
let exporter = builder
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;
|
||||
|
||||
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||
.with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
|
||||
.with_batch_exporter(exporter)
|
||||
.with_resource(
|
||||
opentelemetry_sdk::Resource::builder()
|
||||
.with_service_name(service_name.to_owned())
|
||||
.with_attribute(opentelemetry::KeyValue::new(
|
||||
"deployment.environment",
|
||||
environment.to_owned(),
|
||||
))
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
|
||||
let tracer = tracer_provider.tracer(service_name.to_owned());
|
||||
|
||||
Ok((
|
||||
tracing_opentelemetry::layer().with_tracer(tracer),
|
||||
tracer_provider,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn banner(crate_name: &str, crate_version: &str) -> String {
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
error::ClientCoreError,
|
||||
};
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
|
||||
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
|
||||
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
|
||||
|
||||
pub use nym_client_core_gateways_storage as gateways_storage;
|
||||
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
|
||||
|
||||
@@ -128,54 +128,95 @@ impl ManagedConnection {
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
debug!(
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "timeout",
|
||||
"failed to connect to {address} within {:?}",
|
||||
self.connection_timeout
|
||||
);
|
||||
|
||||
// we failed to connect - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Take whatever the receiver channel produces and put it on the connection.
|
||||
// We could have as well used conn.send_all(receiver.map(Ok)), but considering we don't care
|
||||
// about neither receiver nor the connection, it doesn't matter which one gets consumed
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!("Failed to forward packets to {address}: {err}");
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped",
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -272,16 +313,18 @@ impl SendWithoutResponse for Client {
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
debug!("establishing initial connection to {address}");
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
|
||||
// for sending for as soon as the connection is created
|
||||
debug!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "not_connected",
|
||||
"establishing initial connection to {address}"
|
||||
);
|
||||
self.make_connection(address, framed_packet);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
@@ -289,15 +332,24 @@ impl SendWithoutResponse for Client {
|
||||
));
|
||||
};
|
||||
|
||||
let channel_capacity = sender.channel.max_capacity();
|
||||
let channel_available = sender.channel.capacity();
|
||||
let channel_used = channel_capacity - channel_available;
|
||||
|
||||
let sending_res = sender.channel.try_send(framed_packet);
|
||||
drop(sender);
|
||||
|
||||
sending_res.map_err(|err| {
|
||||
match err {
|
||||
TrySendError::Full(_) => {
|
||||
debug!("Connection to {address} seems to not be able to handle all the traffic - dropping the current packet");
|
||||
// it's not a 'big' error, but we did not manage to send the packet
|
||||
// if the queue is full, we can't really do anything but to drop the packet
|
||||
warn!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "full_dropped",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"dropping packet: connection buffer to {address} is full ({channel_used}/{channel_capacity})"
|
||||
);
|
||||
io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"connection queue is full",
|
||||
@@ -305,11 +357,13 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
TrySendError::Closed(dropped) => {
|
||||
debug!(
|
||||
"Connection to {address} seems to be dead. attempting to re-establish it...",
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "closed_reconnecting",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"connection to {address} dead, attempting re-establishment"
|
||||
);
|
||||
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue
|
||||
// it up to send it as soon as the connection is re-established
|
||||
self.make_connection(address, dropped);
|
||||
io::Error::new(
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
|
||||
@@ -76,7 +76,7 @@ features = ["json"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
|
||||
workspace = true
|
||||
features = ["json", "rustls-tls"]
|
||||
features = ["json", "rustls"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
|
||||
MixnodeCoreStatusResponse, NymNodeDescription,
|
||||
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
@@ -273,48 +273,23 @@ impl<C, S> Client<C, S> {
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
// TODO: combine with NymApiClient...
|
||||
// #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
|
||||
pub async fn get_all_cached_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
|
||||
|
||||
descriptions.append(&mut res.data);
|
||||
if descriptions.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(descriptions)
|
||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||
Ok(self.nym_api.get_all_described_nodes().await?)
|
||||
}
|
||||
|
||||
// TODO: combine with NymApiClient...
|
||||
// pub async fn get_all_cached_described_nodes_v2(
|
||||
// &self,
|
||||
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
// Ok(self.nym_api.get_all_described_nodes_v2().await?)
|
||||
// }
|
||||
|
||||
pub async fn get_all_cached_bonded_nym_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut bonds = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
|
||||
|
||||
bonds.append(&mut res.data);
|
||||
if bonds.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bonds)
|
||||
self.nym_api.get_all_bonded_nym_nodes().await
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
@@ -498,9 +473,10 @@ impl NymApiClient {
|
||||
Ok(self.nym_api.health().await?)
|
||||
}
|
||||
|
||||
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
pub async fn get_all_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
|
||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
@@ -519,6 +495,30 @@ impl NymApiClient {
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
// pub async fn get_all_described_nodes_v2(
|
||||
// &self,
|
||||
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
// let mut page = 0;
|
||||
// let mut descriptions = Vec::new();
|
||||
//
|
||||
// loop {
|
||||
// let mut res = self
|
||||
// .nym_api
|
||||
// .get_nodes_described_v2(Some(page), None)
|
||||
// .await?;
|
||||
//
|
||||
// descriptions.append(&mut res.data);
|
||||
// if descriptions.len() < res.pagination.total {
|
||||
// page += 1
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(descriptions)
|
||||
// }
|
||||
|
||||
pub async fn get_all_bonded_nym_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||
|
||||
@@ -17,7 +17,8 @@ use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
@@ -116,11 +117,12 @@ pub trait NymApiClientExt: ApiClient {
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
// #[deprecated(note = "use .get_nodes_described_v2 instead")]
|
||||
async fn get_nodes_described(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
|
||||
) -> Result<PaginatedResponse<NymNodeDescriptionV1>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(page) = page {
|
||||
@@ -142,6 +144,33 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
// #[tracing::instrument(level = "debug", skip_all)]
|
||||
// async fn get_nodes_described_v2(
|
||||
// &self,
|
||||
// page: Option<u32>,
|
||||
// per_page: Option<u32>,
|
||||
// ) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
|
||||
// let mut params = Vec::new();
|
||||
//
|
||||
// if let Some(page) = page {
|
||||
// params.push(("page", page.to_string()))
|
||||
// }
|
||||
//
|
||||
// if let Some(per_page) = per_page {
|
||||
// params.push(("per_page", per_page.to_string()))
|
||||
// }
|
||||
//
|
||||
// self.get_json(
|
||||
// &[
|
||||
// routes::V2_API_VERSION,
|
||||
// routes::NYM_NODES_ROUTES,
|
||||
// routes::NYM_NODES_DESCRIBED,
|
||||
// ],
|
||||
// ¶ms,
|
||||
// )
|
||||
// .await
|
||||
// }
|
||||
|
||||
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||
self.get_rewarded_set().await
|
||||
}
|
||||
@@ -273,7 +302,9 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescription>, NymAPIError> {
|
||||
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
// #[allow(deprecated)]
|
||||
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescriptionV1>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
@@ -292,6 +323,25 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
// async fn (&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
|
||||
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
// let mut page = 0;
|
||||
// let mut descriptions = Vec::new();
|
||||
//
|
||||
// loop {
|
||||
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
|
||||
//
|
||||
// descriptions.append(&mut res.data);
|
||||
// if descriptions.len() < res.pagination.total {
|
||||
// page += 1
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(descriptions)
|
||||
// }
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_nym_nodes(
|
||||
&self,
|
||||
|
||||
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -22,6 +22,8 @@ pub mod ecash;
|
||||
pub mod error;
|
||||
pub mod upgrade_mode;
|
||||
|
||||
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
|
||||
|
||||
// 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];
|
||||
@@ -111,6 +113,13 @@ impl CredentialVerifier {
|
||||
}
|
||||
|
||||
pub async fn verify(&mut self) -> Result<i64> {
|
||||
if self.ecash_verifier.is_mock() {
|
||||
// if we're in the mock mode (local testing), skip cryptographic verification
|
||||
// and just return a dummy bandwidth value since we don't have blockchain access
|
||||
// Return a reasonable test bandwidth value (e.g., 1GB in bytes)
|
||||
return Ok(MOCK_BANDWIDTH);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
nym_metrics::inc!("ecash_verification_attempts");
|
||||
|
||||
|
||||
@@ -291,3 +291,40 @@ struct UpgradeModeStateInner {
|
||||
// (and dealing with the async consequences of that)
|
||||
status: UpgradeModeStatus,
|
||||
}
|
||||
|
||||
pub mod testing {
|
||||
use crate::UpgradeModeState;
|
||||
use crate::upgrade_mode::{
|
||||
CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
|
||||
};
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn mock_dummy_upgrade_mode_details() -> (UpgradeModeDetails, UnboundedReceiver<CheckRequest>)
|
||||
{
|
||||
let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [
|
||||
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
];
|
||||
|
||||
pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey {
|
||||
let private_key =
|
||||
ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap();
|
||||
private_key.public_key()
|
||||
}
|
||||
|
||||
let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key());
|
||||
let upgrade_mode_details = UpgradeModeDetails::new(
|
||||
UpgradeModeCheckConfig {
|
||||
// essentially we never want to trigger this in our tests
|
||||
min_staleness_recheck: Duration::from_nanos(1),
|
||||
},
|
||||
UpgradeModeCheckRequestSender::new(um_recheck_tx),
|
||||
upgrade_mode_state.clone(),
|
||||
);
|
||||
(upgrade_mode_details, um_recheck_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { workspace = true }
|
||||
nym-sphinx-types = { workspace = true, optional = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -51,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ description = "Functions to interact with zknym signers, checking their status a
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub use error::SignerCheckError;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
||||
use nym_validator_client::models::{
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
||||
@@ -18,6 +13,12 @@ use nym_validator_client::models::{
|
||||
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||
ContractVKShare, DealerDetails, Epoch,
|
||||
};
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub use error::SignerCheckError;
|
||||
|
||||
mod client_check;
|
||||
pub mod error;
|
||||
@@ -31,6 +32,7 @@ pub type TypedSignerResult = SignerResult<
|
||||
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
||||
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SignersTestResult {
|
||||
pub threshold: Option<u64>,
|
||||
pub results: Vec<TypedSignerResult>,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
ALTER TABLE wireguard_peer
|
||||
ADD COLUMN psk VARCHAR;
|
||||
@@ -577,4 +577,21 @@ impl BandwidthGatewayStorage for GatewayStorage {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the stored PSK of the wireguard peer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `public_key`: the unique public key of the wireguard peer.
|
||||
/// * `psk`: the PSK of the wireguard peer.
|
||||
async fn update_peer_psk(
|
||||
&self,
|
||||
public_key: &str,
|
||||
psk: Option<&str>,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
self.wireguard_peer_manager
|
||||
.update_peer_psk(public_key, psk)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{error::GatewayStorageError, make_bincode_serializer};
|
||||
use defguard_wireguard_rs::key::Key;
|
||||
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use sqlx::FromRow;
|
||||
@@ -95,6 +96,7 @@ pub struct WireguardPeer {
|
||||
pub public_key: String,
|
||||
pub allowed_ips: Vec<u8>,
|
||||
pub client_id: i64,
|
||||
pub psk: Option<String>,
|
||||
}
|
||||
|
||||
impl WireguardPeer {
|
||||
@@ -110,6 +112,7 @@ impl WireguardPeer {
|
||||
source,
|
||||
})?,
|
||||
client_id,
|
||||
psk: value.preshared_key.map(|psk| psk.to_lower_hex()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -132,6 +135,11 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
|
||||
field_key: "allowed_ips",
|
||||
source,
|
||||
})?,
|
||||
preshared_key: value
|
||||
.psk
|
||||
.map(|psk| Key::decode(&psk))
|
||||
.transpose()
|
||||
.map_err(|_| Self::Error::TypeConversion { field_key: "psk" })?,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,6 +170,18 @@ pub trait BandwidthGatewayStorage: dyn_clone::DynClone {
|
||||
/// * `peer_public_key`: wireguard public key of the peer to be removed.
|
||||
async fn remove_wireguard_peer(&self, peer_public_key: &str)
|
||||
-> Result<(), GatewayStorageError>;
|
||||
|
||||
/// Update the stored PSK of the wireguard peer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `public_key`: the unique public key of the wireguard peer.
|
||||
/// * `psk`: the PSK of the wireguard peer.
|
||||
async fn update_peer_psk(
|
||||
&self,
|
||||
public_key: &str,
|
||||
psk: Option<&str>,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
@@ -507,5 +519,16 @@ pub mod mock {
|
||||
self.write().await.wireguard_peers.remove(peer_public_key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_peer_psk(
|
||||
&self,
|
||||
public_key: &str,
|
||||
psk: Option<&str>,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
if let Some(peer) = self.write().await.wireguard_peers.get_mut(public_key) {
|
||||
peer.psk = psk.map(|psk| psk.to_owned())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,29 @@ impl WgPeerManager {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the stored PSK of the wireguard peer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `public_key`: the unique public key of the wireguard peer.
|
||||
/// * `psk`: the PSK of the wireguard peer.
|
||||
pub(crate) async fn update_peer_psk(
|
||||
&self,
|
||||
public_key: &str,
|
||||
psk: Option<&str>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE wireguard_peer
|
||||
SET psk = ?
|
||||
WHERE public_key = ?
|
||||
"#,
|
||||
psk,
|
||||
public_key,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
|
||||
async-trait = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
cfg-if = { workspace = true}
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -46,7 +46,10 @@ use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
sync::{
|
||||
Arc, LazyLock,
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -70,14 +73,23 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
|
||||
self = self.non_shared();
|
||||
// because of the call to non-shared this conditional should always run.
|
||||
if let Some(rb) = self.reqwest_client_builder {
|
||||
self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
|
||||
}
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
/// Override the DNS resolver implementation used by the underlying http client. If
|
||||
/// [`Self::dns_resolver`] is called directly that will take priority over this, there is no
|
||||
/// need to call both.
|
||||
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||
pub fn no_hickory_dns(mut self) -> Self {
|
||||
self = self.non_shared();
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
@@ -129,7 +141,8 @@ pub struct HickoryDnsResolver {
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
use_system: Arc<AtomicBool>,
|
||||
system_resolver: Arc<OnceCell<TokioResolver>>,
|
||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
use_shared: bool,
|
||||
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
||||
@@ -141,7 +154,8 @@ impl Default for HickoryDnsResolver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
fallback: Default::default(),
|
||||
use_system: Arc::new(AtomicBool::new(false)),
|
||||
system_resolver: Default::default(),
|
||||
static_base: Some(Default::default()),
|
||||
use_shared: true,
|
||||
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
||||
@@ -151,16 +165,28 @@ impl Default for HickoryDnsResolver {
|
||||
|
||||
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 use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let use_shared = self.use_shared;
|
||||
let resolver = if use_system {
|
||||
match self
|
||||
.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
|
||||
{
|
||||
Ok(r) => r.clone(),
|
||||
Err(e) => return Box::pin(return_err(e)),
|
||||
}
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
.clone()
|
||||
};
|
||||
|
||||
let maybe_static = self.static_base.clone();
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
resolve(
|
||||
name,
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
use_shared,
|
||||
overall_dns_timeout,
|
||||
@@ -171,16 +197,17 @@ impl Resolve for HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
name: Name,
|
||||
resolver: Arc<OnceCell<TokioResolver>>,
|
||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
resolver: TokioResolver,
|
||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
independent: bool,
|
||||
overall_dns_timeout: Duration,
|
||||
) -> Result<Addrs, ResolveError> {
|
||||
let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent));
|
||||
|
||||
// try checking the static table to see if any of the addresses in the table have been
|
||||
// looked up previously within the timeout to where we are not yet ready to try the
|
||||
// default resolver yet again.
|
||||
@@ -214,22 +241,6 @@ async fn resolve(
|
||||
}
|
||||
};
|
||||
|
||||
// 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 {
|
||||
@@ -258,6 +269,11 @@ impl Iterator for SocketAddrs {
|
||||
}
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Returns an instance of the shared resolver.
|
||||
pub fn shared() -> Self {
|
||||
SHARED_RESOLVER.clone()
|
||||
}
|
||||
|
||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||
pub async fn resolve_str(
|
||||
&self,
|
||||
@@ -265,10 +281,20 @@ impl HickoryDnsResolver {
|
||||
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
||||
let n =
|
||||
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
||||
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let resolver = if use_system {
|
||||
self.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
|
||||
.clone()
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
|
||||
.clone()
|
||||
};
|
||||
|
||||
resolve(
|
||||
n,
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
resolver,
|
||||
self.static_base.clone(),
|
||||
self.use_shared,
|
||||
self.overall_dns_timeout,
|
||||
@@ -298,13 +324,11 @@ impl HickoryDnsResolver {
|
||||
fn new_resolver_system(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 !use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
if !use_shared {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.system_resolver
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
@@ -320,45 +344,80 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
|
||||
self.fallback = Some(Default::default());
|
||||
let _ = self
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?;
|
||||
/// Swap the primary internal resolver to the system resolver rather than the
|
||||
/// configured custom resolver.
|
||||
pub fn use_system_resolver(&self) {
|
||||
self.use_system.store(true, Relaxed);
|
||||
|
||||
// IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.enable_system_fallback()?;
|
||||
// }
|
||||
Ok(())
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_system_resolver();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable fallback resolution. If the primary resolver fails the error is
|
||||
/// returned immediately
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
/// Swap the primary internal resolver to the configured custom resolver rather than the
|
||||
/// system resolver.
|
||||
pub fn use_configured_resolver(&self) {
|
||||
self.use_system.store(false, Relaxed);
|
||||
|
||||
// // IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.fallback = None;
|
||||
// }
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_configured_resolver();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
|
||||
pub fn clear_preresolve(&self) {
|
||||
debug!("clearing pre-resolve table");
|
||||
if let Some(cell) = &self.static_base
|
||||
&& let Some(static_base) = cell.get()
|
||||
{
|
||||
static_base.clear_preresolve()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current map of hostnames to addresses used in the fallback static lookup stage if one
|
||||
/// exists.
|
||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_addrs())
|
||||
Some(self.static_base.as_ref()?.get()?.get_fallback_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));
|
||||
pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
debug!("setting fallback entries for {:?}", addrs.keys());
|
||||
if self.static_base.is_none() {
|
||||
let cell = OnceCell::new();
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
self.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||
.set_fallback(addrs);
|
||||
}
|
||||
|
||||
/// Get the current map of hostnames to addresses used in the preresolve static lookup stage
|
||||
/// if one exists.
|
||||
pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of addresses used in the preresolve static hostname lookup
|
||||
pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
debug!("setting pre-resolve entries for {:?}", addrs.keys());
|
||||
if self.static_base.is_none() {
|
||||
let cell = OnceCell::new();
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
self.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||
.set_preresolve(addrs);
|
||||
}
|
||||
|
||||
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
||||
@@ -495,7 +554,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
StaticResolver::new().with_fallback(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
@@ -532,10 +591,7 @@ mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
@@ -552,7 +608,7 @@ mod test {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver.into())
|
||||
.dns_resolver(resolver)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -597,7 +653,7 @@ mod test {
|
||||
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);
|
||||
resolver.set_fallback_addrs(addr_map);
|
||||
|
||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||
assert!(addrs.contains(&example_ip4));
|
||||
@@ -738,18 +794,19 @@ mod test {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the default
|
||||
// resolvers. Otherwise the default resolvers will succeed without using the static fallback,
|
||||
// making the test pointless
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
// interference with other tests.
|
||||
//
|
||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the
|
||||
// default resolvers. Otherwise the default resolvers will succeed without using the static
|
||||
// fallback, making the test pointless
|
||||
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
||||
let time_start = Instant::now();
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
let resolver1 = HickoryDnsResolver::shared();
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let time_start = std::time::Instant::now();
|
||||
// create a new resolver that uses the shared resolver
|
||||
let resolver = HickoryDnsResolver::shared();
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "rpc.nymtech.net";
|
||||
@@ -758,9 +815,27 @@ mod test {
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
println!(
|
||||
"{}ms resolved {domain}",
|
||||
(Instant::now() - time_start).as_millis()
|
||||
let lookup_dur = Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur > resolver.overall_dns_timeout,
|
||||
"expected lookup timeout - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
let time_start = std::time::Instant::now();
|
||||
// successful lookup using pre-resolve entry promoted from fallback
|
||||
let domain = "rpc.nymtech.net";
|
||||
let _ = resolver1
|
||||
.resolve_str(domain)
|
||||
.await
|
||||
.expect("domain expected to be in pre-resolve");
|
||||
|
||||
// this lookup should basically be instant as we are using pre-resolve
|
||||
let lookup_dur = std::time::Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur < Duration::from_millis(10),
|
||||
"expected instant - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
@@ -771,5 +846,62 @@ mod test {
|
||||
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
// interference with other tests.
|
||||
async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
|
||||
let resolver1 = HickoryDnsResolver::shared();
|
||||
|
||||
// create a new resolver that uses the shared resolver
|
||||
let mut resolver = HickoryDnsResolver::shared();
|
||||
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
resolver.set_static_preresolve(addr_map1);
|
||||
|
||||
let time_start = std::time::Instant::now();
|
||||
// successful lookup using pre-resolve entry promoted from fallback
|
||||
let _ = resolver1
|
||||
.resolve_str(&example_domains[0])
|
||||
.await
|
||||
.expect("domain expected to be in pre-resolve");
|
||||
|
||||
// this lookup should basically be instant as we are using pre-resolve
|
||||
let lookup_dur = std::time::Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur < Duration::from_millis(10),
|
||||
"expected instant - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
// After clearing the pre-resolve in one instance of the shared resolver ...
|
||||
resolver.clear_preresolve();
|
||||
|
||||
// ... other instances have their pre-resolve entries cleared.
|
||||
let prereslve_lookup = resolver1
|
||||
.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get()
|
||||
.unwrap()
|
||||
.pre_resolve(&example_domains[0]);
|
||||
assert!(prereslve_lookup.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ pub const VERCEL_COM_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
|
||||
];
|
||||
|
||||
pub const NYM_API_CDN: &str = "cdn1.media-platform.net";
|
||||
pub const NYM_API_CDN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(172, 104, 178, 252))];
|
||||
|
||||
pub const NYM_COM_DOMAIN: &str = "nym.com";
|
||||
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
|
||||
|
||||
@@ -69,6 +72,12 @@ pub const NYM_RPC_IPS: &[IpAddr] = &[
|
||||
)),
|
||||
];
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn empty_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
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());
|
||||
@@ -88,6 +97,7 @@ pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||
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_API_CDN.to_string(), NYM_API_CDN_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());
|
||||
|
||||
@@ -14,42 +14,78 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StaticResolver {
|
||||
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
pre_resolve_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
enum PreResolveStatus {
|
||||
#[default]
|
||||
Valid,
|
||||
ValidUntil(Instant),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Entry {
|
||||
valid_for_pre_resolve_until: Option<Instant>,
|
||||
status: PreResolveStatus,
|
||||
addrs: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new(addrs: Vec<IpAddr>) -> Self {
|
||||
Self {
|
||||
valid_for_pre_resolve_until: None,
|
||||
status: PreResolveStatus::Valid,
|
||||
addrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_timeout(addrs: Vec<IpAddr>, timeout: Duration) -> Self {
|
||||
Self {
|
||||
status: PreResolveStatus::ValidUntil(Instant::now() + timeout),
|
||||
addrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
match self.status {
|
||||
PreResolveStatus::Valid => true,
|
||||
PreResolveStatus::ValidUntil(t) => t > Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticResolver {
|
||||
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
|
||||
debug!("building static resolver");
|
||||
let static_entries = static_entries
|
||||
.into_iter()
|
||||
.map(|(name, ips)| (name, Entry::new(ips)))
|
||||
.collect();
|
||||
pub fn new() -> StaticResolver {
|
||||
Self {
|
||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
||||
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the full set of domain names and associated addresses stored in this static lookup table
|
||||
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
/// Initialize the contents of the pre-resolve table for this instance of the static resolver
|
||||
#[allow(unused)]
|
||||
pub fn with_preresolve(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|(name, ips)| (name, Entry::new(ips)))
|
||||
.collect();
|
||||
self.preresolve_addr_map = Arc::new(Mutex::new(entries));
|
||||
self
|
||||
}
|
||||
|
||||
/// Initialize the contenes of the fallback table for this instance of the static resolver
|
||||
pub fn with_fallback(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||
self.fallback_addr_map = Arc::new(Mutex::new(entries));
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the set of domain names and associated addresses stored in the pre-resolve static
|
||||
/// lookup table
|
||||
pub fn get_preresolve_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
let mut out = HashMap::new();
|
||||
self.static_addr_map
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
@@ -59,6 +95,38 @@ impl StaticResolver {
|
||||
out
|
||||
}
|
||||
|
||||
/// Return the set of domain names and associated addresses stored in the fallback static lookup
|
||||
/// table
|
||||
pub fn get_fallback_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
self.fallback_addr_map.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of static addresses to be returned only after attempting a lookup
|
||||
/// over the network resolver.
|
||||
pub fn set_fallback(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
self.fallback_addr_map.lock().unwrap().extend(addrs);
|
||||
}
|
||||
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::preresolve_to_addrs`]) will be removed.
|
||||
pub fn clear_preresolve(&self) {
|
||||
*self.preresolve_addr_map.lock().unwrap() = HashMap::new();
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of static addresses and mark these domains to be returned
|
||||
/// WITHOUT attempting a lookup over the network resolver.
|
||||
pub fn set_preresolve(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
let mut current_map = self.preresolve_addr_map.lock().unwrap();
|
||||
for (domain, ips) in addrs.into_iter() {
|
||||
_ = current_map.insert(domain, Entry::new(ips))
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the timeout for which domains can be pre-resolved after they are looked up in the
|
||||
/// static lookup table.
|
||||
#[allow(unused)]
|
||||
@@ -71,44 +139,58 @@ impl StaticResolver {
|
||||
/// recently (within the configured timeout) looked it up previously in this static table using
|
||||
/// a regular resolve.
|
||||
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||
debug!("found {name:?} in pre-resolve static table resolver");
|
||||
|
||||
self.pre_resolve_timeout?;
|
||||
|
||||
self.static_addr_map
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.filter(|e| {
|
||||
e.valid_for_pre_resolve_until
|
||||
.is_some_and(|t| t > Instant::now())
|
||||
.filter(|entry| entry.is_valid())
|
||||
.map(|entry| {
|
||||
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
|
||||
entry.addrs.clone()
|
||||
})
|
||||
.map(|e| e.addrs.clone())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||
Self::resolve_inner(
|
||||
self.static_addr_map.lock().unwrap(),
|
||||
self.fallback_addr_map.lock().unwrap(),
|
||||
self.preresolve_addr_map.lock().unwrap(),
|
||||
name,
|
||||
self.pre_resolve_timeout,
|
||||
)
|
||||
.map(|e| e.addrs)
|
||||
}
|
||||
|
||||
fn resolve_inner(
|
||||
mut table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
|
||||
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
name: &str,
|
||||
timeout: Option<Duration>,
|
||||
) -> Option<Entry> {
|
||||
let resolved = table.get_mut(name)?;
|
||||
pre_resolve_cache_timeout: Option<Duration>,
|
||||
) -> Option<Vec<IpAddr>> {
|
||||
let resolved = fallback_table.get(name)?;
|
||||
|
||||
debug!("found {name:?} in static table resolver");
|
||||
debug!("lookup hit for \"{name:?}\" in static table resolver");
|
||||
|
||||
if let Some(pre_resolve_timeout) = timeout {
|
||||
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
||||
// trigger in pre-resolve lookups for the next _timeout_ window.
|
||||
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout);
|
||||
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
||||
// trigger in pre-resolve lookups for the next _timeout_ window if it wasn't already
|
||||
// triggering.
|
||||
if let Some(pre_resolve_timeout) = pre_resolve_cache_timeout {
|
||||
match preresolve_table.get_mut(name) {
|
||||
None => {
|
||||
_ = preresolve_table.insert(
|
||||
name.to_string(),
|
||||
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||
);
|
||||
}
|
||||
// Not sure how we would get cases where this is Some( ) -- it requires having a
|
||||
// Valid entry in the preresolve table and still doing a lookup against fallback.
|
||||
Some(entry) if matches!(entry.status, PreResolveStatus::ValidUntil(_)) => {
|
||||
_ = preresolve_table.insert(
|
||||
name.to_string(),
|
||||
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(resolved.clone())
|
||||
}
|
||||
@@ -117,13 +199,23 @@ impl StaticResolver {
|
||||
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();
|
||||
// these should clone arcs, not the actual tables
|
||||
let fallback_addr_map = self.fallback_addr_map.clone();
|
||||
let presesolve_addr_map = self.preresolve_addr_map.clone();
|
||||
let timeout = self.pre_resolve_timeout;
|
||||
// Also the returned future doesn't try to take the lock on the tables until the
|
||||
// future is awaited, so no blocking issues.
|
||||
Box::pin(async move {
|
||||
let addr_map = addr_map.lock().unwrap();
|
||||
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
|
||||
let fallback_addr_map = fallback_addr_map.lock().unwrap();
|
||||
let presesolve_addr_map = presesolve_addr_map.lock().unwrap();
|
||||
let lookup = match Self::resolve_inner(
|
||||
fallback_addr_map,
|
||||
presesolve_addr_map,
|
||||
name.as_str(),
|
||||
timeout,
|
||||
) {
|
||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||
Some(entry) => entry.addrs,
|
||||
Some(addrs) => addrs,
|
||||
};
|
||||
let addrs: Addrs = Box::new(
|
||||
lookup
|
||||
@@ -142,6 +234,7 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -149,7 +242,7 @@ mod test {
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
|
||||
// lookup for domain for which there is no entry
|
||||
let resolver = StaticResolver::new(HashMap::new());
|
||||
let resolver = StaticResolver::new();
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let result = resolver.resolve(url).await;
|
||||
@@ -166,7 +259,7 @@ mod test {
|
||||
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 resolver = StaticResolver::new().with_fallback(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)));
|
||||
@@ -175,7 +268,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_lookup_pre_resolve() {
|
||||
fn elevate_fallback_to_pre_resolve() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
let mut addr_map = HashMap::new();
|
||||
@@ -183,24 +276,23 @@ mod test {
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||
|
||||
let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration);
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// ensure that attempting to pre-resolve without first resolving returns none
|
||||
let result = resolver.pre_resolve(&example_domain);
|
||||
assert!(result.is_none());
|
||||
|
||||
// resolving should now update the pre-resolve validity timeout for the entry
|
||||
let entry = StaticResolver::resolve_inner(
|
||||
resolver.static_addr_map.lock().unwrap(),
|
||||
&example_domain,
|
||||
Some(example_duration),
|
||||
)
|
||||
.expect("missing entry???!!!!");
|
||||
assert!(
|
||||
entry
|
||||
.valid_for_pre_resolve_until
|
||||
.is_some_and(|t| t < Instant::now() + example_duration)
|
||||
);
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domain)
|
||||
.expect("entry should exist");
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domain),
|
||||
Some(PreResolveStatus::ValidUntil(t))
|
||||
if t < Instant::now() + example_duration
|
||||
));
|
||||
|
||||
// check that pre-resolve now returns the expected record
|
||||
let addrs = resolver
|
||||
@@ -214,4 +306,139 @@ mod test {
|
||||
let result = resolver.pre_resolve(&example_domain);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_use_preresolve() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
String::from("preresolve.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
let mut addr_map2 = HashMap::new();
|
||||
addr_map2.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
addr_map2.insert(
|
||||
example_domains[2].clone(),
|
||||
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||
);
|
||||
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map1)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// Attempting to pre-resolve without setting the table returns none
|
||||
let result = resolver.pre_resolve(&example_domains[0]);
|
||||
assert!(result.is_none());
|
||||
|
||||
resolver.set_preresolve(addr_map2);
|
||||
|
||||
// After setting the pre-resolve, addresses in the the table are returned
|
||||
let result = resolver.pre_resolve(&example_domains[1]);
|
||||
assert!(result.is_some());
|
||||
|
||||
// If the domain wasn't in the pre-resolve table it returns none.
|
||||
let result = resolver.pre_resolve(&example_domains[0]);
|
||||
assert!(result.is_none());
|
||||
|
||||
resolver.clear_preresolve();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preresolve_with_fallback() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
String::from("preresolve.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
let mut addr_map2 = HashMap::new();
|
||||
addr_map2.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
addr_map2.insert(
|
||||
example_domains[2].clone(),
|
||||
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||
);
|
||||
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map1)
|
||||
.with_preresolve(addr_map2)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// when using both pre-resolve and fallback elevating entries from fallback to pre-resolve
|
||||
// leaves the entries as `Valid`.
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[1]),
|
||||
Some(PreResolveStatus::Valid)
|
||||
));
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domains[1])
|
||||
.expect("entry should exist");
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[1]),
|
||||
Some(PreResolveStatus::Valid)
|
||||
));
|
||||
|
||||
// entries not already in pre-resolve get elevated with a timeout.
|
||||
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domains[0])
|
||||
.expect("entry should exist");
|
||||
assert!(resolver.preresolve_contains(&example_domains[0]));
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[0]),
|
||||
Some(PreResolveStatus::ValidUntil(_))
|
||||
));
|
||||
|
||||
// clearing the pre-resolve table doesn't impact the fallback table.
|
||||
resolver.clear_preresolve();
|
||||
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||
assert!(!resolver.preresolve_contains(&example_domains[1]));
|
||||
assert!(!resolver.preresolve_contains(&example_domains[2]));
|
||||
assert!(!resolver.fallback_contains(&example_domains[0]));
|
||||
assert!(!resolver.fallback_contains(&example_domains[1]));
|
||||
}
|
||||
|
||||
/// convenience functions for testing
|
||||
impl StaticResolver {
|
||||
fn preresolve_status(&self, name: &str) -> Option<PreResolveStatus> {
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.map(|e| e.status.clone())
|
||||
}
|
||||
|
||||
fn preresolve_contains(&self, name: &str) -> bool {
|
||||
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||
}
|
||||
|
||||
fn fallback_contains(&self, name: &str) -> bool {
|
||||
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
|
||||
//! Utilities for and implementation of request tunneling
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{
|
||||
Arc, LazyLock, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ClientBuilder;
|
||||
use crate::{Client, ClientBuilder};
|
||||
|
||||
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
|
||||
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
|
||||
|
||||
// #[cfg(feature = "tunneling")]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Front {
|
||||
pub(crate) policy: FrontPolicy,
|
||||
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
|
||||
enabled: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -19,7 +25,7 @@ impl Clone for Front {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
policy: self.policy.clone(),
|
||||
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
|
||||
enabled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,13 +33,30 @@ impl Clone for Front {
|
||||
impl Front {
|
||||
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
||||
Self {
|
||||
enabled: AtomicBool::new(policy == FrontPolicy::Always),
|
||||
enabled: AtomicBool::new(false),
|
||||
policy: Arc::new(RwLock::new(policy)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn off() -> Self {
|
||||
Self::new(FrontPolicy::Off)
|
||||
}
|
||||
|
||||
pub(crate) fn shared() -> Self {
|
||||
let policy = SHARED_FRONTING_POLICY.clone();
|
||||
Self {
|
||||
enabled: AtomicBool::new(false),
|
||||
policy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_policy(&self, policy: FrontPolicy) {
|
||||
*self.policy.write().unwrap() = policy;
|
||||
self.enabled.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool {
|
||||
match self.policy {
|
||||
match *self.policy.read().unwrap() {
|
||||
FrontPolicy::Off => false,
|
||||
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
||||
FrontPolicy::Always => true,
|
||||
@@ -46,14 +69,13 @@ impl Front {
|
||||
if self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if matches!(self.policy, FrontPolicy::OnRetry) {
|
||||
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
|
||||
self.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
#[cfg(feature = "tunneling")]
|
||||
/// Policy for when to use domain fronting for HTTP requests.
|
||||
pub enum FrontPolicy {
|
||||
/// Always use domain fronting for all requests.
|
||||
@@ -66,29 +88,208 @@ pub enum FrontPolicy {
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Enable and configure request tunneling for API requests.
|
||||
#[cfg(feature = "tunneling")]
|
||||
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
|
||||
let front = Front::new(policy);
|
||||
/// Enable and configure request tunneling for API requests. If no front policy is
|
||||
/// provided the shared fronting policy will be used.
|
||||
pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
|
||||
let front = if let Some(p) = policy {
|
||||
Front::new(p)
|
||||
} else {
|
||||
Front::shared()
|
||||
};
|
||||
|
||||
// Check if any of the supplied urls even support fronting
|
||||
if !self.urls.iter().any(|url| url.has_front()) {
|
||||
warn!(
|
||||
"fronting is enabled, but none of the supplied urls have configured fronting domains"
|
||||
"fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
|
||||
self.urls
|
||||
);
|
||||
}
|
||||
|
||||
self.front = Some(front);
|
||||
self.front = front;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Set the policy for enabling fronting. If fronting was previously unset this will set it, and
|
||||
/// make it possible to enable (i.e [`FrontPolicy::Off`] will not enable it).
|
||||
///
|
||||
/// Calling this function sets a custom policy for this client, disconnecting it from the shared
|
||||
/// fronting policy -- i.e. changes applied through [`Client::set_shared_front_policy`] will not
|
||||
/// be impact this client.
|
||||
pub fn set_front_policy(&mut self, policy: FrontPolicy) {
|
||||
self.front.set_policy(policy)
|
||||
}
|
||||
|
||||
/// Set the fronting policy for this client to follow the shared policy.
|
||||
pub fn use_shared_front_policy(&mut self) {
|
||||
self.front = Front::shared();
|
||||
}
|
||||
|
||||
/// Set the fronting policy for all clients using the shared policy.
|
||||
//
|
||||
// NOTE: this does not reset the per-instance enabled flag like it will when using
|
||||
// [`Front::set_front_policy`]. So if a client is using shared policy with the `OnRetry` policy
|
||||
// and this function is used to swap that policy away from and then back to `OnRetry` the
|
||||
// fronting will still be enabled. Noting this here just in case this triggers any corner cases
|
||||
// down the road.
|
||||
pub fn set_shared_front_policy(policy: FrontPolicy) {
|
||||
*SHARED_FRONTING_POLICY.write().unwrap() = policy;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ApiClientCore, NO_PARAMS, Url};
|
||||
|
||||
impl Front {
|
||||
pub(crate) fn policy(&self) -> FrontPolicy {
|
||||
self.policy.read().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy can be set for an independent client and the update is applied properly
|
||||
#[test]
|
||||
fn set_policy_independent_client() {
|
||||
let url1 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut client1 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::Off))
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||
|
||||
let client2 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::OnRetry))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Ensure that setting the policy for a client it gets properly applied.
|
||||
client1.set_front_policy(FrontPolicy::Always);
|
||||
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// ensure that setting the policy in a client NOT using the shared policy does NOT update
|
||||
// the policy used by another client.
|
||||
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||
|
||||
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||
// requests
|
||||
let req = client1
|
||||
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let expected_host = url1.host_str().unwrap();
|
||||
assert!(
|
||||
req.headers()
|
||||
.get(reqwest::header::HOST)
|
||||
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||
"{:?} != {:?}",
|
||||
expected_host,
|
||||
req,
|
||||
);
|
||||
|
||||
let expected_front = url1.front_str().unwrap();
|
||||
assert!(
|
||||
req.url()
|
||||
.host()
|
||||
.is_some_and(|url| url.to_string() == expected_front),
|
||||
"{:?} != {:?}",
|
||||
expected_front,
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
/// Policy can be set for the shared client and the update is applied properly
|
||||
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH THE SHARED POLICY AND AS SUCH CAN HAVE
|
||||
// AN IMPACT ON OTHER TESTS
|
||||
#[test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
fn set_policy_shared_client() {
|
||||
let url1 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Client::set_shared_front_policy(FrontPolicy::Off);
|
||||
assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
|
||||
|
||||
let client1 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(None)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||
|
||||
let mut client2 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::Off))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Ensure that setting the shared policy gets properly applied
|
||||
Client::set_shared_front_policy(FrontPolicy::Always);
|
||||
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// Setting the shared policy should NOT update clients NOT using the shared policy.
|
||||
assert!(client2.front.policy() == FrontPolicy::Off);
|
||||
|
||||
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||
// requests
|
||||
let req = client1
|
||||
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let expected_host = url1.host_str().unwrap();
|
||||
assert!(
|
||||
req.headers()
|
||||
.get(reqwest::header::HOST)
|
||||
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||
"{:?} != {:?}",
|
||||
expected_host,
|
||||
req,
|
||||
);
|
||||
|
||||
let expected_front = url1.front_str().unwrap();
|
||||
assert!(
|
||||
req.url()
|
||||
.host()
|
||||
.is_some_and(|url| url.to_string() == expected_front),
|
||||
"{:?} != {:?}",
|
||||
expected_front,
|
||||
req,
|
||||
);
|
||||
|
||||
// ensure that setting to the shared policy works
|
||||
client2.use_shared_front_policy();
|
||||
assert!(client2.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// ensure that if the policy is OnRetry then the `enabled` fields are still independent,
|
||||
// despite the policy being shared.
|
||||
Client::set_shared_front_policy(FrontPolicy::OnRetry);
|
||||
assert!(client1.front.policy() == FrontPolicy::OnRetry);
|
||||
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||
|
||||
assert!(!client1.front.is_enabled());
|
||||
assert!(!client2.front.is_enabled());
|
||||
|
||||
client1.front.retry_enable();
|
||||
assert!(client1.front.is_enabled());
|
||||
assert!(!client2.front.is_enabled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nym_api_works() {
|
||||
let url1 = Url::new(
|
||||
@@ -104,7 +305,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new(url1)
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
@@ -140,7 +341,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
|
||||
+191
-102
@@ -136,6 +136,7 @@
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use http::header::USER_AGENT;
|
||||
pub use inventory;
|
||||
pub use reqwest;
|
||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||
@@ -147,6 +148,7 @@ pub mod registry;
|
||||
use crate::path::RequestPath;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use cfg_if::cfg_if;
|
||||
use http::HeaderMap;
|
||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||
use itertools::Itertools;
|
||||
@@ -161,9 +163,7 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
mod fronted;
|
||||
@@ -195,6 +195,8 @@ use nym_http_api_client_macro::client_defaults;
|
||||
/// high and chatty protocols take a while to complete.
|
||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
client_defaults!(
|
||||
priority = -100;
|
||||
@@ -206,6 +208,24 @@ client_defaults!(
|
||||
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
|
||||
);
|
||||
|
||||
static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
tracing::info!("Initializing shared HTTP client");
|
||||
cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
reqwest::ClientBuilder::new().build()
|
||||
.expect("failed to initialize shared http client")
|
||||
} else {
|
||||
let mut builder = default_builder();
|
||||
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
|
||||
builder
|
||||
.build()
|
||||
.expect("failed to initialize shared http client")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// Collection of URL Path Segments
|
||||
pub type PathSegments<'a> = &'a [&'a str];
|
||||
/// Collection of HTTP Request Parameters
|
||||
@@ -327,16 +347,22 @@ pub enum HttpClientError {
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse header value: {source}")]
|
||||
InvalidHeaderValue {
|
||||
#[source]
|
||||
source: http::Error,
|
||||
},
|
||||
|
||||
#[error("failed to send request for {url}: {source}")]
|
||||
RequestSendFailure {
|
||||
url: reqwest::Url,
|
||||
url: Box<reqwest::Url>,
|
||||
#[source]
|
||||
source: ReqwestErrorWrapper,
|
||||
},
|
||||
|
||||
#[error("failed to read response body from {url}: {source}")]
|
||||
ResponseReadFailure {
|
||||
url: reqwest::Url,
|
||||
url: Box<reqwest::Url>,
|
||||
headers: Box<HeaderMap>,
|
||||
status: StatusCode,
|
||||
#[source]
|
||||
@@ -353,7 +379,7 @@ pub enum HttpClientError {
|
||||
},
|
||||
|
||||
#[error("the requested resource could not be found at {url}")]
|
||||
NotFound { url: reqwest::Url },
|
||||
NotFound { url: Box<reqwest::Url> },
|
||||
|
||||
#[error("attempted to use domain fronting and clone a request containing stream data")]
|
||||
AttemptedToCloneStreamRequest,
|
||||
@@ -365,7 +391,7 @@ pub enum HttpClientError {
|
||||
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
|
||||
)]
|
||||
RequestFailure {
|
||||
url: reqwest::Url,
|
||||
url: Box<reqwest::Url>,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
},
|
||||
@@ -374,7 +400,7 @@ pub enum HttpClientError {
|
||||
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
|
||||
)]
|
||||
EmptyResponse {
|
||||
url: reqwest::Url,
|
||||
url: Box<reqwest::Url>,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
},
|
||||
@@ -383,7 +409,7 @@ pub enum HttpClientError {
|
||||
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
|
||||
)]
|
||||
EndpointFailure {
|
||||
url: reqwest::Url,
|
||||
url: Box<reqwest::Url>,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
error: String,
|
||||
@@ -453,7 +479,7 @@ impl HttpClientError {
|
||||
|
||||
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
|
||||
HttpClientError::RequestSendFailure {
|
||||
url,
|
||||
url: Box::new(url),
|
||||
source: ReqwestErrorWrapper(source),
|
||||
}
|
||||
}
|
||||
@@ -554,6 +580,19 @@ pub trait ApiClientCore {
|
||||
let req = self.create_request(method, path, params, json_body)?;
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
/// 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 maybe_rotate_hosts(&self, offending_url: Option<Url>);
|
||||
|
||||
/// If the fronting policy for the client is set to `OnRetry` this function will enable the
|
||||
/// fronting if not already enabled.
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
|
||||
}
|
||||
|
||||
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
|
||||
@@ -562,16 +601,18 @@ pub struct ClientBuilder {
|
||||
urls: Vec<Url>,
|
||||
|
||||
timeout: Option<Duration>,
|
||||
custom_user_agent: bool,
|
||||
reqwest_client_builder: reqwest::ClientBuilder,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
reqwest_client_builder: Option<reqwest::ClientBuilder>,
|
||||
#[allow(dead_code)] // not dead code, just unused in wasm
|
||||
use_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
front: fronted::Front,
|
||||
|
||||
retry_limit: usize,
|
||||
serialization: SerializationFormat,
|
||||
|
||||
error: Option<HttpClientError>,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
@@ -642,10 +683,10 @@ impl ClientBuilder {
|
||||
|
||||
let mut builder = Self::new_with_urls(urls)?;
|
||||
|
||||
// Enable domain fronting by default (on retry)
|
||||
// Enable domain fronting using the shared fronting policy
|
||||
#[cfg(feature = "tunneling")]
|
||||
{
|
||||
builder = builder.with_fronting(FrontPolicy::OnRetry);
|
||||
builder = builder.with_fronting(None);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
@@ -659,26 +700,31 @@ impl ClientBuilder {
|
||||
|
||||
let urls = Self::check_urls(urls);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reqwest_client_builder = reqwest::ClientBuilder::new();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let reqwest_client_builder = default_builder();
|
||||
|
||||
Ok(ClientBuilder {
|
||||
urls,
|
||||
timeout: None,
|
||||
custom_user_agent: false,
|
||||
reqwest_client_builder,
|
||||
custom_user_agent: None,
|
||||
reqwest_client_builder: None,
|
||||
use_secure_dns: true,
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: None,
|
||||
front: fronted::Front::off(),
|
||||
|
||||
retry_limit: 0,
|
||||
serialization: SerializationFormat::Json,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure use of an independent HTTP request executor. This prevents use of beneficial
|
||||
/// features like connection pooling under the hood.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn non_shared(mut self) -> Self {
|
||||
if self.reqwest_client_builder.is_none() {
|
||||
self.reqwest_client_builder = Some(default_builder());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an additional URL to the set usable by this constructed `Client`
|
||||
pub fn add_url(mut self, url: Url) -> Self {
|
||||
self.urls.push(url);
|
||||
@@ -723,7 +769,7 @@ impl ClientBuilder {
|
||||
|
||||
/// Provide a pre-configured [`reqwest::ClientBuilder`]
|
||||
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
|
||||
self.reqwest_client_builder = reqwest_builder;
|
||||
self.reqwest_client_builder = Some(reqwest_builder);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -733,18 +779,12 @@ impl ClientBuilder {
|
||||
V: TryInto<HeaderValue>,
|
||||
V::Error: Into<http::Error>,
|
||||
{
|
||||
self.custom_user_agent = true;
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Override DNS resolution for specific domains to particular IP addresses.
|
||||
///
|
||||
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
|
||||
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
|
||||
match value.try_into() {
|
||||
Ok(v) => self.custom_user_agent = Some(v),
|
||||
Err(err) => {
|
||||
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -761,30 +801,33 @@ impl ClientBuilder {
|
||||
|
||||
/// Returns a Client that uses this ClientBuilder configuration.
|
||||
pub fn build(self) -> Result<Client, HttpClientError> {
|
||||
if let Some(err) = self.error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reqwest_client = self.reqwest_client_builder.build()?;
|
||||
let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
|
||||
|
||||
// TODO: we should probably be propagating the error rather than panicking,
|
||||
// but that'd break bunch of things due to type changes
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let reqwest_client = {
|
||||
let mut builder = self.reqwest_client_builder;
|
||||
let reqwest_client = self
|
||||
.reqwest_client_builder
|
||||
.map(|mut builder| {
|
||||
// unless explicitly disabled use the DoT/DoH enabled resolver
|
||||
if self.use_secure_dns {
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
}
|
||||
|
||||
// unless explicitly disabled use the DoT/DoH enabled resolver
|
||||
if self.use_secure_dns {
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)?
|
||||
};
|
||||
builder
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let client = Client {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
using_secure_dns: self.use_secure_dns,
|
||||
custom_user_agent: self.custom_user_agent,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
@@ -804,11 +847,11 @@ impl ClientBuilder {
|
||||
pub struct Client {
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: reqwest::Client,
|
||||
using_secure_dns: bool,
|
||||
reqwest_client: Option<reqwest::Client>,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
front: fronted::Front,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: Duration,
|
||||
@@ -862,8 +905,8 @@ impl Client {
|
||||
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,
|
||||
reqwest_client: None,
|
||||
custom_user_agent: None,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front.clone(),
|
||||
@@ -897,9 +940,7 @@ impl Client {
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn matches_current_host(&self, url: &Url) -> bool {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
url.host_str() == self.current_url().front_str()
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
@@ -926,9 +967,7 @@ impl Client {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
// if we are using fronting, try updating to the next front
|
||||
let url = self.current_url();
|
||||
|
||||
@@ -948,9 +987,7 @@ impl Client {
|
||||
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
while next != orig {
|
||||
if self.base_urls[next].has_front() {
|
||||
// we have a front for the next host, so we can use it
|
||||
@@ -981,14 +1018,12 @@ impl Client {
|
||||
/// this method. For example, if the client is configured to rotate hosts after each error, this
|
||||
/// method should be called after the host has been updated -- i.e. as part of the subsequent
|
||||
/// send.
|
||||
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||
let url = self.current_url();
|
||||
r.url_mut().set_host(url.host_str()).unwrap();
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
if let Some(front_host) = url.front_str() {
|
||||
if let Some(actual_host) = url.host_str() {
|
||||
tracing::debug!(
|
||||
@@ -1008,6 +1043,13 @@ impl Client {
|
||||
.headers_mut()
|
||||
.insert(reqwest::header::HOST, actual_host_header);
|
||||
|
||||
// Set a custom header to capture the outer host (used in the SNI) of the request
|
||||
let front_host_header: HeaderValue =
|
||||
front_host.parse().unwrap_or(HeaderValue::from_static(""));
|
||||
_ = r
|
||||
.headers_mut()
|
||||
.insert(NYM_OUTER_SNI_HEADER, front_host_header);
|
||||
|
||||
return (url.as_str(), url.front_str());
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -1048,12 +1090,21 @@ impl ApiClientCore for Client {
|
||||
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
|
||||
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
|
||||
let client = if let Some(client) = &self.reqwest_client {
|
||||
client.clone()
|
||||
} else {
|
||||
SHARED_CLIENT.clone()
|
||||
};
|
||||
let mut rb = RequestBuilder::from_parts(client, req);
|
||||
|
||||
rb = rb
|
||||
.header(ACCEPT, self.serialization.content_type())
|
||||
.header(CONTENT_TYPE, self.serialization.content_type());
|
||||
|
||||
if let Some(user_agent) = &self.custom_user_agent {
|
||||
rb = rb.header(USER_AGENT, user_agent.clone());
|
||||
}
|
||||
|
||||
if let Some(body) = body {
|
||||
match self.serialization {
|
||||
SerializationFormat::Json => {
|
||||
@@ -1096,16 +1147,19 @@ impl ApiClientCore for Client {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
Ok(wasmtimer::tokio::timeout(
|
||||
self.request_timeout,
|
||||
self.reqwest_client.execute(req),
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
Ok(
|
||||
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
|
||||
)
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let response = self.reqwest_client.execute(req).await;
|
||||
let response = {
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
client.execute(req).await
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
@@ -1121,20 +1175,10 @@ impl ApiClientCore for Client {
|
||||
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host(Some(url.clone()));
|
||||
self.maybe_rotate_hosts(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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
self.maybe_enable_fronting(("network", url.as_str(), &err));
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
@@ -1158,6 +1202,21 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_rotate_hosts(&self, offending: Option<Url>) {
|
||||
self.update_host(offending);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
|
||||
// If fronting is set to be OnRetry, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = self.front.is_enabled();
|
||||
self.front.retry_enable();
|
||||
if !was_enabled && self.front.is_enabled() {
|
||||
tracing::debug!("Domain fronting activated after failure: {context:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common usage functionality for the http client.
|
||||
@@ -1310,6 +1369,35 @@ pub trait ApiClient: ApiClientCore {
|
||||
self.get_response(path, params).await
|
||||
}
|
||||
|
||||
/// Attempt to parse a response object from an HTTP response
|
||||
async fn parse_response<T>(
|
||||
&self,
|
||||
res: Response,
|
||||
allow_empty: bool,
|
||||
) -> Result<T, HttpClientError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let url = Url::from(res.url());
|
||||
parse_response(res, allow_empty).await.inspect_err(|e| {
|
||||
if matches!(
|
||||
// if we encounter a read error while we attempt to parse it could be caused by censorship and we should
|
||||
// rotate hosts / enable fronting.
|
||||
e,
|
||||
HttpClientError::ResponseReadFailure {
|
||||
url: _,
|
||||
headers: _,
|
||||
status: _,
|
||||
source: _,
|
||||
}
|
||||
) {
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
#[cfg(feature = "tunneling")]
|
||||
self.maybe_enable_fronting(("parse/read", url.as_str(), e));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
|
||||
/// into the provided type `T` based on the content type header
|
||||
@@ -1327,7 +1415,8 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::GET, path, params, None::<&()>)
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1349,7 +1438,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::POST, path, params, Some(json_body))
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
|
||||
@@ -1369,7 +1458,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1391,7 +1480,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1403,7 +1492,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1420,7 +1509,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `delete` json data from the provided absolute endpoint, e.g.
|
||||
@@ -1432,7 +1521,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1450,7 +1539,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let req =
|
||||
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1517,7 +1606,7 @@ where
|
||||
|
||||
if !allow_empty && let Some(0) = res.content_length() {
|
||||
return Err(HttpClientError::EmptyResponse {
|
||||
url,
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
});
|
||||
@@ -1530,25 +1619,25 @@ where
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|source| HttpClientError::ResponseReadFailure {
|
||||
url,
|
||||
url: Box::new(url),
|
||||
headers: Box::new(headers.clone()),
|
||||
status,
|
||||
source: ReqwestErrorWrapper(source),
|
||||
})?;
|
||||
decode_raw_response(&headers, full)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound { url })
|
||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
url,
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
});
|
||||
};
|
||||
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url,
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: plaintext,
|
||||
|
||||
@@ -89,7 +89,8 @@ fn sanitizing_urls() {
|
||||
// - on error without retries is where we have multiple urls, is the url updated?
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // test relies on external services being available and behaving in a specific way.
|
||||
#[cfg(any())] // #[ignore] we run ignore assuming it just means slow in Ci/CD -_-
|
||||
// test relies on external services being available and behaving in a specific way.
|
||||
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = ClientBuilder::new_with_urls(vec![
|
||||
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
|
||||
@@ -199,7 +200,7 @@ fn fronted_host_updating() {
|
||||
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
|
||||
let mut client = ClientBuilder::new(url)
|
||||
.unwrap()
|
||||
.with_fronting(crate::fronted::FrontPolicy::Always)
|
||||
.with_fronting(Some(crate::fronted::FrontPolicy::Always))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -123,6 +123,16 @@ impl From<reqwest::Url> for Url {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&reqwest::Url> for Url {
|
||||
fn from(url: &url::Url) -> Self {
|
||||
Self {
|
||||
url: url.clone(),
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<url::Url> for Url {
|
||||
fn as_ref(&self) -> &url::Url {
|
||||
&self.url
|
||||
|
||||
@@ -11,8 +11,10 @@ pub mod response;
|
||||
#[cfg(feature = "output")]
|
||||
pub use response::*;
|
||||
|
||||
pub use ::bincode::Options as BincodeOptions;
|
||||
|
||||
// be explicit about those values because bincode uses different defaults in different places
|
||||
pub fn make_bincode_serializer() -> impl ::bincode::Options {
|
||||
pub fn make_bincode_serializer() -> impl BincodeOptions {
|
||||
use ::bincode::Options;
|
||||
::bincode::DefaultOptions::new()
|
||||
.with_little_endian()
|
||||
|
||||
@@ -6,12 +6,17 @@ edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
# of env files and requires workspace context
|
||||
exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
serde_json = {workspace = true, optional = true }
|
||||
tracing = {workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -20,7 +25,7 @@ utoipa = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["env", "network"]
|
||||
env = ["dotenvy", "log"]
|
||||
env = ["dotenvy", "log", "serde_json", "tracing"]
|
||||
network = ["schemars", "serde", "url"]
|
||||
utoipa = [ "dep:utoipa" ]
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://cdn1.media-platform.net/api/",
|
||||
front_hosts: None,
|
||||
},
|
||||
];
|
||||
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
@@ -72,6 +76,13 @@ pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
},
|
||||
];
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
fn serialize_api_urls(urls: &[ApiUrlConst]) -> String {
|
||||
serde_json::to_string(urls)
|
||||
.inspect_err(|e| tracing::warn!("failed to serialize nym_api_urls for env: {e}"))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
|
||||
@@ -162,9 +173,11 @@ pub fn export_to_env() {
|
||||
);
|
||||
set_var_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_to_default(var_names::NYM_APIS, &serialize_api_urls(NYM_APIS));
|
||||
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
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::NYM_VPN_APIS, &serialize_api_urls(NYM_VPN_APIS));
|
||||
set_var_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTATION_URL,
|
||||
UPGRADE_MODE_ATTESTATION_URL,
|
||||
@@ -211,6 +224,9 @@ pub fn export_to_env_if_not_set() {
|
||||
);
|
||||
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_conditionally_to_default(var_names::NYM_APIS, &serialize_api_urls(NYM_APIS));
|
||||
set_var_conditionally_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
set_var_conditionally_to_default(var_names::NYM_VPN_APIS, &serialize_api_urls(NYM_VPN_APIS));
|
||||
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(
|
||||
|
||||
@@ -7,6 +7,13 @@ use serde::{Deserialize, Serialize};
|
||||
use std::ops::Not;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
use crate::var_names;
|
||||
#[cfg(feature = "env")]
|
||||
use std::env::{VarError, var};
|
||||
#[cfg(feature = "env")]
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ChainDetails {
|
||||
@@ -55,7 +62,7 @@ pub struct ApiUrl {
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
pub front_hosts: Option<&'a [&'a str]>,
|
||||
@@ -106,10 +113,6 @@ impl NymNetworkDetails {
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
pub fn new_from_env() -> Self {
|
||||
use crate::var_names;
|
||||
use std::env::{VarError, var};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
fn get_optional_env<K: AsRef<OsStr>>(env: K) -> Option<String> {
|
||||
match var(env) {
|
||||
Ok(var) => {
|
||||
@@ -125,6 +128,11 @@ impl NymNetworkDetails {
|
||||
}
|
||||
|
||||
let nym_api = var(var_names::NYM_API).expect("nym api not set");
|
||||
let nym_api_urls = try_parse_api_urls(var_names::NYM_APIS).unwrap_or(vec![ApiUrl {
|
||||
url: nym_api.clone(),
|
||||
front_hosts: None,
|
||||
}]);
|
||||
let nym_vpn_api_urls = try_parse_api_urls(var_names::NYM_VPN_APIS);
|
||||
|
||||
NymNetworkDetails::new_empty()
|
||||
.with_network_name(var(var_names::NETWORK_NAME).expect("network name not set"))
|
||||
@@ -161,10 +169,8 @@ impl NymNetworkDetails {
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
.with_nym_api_urls(Some(vec![ApiUrl {
|
||||
url: nym_api,
|
||||
front_hosts: None,
|
||||
}]))
|
||||
.with_nym_vpn_api_urls(nym_vpn_api_urls)
|
||||
.with_nym_api_urls(nym_api_urls)
|
||||
}
|
||||
|
||||
pub fn new_mainnet() -> Self {
|
||||
@@ -218,6 +224,9 @@ impl NymNetworkDetails {
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
let nym_api_urls = self.nym_api_urls();
|
||||
let nym_vpn_api_urls = self.nym_vpn_api_urls();
|
||||
|
||||
set_var(var_names::NETWORK_NAME, self.network_name);
|
||||
set_var(var_names::BECH32_PREFIX, self.chain_details.bech32_account_prefix);
|
||||
|
||||
@@ -243,9 +252,10 @@ impl NymNetworkDetails {
|
||||
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
|
||||
|
||||
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
|
||||
set_optional_var(var_names::NYM_VPN_APIS, nym_vpn_api_urls);
|
||||
set_optional_var(var_names::NYM_APIS, nym_api_urls);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn default_gas_price_amount(&self) -> f64 {
|
||||
@@ -355,8 +365,14 @@ impl NymNetworkDetails {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
|
||||
self.nym_api_urls = urls;
|
||||
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
|
||||
self.nym_api_urls = Some(urls);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
|
||||
self.nym_vpn_api_urls = urls;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -366,6 +382,28 @@ impl NymNetworkDetails {
|
||||
.expect("the provided nym-vpn api url is invalid!")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
fn nym_api_urls(&self) -> Option<String> {
|
||||
serde_json::to_string(self.nym_api_urls.as_deref()?)
|
||||
.inspect_err(|e| tracing::warn!("failed to serialize nym_api_urls for env: {e}"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
fn nym_vpn_api_urls(&self) -> Option<String> {
|
||||
serde_json::to_string(self.nym_vpn_api_urls.as_deref()?)
|
||||
.inspect_err(|e| tracing::warn!("failed to serialize nym_vpn_api_urls for env: {e}"))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
fn try_parse_api_urls(k: impl AsRef<OsStr>) -> Option<Vec<ApiUrl>> {
|
||||
let raw = var(k).ok()?;
|
||||
serde_json::from_str(&raw)
|
||||
.inspect_err(|e| tracing::warn!("failed to parse api urls from env \"{raw:?}\": {e}"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -21,9 +21,11 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYM_APIS: &str = "NYM_APIS";
|
||||
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 NYM_VPN_APIS: &str = "NYM_VPN_APIS";
|
||||
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";
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "nym-kkt-ciphersuite"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
|
||||
blake3 = { workspace = true, optional = true }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
|
||||
|
||||
[features]
|
||||
digests = ["blake3", "libcrux-sha3"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KKTCiphersuiteError {
|
||||
#[error(
|
||||
"attempted to use an insecure encapsulation key hash length. requested: {requested}. minimum: {minimum}"
|
||||
)]
|
||||
InsecureHashLen { requested: u8, minimum: u8 },
|
||||
|
||||
#[error("{raw} does not correspond to any known KEM type encoding")]
|
||||
UnknownKEMType { raw: u8 },
|
||||
|
||||
#[error("{raw} does not correspond to any known Hash Function type encoding")]
|
||||
UnknownHashFunctionType { raw: u8 },
|
||||
|
||||
#[error("{raw} does not correspond to any known Signature Scheme type encoding")]
|
||||
UnknownSignatureSchemeType { raw: u8 },
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::KKTCiphersuiteError;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use strum_macros::{Display, EnumIter, EnumString};
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub const DEFAULT_HASH_LEN: usize = 32;
|
||||
const _: () = assert!(DEFAULT_HASH_LEN <= u8::MAX as usize);
|
||||
|
||||
pub const MINIMUM_SECURE_HASH_LEN: u8 = 16;
|
||||
const _: () = assert!(MINIMUM_SECURE_HASH_LEN <= DEFAULT_HASH_LEN as u8);
|
||||
|
||||
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
|
||||
|
||||
// no point in importing curve libraries for well-defined constants
|
||||
pub mod ed25519 {
|
||||
pub const SECRET_KEY_LENGTH: usize = 32;
|
||||
pub const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
pub const SIGNATURE_LENGTH: usize = 64;
|
||||
}
|
||||
|
||||
pub mod x25519 {
|
||||
pub const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
pub const SECRET_KEY_LENGTH: usize = 32;
|
||||
}
|
||||
|
||||
pub mod ml_kem768 {
|
||||
pub const PUBLIC_KEY_LENGTH: usize = 1184;
|
||||
}
|
||||
|
||||
pub mod mceliece {
|
||||
pub const PUBLIC_KEY_LENGTH: usize = 524160;
|
||||
pub const SECRET_KEY_LENGTH: usize = 13608;
|
||||
pub const CIPHERTEXT_LENGTH: usize = 156;
|
||||
}
|
||||
|
||||
pub mod xwing {
|
||||
use crate::{ml_kem768, x25519};
|
||||
|
||||
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
|
||||
}
|
||||
|
||||
pub type KEMKeyDigests = KeyDigests;
|
||||
pub type SigningKeyDigests = KeyDigests;
|
||||
|
||||
pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Debug,
|
||||
IntoPrimitive,
|
||||
TryFromPrimitive,
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[repr(u8)]
|
||||
pub enum HashFunction {
|
||||
Blake3 = 0,
|
||||
Shake256 = 1,
|
||||
Shake128 = 2,
|
||||
SHA256 = 3,
|
||||
}
|
||||
|
||||
impl HashFunction {
|
||||
#[cfg(feature = "digests")]
|
||||
pub fn digest<M: AsRef<[u8]>>(&self, data: M, output_length: usize) -> Vec<u8> {
|
||||
let mut out = vec![0u8; output_length];
|
||||
match self {
|
||||
HashFunction::Blake3 => {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(data.as_ref());
|
||||
hasher.finalize_xof().fill(&mut out);
|
||||
hasher.reset();
|
||||
}
|
||||
HashFunction::Shake256 => libcrux_sha3::shake256_ema(&mut out, data.as_ref()),
|
||||
HashFunction::Shake128 => libcrux_sha3::shake128_ema(&mut out, data.as_ref()),
|
||||
HashFunction::SHA256 => libcrux_sha3::sha256_ema(&mut out, data.as_ref()),
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum HashLength {
|
||||
Default = 0,
|
||||
#[num_enum(catch_all)]
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
impl Display for HashLength {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HashLength::Default => DEFAULT_HASH_LEN.fmt(f),
|
||||
HashLength::Custom(custom_len) => custom_len.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashLength {
|
||||
pub fn decode(raw: u8) -> Result<Self, KKTCiphersuiteError> {
|
||||
// check if we're using encoding for 'default' value
|
||||
if raw == u8::from(Self::Default) {
|
||||
return Ok(Self::Default);
|
||||
}
|
||||
// otherwise, we treat it as a custom length, and we have to validate its security
|
||||
let custom_len = raw;
|
||||
|
||||
if custom_len < MINIMUM_SECURE_HASH_LEN {
|
||||
return Err(KKTCiphersuiteError::InsecureHashLen {
|
||||
requested: custom_len,
|
||||
minimum: MINIMUM_SECURE_HASH_LEN,
|
||||
});
|
||||
}
|
||||
Ok(Self::Custom(custom_len))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<u8>> for HashLength {
|
||||
type Error = KKTCiphersuiteError;
|
||||
|
||||
fn try_from(value: Option<u8>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
None => Ok(Self::Default),
|
||||
Some(custom_len) => Self::decode(custom_len),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashLength {
|
||||
pub const fn value(&self) -> usize {
|
||||
match self {
|
||||
HashLength::Default => DEFAULT_HASH_LEN,
|
||||
HashLength::Custom(custom_len) => *custom_len as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Debug,
|
||||
IntoPrimitive,
|
||||
TryFromPrimitive,
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[repr(u8)]
|
||||
pub enum SignatureScheme {
|
||||
Ed25519 = 0,
|
||||
}
|
||||
|
||||
impl SignatureScheme {
|
||||
pub const fn signing_key_length(&self) -> usize {
|
||||
match self {
|
||||
// 32 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn verification_key_length(&self) -> usize {
|
||||
match self {
|
||||
// 32 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn signature_length(&self) -> usize {
|
||||
match self {
|
||||
// 64 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Debug,
|
||||
IntoPrimitive,
|
||||
TryFromPrimitive,
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[repr(u8)]
|
||||
pub enum KEM {
|
||||
XWing = 0,
|
||||
MlKem768 = 1,
|
||||
McEliece = 2,
|
||||
X25519 = 255,
|
||||
}
|
||||
|
||||
impl KEM {
|
||||
pub fn encapsulation_key_length(&self) -> usize {
|
||||
match self {
|
||||
KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH,
|
||||
KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
|
||||
KEM::X25519 => x25519::PUBLIC_KEY_LENGTH,
|
||||
KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct Ciphersuite {
|
||||
hash_function: HashFunction,
|
||||
signature_scheme: SignatureScheme,
|
||||
kem: KEM,
|
||||
hash_length: HashLength,
|
||||
encapsulation_key_length: usize,
|
||||
signing_key_length: usize,
|
||||
verification_key_length: usize,
|
||||
signature_length: usize,
|
||||
}
|
||||
|
||||
impl Ciphersuite {
|
||||
pub fn new(
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
signature_scheme: SignatureScheme,
|
||||
hash_length: HashLength,
|
||||
) -> Self {
|
||||
Self {
|
||||
hash_function,
|
||||
signature_scheme,
|
||||
kem,
|
||||
hash_length,
|
||||
encapsulation_key_length: kem.encapsulation_key_length(),
|
||||
signing_key_length: signature_scheme.signing_key_length(),
|
||||
verification_key_length: signature_scheme.verification_key_length(),
|
||||
signature_length: signature_scheme.signature_length(),
|
||||
}
|
||||
}
|
||||
|
||||
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.value()
|
||||
}
|
||||
|
||||
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, KKTCiphersuiteError> {
|
||||
let hash_length = HashLength::try_from(custom_hash_length)?;
|
||||
|
||||
Ok(Ciphersuite::new(
|
||||
kem,
|
||||
hash_function,
|
||||
signature_scheme,
|
||||
hash_length,
|
||||
))
|
||||
}
|
||||
pub fn encode(&self) -> [u8; CIPHERSUITE_ENCODING_LEN] {
|
||||
// [kem, hash, hashlen, sig]
|
||||
[
|
||||
self.kem.into(),
|
||||
self.hash_function.into(),
|
||||
self.hash_length.into(),
|
||||
self.signature_scheme.into(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn decode(encoding: [u8; CIPHERSUITE_ENCODING_LEN]) -> Result<Self, KKTCiphersuiteError> {
|
||||
let raw_kem = encoding[0];
|
||||
let raw_hash_function = encoding[1];
|
||||
let hash_len = encoding[2];
|
||||
let raw_signature_scheme = encoding[3];
|
||||
|
||||
let kem = KEM::try_from(raw_kem)
|
||||
.map_err(|_| KKTCiphersuiteError::UnknownKEMType { raw: raw_kem })?;
|
||||
let hash_function = HashFunction::try_from(raw_hash_function).map_err(|_| {
|
||||
KKTCiphersuiteError::UnknownHashFunctionType {
|
||||
raw: raw_hash_function,
|
||||
}
|
||||
})?;
|
||||
let hash_length = HashLength::decode(hash_len)?;
|
||||
let signature_scheme = SignatureScheme::try_from(raw_signature_scheme).map_err(|_| {
|
||||
KKTCiphersuiteError::UnknownSignatureSchemeType {
|
||||
raw: raw_signature_scheme,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self::new(kem, hash_function, signature_scheme, hash_length))
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn kem_display_consistency() {
|
||||
for kem in KEM::iter() {
|
||||
let display = format!("{kem}");
|
||||
assert_eq!(kem, KEM::from_str(&display).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_function_display_consistency() {
|
||||
for hash_fn in HashFunction::iter() {
|
||||
let display = format!("{hash_fn}");
|
||||
assert_eq!(hash_fn, HashFunction::from_str(&display).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_scheme_display_consistency() {
|
||||
for scheme in SignatureScheme::iter() {
|
||||
let display = format!("{scheme}");
|
||||
assert_eq!(scheme, SignatureScheme::from_str(&display).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,19 @@ publish = false
|
||||
blake3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
|
||||
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
|
||||
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-sha3 = { 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"
|
||||
rand = "0.9.2"
|
||||
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
|
||||
rand09 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ use nym_kkt::{
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rand09::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
rand09::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
@@ -33,13 +33,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
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()
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
@@ -54,8 +54,8 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
// 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;
|
||||
use libcrux_kem::Algorithm;
|
||||
|
||||
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, PartialEq)]
|
||||
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 use nym_kkt_ciphersuite::*;
|
||||
|
||||
pub enum EncapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PublicKey),
|
||||
@@ -87,200 +62,6 @@ impl<'a> EncapsulationKey<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
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, PartialEq)]
|
||||
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, PartialEq)]
|
||||
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; CIPHERSUITE_ENCODING_LEN] {
|
||||
// [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; CIPHERSUITE_ENCODING_LEN]) -> Result<Self, KKTError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Result<Algorithm, KKTError> {
|
||||
match kem {
|
||||
KEM::MlKem768 => Ok(Algorithm::MlKem768),
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_S
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub const KKT_CONTEXT_LEN: usize = 7;
|
||||
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
|
||||
|
||||
// bitmask used: 0b1110_0000
|
||||
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
|
||||
@@ -202,13 +202,11 @@ impl KKTContext {
|
||||
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
|
||||
})?;
|
||||
|
||||
let ciphersuite_bytes = header_bytes[2..6].try_into().map_err(|_| {
|
||||
KKTError::CiphersuiteDecodingError {
|
||||
info: format!(
|
||||
"Incorrect Encoding Length: actual: 4 != expected: {CIPHERSUITE_ENCODING_LEN}",
|
||||
),
|
||||
}
|
||||
})?;
|
||||
// SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ciphersuite_bytes = header_bytes[2..2 + CIPHERSUITE_ENCODING_LEN]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
Ok(KKTContext {
|
||||
version: kkt_version,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::kkt::KKT_INITIAL_FRAME_AAD;
|
||||
use crate::{
|
||||
ciphersuite::CURVE25519_KEY_LEN, context::KKTContext, error::KKTError, frame::KKTFrame,
|
||||
};
|
||||
use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame};
|
||||
use blake3::Hasher;
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
@@ -36,7 +33,7 @@ impl KKTSessionSecret {
|
||||
|
||||
fn try_derive(private_key: &x25519::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]);
|
||||
pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]);
|
||||
|
||||
// Todo: check validity of pk...
|
||||
let pk = x25519::PublicKey::from(pub_key);
|
||||
@@ -71,7 +68,7 @@ where
|
||||
let mut encrypted_frame =
|
||||
encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?;
|
||||
|
||||
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + CURVE25519_KEY_LEN);
|
||||
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE);
|
||||
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
|
||||
output_buffer.append(&mut encrypted_frame);
|
||||
|
||||
@@ -84,19 +81,19 @@ pub fn decrypt_initial_kkt_frame(
|
||||
responder_private_key: &x25519::PrivateKey,
|
||||
encrypted_frame_bytes: &[u8],
|
||||
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
|
||||
if encrypted_frame_bytes.len() < CURVE25519_KEY_LEN + TAG_LEN + NONCE_LEN {
|
||||
if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN {
|
||||
Err(KKTError::AEADError {
|
||||
info: "Encrypted KKT Frame is too short.",
|
||||
})
|
||||
} else {
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_private_key,
|
||||
&encrypted_frame_bytes[0..CURVE25519_KEY_LEN],
|
||||
&encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE],
|
||||
)?;
|
||||
|
||||
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
|
||||
&shared_secret,
|
||||
&encrypted_frame_bytes[CURVE25519_KEY_LEN..],
|
||||
&encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..],
|
||||
KKT_INITIAL_FRAME_AAD,
|
||||
)?;
|
||||
Ok((shared_secret, kkt_frame, kkt_context))
|
||||
@@ -181,12 +178,11 @@ mod test {
|
||||
use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame};
|
||||
use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame};
|
||||
use crate::{
|
||||
ciphersuite::HASH_LEN_256,
|
||||
ciphersuite::DEFAULT_HASH_LEN,
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng, rng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand09::{RngCore, SeedableRng, rng};
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
@@ -209,7 +205,7 @@ mod test {
|
||||
fn test_encryption() {
|
||||
let mut rng = rng();
|
||||
|
||||
let mut secret_key = [0u8; HASH_LEN_256];
|
||||
let mut secret_key = [0u8; DEFAULT_HASH_LEN];
|
||||
rng.fill_bytes(&mut secret_key);
|
||||
|
||||
let mut plaintext = vec![0; 100];
|
||||
@@ -230,7 +226,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
||||
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::context::KKTStatus;
|
||||
use nym_kkt_ciphersuite::error::KKTCiphersuiteError;
|
||||
use std::fmt::Debug;
|
||||
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(transparent)]
|
||||
CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
|
||||
|
||||
#[error("KEM mapping failure: {}", info)]
|
||||
KEMMapping { info: &'static str },
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::ciphersuite::HashFunction;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
|
||||
use libcrux_sha3;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
@@ -60,7 +61,6 @@ pub fn generate_keypair_mceliece<'a, R>(
|
||||
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);
|
||||
@@ -72,26 +72,18 @@ pub fn hash_key_bytes(
|
||||
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);
|
||||
}
|
||||
}
|
||||
hash_function.digest(key_bytes, hash_length)
|
||||
}
|
||||
|
||||
hashed_key
|
||||
/// attempt to produce digests of the provided key using all known [HashFunction] with a default
|
||||
/// hash length where variable output is available
|
||||
pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests {
|
||||
use strum::IntoEnumIterator;
|
||||
let mut digests = HashMap::new();
|
||||
for hash in HashFunction::iter() {
|
||||
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
|
||||
}
|
||||
digests
|
||||
}
|
||||
|
||||
/// This does NOT run in constant time.
|
||||
|
||||
+14
-18
@@ -9,7 +9,7 @@
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
@@ -27,16 +27,13 @@ pub use crate::session::{
|
||||
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
|
||||
use crate::frame::KKTFrame;
|
||||
|
||||
pub(crate) const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
|
||||
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_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
|
||||
/// * `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
|
||||
@@ -93,7 +90,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
@@ -202,14 +199,14 @@ mod tests {
|
||||
|
||||
fn random_x25519_key() -> x25519::PrivateKey {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
@@ -244,7 +241,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
||||
let (session_key, context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
@@ -265,7 +262,7 @@ mod tests {
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
@@ -279,7 +276,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
@@ -307,8 +304,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (mut context, request_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
@@ -327,7 +323,7 @@ mod tests {
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
@@ -340,7 +336,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -393,7 +389,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -420,7 +416,7 @@ mod tests {
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, mut context, request_frame) = request_kem_key(
|
||||
let (session_key, context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
@@ -440,7 +436,7 @@ mod tests {
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
|
||||
+34
-32
@@ -7,17 +7,19 @@ pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod key_utils;
|
||||
pub mod kkt;
|
||||
// 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);
|
||||
pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
|
||||
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME";
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::kkt::KKT_RESPONSE_AAD;
|
||||
use crate::{
|
||||
KKT_RESPONSE_AAD,
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
|
||||
encryption::{
|
||||
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
|
||||
@@ -36,7 +38,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -46,8 +48,8 @@ mod test {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
@@ -104,18 +106,18 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -127,7 +129,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -139,7 +141,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -152,7 +154,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -163,7 +165,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -175,7 +177,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -188,7 +190,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -201,7 +203,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -212,7 +214,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -224,7 +226,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -239,7 +241,7 @@ mod test {
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -252,8 +254,8 @@ mod test {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
@@ -310,7 +312,7 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
@@ -328,11 +330,11 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -350,7 +352,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -362,7 +364,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -386,7 +388,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -397,7 +399,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -415,7 +417,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -428,7 +430,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -452,7 +454,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -463,7 +465,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -481,7 +483,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
@@ -73,7 +73,7 @@ where
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
session_id: KKTSessionId,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
|
||||
@@ -12,8 +12,9 @@ readme.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["net"] }
|
||||
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||
nym-test-utils = { path = "../test-utils", optional = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[features]
|
||||
io-mocks = ["nym-test-utils"]
|
||||
|
||||
@@ -4,15 +4,100 @@
|
||||
#[cfg(feature = "io-mocks")]
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::debug;
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
pub trait LpTransport: Sized {
|
||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
||||
|
||||
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `packet_data` - The serialised LP packet to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
|
||||
|
||||
/// Receives an LP packet from a TCP stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
async fn send_serialised_packet_async_write<W>(
|
||||
writer: &mut W,
|
||||
packet_data: &[u8],
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
writer
|
||||
.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Send the actual packet data
|
||||
writer
|
||||
.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Flush to ensure data is sent immediately
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader
|
||||
.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
reader
|
||||
.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
}
|
||||
|
||||
impl LpTransport for TcpStream {
|
||||
@@ -24,6 +109,14 @@ impl LpTransport for TcpStream {
|
||||
// Set TCP_NODELAY for low latency
|
||||
self.set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "io-mocks")]
|
||||
@@ -35,4 +128,12 @@ impl LpTransport for MockIOStream {
|
||||
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||
rand09 = { package = "rand", version = "0.9.2" }
|
||||
rand09 = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-lp-transport = { path = "../nym-lp-transport" }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||
@@ -34,11 +35,21 @@ num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
# needed for the 'mock 'feature
|
||||
nym-test-utils = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
#rand_chacha = "0.3"
|
||||
mock_instant = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
|
||||
|
||||
[features]
|
||||
mock = ["nym-test-utils", "nym-crypto/rand"]
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use nym_test_utils::helpers::u64_seeded_rng;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let mut rng = u64_seeded_rng(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
|
||||
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
|
||||
// Need payload and trailer to meet min_size requirement
|
||||
let payload_size = 10; // Arbitrary
|
||||
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
||||
@@ -565,7 +565,7 @@ mod tests {
|
||||
let result = parse_lp_packet(&buf, None);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::InvalidMessageType(255)) => {} // Expected error
|
||||
Err(LpError::InvalidMessageType(231)) => {} // Expected error
|
||||
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
@@ -628,7 +628,7 @@ mod tests {
|
||||
receiver_idx: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -681,7 +681,7 @@ mod tests {
|
||||
receiver_idx: 100,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [55; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -757,12 +757,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forward_packet_encode_decode_roundtrip() {
|
||||
fn test_forward_packet_encode_decode_roundtrip_v4() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let forward_data = crate::message::ForwardPacketData {
|
||||
target_gateway_identity: [77u8; 32],
|
||||
target_lp_address: "1.2.3.4:41264".to_string(),
|
||||
target_lp_address: "1.2.3.4:41264".parse().unwrap(),
|
||||
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
|
||||
};
|
||||
|
||||
@@ -789,7 +789,50 @@ mod tests {
|
||||
|
||||
if let LpMessage::ForwardPacket(data) = decoded.message {
|
||||
assert_eq!(data.target_gateway_identity, [77u8; 32]);
|
||||
assert_eq!(data.target_lp_address, "1.2.3.4:41264");
|
||||
assert_eq!(data.target_lp_address, "1.2.3.4:41264".parse().unwrap());
|
||||
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
|
||||
} else {
|
||||
panic!("Expected ForwardPacket message");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forward_packet_encode_decode_roundtrip_v6() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let forward_data = crate::message::ForwardPacketData {
|
||||
target_gateway_identity: [77u8; 32],
|
||||
target_lp_address: "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap(),
|
||||
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 999,
|
||||
counter: 555,
|
||||
},
|
||||
message: LpMessage::ForwardPacket(forward_data),
|
||||
trailer: [0xff; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||
|
||||
// Parse back
|
||||
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||
|
||||
// Verify LP protocol handling works correctly
|
||||
assert_eq!(decoded.header.receiver_idx, 999);
|
||||
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
|
||||
|
||||
if let LpMessage::ForwardPacket(data) = decoded.message {
|
||||
assert_eq!(data.target_gateway_identity, [77u8; 32]);
|
||||
assert_eq!(
|
||||
data.target_lp_address,
|
||||
"[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap()
|
||||
);
|
||||
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
|
||||
} else {
|
||||
panic!("Expected ForwardPacket message");
|
||||
@@ -1246,4 +1289,37 @@ mod tests {
|
||||
_ => panic!("Expected SubsessionKK1 message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_error() {
|
||||
use crate::message::ErrorPacketData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let error_data = ErrorPacketData {
|
||||
message: "this is an error".to_string(),
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::Error(error_data.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||
|
||||
assert_eq!(decoded.header.receiver_idx, 42);
|
||||
match decoded.message {
|
||||
LpMessage::Error(data) => {
|
||||
assert_eq!(data.message, "this is an error");
|
||||
}
|
||||
_ => panic!("Expected Error message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::MessageType;
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||
use nym_kkt::error::KKTError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -90,4 +93,36 @@ pub enum LpError {
|
||||
/// Received an LP packet with an incompatible, legacy, version
|
||||
#[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")]
|
||||
IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 },
|
||||
|
||||
#[error("attempted to create an LP responder without providing a valid KEM key")]
|
||||
ResponderWithMissingKEMKey,
|
||||
|
||||
#[error(
|
||||
"there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function"
|
||||
)]
|
||||
NoKnownKEMKeyDigests {
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
},
|
||||
|
||||
#[error("failed to complete KKT/PSQ handshake: {0}")]
|
||||
KKTPSQHandshake(String),
|
||||
|
||||
#[error("failed to complete the KKT exchange: {source}")]
|
||||
KKTFailure {
|
||||
#[from]
|
||||
source: KKTError,
|
||||
},
|
||||
}
|
||||
|
||||
impl LpError {
|
||||
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
|
||||
Self::KKTPSQHandshake(msg.into())
|
||||
}
|
||||
|
||||
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
|
||||
Self::KKTPSQHandshake(format!(
|
||||
"received unexpected response, got: {got:?}, expected: {expected:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ pub fn handle_request<'a>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme};
|
||||
use nym_kkt::key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519,
|
||||
@@ -362,16 +363,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
let (init, resp) = mock_peers();
|
||||
let responder_kem_key = resp.encapsulate_kem_key().unwrap();
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
@@ -386,16 +379,16 @@ mod tests {
|
||||
|
||||
let (session_secret, context, request_data) = create_request(
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
responder_x25519.public_key(),
|
||||
init.ed25519.private_key(),
|
||||
resp.x25519.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response_data = handle_request(
|
||||
&request_data,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
responder_x25519.private_key(),
|
||||
Some(init.ed25519.public_key()),
|
||||
resp.ed25519.private_key(),
|
||||
resp.x25519.private_key(),
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -404,7 +397,7 @@ mod tests {
|
||||
let result = process_response(
|
||||
context,
|
||||
&session_secret,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
resp.ed25519.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
&response_data,
|
||||
);
|
||||
|
||||
+137
-85
@@ -4,11 +4,13 @@
|
||||
pub mod codec;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod kkt_orchestrator;
|
||||
// pub mod kkt_orchestrator;
|
||||
pub mod message;
|
||||
pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod peer;
|
||||
pub mod psk;
|
||||
mod psq;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
@@ -20,61 +22,139 @@ pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::{LpSession, generate_fresh_salt};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub struct SessionsMock {
|
||||
pub initiator: LpSession,
|
||||
pub responder: LpSession,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl SessionsMock {
|
||||
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
let init_remote = init.as_remote();
|
||||
let salt = [42u8; 32];
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
|
||||
// skip KKT by just deriving the kem key locally
|
||||
let kem_keys = resp.kem_psq.as_ref().unwrap();
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
// INIT -> RESP: PSQ MSG1
|
||||
let psq_initiator = crate::psk::psq_initiator_create_message(
|
||||
init.x25519.private_key(),
|
||||
&resp_remote.x25519_public,
|
||||
&encapsulation_key,
|
||||
init.ed25519.private_key(),
|
||||
init.ed25519.public_key(),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
|
||||
|
||||
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(init.x25519().private_key().as_bytes())
|
||||
.remote_public_key(resp_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_initiator()
|
||||
.unwrap();
|
||||
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
|
||||
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
let psq_responder = crate::psk::psq_responder_process_message(
|
||||
resp.x25519.private_key(),
|
||||
&init_remote.x25519_public,
|
||||
(&decapsulation_key, &encapsulation_key),
|
||||
&init_remote.ed25519_public,
|
||||
&psq_payload,
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(resp.x25519().private_key().as_bytes())
|
||||
.remote_public_key(init_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_responder()
|
||||
.unwrap();
|
||||
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
|
||||
noise_protocol_resp.read_message(&noise_msg1).unwrap();
|
||||
|
||||
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
|
||||
noise_protocol_init.read_message(&noise_msg2).unwrap();
|
||||
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
assert!(noise_protocol_init.is_handshake_finished());
|
||||
|
||||
noise_protocol_resp.read_message(&noise_msg3).unwrap();
|
||||
assert!(noise_protocol_resp.is_handshake_finished());
|
||||
|
||||
SessionsMock {
|
||||
initiator: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key.clone(),
|
||||
init,
|
||||
resp_remote,
|
||||
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
noise_protocol_init,
|
||||
),
|
||||
responder: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key,
|
||||
resp,
|
||||
init_remote,
|
||||
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
noise_protocol_resp,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// we just need a dummy 'valid' session for simpler tests
|
||||
pub fn mock_initiator() -> LpSession {
|
||||
Self::mock_post_handshake(1234).initiator
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use std::sync::Arc;
|
||||
let sessions = SessionsMock::mock_post_handshake(69);
|
||||
(sessions.initiator, sessions.responder)
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// X25519 keypairs for Noise protocol
|
||||
let keypair_1 = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
let keypair_2 = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
|
||||
// Use a fixed receiver_index for deterministic tests
|
||||
let receiver_index: u32 = 12345;
|
||||
|
||||
// Ed25519 keypairs for PSQ authentication (placeholders for testing)
|
||||
let ed25519_keypair_1 = ed25519::KeyPair::from_secret([1u8; 32], 0);
|
||||
let ed25519_keypair_2 = ed25519::KeyPair::from_secret([2u8; 32], 1);
|
||||
|
||||
let ed25519_keypair1_pubkey = *ed25519_keypair_1.public_key();
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// PSQ will always derive the PSK during handshake using X25519 as DHKEM
|
||||
|
||||
let initiator_session = LpSession::new(
|
||||
receiver_index,
|
||||
true,
|
||||
Arc::new(ed25519_keypair_1),
|
||||
keypair_1.clone(),
|
||||
ed25519_keypair_2.public_key(),
|
||||
keypair_2.public_key(),
|
||||
&salt,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
let responder_session = LpSession::new(
|
||||
receiver_index,
|
||||
false,
|
||||
Arc::new(ed25519_keypair_2),
|
||||
keypair_2.clone(),
|
||||
&ed25519_keypair1_pubkey,
|
||||
keypair_1.public_key(),
|
||||
&salt,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
(initiator_session, responder_session)
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_session_for_test() -> LpSession {
|
||||
SessionsMock::mock_initiator()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -82,9 +162,8 @@ mod tests {
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{LpError, sessions_for_tests};
|
||||
use crate::{LpError, SessionsMock, mock_session_for_test};
|
||||
use bytes::BytesMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
@@ -92,7 +171,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let session = sessions_for_tests().0;
|
||||
let mut session = mock_session_for_test();
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
@@ -190,48 +269,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
// Create session manager
|
||||
let local_manager = SessionManager::new();
|
||||
let remote_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypairs for PSQ authentication
|
||||
let ed25519_keypair_local = ed25519::KeyPair::from_secret([8u8; 32], 0);
|
||||
let ed25519_keypair_remote = ed25519::KeyPair::from_secret([9u8; 32], 1);
|
||||
|
||||
let ed25519_keypair_local_pubkey = *ed25519_keypair_local.public_key();
|
||||
let x25519_keypair_local_pubkey = ed25519_keypair_local_pubkey.to_x25519().unwrap();
|
||||
let x25519_keypair_remote_pubkey = ed25519_keypair_remote.public_key().to_x25519().unwrap();
|
||||
let mut local_manager = SessionManager::new();
|
||||
let mut remote_manager = SessionManager::new();
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
|
||||
// Test salt
|
||||
let salt = [46u8; 32];
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let local_session = sessions.initiator;
|
||||
let remote_session = sessions.responder;
|
||||
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
Arc::new(ed25519_keypair_local),
|
||||
ed25519_keypair_remote.public_key(),
|
||||
&x25519_keypair_remote_pubkey,
|
||||
true,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = local_manager.create_session_state_machine(local_session);
|
||||
let _ = remote_manager.create_session_state_machine(remote_session);
|
||||
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
Arc::new(ed25519_keypair_remote),
|
||||
&ed25519_keypair_local_pubkey,
|
||||
&x25519_keypair_local_pubkey,
|
||||
false,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
|
||||
+212
-30
@@ -1,16 +1,19 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError};
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClientHelloData {
|
||||
/// Client-proposed receiver index for session identification (4 bytes)
|
||||
/// Auto-generated randomly by the client
|
||||
@@ -27,6 +30,17 @@ impl ClientHelloData {
|
||||
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
||||
pub const LEN: usize = 100;
|
||||
|
||||
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
|
||||
LpPacket::new(
|
||||
LpHeader::new(
|
||||
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
|
||||
0, // counter starts at 0
|
||||
protocol_version,
|
||||
),
|
||||
LpMessage::ClientHello(self),
|
||||
)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
Self::LEN
|
||||
}
|
||||
@@ -109,6 +123,11 @@ impl ClientHelloData {
|
||||
salt: b[68..].try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to construct remote peer information based on the data provided in this packet.
|
||||
pub fn to_remote_peer(&self) -> LpRemotePeer {
|
||||
LpRemotePeer::new(self.client_ed25519_public_key, self.client_lp_public_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
|
||||
@@ -135,6 +154,8 @@ pub enum MessageType {
|
||||
SubsessionReady = 0x000C,
|
||||
/// Subsession abort - race winner tells loser to become responder
|
||||
SubsessionAbort = 0x000D,
|
||||
/// General error
|
||||
Error = 0x00FF,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
@@ -151,6 +172,9 @@ impl MessageType {
|
||||
pub struct HandshakeData(pub Vec<u8>);
|
||||
|
||||
impl HandshakeData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -168,6 +192,11 @@ impl HandshakeData {
|
||||
pub struct EncryptedDataPayload(pub Vec<u8>);
|
||||
|
||||
impl EncryptedDataPayload {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -186,6 +215,10 @@ impl EncryptedDataPayload {
|
||||
pub struct KKTRequestData(pub Vec<u8>);
|
||||
|
||||
impl KKTRequestData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -204,6 +237,10 @@ impl KKTRequestData {
|
||||
pub struct KKTResponseData(pub Vec<u8>);
|
||||
|
||||
impl KKTResponseData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -217,15 +254,60 @@ impl KKTResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
/// General human-readable error message
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorPacketData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorPacketData {
|
||||
pub(crate) fn new(message: impl Into<String>) -> Self {
|
||||
ErrorPacketData {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// length-encoding + message
|
||||
4 + self.message.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.message.len() as u32);
|
||||
dst.put_slice(self.message.as_bytes());
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ErrorPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||
if bytes[4..].len() != message_len {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
|
||||
bytes.len(),
|
||||
4 + message_len
|
||||
)));
|
||||
}
|
||||
|
||||
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
|
||||
|
||||
Ok(ErrorPacketData { message })
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardPacketData {
|
||||
/// Target gateway's Ed25519 identity (32 bytes)
|
||||
pub target_gateway_identity: [u8; 32],
|
||||
|
||||
// TODO: replace it with `SocketAddr`
|
||||
/// Target gateway's LP address (IP:port string)
|
||||
pub target_lp_address: String,
|
||||
pub target_lp_address: SocketAddr,
|
||||
|
||||
/// Complete inner LP packet bytes (serialized LpPacket)
|
||||
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
||||
@@ -233,23 +315,46 @@ pub struct ForwardPacketData {
|
||||
}
|
||||
|
||||
impl ForwardPacketData {
|
||||
pub fn new(
|
||||
target_gateway_identity: ed25519::PublicKey,
|
||||
target_lp_address: SocketAddr,
|
||||
inner_packet_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
ForwardPacketData {
|
||||
target_gateway_identity: target_gateway_identity.to_bytes(),
|
||||
target_lp_address,
|
||||
inner_packet_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// 32 bytes target gateway identity
|
||||
// +
|
||||
// 4 bytes length of target lp address
|
||||
// 1 byte length of target lp address type
|
||||
// +
|
||||
// target_lp_address.len()
|
||||
// {4,16} target_lp_address IPv{4,6}
|
||||
// +
|
||||
// 2 bytes target_lp_address port
|
||||
// +
|
||||
// 4 bytes of length of inner packet bytes
|
||||
// +
|
||||
// inner_packet_bytes.len()
|
||||
32 + 4 + self.target_lp_address.len() + 4 + self.inner_packet_bytes.len()
|
||||
match self.target_lp_address {
|
||||
SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
|
||||
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
|
||||
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
|
||||
};
|
||||
|
||||
dst.put_slice(&self.target_gateway_identity);
|
||||
dst.put_u16_le(self.target_lp_address.len() as u16);
|
||||
dst.put_slice(self.target_lp_address.as_bytes());
|
||||
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
|
||||
dst.put_slice(&ip_bytes); // IP bytes
|
||||
dst.put_u16_le(self.target_lp_address.port()); // Port
|
||||
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
|
||||
dst.put_slice(&self.inner_packet_bytes);
|
||||
}
|
||||
@@ -261,42 +366,56 @@ impl ForwardPacketData {
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
// smallest possible packet with empty address and empty data
|
||||
if bytes.len() < 38 {
|
||||
// smallest possible packet with ipv4 and empty data
|
||||
if bytes.len() < 43 {
|
||||
// 32 + 1 + 4 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData[1]. got {}",
|
||||
"Too few bytes to deserialise ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let target_gateway_identity = bytes[0..32].try_into().unwrap();
|
||||
let target_lp_address_len = u16::from_le_bytes([bytes[32], bytes[33]]);
|
||||
let target_lp_address_is_ipv6 = bytes[32] != 0;
|
||||
|
||||
// smallest possible packet with empty data
|
||||
if bytes[34..].len() < 4 + target_lp_address_len as usize {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData[2]. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
|
||||
// IPv6, first check we have actually enough bytes
|
||||
// smallest possible packet with ipv6 and empty data
|
||||
if bytes.len() < 55 {
|
||||
// 32 + 1 + 16 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[49], bytes[50]]);
|
||||
(SocketAddr::new(ipv6, port), 51)
|
||||
} else {
|
||||
// IPv4. Length check done at the start
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[37], bytes[38]]);
|
||||
(SocketAddr::new(ipv4, port), 39)
|
||||
};
|
||||
|
||||
let target_lp_address =
|
||||
String::from_utf8_lossy(&bytes[34..34 + target_lp_address_len as usize]).to_string();
|
||||
let inner_packet_bytes_len = u32::from_le_bytes([
|
||||
bytes[34 + target_lp_address_len as usize],
|
||||
bytes[34 + target_lp_address_len as usize + 1],
|
||||
bytes[34 + target_lp_address_len as usize + 2],
|
||||
bytes[34 + target_lp_address_len as usize + 3],
|
||||
bytes[next_index],
|
||||
bytes[next_index + 1],
|
||||
bytes[next_index + 2],
|
||||
bytes[next_index + 3],
|
||||
]);
|
||||
if bytes[34 + target_lp_address_len as usize + 4..].len() != inner_packet_bytes_len as usize
|
||||
{
|
||||
if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
|
||||
bytes[34 + target_lp_address_len as usize + 4..].len()
|
||||
bytes[next_index + 4..].len()
|
||||
)));
|
||||
}
|
||||
let inner_packet_bytes = bytes[34 + target_lp_address_len as usize + 4..].to_vec();
|
||||
let inner_packet_bytes = bytes[next_index + 4..].to_vec();
|
||||
|
||||
Ok(ForwardPacketData {
|
||||
target_gateway_identity,
|
||||
@@ -406,6 +525,62 @@ pub enum LpMessage {
|
||||
SubsessionReady(SubsessionReadyData),
|
||||
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
||||
SubsessionAbort,
|
||||
/// An error has occurred
|
||||
Error(ErrorPacketData),
|
||||
}
|
||||
|
||||
impl From<HandshakeData> for LpMessage {
|
||||
fn from(value: HandshakeData) -> Self {
|
||||
LpMessage::Handshake(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptedDataPayload> for LpMessage {
|
||||
fn from(value: EncryptedDataPayload) -> Self {
|
||||
LpMessage::EncryptedData(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientHelloData> for LpMessage {
|
||||
fn from(value: ClientHelloData) -> Self {
|
||||
LpMessage::ClientHello(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTRequestData> for LpMessage {
|
||||
fn from(value: KKTRequestData) -> Self {
|
||||
LpMessage::KKTRequest(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTResponseData> for LpMessage {
|
||||
fn from(value: KKTResponseData) -> Self {
|
||||
LpMessage::KKTResponse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ForwardPacketData> for LpMessage {
|
||||
fn from(value: ForwardPacketData) -> Self {
|
||||
LpMessage::ForwardPacket(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK1Data> for LpMessage {
|
||||
fn from(value: SubsessionKK1Data) -> Self {
|
||||
LpMessage::SubsessionKK1(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK2Data> for LpMessage {
|
||||
fn from(value: SubsessionKK2Data) -> Self {
|
||||
LpMessage::SubsessionKK2(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionReadyData> for LpMessage {
|
||||
fn from(value: SubsessionReadyData) -> Self {
|
||||
LpMessage::SubsessionReady(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LpMessage {
|
||||
@@ -425,6 +600,7 @@ impl Display for LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||
LpMessage::Error(_) => write!(f, "Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,6 +622,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionAbort => &[],
|
||||
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +642,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||
LpMessage::SubsessionAbort => true, // Empty signal
|
||||
LpMessage::Error(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +662,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(payload) => payload.len(),
|
||||
LpMessage::SubsessionReady(payload) => payload.len(),
|
||||
LpMessage::SubsessionAbort => 0,
|
||||
LpMessage::Error(payload) => payload.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +682,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
||||
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
||||
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
||||
LpMessage::Error(_) => MessageType::Error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +702,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
||||
LpMessage::SubsessionReady(data) => data.encode(dst),
|
||||
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
||||
LpMessage::Error(data) => data.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,6 +755,7 @@ impl LpMessage {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::SubsessionAbort)
|
||||
}
|
||||
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,13 @@ pub enum ReadResult {
|
||||
// --- Implementation ---
|
||||
|
||||
impl NoiseProtocol {
|
||||
pub fn params() -> NoiseParams {
|
||||
// SAFETY: the hardcoded pattern must be valid
|
||||
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
|
||||
#[allow(clippy::unwrap_used)]
|
||||
crate::NOISE_PATTERN.parse().unwrap()
|
||||
}
|
||||
|
||||
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
||||
///
|
||||
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
||||
@@ -91,6 +98,46 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_handshake_state<'a>(
|
||||
local_private_key: &'a [u8],
|
||||
remote_public_key: &'a [u8],
|
||||
psk: &'a [u8],
|
||||
) -> snow::Builder<'a> {
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params = NoiseProtocol::params();
|
||||
|
||||
snow::Builder::new(noise_params)
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key)
|
||||
.psk(psk_index, psk)
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_initiator(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_initiator()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_responder()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Processes a single, complete incoming Noise message frame.
|
||||
///
|
||||
/// Assumes the caller handles buffering and framing to provide one full message.
|
||||
@@ -288,43 +335,3 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_noise_state(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_initiator()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
pub fn create_noise_state_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_responder()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use crate::message::LpMessage;
|
||||
use crate::message::{LpMessage, MessageType};
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_lp_common::format_debug_bytes;
|
||||
@@ -53,6 +53,10 @@ impl LpPacket {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> MessageType {
|
||||
self.message.typ()
|
||||
}
|
||||
|
||||
/// Compute a hash of the message payload
|
||||
///
|
||||
/// This can be used for message integrity verification or deduplication
|
||||
@@ -203,9 +207,9 @@ impl LpHeader {
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub fn new(receiver_idx: u32, counter: u64) -> Self {
|
||||
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
|
||||
Self {
|
||||
protocol_version: version::CURRENT,
|
||||
protocol_version,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx,
|
||||
counter,
|
||||
@@ -265,7 +269,7 @@ impl LpHeader {
|
||||
|
||||
Ok(LpHeader {
|
||||
protocol_version,
|
||||
reserved: [0u8; 3],
|
||||
reserved,
|
||||
receiver_idx,
|
||||
counter,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ClientHelloData, LpError};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpLocalPeer {
|
||||
/// Local Ed25519 keys for PSQ authentication
|
||||
pub(crate) ed25519: Arc<ed25519::KeyPair>,
|
||||
|
||||
/// Local x25519 keys (Noise static key)
|
||||
pub(crate) x25519: Arc<x25519::KeyPair>,
|
||||
|
||||
/// Local KEM key used for PSQ
|
||||
pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>,
|
||||
}
|
||||
|
||||
impl LpLocalPeer {
|
||||
pub fn new(ed25519: Arc<ed25519::KeyPair>, x25519: Arc<x25519::KeyPair>) -> Self {
|
||||
LpLocalPeer {
|
||||
ed25519,
|
||||
x25519,
|
||||
kem_psq: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
|
||||
ClientHelloData::new_with_fresh_salt(
|
||||
*self.x25519().public_key(),
|
||||
*self.ed25519().public_key(),
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
|
||||
self.kem_psq = Some(key);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ed25519(&self) -> &Arc<ed25519::KeyPair> {
|
||||
&self.ed25519
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> &Arc<x25519::KeyPair> {
|
||||
&self.x25519
|
||||
}
|
||||
|
||||
/// Returns the reference to the KEM Public key of the peer (if available).
|
||||
pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
|
||||
self.kem_psq
|
||||
.as_ref()
|
||||
.map(|kp| kp.public_key())
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)
|
||||
}
|
||||
|
||||
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
|
||||
#[doc(hidden)]
|
||||
pub fn as_remote(&self) -> LpRemotePeer {
|
||||
let expected_kem_key_digests = match &self.kem_psq {
|
||||
None => HashMap::new(),
|
||||
Some(kem_keys) => {
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(
|
||||
KEM::X25519,
|
||||
nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()),
|
||||
);
|
||||
digests
|
||||
}
|
||||
};
|
||||
|
||||
let mut expected_signing_key_digests = HashMap::new();
|
||||
expected_signing_key_digests.insert(
|
||||
SignatureScheme::Ed25519,
|
||||
nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()),
|
||||
);
|
||||
|
||||
LpRemotePeer {
|
||||
ed25519_public: *self.ed25519.public_key(),
|
||||
x25519_public: *self.x25519.public_key(),
|
||||
expected_kem_key_digests,
|
||||
expected_signing_key_digests,
|
||||
}
|
||||
}
|
||||
|
||||
// this is only exposed in tests as ideally we should be storing the proper types to begin with
|
||||
#[cfg(test)]
|
||||
pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey<'_>> {
|
||||
let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes();
|
||||
let libcrux_pk =
|
||||
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?;
|
||||
|
||||
Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk))
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpRemotePeer {
|
||||
/// Remote Ed25519 public key for PSQ authentication
|
||||
pub(crate) ed25519_public: ed25519::PublicKey,
|
||||
|
||||
/// Remote X25519 public key (Noise static key)
|
||||
pub(crate) x25519_public: x25519::PublicKey,
|
||||
|
||||
/// Expected digests of the remote's KEM key
|
||||
pub(crate) expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
|
||||
|
||||
/// Expected digests of the remote's signing key
|
||||
pub(crate) expected_signing_key_digests: HashMap<SignatureScheme, SigningKeyDigests>,
|
||||
}
|
||||
|
||||
impl LpRemotePeer {
|
||||
pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self {
|
||||
LpRemotePeer {
|
||||
ed25519_public,
|
||||
x25519_public,
|
||||
expected_kem_key_digests: Default::default(),
|
||||
expected_signing_key_digests: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519(&self) -> ed25519::PublicKey {
|
||||
self.ed25519_public
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> x25519::PublicKey {
|
||||
self.x25519_public
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_key_digests(
|
||||
mut self,
|
||||
expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
|
||||
expected_signing_key_digests: HashMap<SignatureScheme, SigningKeyDigests>,
|
||||
) -> Self {
|
||||
self.expected_kem_key_digests = expected_kem_key_digests;
|
||||
self.expected_signing_key_digests = expected_signing_key_digests;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempt to retrieve expected KEM key hash of the remote
|
||||
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
|
||||
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
|
||||
pub(crate) fn expected_kem_key_hash(
|
||||
&self,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<Vec<u8>, LpError> {
|
||||
let kem = ciphersuite.kem();
|
||||
let hash_function = ciphersuite.hash_function();
|
||||
|
||||
let digests = self
|
||||
.expected_kem_key_digests
|
||||
.get(&kem)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
|
||||
|
||||
digests
|
||||
.get(&hash_function)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peer() -> LpLocalPeer {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
random_peer(&mut rng)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
let x25519 = Arc::new(ed25519.to_x25519());
|
||||
let kem_psq = Some(x25519.clone());
|
||||
|
||||
LpLocalPeer {
|
||||
ed25519,
|
||||
x25519,
|
||||
kem_psq,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
|
||||
(random_peer(&mut rng), random_peer(&mut rng))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||
use crate::{LpError, LpPacket};
|
||||
use bytes::BytesMut;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(not(test))]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
|
||||
.map(|d| d.as_secs())
|
||||
}
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransportHandshakeExt: LpTransport {
|
||||
// the outer key is temporary until the algorithm is changed with psqv2
|
||||
async fn receive_packet(
|
||||
&mut self,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let raw = self.receive_raw_packet().await?;
|
||||
parse_lp_packet(&raw, outer_key)
|
||||
}
|
||||
|
||||
async fn send_packet(
|
||||
&mut self,
|
||||
packet: LpPacket,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let mut packet_buf = BytesMut::new();
|
||||
|
||||
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
|
||||
self.send_serialised_packet(&packet_buf).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
|
||||
@@ -0,0 +1,391 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTRequestData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_initiator_create_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
|
||||
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use tracing::debug;
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
/// Generate and send client hello to the responder
|
||||
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
let timestamp = current_timestamp()?;
|
||||
|
||||
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
|
||||
self.connection
|
||||
.send_packet(client_hello_data.into_lp_packet(protocol), None)
|
||||
.await?;
|
||||
Ok(client_hello_data)
|
||||
}
|
||||
|
||||
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
|
||||
/// whether the request has been successful or whether there has been a collision in receiver
|
||||
/// index requiring a retry
|
||||
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
|
||||
match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Ack => Ok(true),
|
||||
LpMessage::Collision => Ok(false),
|
||||
other => {
|
||||
// TODO: retry on collision
|
||||
Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to send KKT request to begin the handshake
|
||||
pub(crate) async fn send_kkt_request(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
|
||||
let (session_secret, encrypted_frame) =
|
||||
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
|
||||
let lp_message = KKTRequestData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((kkt_context, session_secret))
|
||||
}
|
||||
|
||||
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
|
||||
/// the received encapsulation key
|
||||
pub(crate) async fn receive_kkt_response(
|
||||
&mut self,
|
||||
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<EncapsulationKey<'static>, LpError> {
|
||||
let kkt_response = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTResponse(response) => response,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTResponse,
|
||||
));
|
||||
}
|
||||
};
|
||||
debug!("received KKT response");
|
||||
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
|
||||
|
||||
let (response_frame, remote_context) =
|
||||
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
|
||||
let encapsulation_key = initiator_ingest_response(
|
||||
&kkt_context,
|
||||
&response_frame,
|
||||
&remote_context,
|
||||
&remote_peer.ed25519_public,
|
||||
&expected_kem_key_digest,
|
||||
)?;
|
||||
Ok(encapsulation_key)
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send initial PSQ msg1
|
||||
pub(crate) async fn send_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
let session_id = u32::from_le_bytes(*session_id_bytes);
|
||||
|
||||
let psq_initiator = psq_initiator_create_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
encapsulation_key,
|
||||
self.local_peer.ed25519.private_key(),
|
||||
self.local_peer.ed25519.public_key(),
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
// prepare noise state and msg1
|
||||
let mut noise_protocol = NoiseProtocol::build_new_initiator(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
|
||||
// prepare noise msg1
|
||||
let noise_msg1 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
|
||||
let psq_len = psq_payload.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
|
||||
combined.extend_from_slice(&psq_len.to_le_bytes());
|
||||
combined.extend_from_slice(&psq_payload);
|
||||
combined.extend_from_slice(&noise_msg1);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate received PSQ msg2
|
||||
pub(crate) async fn receive_psq_responder_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg2 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
|
||||
if psq_msg2.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
|
||||
if psq_msg2.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
// Extract and "store" the PSK handle
|
||||
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg2[2 + handle_len..];
|
||||
|
||||
// *sigh* ignore the message
|
||||
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send final PSQ msg3
|
||||
pub(crate) async fn send_final_psq_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let noise_msg3 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
|
||||
|
||||
let lp_message = HandshakeData::new(noise_msg3).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive final ACK that indicates finalisation of the handshake
|
||||
pub(crate) async fn receive_final_ack(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Ack => Ok(()),
|
||||
other => Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete_as_initiator_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 0. retrieve the expected kem key hash. if we don't know it,
|
||||
// there's no point in even trying to start the handshake
|
||||
let Some(remote_peer) = self.remote_peer.take() else {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
|
||||
));
|
||||
};
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
// and keep retrying until we manage to establish a receiver index without collisions
|
||||
let mut attempt = 0;
|
||||
let client_hello_data = loop {
|
||||
attempt += 1;
|
||||
|
||||
debug!("sending client hello");
|
||||
let client_hello = self
|
||||
.send_client_hello()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?;
|
||||
if self
|
||||
.receive_client_hello_ack()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?
|
||||
{
|
||||
debug!("received client hello ACK");
|
||||
break client_hello;
|
||||
}
|
||||
debug!("received client hello collision");
|
||||
|
||||
// TODO: make it configurable
|
||||
if attempt > 3 {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake(
|
||||
"failed to establish receiver index without collision",
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 3. prepare and send KKT request
|
||||
debug!("sending KKT request");
|
||||
let kkt_data = self
|
||||
.send_kkt_request(session_id, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. receive and process KKT response
|
||||
let encapsulation_key = self
|
||||
.receive_kkt_response(kkt_data, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT response");
|
||||
|
||||
// 5. prepare and send PSQ msg1
|
||||
debug!("sending PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
|
||||
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. receive and process PSQ msg2
|
||||
debug!("received PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. prepare and send PSQ msg3
|
||||
debug!("sending PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. receive final ACK and finalise
|
||||
debug!("received final ACK");
|
||||
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
// TODO: missing: receive counter check
|
||||
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_initiator_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::ErrorPacketData;
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use nym_kkt::ciphersuite::Ciphersuite;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use tracing::debug;
|
||||
|
||||
mod helpers;
|
||||
mod initiator;
|
||||
mod responder;
|
||||
|
||||
pub(crate) struct IntermediateHandshakeFailure {
|
||||
/// Session id established during exchange if we managed to derive it
|
||||
session_id: Option<u32>,
|
||||
|
||||
/// Protocol version established during the exchange
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Outer aead key established during exchange if we managed to derive it
|
||||
outer_aead_key: Option<OuterAeadKey>,
|
||||
|
||||
/// The error source
|
||||
source: LpError,
|
||||
}
|
||||
|
||||
impl IntermediateHandshakeFailure {
|
||||
fn plain(source: LpError) -> IntermediateHandshakeFailure {
|
||||
IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: None,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQHandshakeState<'a, S> {
|
||||
/// The underlying connection established for the handshake
|
||||
connection: &'a mut S,
|
||||
|
||||
/// Protocol version used for the exchange.
|
||||
/// either known implicitly through the directory (initiator)
|
||||
/// or established through client hello (responder)
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Ciphersuite selected for the KKT/PSQ exchange
|
||||
ciphersuite: Ciphersuite,
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
local_peer: LpLocalPeer,
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
remote_peer: Option<LpRemotePeer>,
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: u64,
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
|
||||
PSQHandshakeState {
|
||||
connection,
|
||||
protocol_version: None,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer: None,
|
||||
sending_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
|
||||
self.protocol_version = Some(protocol_version);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
|
||||
self.remote_peer = Some(remote_peer);
|
||||
self
|
||||
}
|
||||
|
||||
fn protocol_version(&self) -> Result<u8, LpError> {
|
||||
self.protocol_version
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
|
||||
}
|
||||
|
||||
/// Generates the next counter value for outgoing packets.
|
||||
pub fn next_counter(&mut self) -> u64 {
|
||||
let counter = self.sending_counter;
|
||||
self.sending_counter += 1;
|
||||
counter
|
||||
}
|
||||
|
||||
pub fn next_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
message: LpMessage,
|
||||
) -> LpPacket {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(session_id, counter, protocol_version);
|
||||
LpPacket::new(header, message)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_send_error_packet(
|
||||
&mut self,
|
||||
err: IntermediateHandshakeFailure,
|
||||
) -> LpError {
|
||||
// if session_id is not known, we can't send the packet back (with the current design)
|
||||
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
|
||||
return err.source;
|
||||
};
|
||||
if let Err(err) = self
|
||||
.send_error_packet(
|
||||
session_id,
|
||||
protocol,
|
||||
err.source.to_string(),
|
||||
err.outer_aead_key.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to send back error response: {err}")
|
||||
}
|
||||
err.source
|
||||
}
|
||||
|
||||
/// Attempt to send an error packet
|
||||
pub(crate) async fn send_error_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
msg: impl Into<String>,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError> {
|
||||
let packet = self.next_packet(
|
||||
session_id,
|
||||
protocol_version,
|
||||
LpMessage::Error(ErrorPacketData::new(msg)),
|
||||
);
|
||||
self.connection.send_packet(packet, outer_aead_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive a packet from connection, explicitly checking for an error response
|
||||
/// and returning corresponding message if received
|
||||
pub(crate) async fn receive_non_error(
|
||||
&mut self,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError> {
|
||||
let packet = self.connection.receive_packet(outer_aead_key).await?;
|
||||
|
||||
match &packet.message {
|
||||
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
|
||||
"remote error: {}",
|
||||
error_packet.message
|
||||
))),
|
||||
_ => Ok(packet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::mock_peers;
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
|
||||
use mock_instant::thread_local::MockClock;
|
||||
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn extract_error(conn: &mut MockIOStream) -> String {
|
||||
let packet = conn.receive_packet(None).await.unwrap();
|
||||
match packet.message {
|
||||
LpMessage::Error(error) => error.message,
|
||||
_ => panic!("non error packet"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn e2e_psq_handshake() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
|
||||
|
||||
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
|
||||
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
|
||||
|
||||
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||
|
||||
let session_init = session_init???;
|
||||
let session_resp = session_resp???;
|
||||
|
||||
assert_eq!(session_init.id(), session_resp.id());
|
||||
assert_eq!(
|
||||
session_init.outer_aead_key().as_bytes(),
|
||||
session_resp.outer_aead_key().as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
session_init.pq_shared_secret().as_bytes(),
|
||||
session_resp.pq_shared_secret().as_bytes()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let client_hello = handshake_init.send_client_hello().await?;
|
||||
let LpMessage::ClientHello(received_client_hello) =
|
||||
conn_resp.receive_packet(None).await?.message
|
||||
else {
|
||||
panic!("wrong message type");
|
||||
};
|
||||
assert_eq!(client_hello, received_client_hello);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// essentially make sure you can't accidentally trigger the handshake as the responder
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (_, resp) = mock_peers();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let sending_res = handshake_resp.send_client_hello().await;
|
||||
assert!(sending_res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
|
||||
let current_time = Duration::from_secs(10000);
|
||||
MockClock::set_system_time(current_time);
|
||||
|
||||
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
|
||||
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
// TOO OLD
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_old.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
assert!(err.to_string().contains("too old"));
|
||||
|
||||
// TOO RECENT
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("too future"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTResponseData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_responder_process_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
|
||||
use nym_kkt::frame::KKTSessionId;
|
||||
use nym_kkt::session::{responder_ingest_message, responder_process};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
|
||||
|
||||
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
|
||||
fn validate_client_hello_timestamp(
|
||||
client_timestamp: u64,
|
||||
tolerance: Duration,
|
||||
) -> Result<(), LpError> {
|
||||
let now = current_timestamp()?;
|
||||
|
||||
let age = now.abs_diff(client_timestamp);
|
||||
if age > tolerance.as_secs() {
|
||||
let direction = if now >= client_timestamp {
|
||||
"old"
|
||||
} else {
|
||||
"future"
|
||||
};
|
||||
|
||||
return Err(LpError::kkt_psq_handshake(format!(
|
||||
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
|
||||
tolerance.as_secs()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub(crate) fn encapsulated_kem_keys(
|
||||
&self,
|
||||
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
|
||||
let kem_keys = self
|
||||
.local_peer
|
||||
.kem_psq
|
||||
.as_ref()
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)?;
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
Ok((dec_key, enc_key))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate ClientHello
|
||||
pub(crate) async fn receive_client_hello(
|
||||
&mut self,
|
||||
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
|
||||
let client_hello_packet = self.receive_non_error(None).await?;
|
||||
let client_hello = match client_hello_packet.message {
|
||||
LpMessage::ClientHello(client_hello) => client_hello,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::ClientHello,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
validate_client_hello_timestamp(
|
||||
client_hello.extract_timestamp(),
|
||||
DEFAULT_TIMESTAMP_TOLERANCE,
|
||||
)?;
|
||||
|
||||
// TODO: somehow check for collision
|
||||
|
||||
// set version and remote peer information
|
||||
self.protocol_version = Some(client_hello_packet.header.protocol_version);
|
||||
let remote_peer = LpRemotePeer::new(
|
||||
client_hello.client_ed25519_public_key,
|
||||
client_hello.client_lp_public_key,
|
||||
);
|
||||
|
||||
Ok((client_hello, remote_peer))
|
||||
}
|
||||
|
||||
/// Send client hello ACK
|
||||
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection.send_packet(ack, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a KKT request
|
||||
pub(crate) async fn receive_kkt_request(
|
||||
&mut self,
|
||||
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
|
||||
let kkt_request = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTRequest(request) => request.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (session_secret, request_frame, remote_context) =
|
||||
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
|
||||
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
|
||||
|
||||
Ok((context, session_secret, request_frame.session_id()))
|
||||
}
|
||||
|
||||
/// Attempt to send KKT response to the previously received request
|
||||
pub(crate) async fn send_kkt_response(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let response_frame = responder_process(
|
||||
&kkt_context,
|
||||
kkt_session_id,
|
||||
self.local_peer.ed25519().private_key(),
|
||||
encapsulation_key,
|
||||
)?;
|
||||
let encrypted_frame = encrypt_kkt_frame(
|
||||
&mut rng(),
|
||||
&session_secret,
|
||||
&response_frame,
|
||||
KKT_RESPONSE_AAD,
|
||||
)?;
|
||||
let lp_message = KKTResponseData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a PSQ msg1 request
|
||||
pub(crate) async fn receive_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
|
||||
let psq_msg1 = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
|
||||
if psq_msg1.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
|
||||
if psq_msg1.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let psq_payload = &psq_msg1[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg1[2 + handle_len..];
|
||||
|
||||
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
|
||||
let psq_responder = psq_responder_process_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
local_kem_keypair,
|
||||
&remote_peer.ed25519_public,
|
||||
psq_payload,
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
|
||||
let psk = psq_responder.psk;
|
||||
let psk_handle = psq_responder.psk_handle;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
let mut noise_protocol = NoiseProtocol::build_new_responder(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
noise_protocol.read_message(noise_payload)?;
|
||||
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
psk_handle,
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to prepare and generate a responder PSQ msg2
|
||||
pub(crate) async fn send_psq_responder_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
psk_handle: &[u8],
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let msg2 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
|
||||
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
|
||||
let handle_len = psk_handle.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
|
||||
combined.extend_from_slice(&handle_len.to_le_bytes());
|
||||
combined.extend_from_slice(psk_handle);
|
||||
combined.extend_from_slice(&msg2);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process final PSQ msg3
|
||||
pub(crate) async fn receive_final_psq_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg3 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
noise_protocol.read_message(&psq_msg3)?;
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send final ACK to indicate finalisation of the handshake
|
||||
pub(crate) async fn send_final_ack(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection
|
||||
.send_packet(ack, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn complete_as_responder_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 1. receive and validate ClientHello
|
||||
let (client_hello_data, remote_peer) =
|
||||
self.receive_client_hello()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received client hello");
|
||||
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 2. send ack
|
||||
debug!("sending client hello ACK");
|
||||
self.send_client_hello_ack(session_id)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 3. receive and process KKT request
|
||||
let kkt_data =
|
||||
self.receive_kkt_request()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT request");
|
||||
|
||||
// TEMP: 'derive' KEM keys
|
||||
let (dec_key, enc_key) =
|
||||
self.encapsulated_kem_keys()
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. prepare and send KKT response
|
||||
debug!("sending KKT response");
|
||||
self.send_kkt_response(session_id, kkt_data, &enc_key)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 5. receive and process PSQ msg1
|
||||
debug!("received PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
|
||||
.receive_psq_initiator_message(
|
||||
&remote_peer,
|
||||
(&dec_key, &enc_key),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. prepare and send PSQ msg2
|
||||
debug!("sending PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.send_psq_responder_message(
|
||||
session_id,
|
||||
&psk_handle,
|
||||
&outer_aead_key,
|
||||
&mut noise_protocol,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. receive and process PSQ msg3
|
||||
debug!("received PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. [optionally] send ACK to finalise
|
||||
debug!("sending final ACK");
|
||||
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_responder_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
-1945
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user