Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09fa612a82 |
@@ -3,5 +3,4 @@
|
||||
.gitignore
|
||||
**/node_modules
|
||||
**/target
|
||||
target-otel
|
||||
dist
|
||||
|
||||
@@ -6,8 +6,6 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
env:
|
||||
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
|
||||
defaults:
|
||||
run:
|
||||
working-directory: documentation/docs
|
||||
@@ -43,8 +41,6 @@ jobs:
|
||||
run: pnpm i
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
- name: Generate sitemap
|
||||
run: npx next-sitemap
|
||||
- name: Move files to /dist/
|
||||
run: ../scripts/move-to-dist.sh
|
||||
|
||||
|
||||
@@ -3,28 +3,13 @@ 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: 'Force RUSTFLAGS="--cfg tokio_unstable" (auto-set when tokio-console is selected)'
|
||||
required: false
|
||||
description: 'True to add RUSTFLAGS="--cfg tokio_unstable"'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
enable_deb:
|
||||
description: "Enable cargo-deb installation and .deb package building"
|
||||
description: "True to enable cargo-deb installation and .deb package building"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -36,7 +21,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [arc-linux-latest]
|
||||
platform: [ arc-linux-latest ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
@@ -51,62 +36,38 @@ 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: Resolve cargo features and RUSTFLAGS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
||||
run: |
|
||||
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
|
||||
|
||||
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
|
||||
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Build all binaries
|
||||
shell: bash
|
||||
run: cargo build --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --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'
|
||||
@@ -123,22 +84,24 @@ 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" ]]; 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 }} = true ]; then
|
||||
cp target/debian/*.deb $OUTPUT_DIR
|
||||
fi
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: ci-build-vpn-api-wasm
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'common/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- '.github/workflows/ci-build-vpn-api-wasm.yml'
|
||||
|
||||
jobs:
|
||||
wasm:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '116'
|
||||
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli
|
||||
|
||||
- name: "Build"
|
||||
run: make
|
||||
working-directory: nym-credential-proxy/vpn-api-lib-wasm
|
||||
@@ -10,7 +10,6 @@ on:
|
||||
- 'nym-api/**'
|
||||
- 'nym-authenticator-client/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- 'nym-gateway-probe/**'
|
||||
- 'nym-ip-packet-client/**'
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
@@ -90,7 +89,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
|
||||
|
||||
- name: Clippy (non-macos)
|
||||
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
|
||||
@@ -105,14 +104,6 @@ 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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -1,79 +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
|
||||
|
||||
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
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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
|
||||
@@ -1,86 +0,0 @@
|
||||
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!"
|
||||
@@ -1,74 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
@@ -51,3 +51,25 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nym-wallet
|
||||
NYM_PROJECT_NAME: "nym-wallet"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -10,8 +10,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [ stable, beta ]
|
||||
os: [ ubuntu-22.04, windows-latest, macos-latest ]
|
||||
rust: [stable, beta]
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -93,3 +93,38 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
notification:
|
||||
needs: build
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Collect jobs status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "Nym nightly build"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-22.04, macos-latest, windows-latest ]
|
||||
os: [ubuntu-22.04, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -55,3 +55,38 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
|
||||
|
||||
notification:
|
||||
needs: build
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Collect jobs status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "nym-wallet-nightly-build"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -24,3 +24,34 @@ jobs:
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
notification:
|
||||
needs: cargo-deny
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
- name: Download report from previous job
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications
|
||||
- name: install npm
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: security
|
||||
NYM_PROJECT_NAME: "Daily security report"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_AUDIT }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
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.52.4
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
@@ -4,23 +4,51 @@ This is a collection of scripts and files to support GitHub Actions.
|
||||
|
||||
## Sending Notifications
|
||||
|
||||
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub
|
||||
Actions.
|
||||
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub Actions.
|
||||
|
||||
### Adding notifications to a GitHub Action
|
||||
|
||||
```
|
||||
jobs:
|
||||
build:
|
||||
...
|
||||
- name: Notifications - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files/notifications
|
||||
- name: Notifications - Send
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: "my-component"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
```
|
||||
|
||||
Notifications are run by adding the snippet above to a GitHub Action, and:
|
||||
|
||||
1. Installing node packages needed at run time
|
||||
2. Set the env vars as required:
|
||||
- `NYM_NOTIFICATION_KIND` matches the directory in `.github/workflows/support-files/${NYM_NOTIFICATION_KIND}` to provide the templates and extra scripting in `index.js`
|
||||
- Matrix credentials, room and other env vars for the status of the build and repo
|
||||
3. Replacing the default entry point shell script on the `keybaseio/client:stable-node` docker image to run `.github/workflows/support-files/notifications/entry_point.sh`
|
||||
|
||||
### Running locally
|
||||
|
||||
You will need:
|
||||
|
||||
- Node 16 LTS
|
||||
- npm
|
||||
|
||||
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix
|
||||
credentials.
|
||||
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix credentials.
|
||||
|
||||
Then run `npm install` to get dependencies.
|
||||
|
||||
Start development mode for the notification type you want either by passing the value as an env var called
|
||||
`NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
|
||||
Start development mode for the notification type you want either by passing the value as an env var called `NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
|
||||
|
||||
```bash
|
||||
cd .github/workflows/support-files
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# pass exit codes out to GitHub Actions
|
||||
set -euxo pipefail
|
||||
|
||||
# change to the directory that contains this script
|
||||
cd "${0%/*}"
|
||||
|
||||
# run the node script
|
||||
node send_message.js
|
||||
@@ -0,0 +1,126 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const { sendMatrixMessage } = require('./send_message_to_matrix');
|
||||
|
||||
let context = {
|
||||
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security','ci-docs','cd-docs','ci-dev','cd-dev'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that all required env and context vars are available
|
||||
*/
|
||||
function validateContext() {
|
||||
if (!context.env.NYM_NOTIFICATION_KIND) {
|
||||
throw new Error(
|
||||
'Please set env var NYM_NOTIFICATION_KIND with the project kind that matches a directory in ".github/workflows/support-files"',
|
||||
);
|
||||
}
|
||||
if (!context.kinds.includes(context.env.NYM_NOTIFICATION_KIND)) {
|
||||
throw new Error(`Env var NYM_NOTIFICATION_KIND is not in ${context.kinds}`);
|
||||
}
|
||||
if (!context.env.NYM_PROJECT_NAME) {
|
||||
throw new Error(
|
||||
'Please set env var NYM_PROJECT_NAME with the project name for displaying in notification messages',
|
||||
);
|
||||
}
|
||||
if (context.env.MATRIX_ROOM) {
|
||||
if (!context.env.MATRIX_SERVER) {
|
||||
throw new Error(
|
||||
'Matrix server is not defined. Please set env var MATRIX_SERVER',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_USER_ID) {
|
||||
throw new Error(
|
||||
'Matrix user id is not defined. Please set env var MATRIX_USER_ID',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_TOKEN) {
|
||||
throw new Error(
|
||||
'Matrix token is not defined. Please set env var MATRIX_TOKEN',
|
||||
);
|
||||
}
|
||||
if (!context.env.MATRIX_DEVICE_ID) {
|
||||
throw new Error(
|
||||
'Matrix device id is not defined. Please set env var MATRIX_DEVICE_ID',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context that will be available in the templates for rendering notifications
|
||||
*/
|
||||
function createTemplateContext() {
|
||||
const options = { dateStyle: 'full', timeStyle: 'long' };
|
||||
context.timestamp = new Date().toLocaleString(undefined, options);
|
||||
|
||||
// add environment to template context and validate
|
||||
context.env = process.env;
|
||||
try {
|
||||
validateContext();
|
||||
} catch (e) {
|
||||
if(process.env.SHOW_DEBUG) {
|
||||
// recursively print the context for easy debugging and rethrow the error
|
||||
console.dir({ context }, { depth: null });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
context.kind = context.env.NYM_NOTIFICATION_KIND;
|
||||
|
||||
if (!context.env.GIT_BRANCH_NAME) {
|
||||
context.env.GIT_BRANCH_NAME = context.env.GITHUB_REF.split('/')
|
||||
.slice(2)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
context.status = process.env.IS_SUCCESS === 'true' ? 'success' : 'failure';
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the `kind` set in the context to process the context and generate a notification message
|
||||
* @returns {Promise<string>} A string notification message body
|
||||
*/
|
||||
async function processKindScript() {
|
||||
const script = require(`../${context.kind}`);
|
||||
if (!script.addToContextAndValidate) {
|
||||
throw new Error(
|
||||
`"./${context.kind}/index.js" does not export a method called "async addToContextAndValidate(context)"`,
|
||||
);
|
||||
}
|
||||
if (!script.getMessageBody) {
|
||||
throw new Error(
|
||||
`"./${context.kind}/index.js" does not export a method called "async getMessageBody(context)"`,
|
||||
);
|
||||
}
|
||||
|
||||
// call the script to modify and validate the context
|
||||
await script.addToContextAndValidate(context);
|
||||
|
||||
// let the script create a message body and return the result as a string for sending
|
||||
return await script.getMessageBody(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function, as async so that await syntax is available
|
||||
*/
|
||||
async function main() {
|
||||
createTemplateContext();
|
||||
console.log(`Sending notification for kind "${context.kind}"...`);
|
||||
const messageBody = await processKindScript();
|
||||
if(process.env.SHOW_DEBUG) {
|
||||
console.log('-----------------------------------------');
|
||||
console.log(messageBody);
|
||||
console.log('-----------------------------------------');
|
||||
}
|
||||
if(context.env.MATRIX_ROOM) {
|
||||
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM)
|
||||
}
|
||||
if(context.env.MATRIX_ROOM_OF_SHAME && context.env.IS_SUCCESS !== 'true') {
|
||||
// when a job fails
|
||||
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM_OF_SHAME)
|
||||
}
|
||||
}
|
||||
|
||||
// call main function and let NodeJS handle the promise
|
||||
main();
|
||||
@@ -0,0 +1,67 @@
|
||||
const sdk = require('matrix-js-sdk');
|
||||
global.Olm = require('olm');
|
||||
const { LocalStorage } = require('node-localstorage');
|
||||
const localStorage = new LocalStorage('./scratch');
|
||||
const {
|
||||
LocalStorageCryptoStore,
|
||||
} = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
|
||||
var showdown = require('showdown');
|
||||
|
||||
// hide all matrix client output
|
||||
console.error = (error) => console.log('❌ error: ', error);
|
||||
process.stderr.write = () => {};
|
||||
process.stdout.write = () => {};
|
||||
|
||||
|
||||
function createClient(context, room, message) {
|
||||
const server = context.env.MATRIX_SERVER;
|
||||
const token = context.env.MATRIX_TOKEN;
|
||||
const deviceId = context.env.MATRIX_DEVICE_ID;
|
||||
const userId = context.env.MATRIX_USER_ID;
|
||||
|
||||
const client = sdk.createClient({
|
||||
baseUrl: server,
|
||||
accessToken: token,
|
||||
userId,
|
||||
deviceId,
|
||||
sessionStore: new sdk.WebStorageSessionStore(localStorage),
|
||||
cryptoStore: new LocalStorageCryptoStore(localStorage),
|
||||
});
|
||||
|
||||
client.on('sync', async function(state, prevState, res) {
|
||||
if (state !== 'PREPARED') return;
|
||||
client.setGlobalErrorOnUnknownDevices(false);
|
||||
try {
|
||||
await client.joinRoom(room);
|
||||
await client.sendEvent(
|
||||
room,
|
||||
'm.room.message',
|
||||
{
|
||||
msgtype: 'm.text',
|
||||
format: 'org.matrix.custom.html',
|
||||
body: message,
|
||||
formatted_body: message,
|
||||
},
|
||||
'',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Job failed: ' + error.message);
|
||||
}
|
||||
client.stopClient();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async function sendMatrixMessage(contextArg, messageAsMarkdown, roomId) {
|
||||
const converter = new showdown.Converter();
|
||||
const messageAsHtml = converter.makeHtml(messageAsMarkdown);
|
||||
const client = createClient(contextArg, roomId, messageAsHtml);
|
||||
await client.initCrypto();
|
||||
await client.startClient({ initialSyncLimit: 1 });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMatrixMessage,
|
||||
};
|
||||
+1
-1
@@ -67,6 +67,7 @@ nym-api/redocly/formatted-openapi.json
|
||||
*.profraw
|
||||
.beads
|
||||
CLAUDE.md
|
||||
docs
|
||||
.claude
|
||||
.superego
|
||||
|
||||
@@ -76,4 +77,3 @@ CLAUDE.md
|
||||
.claude/settings.json
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
|
||||
-156
@@ -4,162 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.4-quark] (2026-02-24)
|
||||
|
||||
- Enhance CI workflow with feature inputs ([#6462])
|
||||
- Chore/revert 6433 ([#6445])
|
||||
- Lp/stateless handshake ([#6437])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/client/internal-dev ([#6435])
|
||||
- build(deps-dev): bump webpack from 5.102.1 to 5.104.1 ([#6432])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/mix-fetch/internal-dev ([#6431])
|
||||
- build(deps-dev): bump webpack from 5.94.0 to 5.104.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6430])
|
||||
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/zknym-lib/internal-dev ([#6429])
|
||||
- build(deps-dev): bump webpack from 5.76.0 to 5.105.0 in /clients/native/examples/js-examples/websocket ([#6428])
|
||||
- HTTP & DNS Improvements ([#6423])
|
||||
- Endpoint for exit GW IPs ([#6418])
|
||||
- build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts ([#6416])
|
||||
- build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([#6415])
|
||||
- build(deps): bump bytes from 1.11.0 to 1.11.1 ([#6414])
|
||||
- build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 ([#6407])
|
||||
- build(deps-dev): bump eslint from 8.57.1 to 9.26.0 ([#6405])
|
||||
- Update reqwest to v0.13.1 ([#6401])
|
||||
- build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs ([#6387])
|
||||
- build(deps): bump next from 15.4.10 to 16.1.5 in /nym-node-status-api/nym-node-status-ui ([#6385])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 ([#6369])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 ([#6360])
|
||||
- build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/codegen/contract-clients ([#6359])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/packages/nodejs-client ([#6354])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs ([#6353])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /clients/native/examples/js-examples/websocket ([#6351])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 in /documentation/docs ([#6350])
|
||||
- build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs ([#6345])
|
||||
- Max/crates publishing tweaks ([#6343])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 ([#6339])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs ([#6332])
|
||||
- build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs ([#6325])
|
||||
- build(deps): bump rsa from 0.9.8 to 0.9.10 ([#6311])
|
||||
- build(deps): bump qs and express in /wasm/mix-fetch/internal-dev ([#6308])
|
||||
- build(deps): bump qs and express in /clients/native/examples/js-examples/websocket ([#6307])
|
||||
- feat: introduce on-disk cache persistance for major nym-api caches ([#6302])
|
||||
- Fix migrations in the Data Observatory ([#6271])
|
||||
|
||||
[#6462]: https://github.com/nymtech/nym/pull/6462
|
||||
[#6445]: https://github.com/nymtech/nym/pull/6445
|
||||
[#6437]: https://github.com/nymtech/nym/pull/6437
|
||||
[#6435]: https://github.com/nymtech/nym/pull/6435
|
||||
[#6432]: https://github.com/nymtech/nym/pull/6432
|
||||
[#6431]: https://github.com/nymtech/nym/pull/6431
|
||||
[#6430]: https://github.com/nymtech/nym/pull/6430
|
||||
[#6429]: https://github.com/nymtech/nym/pull/6429
|
||||
[#6428]: https://github.com/nymtech/nym/pull/6428
|
||||
[#6423]: https://github.com/nymtech/nym/pull/6423
|
||||
[#6418]: https://github.com/nymtech/nym/pull/6418
|
||||
[#6416]: https://github.com/nymtech/nym/pull/6416
|
||||
[#6415]: https://github.com/nymtech/nym/pull/6415
|
||||
[#6414]: https://github.com/nymtech/nym/pull/6414
|
||||
[#6407]: https://github.com/nymtech/nym/pull/6407
|
||||
[#6405]: https://github.com/nymtech/nym/pull/6405
|
||||
[#6401]: https://github.com/nymtech/nym/pull/6401
|
||||
[#6387]: https://github.com/nymtech/nym/pull/6387
|
||||
[#6385]: https://github.com/nymtech/nym/pull/6385
|
||||
[#6369]: https://github.com/nymtech/nym/pull/6369
|
||||
[#6360]: https://github.com/nymtech/nym/pull/6360
|
||||
[#6359]: https://github.com/nymtech/nym/pull/6359
|
||||
[#6354]: https://github.com/nymtech/nym/pull/6354
|
||||
[#6353]: https://github.com/nymtech/nym/pull/6353
|
||||
[#6351]: https://github.com/nymtech/nym/pull/6351
|
||||
[#6350]: https://github.com/nymtech/nym/pull/6350
|
||||
[#6345]: https://github.com/nymtech/nym/pull/6345
|
||||
[#6343]: https://github.com/nymtech/nym/pull/6343
|
||||
[#6339]: https://github.com/nymtech/nym/pull/6339
|
||||
[#6332]: https://github.com/nymtech/nym/pull/6332
|
||||
[#6325]: https://github.com/nymtech/nym/pull/6325
|
||||
[#6311]: https://github.com/nymtech/nym/pull/6311
|
||||
[#6308]: https://github.com/nymtech/nym/pull/6308
|
||||
[#6307]: https://github.com/nymtech/nym/pull/6307
|
||||
[#6302]: https://github.com/nymtech/nym/pull/6302
|
||||
[#6271]: https://github.com/nymtech/nym/pull/6271
|
||||
|
||||
## [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])
|
||||
|
||||
Generated
+1845
-2010
File diff suppressed because it is too large
Load Diff
+113
-130
@@ -74,6 +74,7 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-lp-common",
|
||||
"common/nym-kkt",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
@@ -128,6 +129,7 @@ members = [
|
||||
"nym-browser-extension/storage",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-credential-proxy/vpn-api-lib-wasm",
|
||||
"nym-data-observatory",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
@@ -171,9 +173,8 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
# "nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite", "common/nym-kkt-context",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -184,6 +185,7 @@ 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",
|
||||
@@ -202,9 +204,9 @@ homepage = "https://nymtech.net"
|
||||
documentation = "https://nymtech.net"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
rust-version = "1.85"
|
||||
readme = "README.md"
|
||||
version = "1.20.4"
|
||||
version = "1.20.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -231,7 +233,7 @@ blake3 = "1.7.0"
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.11.1"
|
||||
bytes = "1.10.1"
|
||||
cargo_metadata = "0.19.2"
|
||||
celes = "2.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
@@ -273,7 +275,6 @@ futures = "0.3.31"
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
getrandom03 = { package = "getrandom", version = "=0.3.3" }
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
@@ -303,16 +304,13 @@ 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.31.0"
|
||||
opentelemetry_sdk = "0.31.0"
|
||||
opentelemetry-otlp = "0.31.0"
|
||||
tonic = "0.14.4"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
parking_lot = "0.12.3"
|
||||
pem = "0.8"
|
||||
petgraph = "0.6.5"
|
||||
@@ -322,14 +320,12 @@ 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_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
@@ -371,8 +367,9 @@ tower = "0.5.2"
|
||||
tower-http = "0.6.6"
|
||||
tracing = "0.1.41"
|
||||
tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
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"
|
||||
@@ -384,7 +381,7 @@ url = "2.5"
|
||||
utoipa = "5.2"
|
||||
utoipa-swagger-ui = "8.1"
|
||||
utoipauto = "0.2"
|
||||
uuid = "1.19.0"
|
||||
uuid = "*"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
@@ -393,121 +390,107 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-curve25519 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
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-lp = { version = "1.20.4", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
|
||||
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-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-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||
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-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
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" }
|
||||
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" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
|
||||
@@ -104,11 +104,11 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
|
||||
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
||||
|
||||
sdk-wasm-build:
|
||||
# $(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/node-tester
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
# $(MAKE) -C wasm/zknym-lib
|
||||
$(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/full-nym-wasm
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
@@ -119,14 +119,13 @@ sdk-typescript-build:
|
||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
|
||||
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
|
||||
sdk-wasm-test:
|
||||
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
|
||||
sdk-wasm-lint:
|
||||
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
$(MAKE) -C wasm/mix-fetch check-fmt
|
||||
|
||||
# Add to top-level targets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.72"
|
||||
version = "1.1.69"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
+973
-1003
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.105.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.72"
|
||||
version = "1.1.69"
|
||||
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,7 +18,6 @@ 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,7 +7,6 @@ use crate::traits::{
|
||||
TopUpBandwidthResponse, UpgradeModeStatus,
|
||||
};
|
||||
use crate::{v2, v3, v4, v5, v6};
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthenticatorResponse {
|
||||
@@ -18,17 +17,6 @@ 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,38 +1,6 @@
|
||||
// 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,11 +56,27 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
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 (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 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,11 +54,27 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
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 (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 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,12 +3,13 @@
|
||||
|
||||
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")]
|
||||
@@ -19,11 +20,13 @@ 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 {
|
||||
@@ -51,11 +54,27 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
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 (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 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,10 +100,6 @@ 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,22 +57,3 @@ 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,15 +19,12 @@ 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 = ["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 }
|
||||
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
@@ -38,14 +35,13 @@ default = []
|
||||
openapi = ["utoipa"]
|
||||
output_format = ["serde_json", "dep:clap"]
|
||||
bin_info_schema = ["schemars"]
|
||||
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
|
||||
otel-otlp = [
|
||||
basic_tracing = ["dep:tracing", "tracing-subscriber"]
|
||||
tracing = [
|
||||
"basic_tracing",
|
||||
"dep:opentelemetry",
|
||||
"dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry-otlp",
|
||||
"dep:tracing-opentelemetry",
|
||||
"dep:tonic",
|
||||
"tracing-tree",
|
||||
"opentelemetry-jaeger",
|
||||
"tracing-opentelemetry",
|
||||
"opentelemetry",
|
||||
]
|
||||
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
|
||||
models = []
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
// Re-export tracing_subscriber for consumers that need to compose layers
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
#[cfg(feature = "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)]
|
||||
@@ -62,106 +69,40 @@ pub fn setup_tracing_logger() {
|
||||
build_tracing_logger().init()
|
||||
}
|
||||
|
||||
/// 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;
|
||||
// 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;
|
||||
|
||||
// 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 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),
|
||||
);
|
||||
|
||||
let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
|
||||
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 mut builder = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(export_timeout_secs));
|
||||
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
|
||||
// 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,
|
||||
))
|
||||
registry.with(telemetry).init();
|
||||
};
|
||||
}
|
||||
|
||||
pub fn banner(crate_name: &str, crate_version: &str) -> String {
|
||||
|
||||
@@ -121,10 +121,6 @@ features = ["wasm-bindgen"]
|
||||
workspace = true
|
||||
features = ["full"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.getrandom03]
|
||||
workspace = true
|
||||
features = ["wasm_js"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
error::ClientCoreError,
|
||||
};
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
|
||||
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
|
||||
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};
|
||||
|
||||
@@ -15,13 +15,3 @@ pub(crate) fn get_time_now() -> Instant {
|
||||
pub(crate) fn new_interval_stream(polling_rate: Duration) -> IntervalStream {
|
||||
gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "Rust" fn __getrandom_v03_custom(
|
||||
dest: *mut u8,
|
||||
len: usize,
|
||||
) -> Result<(), getrandom03::Error> {
|
||||
let _ = dest;
|
||||
let _ = len;
|
||||
Err(getrandom03::Error::UNSUPPORTED)
|
||||
}
|
||||
|
||||
@@ -128,95 +128,54 @@ 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) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
debug!("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) => {
|
||||
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}"
|
||||
);
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
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}"
|
||||
);
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
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",
|
||||
debug!(
|
||||
"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!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
warn!("Failed to forward packets to {address}: {err}");
|
||||
}
|
||||
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -313,18 +272,16 @@ impl SendWithoutResponse for Client {
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
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!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "not_connected",
|
||||
"establishing initial connection to {address}"
|
||||
);
|
||||
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
|
||||
self.make_connection(address, framed_packet);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
@@ -332,24 +289,15 @@ 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(_) => {
|
||||
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})"
|
||||
);
|
||||
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
|
||||
io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"connection queue is full",
|
||||
@@ -357,13 +305,11 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
TrySendError::Closed(dropped) => {
|
||||
debug!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "closed_reconnecting",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"connection to {address} dead, attempting re-establishment"
|
||||
"Connection to {address} seems to be dead. attempting to re-establish it...",
|
||||
);
|
||||
|
||||
// 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"]
|
||||
features = ["json", "rustls-tls"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -23,7 +23,7 @@ use nym_api_requests::models::{
|
||||
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNodeV1, SkimmedNodesWithMetadata,
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
@@ -354,12 +354,12 @@ impl NymApiClient {
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
|
||||
pub async fn get_basic_mixnodes(&self) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
pub async fn get_basic_mixnodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
Ok(self.nym_api.get_basic_mixnodes().await?.nodes)
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")]
|
||||
pub async fn get_basic_gateways(&self) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
pub async fn get_basic_gateways(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
Ok(self.nym_api.get_basic_gateways().await?.nodes)
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ impl NymApiClient {
|
||||
#[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
@@ -389,7 +389,7 @@ impl NymApiClient {
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_active_mixing_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
@@ -406,7 +406,7 @@ impl NymApiClient {
|
||||
#[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_mixing_capable_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_mixing_capable_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
@@ -420,7 +420,7 @@ impl NymApiClient {
|
||||
|
||||
/// retrieve basic information for all bonded nodes on the network
|
||||
#[deprecated(note = "use get_all_basic_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNodeV1>, ValidatorClientError> {
|
||||
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
use crate::nym_api::error::NymAPIError;
|
||||
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use crate::nym_nodes::SkimmedNodesWithMetadata;
|
||||
use crate::ValidatorClientError;
|
||||
use async_trait::async_trait;
|
||||
use nym_api_requests::ecash::models::{
|
||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||
@@ -21,14 +20,11 @@ use nym_api_requests::models::{
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
PaginatedCachedNodesResponseV2,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
use nym_mixnet_contract_common::{IdentityKeyRef, NodeId, NymNodeDetails};
|
||||
use std::net::IpAddr;
|
||||
use time::format_description::BorrowedFormatItem;
|
||||
use time::Date;
|
||||
use tracing::instrument;
|
||||
|
||||
pub use nym_api_requests::{
|
||||
ecash::{
|
||||
models::SpentCredentialsResponse, BlindSignRequestBody, BlindedSignatureResponse,
|
||||
@@ -40,14 +36,17 @@ pub use nym_api_requests::{
|
||||
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
|
||||
MixnodeUptimeHistoryResponse, StakeSaturationResponse, UptimeResponse,
|
||||
},
|
||||
nym_nodes::{
|
||||
CachedNodesResponse, NodesByAddressesRequestBody, NodesByAddressesResponse,
|
||||
PaginatedCachedNodesResponseV1, PaginatedCachedNodesResponseV2, SemiSkimmedNodeV1,
|
||||
SemiSkimmedNodeV3, SemiSkimmedNodesWithMetadata, SkimmedNodeV1,
|
||||
},
|
||||
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode},
|
||||
NymNetworkDetailsResponse,
|
||||
};
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
use nym_mixnet_contract_common::{IdentityKeyRef, NodeId, NymNodeDetails};
|
||||
use std::net::IpAddr;
|
||||
use time::format_description::BorrowedFormatItem;
|
||||
use time::Date;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::ValidatorClientError;
|
||||
pub use nym_coconut_dkg_common::types::EpochId;
|
||||
|
||||
pub mod error;
|
||||
@@ -391,7 +390,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
#[deprecated]
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -407,7 +406,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -444,7 +443,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -486,7 +485,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -528,7 +527,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -570,7 +569,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -613,7 +612,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -655,7 +654,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -696,7 +695,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -734,7 +733,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -771,7 +770,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNodeV1>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -798,21 +797,6 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_expanded_nodes_v3(
|
||||
&self,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNodeV3>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response("/v3/unstable/nym-nodes/semi-skimmed", ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnode_report(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
pub const V1_API_VERSION: &str = "v1";
|
||||
pub const V2_API_VERSION: &str = "v2";
|
||||
pub const V3_API_VERSION: &str = "v3";
|
||||
pub const MIXNODES: &str = "mixnodes";
|
||||
pub const GATEWAYS: &str = "gateways";
|
||||
pub const DESCRIBED: &str = "described";
|
||||
|
||||
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -22,8 +22,6 @@ 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];
|
||||
@@ -113,13 +111,6 @@ 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,40 +291,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,10 @@ generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
hmac = { workspace = true, optional = true }
|
||||
jwt-simple = { workspace = true, optional = true }
|
||||
libcrux-psq = { workspace = true, optional = true }
|
||||
libcrux-curve25519 = { workspace = true, optional = true }
|
||||
cipher = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
|
||||
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
rand09 = { workspace = true, optional = true }
|
||||
serde_bytes = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
@@ -36,26 +33,25 @@ thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { workspace = true, optional = true }
|
||||
nym-sphinx-types = { workspace = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
libcrux_x25519 = ["libcrux-psq", "libcrux-curve25519"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize", "rand09"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -17,9 +17,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod serde_helpers;
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
|
||||
|
||||
/// Size of a X25519 private key
|
||||
pub const PRIVATE_KEY_SIZE: usize = 32;
|
||||
|
||||
@@ -48,9 +45,6 @@ pub enum KeyRecoveryError {
|
||||
#[source]
|
||||
source: bs58::decode::Error,
|
||||
},
|
||||
|
||||
#[error("the x25519 private key could not be converted to its PSQ representation")]
|
||||
IncompatiblePSQPrivateKey,
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
@@ -419,88 +413,6 @@ impl AsRef<[u8]> for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
// libcrux-psq conversion
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl TryFrom<PrivateKey> for libcrux_psq::handshake::types::DHPrivateKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
fn try_from(
|
||||
key: PrivateKey,
|
||||
) -> Result<libcrux_psq::handshake::types::DHPrivateKey, Self::Error> {
|
||||
Self::try_from(&key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl From<libcrux_psq::handshake::types::DHPrivateKey> for PrivateKey {
|
||||
fn from(key: libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey {
|
||||
// SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length
|
||||
#[allow(clippy::unwrap_used)]
|
||||
PrivateKey::from_bytes(key.as_ref()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl TryFrom<&PrivateKey> for libcrux_psq::handshake::types::DHPrivateKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
fn try_from(
|
||||
key: &PrivateKey,
|
||||
) -> Result<libcrux_psq::handshake::types::DHPrivateKey, Self::Error> {
|
||||
let mut private_key_bytes = zeroize::Zeroizing::new(key.to_bytes());
|
||||
libcrux_curve25519::clamp(&mut private_key_bytes);
|
||||
match libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&private_key_bytes) {
|
||||
Ok(key) => Ok(key),
|
||||
Err(_) => Err(KeyRecoveryError::IncompatiblePSQPrivateKey),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl From<&libcrux_psq::handshake::types::DHPrivateKey> for PrivateKey {
|
||||
fn from(key: &libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey {
|
||||
// SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length
|
||||
#[allow(clippy::unwrap_used)]
|
||||
PrivateKey::from_bytes(key.as_ref()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl From<PublicKey> for libcrux_psq::handshake::types::DHPublicKey {
|
||||
fn from(key: PublicKey) -> libcrux_psq::handshake::types::DHPublicKey {
|
||||
libcrux_psq::handshake::types::DHPublicKey::from_bytes(key.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl From<libcrux_psq::handshake::types::DHPublicKey> for PublicKey {
|
||||
fn from(key: libcrux_psq::handshake::types::DHPublicKey) -> PublicKey {
|
||||
// SAFETY: the DHPublicKey is guaranteed to be 32 bytes in length
|
||||
#[allow(clippy::unwrap_used)]
|
||||
PublicKey::from_bytes(key.as_ref()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl TryFrom<KeyPair> for libcrux_psq::handshake::types::DHKeyPair {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
fn try_from(
|
||||
key: KeyPair,
|
||||
) -> Result<libcrux_psq::handshake::types::DHKeyPair, KeyRecoveryError> {
|
||||
Ok(libcrux_psq::handshake::types::DHKeyPair::from(
|
||||
libcrux_psq::handshake::types::DHPrivateKey::try_from(&key.private_key)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl From<libcrux_psq::handshake::types::DHKeyPair> for KeyPair {
|
||||
fn from(key: libcrux_psq::handshake::types::DHKeyPair) -> KeyPair {
|
||||
KeyPair::from(PrivateKey::from(key.sk()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -509,19 +421,6 @@ mod tests {
|
||||
|
||||
fn assert_zeroize<T: Zeroize>() {}
|
||||
|
||||
#[test]
|
||||
fn test_key_conversion() {
|
||||
let dalek_kp = KeyPair::new(&mut rand::thread_rng());
|
||||
|
||||
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
|
||||
|
||||
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
|
||||
let libcrux_private_key = DHPrivateKey::from_bytes(&dalek_private_key_bytes).unwrap();
|
||||
let libcrux_public_key = libcrux_private_key.to_public();
|
||||
|
||||
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_key_is_zeroized() {
|
||||
assert_zeroize::<PrivateKey>();
|
||||
|
||||
@@ -44,25 +44,3 @@ pub mod option_bs58_x25519_pubkey {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
pub mod bs58_dh_public_key {
|
||||
use crate::asymmetric::x25519;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
key: &libcrux_psq::handshake::types::DHPublicKey,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let x25519: x25519::PublicKey = (*key).into();
|
||||
serializer.serialize_str(&x25519.to_base58_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<libcrux_psq::handshake::types::DHPublicKey, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let x25519 = x25519::PublicKey::from_base58_string(s).map_err(serde::de::Error::custom)?;
|
||||
Ok(x25519.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,152 +109,3 @@ impl DerivationMaterial {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod blake3 {
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
use blake3::Hasher;
|
||||
|
||||
use rand09::{RngCore, rng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub fn derive_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
salt: &[u8],
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = Hasher::new_derive_key(info);
|
||||
|
||||
for input_key in input_key_material {
|
||||
hasher.update(input_key);
|
||||
}
|
||||
|
||||
hasher.update(salt);
|
||||
|
||||
hasher.finalize().as_bytes().to_owned()
|
||||
}
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
derive_key_blake3_multi_input(info, &[input_key_material], salt)
|
||||
}
|
||||
|
||||
pub fn derive_fresh_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
) -> [u8; 32] {
|
||||
let mut salt = [0u8; 32];
|
||||
rng().fill_bytes(&mut salt);
|
||||
|
||||
let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt);
|
||||
|
||||
// Zeroize salt
|
||||
salt.zeroize();
|
||||
|
||||
derived_key
|
||||
}
|
||||
|
||||
/// Derives a fresh 32-byte key using Blake3's key derivation mode.
|
||||
/// The function calls a random number generator to generate a fresh salt.
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes());
|
||||
/// ```
|
||||
pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] {
|
||||
derive_fresh_key_blake3_multi_input(info, &[input_key_material])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -15,7 +15,6 @@ 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,9 +3,14 @@
|
||||
|
||||
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,
|
||||
@@ -13,12 +18,6 @@ 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;
|
||||
@@ -32,7 +31,6 @@ 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>,
|
||||
|
||||
@@ -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"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -46,10 +46,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
Arc, LazyLock,
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
},
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -73,23 +70,14 @@ 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 = 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.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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`]).
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
pub fn no_hickory_dns(mut self) -> Self {
|
||||
self = self.non_shared();
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
@@ -141,8 +129,7 @@ pub struct HickoryDnsResolver {
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
use_system: Arc<AtomicBool>,
|
||||
system_resolver: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<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,
|
||||
@@ -154,8 +141,7 @@ impl Default for HickoryDnsResolver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
use_system: Arc::new(AtomicBool::new(false)),
|
||||
system_resolver: Default::default(),
|
||||
fallback: Default::default(),
|
||||
static_base: Some(Default::default()),
|
||||
use_shared: true,
|
||||
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
||||
@@ -165,28 +151,16 @@ impl Default for HickoryDnsResolver {
|
||||
|
||||
impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
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 resolver = self.state.clone();
|
||||
let maybe_fallback = self.fallback.clone();
|
||||
let maybe_static = self.static_base.clone();
|
||||
let use_shared = self.use_shared;
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
resolve(
|
||||
name,
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
use_shared,
|
||||
overall_dns_timeout,
|
||||
@@ -197,17 +171,16 @@ 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: TokioResolver,
|
||||
resolver: Arc<OnceCell<TokioResolver>>,
|
||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
independent: bool,
|
||||
overall_dns_timeout: Duration,
|
||||
) -> Result<Addrs, ResolveError> {
|
||||
let resolver = resolver.get_or_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.
|
||||
@@ -241,6 +214,22 @@ 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 {
|
||||
@@ -269,11 +258,6 @@ 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,
|
||||
@@ -281,20 +265,10 @@ 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,
|
||||
resolver,
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
self.static_base.clone(),
|
||||
self.use_shared,
|
||||
self.overall_dns_timeout,
|
||||
@@ -324,11 +298,13 @@ 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 {
|
||||
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.system_resolver
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
@@ -344,80 +320,45 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// 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)?;
|
||||
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_system_resolver();
|
||||
}
|
||||
// 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(())
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// Disable fallback resolution. If the primary resolver fails the error is
|
||||
/// returned immediately
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_configured_resolver();
|
||||
}
|
||||
// // 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;
|
||||
// }
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
/// exists.
|
||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
|
||||
Some(self.static_base.as_ref()?.get()?.get_addrs())
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
|
||||
pub fn set_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);
|
||||
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));
|
||||
}
|
||||
|
||||
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
||||
@@ -554,7 +495,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new().with_fallback(constants::default_static_addrs())
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
@@ -591,7 +532,10 @@ mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
@@ -608,7 +552,7 @@ mod test {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver)
|
||||
.dns_resolver(resolver.into())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -653,7 +597,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_fallback_addrs(addr_map);
|
||||
resolver.set_static_fallbacks(addr_map);
|
||||
|
||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||
assert!(addrs.contains(&example_ip4));
|
||||
@@ -794,19 +738,18 @@ mod test {
|
||||
}
|
||||
|
||||
#[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.
|
||||
//
|
||||
// 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
|
||||
#[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
|
||||
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
||||
let resolver1 = HickoryDnsResolver::shared();
|
||||
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 time_start = std::time::Instant::now();
|
||||
// create a new resolver that uses the shared resolver
|
||||
let resolver = HickoryDnsResolver::shared();
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "rpc.nymtech.net";
|
||||
@@ -815,27 +758,9 @@ mod test {
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
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()
|
||||
println!(
|
||||
"{}ms resolved {domain}",
|
||||
(Instant::now() - time_start).as_millis()
|
||||
);
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
@@ -846,62 +771,5 @@ 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,9 +51,6 @@ 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))];
|
||||
|
||||
@@ -72,12 +69,6 @@ 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());
|
||||
@@ -97,7 +88,6 @@ 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,78 +14,42 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StaticResolver {
|
||||
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
static_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 {
|
||||
status: PreResolveStatus,
|
||||
valid_for_pre_resolve_until: Option<Instant>,
|
||||
addrs: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new(addrs: Vec<IpAddr>) -> Self {
|
||||
Self {
|
||||
status: PreResolveStatus::Valid,
|
||||
valid_for_pre_resolve_until: None,
|
||||
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() -> 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();
|
||||
Self {
|
||||
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
||||
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
/// 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>> {
|
||||
let mut out = HashMap::new();
|
||||
self.preresolve_addr_map
|
||||
self.static_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
@@ -95,38 +59,6 @@ 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)]
|
||||
@@ -139,58 +71,44 @@ 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>> {
|
||||
self.preresolve_addr_map
|
||||
debug!("found {name:?} in pre-resolve static table resolver");
|
||||
|
||||
self.pre_resolve_timeout?;
|
||||
|
||||
self.static_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.filter(|entry| entry.is_valid())
|
||||
.map(|entry| {
|
||||
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
|
||||
entry.addrs.clone()
|
||||
.filter(|e| {
|
||||
e.valid_for_pre_resolve_until
|
||||
.is_some_and(|t| t > Instant::now())
|
||||
})
|
||||
.map(|e| e.addrs.clone())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||
Self::resolve_inner(
|
||||
self.fallback_addr_map.lock().unwrap(),
|
||||
self.preresolve_addr_map.lock().unwrap(),
|
||||
self.static_addr_map.lock().unwrap(),
|
||||
name,
|
||||
self.pre_resolve_timeout,
|
||||
)
|
||||
.map(|e| e.addrs)
|
||||
}
|
||||
|
||||
fn resolve_inner(
|
||||
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
|
||||
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
mut table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
name: &str,
|
||||
pre_resolve_cache_timeout: Option<Duration>,
|
||||
) -> Option<Vec<IpAddr>> {
|
||||
let resolved = fallback_table.get(name)?;
|
||||
timeout: Option<Duration>,
|
||||
) -> Option<Entry> {
|
||||
let resolved = table.get_mut(name)?;
|
||||
|
||||
debug!("lookup hit for \"{name:?}\" in static table resolver");
|
||||
debug!("found {name:?} in static table resolver");
|
||||
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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);
|
||||
}
|
||||
Some(resolved.clone())
|
||||
}
|
||||
@@ -199,23 +117,13 @@ impl StaticResolver {
|
||||
impl Resolve for StaticResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
debug!("looking up {name:?} in static resolver");
|
||||
// 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 addr_map = self.static_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 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,
|
||||
) {
|
||||
let addr_map = addr_map.lock().unwrap();
|
||||
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
|
||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||
Some(addrs) => addrs,
|
||||
Some(entry) => entry.addrs,
|
||||
};
|
||||
let addrs: Addrs = Box::new(
|
||||
lookup
|
||||
@@ -234,7 +142,6 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -242,7 +149,7 @@ mod test {
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
|
||||
// lookup for domain for which there is no entry
|
||||
let resolver = StaticResolver::new();
|
||||
let resolver = StaticResolver::new(HashMap::new());
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let result = resolver.resolve(url).await;
|
||||
@@ -259,7 +166,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().with_fallback(addr_map);
|
||||
let resolver = StaticResolver::new(addr_map);
|
||||
let mut addrs = resolver.resolve(url).await?;
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
||||
@@ -268,7 +175,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevate_fallback_to_pre_resolve() {
|
||||
fn static_lookup_pre_resolve() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
let mut addr_map = HashMap::new();
|
||||
@@ -276,23 +183,24 @@ 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()
|
||||
.with_fallback(addr_map)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
let resolver = StaticResolver::new(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 _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
|
||||
));
|
||||
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)
|
||||
);
|
||||
|
||||
// check that pre-resolve now returns the expected record
|
||||
let addrs = resolver
|
||||
@@ -306,139 +214,4 @@ 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,21 +3,15 @@
|
||||
|
||||
//! Utilities for and implementation of request tunneling
|
||||
|
||||
use std::sync::{
|
||||
Arc, LazyLock, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{Client, ClientBuilder};
|
||||
|
||||
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
|
||||
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
|
||||
use crate::ClientBuilder;
|
||||
|
||||
// #[cfg(feature = "tunneling")]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Front {
|
||||
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
|
||||
pub(crate) policy: FrontPolicy,
|
||||
enabled: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -25,7 +19,7 @@ impl Clone for Front {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
policy: self.policy.clone(),
|
||||
enabled: AtomicBool::new(false),
|
||||
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,30 +27,13 @@ impl Clone for Front {
|
||||
impl Front {
|
||||
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
||||
Self {
|
||||
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),
|
||||
enabled: AtomicBool::new(policy == FrontPolicy::Always),
|
||||
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.read().unwrap() {
|
||||
match self.policy {
|
||||
FrontPolicy::Off => false,
|
||||
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
||||
FrontPolicy::Always => true,
|
||||
@@ -69,13 +46,14 @@ impl Front {
|
||||
if self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
|
||||
if matches!(self.policy, 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.
|
||||
@@ -88,208 +66,29 @@ pub enum FrontPolicy {
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// 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()
|
||||
};
|
||||
/// 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);
|
||||
|
||||
// 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: {:?}",
|
||||
self.urls
|
||||
"fronting is enabled, but none of the supplied urls have configured fronting domains"
|
||||
);
|
||||
}
|
||||
|
||||
self.front = front;
|
||||
self.front = Some(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(
|
||||
@@ -305,7 +104,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new(url1)
|
||||
.expect("bad url")
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
@@ -341,7 +140,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
|
||||
+102
-192
@@ -136,7 +136,6 @@
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use http::header::USER_AGENT;
|
||||
pub use inventory;
|
||||
pub use reqwest;
|
||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||
@@ -148,7 +147,6 @@ 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;
|
||||
@@ -163,7 +161,9 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use std::sync::{Arc, LazyLock};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
mod fronted;
|
||||
@@ -195,8 +195,6 @@ 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;
|
||||
@@ -208,24 +206,6 @@ 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
|
||||
@@ -347,22 +327,16 @@ 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: Box<reqwest::Url>,
|
||||
url: reqwest::Url,
|
||||
#[source]
|
||||
source: ReqwestErrorWrapper,
|
||||
},
|
||||
|
||||
#[error("failed to read response body from {url}: {source}")]
|
||||
ResponseReadFailure {
|
||||
url: Box<reqwest::Url>,
|
||||
url: reqwest::Url,
|
||||
headers: Box<HeaderMap>,
|
||||
status: StatusCode,
|
||||
#[source]
|
||||
@@ -379,7 +353,7 @@ pub enum HttpClientError {
|
||||
},
|
||||
|
||||
#[error("the requested resource could not be found at {url}")]
|
||||
NotFound { url: Box<reqwest::Url> },
|
||||
NotFound { url: reqwest::Url },
|
||||
|
||||
#[error("attempted to use domain fronting and clone a request containing stream data")]
|
||||
AttemptedToCloneStreamRequest,
|
||||
@@ -391,7 +365,7 @@ pub enum HttpClientError {
|
||||
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
|
||||
)]
|
||||
RequestFailure {
|
||||
url: Box<reqwest::Url>,
|
||||
url: reqwest::Url,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
},
|
||||
@@ -400,7 +374,7 @@ pub enum HttpClientError {
|
||||
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
|
||||
)]
|
||||
EmptyResponse {
|
||||
url: Box<reqwest::Url>,
|
||||
url: reqwest::Url,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
},
|
||||
@@ -409,7 +383,7 @@ pub enum HttpClientError {
|
||||
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
|
||||
)]
|
||||
EndpointFailure {
|
||||
url: Box<reqwest::Url>,
|
||||
url: reqwest::Url,
|
||||
status: StatusCode,
|
||||
headers: Box<HeaderMap>,
|
||||
error: String,
|
||||
@@ -479,7 +453,7 @@ impl HttpClientError {
|
||||
|
||||
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
|
||||
HttpClientError::RequestSendFailure {
|
||||
url: Box::new(url),
|
||||
url,
|
||||
source: ReqwestErrorWrapper(source),
|
||||
}
|
||||
}
|
||||
@@ -580,19 +554,6 @@ 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
|
||||
@@ -601,18 +562,16 @@ pub struct ClientBuilder {
|
||||
urls: Vec<Url>,
|
||||
|
||||
timeout: Option<Duration>,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
reqwest_client_builder: Option<reqwest::ClientBuilder>,
|
||||
custom_user_agent: bool,
|
||||
reqwest_client_builder: reqwest::ClientBuilder,
|
||||
#[allow(dead_code)] // not dead code, just unused in wasm
|
||||
use_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: fronted::Front,
|
||||
front: Option<fronted::Front>,
|
||||
|
||||
retry_limit: usize,
|
||||
serialization: SerializationFormat,
|
||||
|
||||
error: Option<HttpClientError>,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
@@ -683,10 +642,10 @@ impl ClientBuilder {
|
||||
|
||||
let mut builder = Self::new_with_urls(urls)?;
|
||||
|
||||
// Enable domain fronting using the shared fronting policy
|
||||
// Enable domain fronting by default (on retry)
|
||||
#[cfg(feature = "tunneling")]
|
||||
{
|
||||
builder = builder.with_fronting(None);
|
||||
builder = builder.with_fronting(FrontPolicy::OnRetry);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
@@ -700,31 +659,26 @@ 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: None,
|
||||
reqwest_client_builder: None,
|
||||
custom_user_agent: false,
|
||||
reqwest_client_builder,
|
||||
use_secure_dns: true,
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: fronted::Front::off(),
|
||||
front: None,
|
||||
|
||||
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);
|
||||
@@ -769,7 +723,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 = Some(reqwest_builder);
|
||||
self.reqwest_client_builder = reqwest_builder;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -779,12 +733,18 @@ impl ClientBuilder {
|
||||
V: TryInto<HeaderValue>,
|
||||
V::Error: Into<http::Error>,
|
||||
{
|
||||
match value.try_into() {
|
||||
Ok(v) => self.custom_user_agent = Some(v),
|
||||
Err(err) => {
|
||||
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
|
||||
}
|
||||
}
|
||||
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);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -801,33 +761,30 @@ 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 = Some(reqwest::ClientBuilder::new().build()?);
|
||||
let reqwest_client = self.reqwest_client_builder.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 = 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()));
|
||||
}
|
||||
let reqwest_client = {
|
||||
let mut builder = self.reqwest_client_builder;
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)
|
||||
})
|
||||
.transpose()?;
|
||||
// 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)?
|
||||
};
|
||||
|
||||
let client = Client {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
custom_user_agent: self.custom_user_agent,
|
||||
using_secure_dns: self.use_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
@@ -847,11 +804,11 @@ impl ClientBuilder {
|
||||
pub struct Client {
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: Option<reqwest::Client>,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
reqwest_client: reqwest::Client,
|
||||
using_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: fronted::Front,
|
||||
front: Option<fronted::Front>,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: Duration,
|
||||
@@ -905,8 +862,8 @@ impl Client {
|
||||
Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: None,
|
||||
custom_user_agent: None,
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
using_secure_dns: self.using_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front.clone(),
|
||||
@@ -940,7 +897,9 @@ impl Client {
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn matches_current_host(&self, url: &Url) -> bool {
|
||||
if self.front.is_enabled() {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
url.host_str() == self.current_url().front_str()
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
@@ -967,7 +926,9 @@ impl Client {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if self.front.is_enabled() {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
// if we are using fronting, try updating to the next front
|
||||
let url = self.current_url();
|
||||
|
||||
@@ -987,7 +948,9 @@ impl Client {
|
||||
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
#[cfg(feature = "tunneling")]
|
||||
if self.front.is_enabled() {
|
||||
if let Some(ref front) = self.front
|
||||
&& 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
|
||||
@@ -1018,12 +981,14 @@ 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.
|
||||
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||
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 self.front.is_enabled() {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if let Some(front_host) = url.front_str() {
|
||||
if let Some(actual_host) = url.host_str() {
|
||||
tracing::debug!(
|
||||
@@ -1043,13 +1008,6 @@ 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!(
|
||||
@@ -1090,21 +1048,12 @@ impl ApiClientCore for Client {
|
||||
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
|
||||
let client = if let Some(client) = &self.reqwest_client {
|
||||
client.clone()
|
||||
} else {
|
||||
SHARED_CLIENT.clone()
|
||||
};
|
||||
let mut rb = RequestBuilder::from_parts(client, req);
|
||||
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), 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 => {
|
||||
@@ -1147,19 +1096,16 @@ impl ApiClientCore for Client {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
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)??,
|
||||
Ok(wasmtimer::tokio::timeout(
|
||||
self.request_timeout,
|
||||
self.reqwest_client.execute(req),
|
||||
)
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let response = {
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
client.execute(req).await
|
||||
};
|
||||
let response = self.reqwest_client.execute(req).await;
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
@@ -1175,10 +1121,20 @@ impl ApiClientCore for Client {
|
||||
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
self.update_host(Some(url.clone()));
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
self.maybe_enable_fronting(("network", url.as_str(), &err));
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
@@ -1202,21 +1158,6 @@ 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.
|
||||
@@ -1369,39 +1310,9 @@ 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
|
||||
#[instrument(level = "debug", skip_all, fields(path=?path))]
|
||||
async fn get_response<P, T, K, V>(
|
||||
&self,
|
||||
path: P,
|
||||
@@ -1416,8 +1327,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::GET, path, params, None::<&()>)
|
||||
.await?;
|
||||
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1439,7 +1349,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::POST, path, params, Some(json_body))
|
||||
.await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
|
||||
@@ -1459,7 +1369,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
|
||||
.await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1481,7 +1391,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
|
||||
.await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1493,7 +1403,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1510,7 +1420,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `delete` json data from the provided absolute endpoint, e.g.
|
||||
@@ -1522,7 +1432,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1540,7 +1450,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let req =
|
||||
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
self.parse_response(res, false).await
|
||||
parse_response(res, false).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1607,7 +1517,7 @@ where
|
||||
|
||||
if !allow_empty && let Some(0) = res.content_length() {
|
||||
return Err(HttpClientError::EmptyResponse {
|
||||
url: Box::new(url),
|
||||
url,
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
});
|
||||
@@ -1620,25 +1530,25 @@ where
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|source| HttpClientError::ResponseReadFailure {
|
||||
url: Box::new(url),
|
||||
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: Box::new(url) })
|
||||
Err(HttpClientError::NotFound { url })
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
url: Box::new(url),
|
||||
url,
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
});
|
||||
};
|
||||
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url: Box::new(url),
|
||||
url,
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: plaintext,
|
||||
|
||||
@@ -89,8 +89,7 @@ fn sanitizing_urls() {
|
||||
// - on error without retries is where we have multiple urls, is the url updated?
|
||||
|
||||
#[tokio::test]
|
||||
#[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.
|
||||
#[ignore] // 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)
|
||||
@@ -200,7 +199,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(Some(crate::fronted::FrontPolicy::Always))
|
||||
.with_fronting(crate::fronted::FrontPolicy::Always)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -123,16 +123,6 @@ 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
|
||||
|
||||
@@ -6,9 +6,6 @@ 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 }
|
||||
|
||||
@@ -51,10 +51,6 @@ 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/";
|
||||
|
||||
@@ -6,7 +6,6 @@ edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
|
||||
@@ -9,17 +9,15 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
|
||||
blake3 = { workspace = true, optional = true }
|
||||
libcrux-sha3 = { workspace = true, optional = true }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
|
||||
|
||||
[features]
|
||||
digests = ["blake3", "libcrux-sha3"]
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
|
||||
use crate::error::KKTCiphersuiteError;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use strum_macros::{Display, EnumIter, EnumString};
|
||||
|
||||
pub use strum::IntoEnumIterator;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub const DEFAULT_HASH_LEN: usize = 32;
|
||||
@@ -47,13 +45,10 @@ pub mod xwing {
|
||||
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
|
||||
}
|
||||
|
||||
pub type KEMKeyDigests = BTreeMap<HashFunction, Vec<u8>>;
|
||||
pub type KEMKeyDigests = KeyDigests;
|
||||
pub type SigningKeyDigests = KeyDigests;
|
||||
|
||||
pub mod node_compatibility {
|
||||
/// Indicates the initial version where kkt has been introduced
|
||||
/// 1.27.0 Raclette release
|
||||
pub const INTRODUCTION: semver::Version = semver::Version::new(1, 27, 0);
|
||||
}
|
||||
pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
@@ -67,8 +62,6 @@ pub mod node_compatibility {
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
@@ -211,26 +204,23 @@ impl SignatureScheme {
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
Default,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[repr(u8)]
|
||||
pub enum KEM {
|
||||
// unsupported
|
||||
// XWing = 0,
|
||||
#[default]
|
||||
XWing = 0,
|
||||
MlKem768 = 1,
|
||||
McEliece = 2,
|
||||
X25519 = 255,
|
||||
}
|
||||
|
||||
impl KEM {
|
||||
pub const fn encapsulation_key_length(&self) -> usize {
|
||||
pub fn encapsulation_key_length(&self) -> usize {
|
||||
match self {
|
||||
KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH,
|
||||
// KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
|
||||
KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
|
||||
KEM::X25519 => x25519::PUBLIC_KEY_LENGTH,
|
||||
KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH,
|
||||
}
|
||||
}
|
||||
@@ -248,17 +238,6 @@ pub struct Ciphersuite {
|
||||
signature_length: usize,
|
||||
}
|
||||
|
||||
impl Default for Ciphersuite {
|
||||
fn default() -> Self {
|
||||
Ciphersuite::new(
|
||||
KEM::MlKem768,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ciphersuite {
|
||||
pub fn new(
|
||||
kem: KEM,
|
||||
@@ -278,51 +257,6 @@ impl Ciphersuite {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine optimal `Ciphersuite` based on remote's node's version
|
||||
pub fn from_node_version(semver: semver::Version) -> Option<Self> {
|
||||
if semver < node_compatibility::INTRODUCTION {
|
||||
// node can't possibly support any Ciphersuite
|
||||
return None;
|
||||
}
|
||||
// currently there are no other branches known to the client
|
||||
// once changes to defaults are introduced, follow pattern similar to the one implemented in
|
||||
// `common/authenticator-requests/src/version.rs`
|
||||
Some(Ciphersuite::new(
|
||||
KEM::MlKem768,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_kem(mut self, kem: KEM) -> Self {
|
||||
self.kem = kem;
|
||||
self.encapsulation_key_length = kem.encapsulation_key_length();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_signature_scheme(mut self, signature_scheme: SignatureScheme) -> Self {
|
||||
self.signature_scheme = signature_scheme;
|
||||
self.signing_key_length = signature_scheme.signing_key_length();
|
||||
self.verification_key_length = signature_scheme.verification_key_length();
|
||||
self.signature_length = signature_scheme.signature_length();
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_hash_function(mut self, hash_function: HashFunction) -> Self {
|
||||
self.hash_function = hash_function;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_hash_length(mut self, hash_length: HashLength) -> Self {
|
||||
self.hash_length = hash_length;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kem_key_len(&self) -> usize {
|
||||
self.encapsulation_key_length
|
||||
}
|
||||
|
||||
+13
-10
@@ -7,31 +7,34 @@ license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
blake3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
|
||||
# internal
|
||||
nym-crypto = { workspace = true, features = ["hashing"] }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
|
||||
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
|
||||
nym-kkt-context = { workspace = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
libcrux-kem = { workspace = true }
|
||||
libcrux-ecdh = { workspace = true, features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { workspace = true }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
rand09 = { workspace = true }
|
||||
rand = "0.9.2"
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
libcrux-psq = { workspace = true, features = ["classic-mceliece"] }
|
||||
libcrux-ml-kem = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.9.0"
|
||||
anyhow = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in benchmarking code
|
||||
#![allow(clippy::expect_used)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_kkt::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme},
|
||||
context::KKTMode,
|
||||
frame::KKTFrame,
|
||||
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Encode Frame - Request",
|
||||
),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Decode Frame - Request",
|
||||
),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Encode Frame",
|
||||
),
|
||||
|b| b.iter(|| r_frame.to_bytes()),
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Encode Frame - Request",),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Decode Frame - Request",),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator OneWay: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Encode Frame",),
|
||||
|b| {
|
||||
b.iter(|| r_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator OneWay: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Encode Frame - Request",),
|
||||
|b| {
|
||||
b.iter(|| i_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Decode Frame - Request",),
|
||||
|b| {
|
||||
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator Mutual: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Encode Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
r_frame.to_bytes();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator Mutual: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
kkt_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,189 +0,0 @@
|
||||
use libcrux_chacha20poly1305::TAG_LEN;
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
// This is arbitrary
|
||||
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
|
||||
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
|
||||
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
|
||||
const CARRIER_KKT_AAD: &[u8] = b"kkt-carrier-v1";
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Carrier {
|
||||
tx_key: [u8; 32],
|
||||
rx_key: [u8; 32],
|
||||
tx_counter: u64,
|
||||
rx_counter: u64,
|
||||
}
|
||||
|
||||
pub enum CarrierRole {
|
||||
Initiator,
|
||||
Responder,
|
||||
}
|
||||
|
||||
fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> {
|
||||
match nonce.checked_add(1) {
|
||||
Some(incremented_nonce) => {
|
||||
*nonce = incremented_nonce;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(KKTError::AEADError {
|
||||
info: "Nonce maxed out.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_nonce_bytes(nonce: u64) -> [u8; 12] {
|
||||
let mut bytes = [0u8; 12];
|
||||
let nonce_bytes = nonce.to_le_bytes();
|
||||
bytes[4..].clone_from_slice(&nonce_bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
impl Carrier {
|
||||
fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
tx_key,
|
||||
rx_key,
|
||||
tx_counter: 1,
|
||||
rx_counter: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &DHPublicKey,
|
||||
context: &[u8],
|
||||
is_initiator: bool,
|
||||
) -> Result<(Self, DHPublicKey), KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let ephemeral_keypair = DHKeyPair::new(rng);
|
||||
let shared_secret = ephemeral_keypair
|
||||
.sk()
|
||||
.diffie_hellman(remote_public_key)
|
||||
.map_err(|_| KKTError::X25519Error {
|
||||
info: "Key Derivation Error",
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
Self::from_secret_slice(shared_secret.as_ref(), context, is_initiator),
|
||||
ephemeral_keypair.pk,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8], is_initiator: bool) -> Self {
|
||||
let (tx_key, rx_key) = if is_initiator {
|
||||
(
|
||||
derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context),
|
||||
derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context),
|
||||
derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context),
|
||||
)
|
||||
};
|
||||
|
||||
Self::init(tx_key, rx_key)
|
||||
}
|
||||
|
||||
pub fn from_secret(secret: [u8; 32], context: &[u8], is_initiator: bool) -> Self {
|
||||
Self::from_secret_slice(Zeroizing::new(secret).as_slice(), context, is_initiator)
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, KKTError> {
|
||||
if plaintext.len() > MAX_PAYLOAD_LEN {
|
||||
return Err(KKTError::AEADError {
|
||||
info: "Plaintext too large",
|
||||
});
|
||||
}
|
||||
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
|
||||
libcrux_chacha20poly1305::encrypt(
|
||||
&self.tx_key,
|
||||
plaintext,
|
||||
&mut output_buffer,
|
||||
CARRIER_KKT_AAD,
|
||||
&as_nonce_bytes(self.tx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.tx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, KKTError> {
|
||||
if ciphertext.len() > MAX_PAYLOAD_LEN + TAG_LEN {
|
||||
return Err(KKTError::AEADError {
|
||||
info: "Ciphertext too large",
|
||||
});
|
||||
}
|
||||
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
|
||||
libcrux_chacha20poly1305::decrypt(
|
||||
&self.rx_key,
|
||||
&mut output_buffer,
|
||||
ciphertext,
|
||||
CARRIER_KKT_AAD,
|
||||
&as_nonce_bytes(self.rx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.rx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{carrier::Carrier, key_utils::generate_lp_keypair_x25519};
|
||||
use rand09::RngCore;
|
||||
|
||||
#[test]
|
||||
fn test_e2e() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate responder x25519 keys
|
||||
let r_x25519 = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
let mut context: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut context);
|
||||
|
||||
let ephemeral_keypair = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap();
|
||||
|
||||
let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap();
|
||||
|
||||
let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context, true);
|
||||
let mut r_carrier = Carrier::from_secret_slice(r_shared_secret.as_ref(), &context, false);
|
||||
|
||||
let test1 = b"test1: i>r #1";
|
||||
let ct1 = i_carrier.encrypt(test1).unwrap();
|
||||
let pt1 = r_carrier.decrypt(&ct1).unwrap();
|
||||
assert_eq!(pt1, test1);
|
||||
|
||||
let test2 = b"test2: r>i #1";
|
||||
let ct2 = i_carrier.encrypt(test2).unwrap();
|
||||
let pt2 = r_carrier.decrypt(&ct2).unwrap();
|
||||
assert_eq!(pt2, test2);
|
||||
let test3 = b"test3: i>r #2";
|
||||
|
||||
let ct3 = i_carrier.encrypt(test3).unwrap();
|
||||
let pt3 = r_carrier.decrypt(&ct3).unwrap();
|
||||
assert_eq!(pt3, test3);
|
||||
|
||||
let test4 = b"test4: i>r #3";
|
||||
let ct4 = i_carrier.encrypt(test4).unwrap();
|
||||
let pt4 = r_carrier.decrypt(&ct4).unwrap();
|
||||
assert_eq!(pt4, test4);
|
||||
|
||||
let test5 = b"test5: r>i #2";
|
||||
let ct5 = i_carrier.encrypt(test5).unwrap();
|
||||
let pt5 = r_carrier.decrypt(&ct5).unwrap();
|
||||
assert_eq!(pt5, test5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::KKTError;
|
||||
use libcrux_kem::Algorithm;
|
||||
|
||||
pub use nym_kkt_ciphersuite::*;
|
||||
|
||||
pub enum EncapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PublicKey),
|
||||
XWing(libcrux_kem::PublicKey),
|
||||
X25519(libcrux_kem::PublicKey),
|
||||
McEliece(classic_mceliece_rust::PublicKey<'a>),
|
||||
}
|
||||
|
||||
pub enum DecapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PrivateKey),
|
||||
XWing(libcrux_kem::PrivateKey),
|
||||
X25519(libcrux_kem::PrivateKey),
|
||||
McEliece(classic_mceliece_rust::SecretKey<'a>),
|
||||
}
|
||||
impl<'a> EncapsulationKey<'a> {
|
||||
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
|
||||
match kem {
|
||||
KEM::McEliece => {
|
||||
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
|
||||
Err(KKTError::KEMError {
|
||||
info: "Received McEliece Encapsulation Key with Invalid Length",
|
||||
})
|
||||
} else {
|
||||
let mut public_key_bytes =
|
||||
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
|
||||
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
|
||||
public_key_bytes.clone_from_slice(bytes);
|
||||
Ok(EncapsulationKey::McEliece(
|
||||
classic_mceliece_rust::PublicKey::from(public_key_bytes),
|
||||
))
|
||||
}
|
||||
}
|
||||
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
EncapsulationKey::XWing(public_key)
|
||||
| EncapsulationKey::MlKem768(public_key)
|
||||
| EncapsulationKey::X25519(public_key) => public_key.encode(),
|
||||
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result<Algorithm, KKTError> {
|
||||
match kem {
|
||||
KEM::MlKem768 => Ok(Algorithm::MlKem768),
|
||||
KEM::XWing => Ok(Algorithm::XWingKemDraft06),
|
||||
KEM::X25519 => Ok(Algorithm::X25519),
|
||||
KEM::McEliece => Err(KKTError::KEMMapping {
|
||||
info: "attempted to map McEliece KEM to libcrux_kem",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,13 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN;
|
||||
use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_kkt_ciphersuite::{CIPHERSUITE_ENCODING_LEN, Ciphersuite};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
// This must be less than 4 bits
|
||||
pub const KKT_VERSION: u8 = 1;
|
||||
const _: () = assert!(KKT_VERSION < 1 << 4);
|
||||
|
||||
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KKTContextEncodingError {
|
||||
#[error("KKT Message Count Limit Reached")]
|
||||
MessageCountLimitReached,
|
||||
|
||||
#[error("{version} is not a valid KKT version")]
|
||||
InvalidVersion { version: u8 },
|
||||
|
||||
#[error("{raw} is not a valid KKTStatus")]
|
||||
InvalidStatus { raw: u8 },
|
||||
|
||||
#[error("{raw} is not a valid KKTRole")]
|
||||
InvalidRole { raw: u8 },
|
||||
|
||||
#[error("{raw} is not a valid KKTMode")]
|
||||
InvalidMode { raw: u8 },
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidCiphersuite(#[from] nym_kkt_ciphersuite::error::KKTCiphersuiteError),
|
||||
}
|
||||
|
||||
// bitmask used: 0b1110_0000
|
||||
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u8)]
|
||||
@@ -40,11 +15,11 @@ pub enum KKTStatus {
|
||||
Ok = 0b0000_0000,
|
||||
InvalidRequestFormat = 0b0010_0000,
|
||||
InvalidResponseFormat = 0b0100_0000,
|
||||
UnsupportedCiphersuite = 0b0110_0000,
|
||||
UnsupportedKKTVersion = 0b1000_0000,
|
||||
InvalidKey = 0b1010_0000,
|
||||
Timeout = 0b1100_0000,
|
||||
UnverifiedKEMKey = 0b1110_0000,
|
||||
InvalidSignature = 0b0110_0000,
|
||||
UnsupportedCiphersuite = 0b1000_0000,
|
||||
UnsupportedKKTVersion = 0b1010_0000,
|
||||
InvalidKey = 0b1100_0000,
|
||||
Timeout = 0b1110_0000,
|
||||
}
|
||||
|
||||
impl Display for KKTStatus {
|
||||
@@ -53,10 +28,10 @@ impl Display for KKTStatus {
|
||||
KKTStatus::Ok => "Ok",
|
||||
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
|
||||
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
|
||||
KKTStatus::InvalidSignature => "Invalid Signature",
|
||||
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
|
||||
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
|
||||
KKTStatus::InvalidKey => "Invalid Key",
|
||||
KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key",
|
||||
KKTStatus::Timeout => "Timeout",
|
||||
})
|
||||
}
|
||||
@@ -68,16 +43,7 @@ impl Display for KKTStatus {
|
||||
pub enum KKTRole {
|
||||
Initiator = 0b0000_0000,
|
||||
Responder = 0b0000_0001,
|
||||
}
|
||||
|
||||
impl KKTRole {
|
||||
pub const fn is_initiator(&self) -> bool {
|
||||
matches!(self, KKTRole::Initiator)
|
||||
}
|
||||
|
||||
pub const fn is_responder(&self) -> bool {
|
||||
matches!(self, KKTRole::Responder)
|
||||
}
|
||||
AnonymousInitiator = 0b0000_0010,
|
||||
}
|
||||
|
||||
// bitmask used: 0b0001_1100
|
||||
@@ -88,16 +54,6 @@ pub enum KKTMode {
|
||||
Mutual = 0b0000_0100,
|
||||
}
|
||||
|
||||
impl KKTMode {
|
||||
pub const fn is_one_way(&self) -> bool {
|
||||
matches!(self, KKTMode::OneWay)
|
||||
}
|
||||
|
||||
pub const fn is_mutual(&self) -> bool {
|
||||
matches!(self, KKTMode::Mutual)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct KKTContext {
|
||||
version: u8,
|
||||
@@ -107,20 +63,24 @@ pub struct KKTContext {
|
||||
role: KKTRole,
|
||||
ciphersuite: Ciphersuite,
|
||||
}
|
||||
|
||||
impl KKTContext {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Self {
|
||||
Self {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
|
||||
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Anonymous Initiator can only use OneWay mode",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
version: KKT_VERSION,
|
||||
message_sequence: 0,
|
||||
status: KKTStatus::Ok,
|
||||
mode,
|
||||
role,
|
||||
ciphersuite,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTContextEncodingError> {
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
|
||||
let mut responder_header = *self;
|
||||
|
||||
responder_header.increment_message_sequence_count()?;
|
||||
@@ -129,12 +89,12 @@ impl KKTContext {
|
||||
Ok(responder_header)
|
||||
}
|
||||
|
||||
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTContextEncodingError> {
|
||||
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> {
|
||||
if self.message_sequence + 1 < (1 << 4) {
|
||||
self.message_sequence += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KKTContextEncodingError::MessageCountLimitReached)
|
||||
Err(KKTError::MessageCountLimitReached)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +118,9 @@ impl KKTContext {
|
||||
}
|
||||
|
||||
pub fn body_len(&self) -> usize {
|
||||
if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey)
|
||||
||
|
||||
// no payload
|
||||
(self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator)
|
||||
if self.status != KKTStatus::Ok
|
||||
|| (self.mode == KKTMode::OneWay
|
||||
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
|
||||
{
|
||||
0
|
||||
} else {
|
||||
@@ -169,18 +128,37 @@ impl KKTContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_len(&self) -> usize {
|
||||
match self.role {
|
||||
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
|
||||
KKTRole::AnonymousInitiator => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn header_len(&self) -> usize {
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
|
||||
pub fn full_message_len(&self) -> usize {
|
||||
self.body_len() + self.header_len()
|
||||
pub const fn session_id_len(&self) -> usize {
|
||||
// note: if anyone decides to update this function and changes the constant value,
|
||||
// you will have to adjust encoding/decoding functions
|
||||
|
||||
// match self.role {
|
||||
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
|
||||
// It doesn't make sense to send a session_id if we send messages in the clear
|
||||
// KKTRole::AnonymousInitiator => 0,
|
||||
// }
|
||||
KKT_SESSION_ID_LEN
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTContextEncodingError> {
|
||||
pub fn full_message_len(&self) -> usize {
|
||||
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> {
|
||||
let mut header_bytes = [0u8; KKT_CONTEXT_LEN];
|
||||
if self.message_sequence >= 1 << 4 {
|
||||
return Err(KKTContextEncodingError::MessageCountLimitReached);
|
||||
return Err(KKTError::MessageCountLimitReached);
|
||||
}
|
||||
|
||||
let ciphersuite_bytes = self.ciphersuite.encode();
|
||||
@@ -197,17 +175,15 @@ impl KKTContext {
|
||||
Ok(header_bytes)
|
||||
}
|
||||
|
||||
pub fn try_decode(
|
||||
header_bytes: [u8; KKT_CONTEXT_LEN],
|
||||
) -> Result<Self, KKTContextEncodingError> {
|
||||
pub fn try_decode(header_bytes: [u8; KKT_CONTEXT_LEN]) -> Result<Self, KKTError> {
|
||||
let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4;
|
||||
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
|
||||
|
||||
// We only check if stuff is valid here, not necessarily if it's compatible
|
||||
|
||||
if kkt_version > KKT_VERSION {
|
||||
return Err(KKTContextEncodingError::InvalidVersion {
|
||||
version: kkt_version,
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Version: {kkt_version}"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,15 +191,16 @@ impl KKTContext {
|
||||
let raw_kkt_role = header_bytes[1] & 0b0000_0011;
|
||||
let raw_kkt_mode = header_bytes[1] & 0b0001_1100;
|
||||
|
||||
let status = KKTStatus::try_from(raw_kkt_status).map_err(|_| {
|
||||
KKTContextEncodingError::InvalidStatus {
|
||||
raw: raw_kkt_status,
|
||||
}
|
||||
let status =
|
||||
KKTStatus::try_from(raw_kkt_status).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Status: {raw_kkt_status}"),
|
||||
})?;
|
||||
let role = KKTRole::try_from(raw_kkt_role).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Role: {raw_kkt_role}"),
|
||||
})?;
|
||||
let mode = KKTMode::try_from(raw_kkt_mode).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
|
||||
})?;
|
||||
let role = KKTRole::try_from(raw_kkt_role)
|
||||
.map_err(|_| KKTContextEncodingError::InvalidRole { raw: raw_kkt_role })?;
|
||||
let mode = KKTMode::try_from(raw_kkt_mode)
|
||||
.map_err(|_| KKTContextEncodingError::InvalidMode { raw: raw_kkt_mode })?;
|
||||
|
||||
// SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
@@ -251,8 +228,9 @@ mod tests {
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([1, 1, 0, 0]).unwrap(),
|
||||
);
|
||||
Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let encoded = valid_context.encode().unwrap();
|
||||
let decoded = KKTContext::try_decode(encoded).unwrap();
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame};
|
||||
use blake3::Hasher;
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
pub struct KKTSessionSecret([u8; 32]);
|
||||
|
||||
impl KKTSessionSecret {
|
||||
pub fn new<R>(rng: &mut R, remote_public_key: &x25519::PublicKey) -> (Self, x25519::PublicKey)
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut private_key_bytes = [0u8; x25519::PRIVATE_KEY_SIZE];
|
||||
rng.fill_bytes(&mut private_key_bytes);
|
||||
|
||||
let ephemeral_private_key = x25519::PrivateKey::from_secret(private_key_bytes);
|
||||
let ephemeral_public_key = x25519::PublicKey::from(&ephemeral_private_key);
|
||||
|
||||
(
|
||||
Self::derive(&ephemeral_private_key, remote_public_key),
|
||||
ephemeral_public_key,
|
||||
)
|
||||
}
|
||||
pub fn from_bytes(secret: [u8; 32]) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
|
||||
fn try_derive(private_key: &x25519::PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
|
||||
let mut pub_key: [u8; 32] = [0u8; 32];
|
||||
pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]);
|
||||
|
||||
// Todo: check validity of pk...
|
||||
let pk = x25519::PublicKey::from(pub_key);
|
||||
Ok(Self::derive(private_key, &pk))
|
||||
}
|
||||
|
||||
pub fn derive(private_key: &x25519::PrivateKey, public_key: &x25519::PublicKey) -> Self {
|
||||
let mut shared_secret = private_key.diffie_hellman(public_key);
|
||||
|
||||
let mut hasher = Hasher::new();
|
||||
|
||||
hasher.update(&shared_secret);
|
||||
shared_secret.zeroize();
|
||||
|
||||
Self(hasher.finalize().as_bytes().to_owned())
|
||||
}
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_initial_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &x25519::PublicKey,
|
||||
kkt_frame: &KKTFrame,
|
||||
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(rng, remote_public_key);
|
||||
|
||||
let mut encrypted_frame =
|
||||
encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?;
|
||||
|
||||
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE);
|
||||
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
|
||||
output_buffer.append(&mut encrypted_frame);
|
||||
|
||||
// [ 32 | 12 | ciphertext | 16];
|
||||
// [eph_pub_key | nonce | ciphertext | tag];
|
||||
Ok((session_secret_key, output_buffer))
|
||||
}
|
||||
|
||||
pub fn decrypt_initial_kkt_frame(
|
||||
responder_private_key: &x25519::PrivateKey,
|
||||
encrypted_frame_bytes: &[u8],
|
||||
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
|
||||
if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN {
|
||||
Err(KKTError::AEADError {
|
||||
info: "Encrypted KKT Frame is too short.",
|
||||
})
|
||||
} else {
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_private_key,
|
||||
&encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE],
|
||||
)?;
|
||||
|
||||
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
|
||||
&shared_secret,
|
||||
&encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..],
|
||||
KKT_INITIAL_FRAME_AAD,
|
||||
)?;
|
||||
Ok((shared_secret, kkt_frame, kkt_context))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame: &KKTFrame,
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let kkt_frame_bytes = kkt_frame.to_bytes();
|
||||
|
||||
// generate nonce
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?;
|
||||
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
let mut output_buffer: Vec<u8> =
|
||||
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);
|
||||
|
||||
output_buffer.extend_from_slice(&nonce);
|
||||
output_buffer.append(&mut ciphertext);
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
// kkt_frame_bytes should look like this
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
pub fn decrypt_kkt_frame(
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame_bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<(KKTFrame, KKTContext), KKTError> {
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);
|
||||
|
||||
let plaintext = decrypt(
|
||||
secret_key.as_bytes(),
|
||||
&kkt_frame_bytes[NONCE_LEN..],
|
||||
aad,
|
||||
&nonce,
|
||||
)?;
|
||||
|
||||
KKTFrame::from_bytes(&plaintext)
|
||||
}
|
||||
|
||||
fn encrypt(
|
||||
secret_key: &[u8; 32],
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
|
||||
libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
fn decrypt(
|
||||
secret_key: &[u8; 32],
|
||||
ciphertext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
|
||||
libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::ciphersuite::Ciphersuite;
|
||||
use crate::context::{KKTContext, KKTMode, KKTRole};
|
||||
use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame};
|
||||
use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame};
|
||||
use crate::{
|
||||
ciphersuite::DEFAULT_HASH_LEN,
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng, rng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
let mut rng = rng();
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let (session_secret_key, ephemeral_public_key) =
|
||||
KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key());
|
||||
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_x25519_keypair.private_key(),
|
||||
ephemeral_public_key.as_bytes().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption() {
|
||||
let mut rng = rng();
|
||||
|
||||
let mut secret_key = [0u8; DEFAULT_HASH_LEN];
|
||||
rng.fill_bytes(&mut secret_key);
|
||||
|
||||
let mut plaintext = vec![0; 100];
|
||||
rng.fill_bytes(&mut plaintext);
|
||||
|
||||
let mut nonce = [0; 12];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut aad = vec![0; 124];
|
||||
rng.fill_bytes(&mut aad);
|
||||
|
||||
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
|
||||
|
||||
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
|
||||
|
||||
assert_eq!(o_plaintext, plaintext)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([255, 1, 0, 0])?,
|
||||
)?;
|
||||
let dummy_frame = KKTFrame::new(
|
||||
valid_context.encode()?,
|
||||
&[2u8; 32],
|
||||
[3u8; KKT_SESSION_ID_LEN],
|
||||
&[4u8; 64],
|
||||
);
|
||||
|
||||
let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?;
|
||||
|
||||
let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?;
|
||||
|
||||
assert_eq!(dummy_frame, frame);
|
||||
assert_eq!(context, valid_context);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+16
-54
@@ -3,91 +3,53 @@
|
||||
|
||||
use crate::context::KKTStatus;
|
||||
use nym_kkt_ciphersuite::error::KKTCiphersuiteError;
|
||||
use nym_kkt_ciphersuite::{HashFunction, KEM};
|
||||
use nym_kkt_context::KKTContextEncodingError;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KKTError {
|
||||
#[error("Signature constructor error")]
|
||||
SigConstructorError,
|
||||
#[error("Signature verification error")]
|
||||
SigVerifError,
|
||||
#[error(transparent)]
|
||||
CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
|
||||
|
||||
#[error(transparent)]
|
||||
MaskedByteError(#[from] MaskedByteError),
|
||||
|
||||
#[error("KEM mapping failure: {info}")]
|
||||
#[error("KEM mapping failure: {}", info)]
|
||||
KEMMapping { info: &'static str },
|
||||
|
||||
#[error("Insecure Encapsulation Key Hash Length")]
|
||||
InsecureHashLen,
|
||||
|
||||
#[error("KKT Frame Decoding Error: {info}")]
|
||||
#[error("KKT Frame Decoding Error: {}", info)]
|
||||
FrameDecodingError { info: String },
|
||||
|
||||
#[error("KKT Frame Encoding Error: {info}")]
|
||||
#[error("KKT Frame Encoding Error: {}", info)]
|
||||
FrameEncodingError { info: String },
|
||||
|
||||
#[error("KKT Incompatibility Error: {info}")]
|
||||
#[error("KKT Incompatibility Error: {}", info)]
|
||||
IncompatibilityError { info: &'static str },
|
||||
|
||||
#[error("KKT Responder Flagged Error: {status}")]
|
||||
#[error("KKT Responder Flagged Error: {}", status)]
|
||||
ResponderFlaggedError { status: KKTStatus },
|
||||
|
||||
#[error("PSQ KEM Error: {info}")]
|
||||
#[error("KKT Message Count Limit Reached")]
|
||||
MessageCountLimitReached,
|
||||
|
||||
#[error("PSQ KEM Error: {}", info)]
|
||||
KEMError { info: &'static str },
|
||||
|
||||
#[error("Local Function Input Error: {info}")]
|
||||
#[error("Local Function Input Error: {}", info)]
|
||||
FunctionInputError { info: &'static str },
|
||||
|
||||
#[error("{info}")]
|
||||
#[error("{}", info)]
|
||||
X25519Error { info: &'static str },
|
||||
|
||||
#[error("{info}")]
|
||||
#[error("{}", info)]
|
||||
AEADError { info: &'static str },
|
||||
|
||||
#[error("{info}")]
|
||||
DecodingError { info: &'static str },
|
||||
|
||||
#[error("{info}")]
|
||||
UnsupportedAlgorithm { info: &'static str },
|
||||
|
||||
#[error("Generic libcrux error")]
|
||||
LibcruxError,
|
||||
|
||||
#[error("failed to derive shared secret: {inner:?}")]
|
||||
SharedSecretDerivationFailure {
|
||||
inner: libcrux_psq::handshake::HandshakeError,
|
||||
},
|
||||
|
||||
#[error("the received encapsulation key hash does not match the expected value")]
|
||||
MismatchedKEMHash,
|
||||
|
||||
#[error(
|
||||
"there are no known digests for initiator's KEM key with {kem} KEM and {hash_function} hash function"
|
||||
)]
|
||||
NoKnownKEMKeyDigests {
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
MalformedContext(#[from] KKTContextEncodingError),
|
||||
}
|
||||
|
||||
impl KKTError {
|
||||
pub fn shared_secret_derivation_failure(inner: libcrux_psq::handshake::HandshakeError) -> Self {
|
||||
KKTError::SharedSecretDerivationFailure { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MaskedByteError {
|
||||
#[error("invalid Masked Byte Length: Expected({expected}), Actual({actual})")]
|
||||
InvalidLength { expected: usize, actual: usize },
|
||||
|
||||
#[error("failed to Unmask Byte")]
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for KKTError {
|
||||
|
||||
+71
-114
@@ -7,158 +7,90 @@
|
||||
// [2..=5] => Ciphersuite
|
||||
// [6] => Reserved
|
||||
|
||||
use crate::context::{KKTMode, KKTRole};
|
||||
use crate::message::{
|
||||
DecryptedRequestFrame, KKTRequest, KKTRequestEncryptionResult, KKTRequestPlaintext,
|
||||
};
|
||||
use crate::{
|
||||
context::{KKT_CONTEXT_LEN, KKTContext},
|
||||
error::KKTError,
|
||||
};
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
pub(crate) const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF";
|
||||
pub const KKT_SESSION_ID_LEN: usize = 16;
|
||||
|
||||
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct KKTFrame {
|
||||
context: KKTContext,
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
session_id: KKTSessionId,
|
||||
body: Vec<u8>,
|
||||
payload: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
// if oneway and message coming from initiator => body is empty.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key.
|
||||
// if coming from responder => body has the responder's kem public key.
|
||||
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
|
||||
// if message coming from anonymous initiator => body is empty, there is no signature.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
|
||||
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
|
||||
|
||||
impl KKTFrame {
|
||||
pub fn new(context: KKTContext, body: &[u8], payload: Vec<u8>) -> Self {
|
||||
pub fn new(
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
body: &[u8],
|
||||
session_id: [u8; KKT_SESSION_ID_LEN],
|
||||
signature: &[u8],
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
body: Vec::from(body),
|
||||
payload,
|
||||
session_id,
|
||||
signature: Vec::from(signature),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn size_excluding_payload(role: KKTRole, mode: KKTMode, kem: KEM) -> usize {
|
||||
match role {
|
||||
KKTRole::Initiator => {
|
||||
match mode {
|
||||
KKTMode::OneWay => {
|
||||
// if oneway and message coming from initiator => body is empty.
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
KKTMode::Mutual => {
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key.
|
||||
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
|
||||
}
|
||||
}
|
||||
}
|
||||
KKTRole::Responder => {
|
||||
// if coming from responder => body has the responder's kem public key.
|
||||
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.payload.len()
|
||||
+ Self::size_excluding_payload(
|
||||
self.context.role(),
|
||||
self.context.mode(),
|
||||
self.context.ciphersuite().kem(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &KKTContext {
|
||||
pub fn context_ref(&self) -> &[u8] {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
self.payload.as_ref()
|
||||
pub fn context(&self) -> Result<KKTContext, KKTError> {
|
||||
KKTContext::try_decode(self.context)
|
||||
}
|
||||
|
||||
pub fn encrypt_initiator_frame<R>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
responder_public_key: &DHPublicKey,
|
||||
version_byte: u8,
|
||||
) -> Result<KKTRequestEncryptionResult, KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let ephemeral_keypair = DHKeyPair::new(rng);
|
||||
|
||||
let plaintext =
|
||||
KKTRequestPlaintext::new(ephemeral_keypair.pk, responder_public_key, version_byte);
|
||||
|
||||
let mut carrier =
|
||||
plaintext.derive_initiator_carrier(ephemeral_keypair.sk(), responder_public_key)?;
|
||||
let full_kkt_message = plaintext.into_request(&mut carrier, self)?;
|
||||
|
||||
Ok(KKTRequestEncryptionResult {
|
||||
carrier,
|
||||
request: full_kkt_message,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt_initiator_frame(
|
||||
responder_keypair: &DHKeyPair,
|
||||
message: KKTRequest,
|
||||
supported_versions: &[u8],
|
||||
request_payload_len: usize,
|
||||
) -> Result<DecryptedRequestFrame, KKTError> {
|
||||
let mask = message.plaintext.version_mask(&responder_keypair.pk);
|
||||
|
||||
// check mask
|
||||
// this could be used later when we have multiple versions
|
||||
// if this call fails, it does before the server has to run a DH
|
||||
let outer_protocol_version = message
|
||||
.plaintext
|
||||
.masked_version_bytes
|
||||
.unmask_check_version(&mask, supported_versions)?;
|
||||
|
||||
// after verifying the version, we can perform the DH and continue processing the request
|
||||
let mut carrier = message
|
||||
.plaintext
|
||||
.derive_responder_carrier(responder_keypair)?;
|
||||
|
||||
let decrypted_message = carrier.decrypt(&message.encrypted_frame)?;
|
||||
let frame = KKTFrame::from_bytes(&decrypted_message, request_payload_len)?;
|
||||
|
||||
Ok(DecryptedRequestFrame {
|
||||
carrier,
|
||||
remote_frame: frame,
|
||||
outer_protocol_version,
|
||||
})
|
||||
pub fn signature_ref(&self) -> &[u8] {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn body_ref(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
pub fn body(self) -> Vec<u8> {
|
||||
self.body
|
||||
pub fn session_id_ref(&self) -> &[u8] {
|
||||
&self.session_id
|
||||
}
|
||||
pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn signature_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.signature
|
||||
}
|
||||
pub fn body_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.body
|
||||
}
|
||||
|
||||
pub fn session_id_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.session_id
|
||||
}
|
||||
|
||||
pub fn frame_length(&self) -> usize {
|
||||
KKT_CONTEXT_LEN + self.body.len()
|
||||
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
|
||||
}
|
||||
|
||||
pub fn try_to_bytes(&self) -> Result<Vec<u8>, KKTError> {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(self.frame_length());
|
||||
bytes.extend_from_slice(&self.context.encode()?);
|
||||
bytes.extend_from_slice(&self.context);
|
||||
bytes.extend_from_slice(&self.body);
|
||||
bytes.extend_from_slice(&self.payload);
|
||||
Ok(bytes)
|
||||
bytes.extend_from_slice(&self.session_id);
|
||||
bytes.extend_from_slice(&self.signature);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8], payload_len: usize) -> Result<Self, KKTError> {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> {
|
||||
let len = bytes.len();
|
||||
if bytes.len() < KKT_CONTEXT_LEN {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
@@ -173,7 +105,7 @@ impl KKTFrame {
|
||||
let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap();
|
||||
let context = KKTContext::try_decode(context_bytes)?;
|
||||
|
||||
if bytes.len() != context.full_message_len() + payload_len {
|
||||
if bytes.len() != context.full_message_len() {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Frame is shorter than expected: actual {len} != expected {}",
|
||||
@@ -183,6 +115,7 @@ impl KKTFrame {
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
let mut signature = Vec::new();
|
||||
|
||||
// decode body
|
||||
if context.body_len() > 0 {
|
||||
@@ -190,9 +123,33 @@ impl KKTFrame {
|
||||
body.extend_from_slice(body_bytes);
|
||||
}
|
||||
|
||||
// decode payload. this could be empty.
|
||||
let payload: Vec<u8> = Vec::from(&bytes[KKT_CONTEXT_LEN + context.body_len()..]);
|
||||
let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN];
|
||||
// SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let session_id = session_bytes.try_into().unwrap();
|
||||
|
||||
Ok(KKTFrame::new(context, &body, payload))
|
||||
// // old code left for reference if session id becomes variable in length:
|
||||
// if context.session_id_len() > 0 {
|
||||
// session_id.extend_from_slice(
|
||||
// &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
// ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
|
||||
// );
|
||||
// }
|
||||
|
||||
// decode signature
|
||||
if context.signature_len() > 0 {
|
||||
let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN
|
||||
..KKT_CONTEXT_LEN
|
||||
+ context.body_len()
|
||||
+ KKT_SESSION_ID_LEN
|
||||
+ context.signature_len()];
|
||||
signature.extend_from_slice(signature_bytes);
|
||||
}
|
||||
|
||||
Ok((
|
||||
KKTFrame::new(context_bytes, &body, session_id, &signature),
|
||||
context,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use libcrux_psq::handshake::types::DHPublicKey;
|
||||
use nym_kkt_ciphersuite::Ciphersuite;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::keys::EncapsulationKey;
|
||||
use crate::message::{KKTRequest, KKTResponse, ProcessedKKTResponse};
|
||||
use crate::{
|
||||
carrier::Carrier,
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::KKTFrame,
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct KKTInitiator<'a> {
|
||||
carrier: Carrier,
|
||||
|
||||
#[zeroize(skip)]
|
||||
context: KKTContext,
|
||||
|
||||
#[zeroize(skip)]
|
||||
expected_hash: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> KKTInitiator<'a> {
|
||||
// to be used by clients
|
||||
pub fn generate_one_way_request<R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
payload: Option<Vec<u8>>,
|
||||
) -> Result<(Self, KKTRequest), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
Self::generate_encrypted_request(
|
||||
rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
None,
|
||||
responder_dh_public_key,
|
||||
expected_hash,
|
||||
outer_protocol_version,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// to be used by nodes
|
||||
pub fn generate_mutual_request<'b, R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
local_encapsulation_key: &'b [u8],
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
payload: Option<Vec<u8>>,
|
||||
) -> Result<(Self, KKTRequest), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
Self::generate_encrypted_request(
|
||||
rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
Some(local_encapsulation_key),
|
||||
responder_dh_public_key,
|
||||
expected_hash,
|
||||
outer_protocol_version,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_encrypted_request<'b, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
local_encapsulation_key: Option<&'b [u8]>,
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
payload: Option<Vec<u8>>,
|
||||
) -> Result<(Self, KKTRequest), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let frame = initiator_process(mode, ciphersuite, local_encapsulation_key, payload)?;
|
||||
let context = *frame.context();
|
||||
|
||||
let request =
|
||||
frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?;
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
carrier: request.carrier,
|
||||
context,
|
||||
expected_hash,
|
||||
},
|
||||
request.request,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn process_response(
|
||||
&mut self,
|
||||
response: KKTResponse,
|
||||
response_payload_len: usize,
|
||||
) -> Result<ProcessedKKTResponse, KKTError> {
|
||||
let decrypted_response_bytes = self.carrier.decrypt(&response.encrypted_frame)?;
|
||||
let response_frame = KKTFrame::from_bytes(&decrypted_response_bytes, response_payload_len)?;
|
||||
initiator_ingest_response(&self.context, &response_frame, self.expected_hash)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initiator_process(
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
own_encapsulation_key: Option<&[u8]>,
|
||||
payload: Option<Vec<u8>>,
|
||||
) -> Result<KKTFrame, KKTError> {
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => encaps_key,
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(KKTFrame::new(context, body, payload.unwrap_or_default()))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response(
|
||||
own_context: &KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<ProcessedKKTResponse, KKTError> {
|
||||
let remote_context = remote_frame.context();
|
||||
let verified_initiator_kem_key = match remote_context.status() {
|
||||
KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => {
|
||||
match validate_encapsulation_key(
|
||||
own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => remote_context.status() != KKTStatus::UnverifiedKEMKey,
|
||||
|
||||
// The key does not match the hash obtained from the directory
|
||||
false => return Err(KKTError::MismatchedKEMHash),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let kem = own_context.ciphersuite().kem();
|
||||
let kem_bytes = remote_frame.body_ref();
|
||||
let encapsulation_key = EncapsulationKey::try_from_bytes(kem_bytes.to_vec(), kem)?;
|
||||
Ok(ProcessedKKTResponse {
|
||||
encapsulation_key,
|
||||
verified_initiator_kem_key,
|
||||
response_payload: remote_frame.payload().to_vec(),
|
||||
})
|
||||
}
|
||||
@@ -1,35 +1,75 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use crate::ciphersuite::HashFunction;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use libcrux_ml_kem::mlkem768::MlKem768KeyPair;
|
||||
use libcrux_psq::handshake::types::DHKeyPair;
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::collections::BTreeMap;
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
|
||||
pub fn generate_lp_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
index: Option<u32>,
|
||||
) -> nym_crypto::asymmetric::ed25519::KeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
DHKeyPair::new(rng)
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> MlKem768KeyPair
|
||||
pub fn generate_keypair_x25519<R>(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng)
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
|
||||
let private_key = nym_crypto::asymmetric::x25519::PrivateKey::from_secret(secret_initiator);
|
||||
private_key.into()
|
||||
}
|
||||
|
||||
pub fn generate_keypair_mceliece<R>(rng: &mut R) -> libcrux_psq::classic_mceliece::KeyPair
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_libcrux<R>(
|
||||
rng: &mut R,
|
||||
kem: crate::ciphersuite::KEM,
|
||||
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng)
|
||||
match kem {
|
||||
crate::ciphersuite::KEM::MlKem768 => {
|
||||
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, rng)?)
|
||||
}
|
||||
crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
rng,
|
||||
)?),
|
||||
crate::ciphersuite::KEM::X25519 => {
|
||||
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?)
|
||||
}
|
||||
_ => Err(crate::error::KKTError::KEMError {
|
||||
info: "Key Generation Error: Unsupported Libcrux Algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_mceliece<'a, R>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
classic_mceliece_rust::SecretKey<'a>,
|
||||
classic_mceliece_rust::PublicKey<'a>,
|
||||
)
|
||||
where
|
||||
// this is annoying because mceliece lib uses rand 0.8.5...
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||
(decapsulation_key, encapsulation_key)
|
||||
}
|
||||
|
||||
pub fn hash_key_bytes(
|
||||
hash_function: HashFunction,
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
) -> Vec<u8> {
|
||||
@@ -38,9 +78,9 @@ pub fn hash_key_bytes(
|
||||
|
||||
/// attempt to produce digests of the provided key using all known [HashFunction] with a default
|
||||
/// hash length where variable output is available
|
||||
pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests {
|
||||
pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests {
|
||||
use strum::IntoEnumIterator;
|
||||
let mut digests = BTreeMap::new();
|
||||
let mut digests = HashMap::new();
|
||||
for hash in HashFunction::iter() {
|
||||
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
|
||||
}
|
||||
@@ -54,7 +94,7 @@ fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
pub fn validate_encapsulation_key(
|
||||
hash_function: HashFunction,
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
@@ -65,8 +105,20 @@ pub fn validate_encapsulation_key(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn validate_key_bytes(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
) -> bool {
|
||||
compare_hashes(
|
||||
&hash_key_bytes(hash_function, hash_length, key_bytes),
|
||||
expected_hash_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hash_encapsulation_key(
|
||||
hash_function: HashFunction,
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
) -> Vec<u8> {
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::KKTError;
|
||||
use libcrux_psq::handshake::types::PQEncapsulationKey;
|
||||
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::key_utils::produce_key_digests;
|
||||
pub use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey};
|
||||
pub use libcrux_psq::classic_mceliece as mceliece;
|
||||
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
|
||||
|
||||
/// Wrapper around keys used for the KEM exchange
|
||||
/// with cheap clones thanks to Arc wrappers
|
||||
#[derive(Clone)]
|
||||
pub struct KEMKeys {
|
||||
mc_eliece_pk: Arc<mceliece::PublicKey>,
|
||||
mc_eliece_sk: Arc<mceliece::SecretKey>,
|
||||
ml_kem768_pk: Arc<MlKem768PublicKey>,
|
||||
ml_kem768_sk: Arc<MlKem768PrivateKey>,
|
||||
}
|
||||
|
||||
impl Debug for KEMKeys {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("KEMKeys")
|
||||
.field("mc_eliece", &"<redacted>")
|
||||
.field("ml_kem768", &"<redacted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl KEMKeys {
|
||||
pub fn new(mc_eliece: mceliece::KeyPair, ml_kem768: MlKem768KeyPair) -> Self {
|
||||
let (ml_kem768_sk, ml_kem768_pk) = ml_kem768.into_parts();
|
||||
Self {
|
||||
mc_eliece_pk: Arc::new(mc_eliece.pk),
|
||||
mc_eliece_sk: Arc::new(mc_eliece.sk),
|
||||
ml_kem768_pk: Arc::new(ml_kem768_pk),
|
||||
ml_kem768_sk: Arc::new(ml_kem768_sk),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encapsulation_keys_digests(&self) -> BTreeMap<KEM, KEMKeyDigests> {
|
||||
let mut digests = BTreeMap::new();
|
||||
|
||||
let mlkem_digests = produce_key_digests(self.ml_kem768_pk.as_slice());
|
||||
let mceliece_digests = produce_key_digests(self.mc_eliece_pk.as_ref().as_ref());
|
||||
|
||||
digests.insert(KEM::MlKem768, mlkem_digests);
|
||||
digests.insert(KEM::McEliece, mceliece_digests);
|
||||
|
||||
digests
|
||||
}
|
||||
|
||||
pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> {
|
||||
match kem {
|
||||
KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()),
|
||||
KEM::MlKem768 => Some(self.ml_kem768_pk.as_slice()),
|
||||
// _ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encapsulation_key(&self, kem: KEM) -> Option<EncapsulationKey> {
|
||||
match kem {
|
||||
KEM::McEliece => Some(EncapsulationKey::McEliece(self.mc_eliece_pk.clone())),
|
||||
KEM::MlKem768 => Some(EncapsulationKey::MlKem768(self.ml_kem768_pk.clone())),
|
||||
// _ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mc_eliece_encapsulation_key(&self) -> &mceliece::PublicKey {
|
||||
&self.mc_eliece_pk
|
||||
}
|
||||
|
||||
pub fn ml_kem768_encapsulation_key(&self) -> &MlKem768PublicKey {
|
||||
self.ml_kem768_pk.as_ref()
|
||||
}
|
||||
|
||||
pub fn mc_eliece_decapsulation_key(&self) -> &mceliece::SecretKey {
|
||||
&self.mc_eliece_sk
|
||||
}
|
||||
|
||||
pub fn ml_kem768_decapsulation_key(&self) -> &MlKem768PrivateKey {
|
||||
&self.ml_kem768_sk
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum EncapsulationKey {
|
||||
McEliece(Arc<mceliece::PublicKey>),
|
||||
MlKem768(Arc<MlKem768PublicKey>),
|
||||
}
|
||||
|
||||
impl Debug for EncapsulationKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EncapsulationKey::McEliece(_) => write!(f, "EncapsulationKey::McEliece"),
|
||||
EncapsulationKey::MlKem768(_) => write!(f, "EncapsulationKey::MlKem768"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncapsulationKey {
|
||||
pub fn kem(&self) -> KEM {
|
||||
match self {
|
||||
EncapsulationKey::McEliece(_) => KEM::McEliece,
|
||||
EncapsulationKey::MlKem768(_) => KEM::MlKem768,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_pq_encapsulation_key(&self) -> PQEncapsulationKey<'_> {
|
||||
match self {
|
||||
EncapsulationKey::McEliece(pk) => PQEncapsulationKey::CMC(pk),
|
||||
EncapsulationKey::MlKem768(pk) => PQEncapsulationKey::MlKem(pk),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(bytes: Vec<u8>, kem: KEM) -> Result<EncapsulationKey, KKTError> {
|
||||
match kem {
|
||||
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(Arc::new(
|
||||
MlKem768PublicKey::try_from(bytes.as_slice()).map_err(|_| KKTError::KEMError {
|
||||
info: "mlkem768 key of invalid length",
|
||||
})?,
|
||||
))),
|
||||
KEM::McEliece => {
|
||||
let boxed_array: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> =
|
||||
bytes
|
||||
.into_boxed_slice()
|
||||
.try_into()
|
||||
.map_err(|_| KKTError::KEMError {
|
||||
info: "mceliece key of invalid length",
|
||||
})?;
|
||||
|
||||
Ok(EncapsulationKey::McEliece(Arc::new(
|
||||
mceliece::PublicKey::from(boxed_array),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
EncapsulationKey::McEliece(k) => k.as_ref().as_ref(),
|
||||
EncapsulationKey::MlKem768(k) => k.as_ref().as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// storage helpers
|
||||
pub mod storage_wrappers {
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MalformedStoredKeyError {
|
||||
#[error("{typ} stored key has an invalid length")]
|
||||
InvalidKeyLength { typ: &'static str },
|
||||
|
||||
#[error("{typ} stored key is malformed: {message}")]
|
||||
MalformedData { typ: &'static str, message: String },
|
||||
|
||||
#[error("attempted to take ownership of a stored {typ} key representation")]
|
||||
IllegalStoredConversion { typ: &'static str },
|
||||
}
|
||||
|
||||
pub trait StorableKey: Sized {
|
||||
type StorableRepresentation<'a>: PemStorableKey
|
||||
+ From<&'a Self>
|
||||
+ TryInto<Self, Error = MalformedStoredKeyError>
|
||||
+ Sized
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn to_storable(&self) -> Self::StorableRepresentation<'_> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn from_storable(
|
||||
repr: Self::StorableRepresentation<'_>,
|
||||
) -> Result<Self, MalformedStoredKeyError> {
|
||||
repr.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! declare_key_wrappers {
|
||||
($pub_key_type:ty, $private_key_type:ty) => {
|
||||
pub enum StorablePublicKey<'a> {
|
||||
Owned(Box<$pub_key_type>),
|
||||
Borrowed(&'a $pub_key_type),
|
||||
}
|
||||
|
||||
impl AsRef<$pub_key_type> for StorablePublicKey<'_> {
|
||||
fn as_ref(&self) -> &$pub_key_type {
|
||||
match self {
|
||||
StorablePublicKey::Owned(k) => k,
|
||||
StorablePublicKey::Borrowed(k) => k,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum StorablePrivateKey<'a> {
|
||||
Owned(Box<$private_key_type>),
|
||||
Borrowed(&'a $private_key_type),
|
||||
}
|
||||
|
||||
impl AsRef<$private_key_type> for StorablePrivateKey<'_> {
|
||||
fn as_ref(&self) -> &$private_key_type {
|
||||
match self {
|
||||
StorablePrivateKey::Owned(k) => k,
|
||||
StorablePrivateKey::Borrowed(k) => k,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a $pub_key_type> for StorablePublicKey<'a> {
|
||||
fn from(value: &'a $pub_key_type) -> Self {
|
||||
StorablePublicKey::Borrowed(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<StorablePublicKey<'a>> for $pub_key_type {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn try_from(value: StorablePublicKey<'a>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
StorablePublicKey::Owned(value) => Ok(*value),
|
||||
StorablePublicKey::Borrowed(_) => {
|
||||
Err(MalformedStoredKeyError::IllegalStoredConversion {
|
||||
typ: <StorablePublicKey as PemStorableKey>::pem_type(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a $private_key_type> for StorablePrivateKey<'a> {
|
||||
fn from(value: &'a $private_key_type) -> Self {
|
||||
StorablePrivateKey::Borrowed(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<StorablePrivateKey<'a>> for $private_key_type {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn try_from(value: StorablePrivateKey<'a>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
StorablePrivateKey::Owned(value) => Ok(*value),
|
||||
StorablePrivateKey::Borrowed(_) => {
|
||||
Err(MalformedStoredKeyError::IllegalStoredConversion {
|
||||
typ: <StorablePrivateKey as PemStorableKey>::pem_type(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::keys::storage_wrappers::StorableKey for $pub_key_type {
|
||||
type StorableRepresentation<'a> = StorablePublicKey<'a>;
|
||||
}
|
||||
|
||||
impl $crate::keys::storage_wrappers::StorableKey for $private_key_type {
|
||||
type StorableRepresentation<'a> = StorablePrivateKey<'a>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub mod mceliece {
|
||||
use crate::keys::storage_wrappers::MalformedStoredKeyError;
|
||||
use libcrux_psq::classic_mceliece;
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
|
||||
declare_key_wrappers!(classic_mceliece::PublicKey, classic_mceliece::SecretKey);
|
||||
|
||||
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"MCELIECE PRIVATE KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::SECRET_KEY_LENGTH]> =
|
||||
bytes.to_vec().into_boxed_slice().try_into().map_err(|_| {
|
||||
MalformedStoredKeyError::InvalidKeyLength {
|
||||
typ: Self::pem_type(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(StorablePrivateKey::Owned(Box::new(
|
||||
classic_mceliece::SecretKey::from(bytes),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PemStorableKey for StorablePublicKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"MCELIECE PUBLIC KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> =
|
||||
bytes.to_vec().into_boxed_slice().try_into().map_err(|_| {
|
||||
MalformedStoredKeyError::InvalidKeyLength {
|
||||
typ: Self::pem_type(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(StorablePublicKey::Owned(Box::new(
|
||||
classic_mceliece::PublicKey::from(bytes),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mlkem768 {
|
||||
use crate::keys::storage_wrappers::MalformedStoredKeyError;
|
||||
use libcrux_ml_kem::mlkem768::{MlKem768PrivateKey, MlKem768PublicKey};
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
|
||||
declare_key_wrappers!(MlKem768PublicKey, MlKem768PrivateKey);
|
||||
|
||||
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"MLKEM768 PRIVATE KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_slice().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let inner = MlKem768PrivateKey::try_from(bytes).map_err(|message| {
|
||||
MalformedStoredKeyError::MalformedData {
|
||||
typ: Self::pem_type(),
|
||||
message: message.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(StorablePrivateKey::Owned(Box::new(inner)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PemStorableKey for StorablePublicKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"MLKEM768 PUBLIC KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_slice().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let inner = MlKem768PublicKey::try_from(bytes).map_err(|message| {
|
||||
MalformedStoredKeyError::MalformedData {
|
||||
typ: Self::pem_type(),
|
||||
message: message.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(StorablePublicKey::Owned(Box::new(inner)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod x25519 {
|
||||
use crate::keys::storage_wrappers::MalformedStoredKeyError;
|
||||
use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey};
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
|
||||
declare_key_wrappers!(DHPublicKey, DHPrivateKey);
|
||||
|
||||
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"LP X25519 PRIVATE KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes =
|
||||
bytes
|
||||
.try_into()
|
||||
.map_err(|_| MalformedStoredKeyError::InvalidKeyLength {
|
||||
typ: Self::pem_type(),
|
||||
})?;
|
||||
Ok(StorablePrivateKey::Owned(Box::new(
|
||||
DHPrivateKey::from_bytes(&bytes).map_err(|err| {
|
||||
MalformedStoredKeyError::MalformedData {
|
||||
typ: Self::pem_type(),
|
||||
message: format!("{err:?}"),
|
||||
}
|
||||
})?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PemStorableKey for StorablePublicKey<'a> {
|
||||
type Error = MalformedStoredKeyError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"LP X25519 PUBLIC KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.as_ref().as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes =
|
||||
bytes
|
||||
.try_into()
|
||||
.map_err(|_| MalformedStoredKeyError::InvalidKeyLength {
|
||||
typ: Self::pem_type(),
|
||||
})?;
|
||||
Ok(StorablePublicKey::Owned(Box::new(DHPublicKey::from_bytes(
|
||||
&bytes,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Convenience wrappers around KKT protocol functions for easier integration.
|
||||
//!
|
||||
//! This module provides simplified APIs for the common use case of exchanging
|
||||
//! KEM public keys between a client (initiator) and gateway (responder).
|
||||
//!
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode},
|
||||
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
// Re-export core session functions for advanced use cases
|
||||
pub use crate::session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
};
|
||||
|
||||
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
|
||||
use crate::frame::KKTFrame;
|
||||
|
||||
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
|
||||
///
|
||||
/// This is the client-side operation that initiates a KKT exchange.
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - Random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
|
||||
/// * `KKTContext` - Context to use when validating the response
|
||||
/// * `Vec<u8>` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let (session_secret, context, request_frame) = request_kem_key(
|
||||
/// &mut rng,
|
||||
/// ciphersuite,
|
||||
/// client_signing_key,
|
||||
/// responder_dh_public_key,
|
||||
/// )?;
|
||||
/// // Send request_frame to gateway
|
||||
/// ```
|
||||
pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_public_key: &x25519::PublicKey,
|
||||
) -> Result<(KKTSessionSecret, KKTContext, Vec<u8>), KKTError> {
|
||||
// OneWay mode: client only wants responder's KEM key
|
||||
// None: client doesn't send their own KEM key
|
||||
let (initiator_context, initiator_frame) =
|
||||
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?;
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?;
|
||||
|
||||
Ok((session_secret, initiator_context, encrypted_request_bytes))
|
||||
}
|
||||
|
||||
/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key.
|
||||
///
|
||||
/// This is the client-side operation that processes the gateway's response.
|
||||
/// It verifies the signature and validates the key hash against the expected value
|
||||
/// (typically retrieved from a directory service).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context from the initial request
|
||||
/// * `session_secret` - Session Secret Key (generated with request)
|
||||
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
|
||||
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
|
||||
/// * `response_bytes` - Serialized response frame from responder
|
||||
///
|
||||
/// # Returns
|
||||
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
/// &response_bytes,
|
||||
/// )?;
|
||||
/// // Use gateway_kem_key for PSQ
|
||||
/// ```
|
||||
pub fn validate_kem_response<'a>(
|
||||
context: &mut KKTContext,
|
||||
session_secret: &KKTSessionSecret,
|
||||
responder_vk: &ed25519::PublicKey,
|
||||
expected_key_hash: &[u8],
|
||||
encrypted_response_bytes: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
let (responder_frame, responder_context) =
|
||||
decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?;
|
||||
|
||||
initiator_ingest_response(
|
||||
context,
|
||||
&responder_frame,
|
||||
&responder_context,
|
||||
responder_vk,
|
||||
expected_key_hash,
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypts and validates an *Encrypted* KKT response
|
||||
///
|
||||
/// This is the client-side operation that processes the gateway's response.
|
||||
pub fn decrypt_kkt_response_frame(
|
||||
session_secret: &KKTSessionSecret,
|
||||
frame_ciphertext: &[u8],
|
||||
) -> Result<(KKTFrame, KKTContext), KKTError> {
|
||||
decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD)
|
||||
}
|
||||
|
||||
/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key.
|
||||
///
|
||||
/// This is the gateway-side operation that processes a client's KKT request.
|
||||
/// It validates the request signature (if authenticated) and responds with
|
||||
/// the gateway's KEM public key, signed for authenticity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `encrypted_request_bytes` - encrypted KEM request
|
||||
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
|
||||
/// * `responder_signing_key` - Gateway's Ed25519 signing key
|
||||
/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key
|
||||
/// * `responder_kem_key` - Gateway's KEM public key to send
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTFrame` - Signed response frame containing the KEM public key
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let response_frame = handle_kem_request(
|
||||
/// &request_frame,
|
||||
/// Some(client_verification_key), // or None for anonymous
|
||||
/// gateway_signing_key,
|
||||
/// &gateway_kem_public_key,
|
||||
/// )?;
|
||||
/// // Send response_frame back to client
|
||||
/// ```
|
||||
pub fn handle_kem_request<'a, R>(
|
||||
rng: &mut R,
|
||||
encrypted_request_bytes: &[u8],
|
||||
initiator_vk: Option<&ed25519::PublicKey>,
|
||||
responder_signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_private_key: &x25519::PrivateKey,
|
||||
responder_kem_key: &EncapsulationKey<'a>,
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Compute the session's shared secret, decrypt and parse context from the request frame
|
||||
|
||||
let (session_secret, request_frame, initiator_context) =
|
||||
decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?;
|
||||
|
||||
// Validate the request (verifies signature if initiator_vk provided)
|
||||
let (mut response_context, _) = responder_ingest_message(
|
||||
&initiator_context,
|
||||
initiator_vk,
|
||||
None, // Not checking initiator's KEM key in OneWay mode
|
||||
&request_frame,
|
||||
)?;
|
||||
|
||||
// Generate signed response with our KEM public key
|
||||
let responder_frame = responder_process(
|
||||
&mut response_context,
|
||||
request_frame.session_id(),
|
||||
responder_signing_key,
|
||||
responder_kem_key,
|
||||
)?;
|
||||
|
||||
// Encrypt the responder's response with the session's shared secret
|
||||
encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
ciphersuite::{HashFunction, KEM, SignatureScheme},
|
||||
key_utils::{generate_keypair_libcrux, hash_encapsulation_key},
|
||||
};
|
||||
|
||||
fn random_x25519_key() -> x25519::PrivateKey {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = rand::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Generate responder's KEM keypair (X25519 for testing)
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// Create ciphersuite
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Hash the KEM key (simulating directory storage)
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway: Handle request
|
||||
let response_frame_ciphertext = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(ed25519_init.public_key()), // Authenticated
|
||||
ed25519_resp.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
&response_frame_ciphertext,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify we got the correct KEM key
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (mut context, request_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap();
|
||||
|
||||
// Gateway: Handle anonymous request
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&encrypted_request_bytes,
|
||||
None, // Anonymous - no verification key
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
&response_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Different keypair for wrong signature
|
||||
let mut wrong_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut wrong_secret);
|
||||
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_session_key, _context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway handles request but we provide WRONG verification key
|
||||
let result = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(wrong_keypair.public_key()), // Wrong key!
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
);
|
||||
|
||||
// Should fail signature verification
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, mut context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame,
|
||||
Some(initiator_keypair.public_key()),
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&mut context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
&response_frame,
|
||||
);
|
||||
|
||||
// Should fail hash validation
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
+450
-249
@@ -1,297 +1,498 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod carrier;
|
||||
pub mod ciphersuite;
|
||||
pub mod context;
|
||||
pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod initiator;
|
||||
pub mod key_utils;
|
||||
pub mod keys;
|
||||
pub mod masked_byte;
|
||||
pub mod message;
|
||||
pub mod rekey;
|
||||
pub mod responder;
|
||||
// pub mod kkt;
|
||||
pub mod session;
|
||||
|
||||
pub use nym_kkt_context as context;
|
||||
// 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::keys::KEMKeys;
|
||||
use crate::{
|
||||
initiator::KKTInitiator,
|
||||
key_utils::{
|
||||
generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519,
|
||||
hash_encapsulation_key,
|
||||
KKT_RESPONSE_AAD,
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
|
||||
encryption::{
|
||||
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
|
||||
encrypt_kkt_frame,
|
||||
},
|
||||
frame::KKTFrame,
|
||||
key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
|
||||
generate_keypair_x25519, hash_encapsulation_key,
|
||||
},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
responder::KKTResponder,
|
||||
};
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use nym_test_utils::helpers::deterministic_rng_09;
|
||||
use rand09::RngCore;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_one_way_encrypted_carrier() {
|
||||
let mut rng = rand09::rng();
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut payload: Vec<u8> = vec![0u8; 900_000];
|
||||
rng.fill_bytes(&mut payload);
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
// generate kem public keys
|
||||
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let responder_kem = KEMKeys::new(responder_mceliece_keypair, responder_mlkem_keypair);
|
||||
|
||||
let r_dir_hash_mlkem = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
);
|
||||
|
||||
let r_dir_hash_mceliece = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref(),
|
||||
);
|
||||
|
||||
let init_hashes = BTreeMap::new();
|
||||
|
||||
let responder = KKTResponder::new(
|
||||
&responder_x25519_keypair,
|
||||
&responder_kem,
|
||||
&init_hashes,
|
||||
&[
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
],
|
||||
&[SignatureScheme::Ed25519],
|
||||
&[1],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// OneWay - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
let result = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.encapsulation_key.as_bytes(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
// OneWay - McEliece
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_mutual_encrypted_carrier() {
|
||||
let mut rng = deterministic_rng_09();
|
||||
|
||||
let mut payload: Vec<u8> = vec![0u8; 50000];
|
||||
rng.fill_bytes(&mut payload);
|
||||
|
||||
// generate kem public keys
|
||||
let initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let responder_x25519_keypair = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
let initiator_kem = KEMKeys::new(initiator_mceliece_keypair, initiator_mlkem_keypair);
|
||||
let responder_kem = KEMKeys::new(responder_mceliece_keypair, responder_mlkem_keypair);
|
||||
|
||||
let init_hashes = initiator_kem.encapsulation_keys_digests();
|
||||
|
||||
let responder = KKTResponder::new(
|
||||
&responder_x25519_keypair,
|
||||
&responder_kem,
|
||||
&init_hashes,
|
||||
&[
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
],
|
||||
&[SignatureScheme::Ed25519],
|
||||
&[1],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let r_dir_hash_mlkem = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
);
|
||||
|
||||
let r_dir_hash_mceliece = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref(),
|
||||
);
|
||||
|
||||
// Mutual - MlKem
|
||||
{
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::MlKem768,
|
||||
kem,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_mutual_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_kem
|
||||
.encoded_encapsulation_key(KEM::MlKem768)
|
||||
.unwrap(),
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
// generate kem public keys
|
||||
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
assert_eq!(
|
||||
processed_request
|
||||
.remote_encapsulation_key
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
initiator_kem
|
||||
.encapsulation_key(KEM::MlKem768)
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
)
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Mutual - McEliece is not supported due to the key being too large
|
||||
{
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
kem,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_mutual_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_kem
|
||||
.encoded_encapsulation_key(KEM::McEliece)
|
||||
.unwrap(),
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
// generate kem public keys
|
||||
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
assert_eq!(
|
||||
processed_request
|
||||
.remote_encapsulation_key
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
initiator_kem
|
||||
.encapsulation_key(KEM::McEliece)
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref()
|
||||
)
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, r_context) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
|
||||
|
||||
use crate::error::{
|
||||
MaskedByteError,
|
||||
MaskedByteError::{Failure, InvalidLength},
|
||||
};
|
||||
|
||||
pub const MASKED_BYTE_LEN: usize = 16;
|
||||
pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1";
|
||||
|
||||
const U8_RANGE: [u8; 256] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
|
||||
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
|
||||
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
|
||||
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
|
||||
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
|
||||
136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
|
||||
155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
|
||||
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192,
|
||||
193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
|
||||
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
|
||||
231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
|
||||
250, 251, 252, 253, 254, 255,
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MaskedByte([u8; MASKED_BYTE_LEN]);
|
||||
|
||||
impl MaskedByte {
|
||||
/// Mask a byte by hashing it with some mask.
|
||||
/// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte)
|
||||
pub fn new(byte: u8, mask: &[u8]) -> Self {
|
||||
let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(MASKED_BYTE_CONTEXT_STR);
|
||||
hasher.update(mask);
|
||||
// avoid zero update
|
||||
hasher.update(&[0xFF, byte]);
|
||||
hasher.finalize_xof_into(&mut output);
|
||||
|
||||
Self(output)
|
||||
}
|
||||
/// Unmasks a byte by trial hashing.
|
||||
/// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF).
|
||||
/// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max).
|
||||
/// If we find an `i` which yields back the hash input, then we found the masked byte.
|
||||
/// Otherwise, the function returns an error.
|
||||
pub fn unmask(&self, mask: &[u8]) -> Result<u8, MaskedByteError> {
|
||||
self.unmask_check_version(mask, &U8_RANGE)
|
||||
}
|
||||
|
||||
// This could be more efficient than unmask,
|
||||
// because we just could check against a smaller list of supported versions.
|
||||
pub fn unmask_check_version(
|
||||
&self,
|
||||
mask: &[u8],
|
||||
supported_versions: &[u8],
|
||||
) -> Result<u8, MaskedByteError> {
|
||||
let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(MASKED_BYTE_CONTEXT_STR);
|
||||
hasher.update(mask);
|
||||
// avoid zero update
|
||||
hasher.update(&[0xFF]);
|
||||
for i in supported_versions {
|
||||
let mut t_hasher = hasher.clone();
|
||||
t_hasher.update(&[*i]);
|
||||
t_hasher.finalize_xof_into(&mut buf);
|
||||
if buf == self.0 {
|
||||
return Ok(*i);
|
||||
}
|
||||
}
|
||||
Err(Failure)
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; MASKED_BYTE_LEN] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte {
|
||||
fn from(value: [u8; MASKED_BYTE_LEN]) -> Self {
|
||||
MaskedByte(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte {
|
||||
fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self {
|
||||
MaskedByte(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for MaskedByte {
|
||||
type Error = MaskedByteError;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
let Ok(inner) = value.try_into() else {
|
||||
return Err(InvalidLength {
|
||||
expected: MASKED_BYTE_LEN,
|
||||
actual: value.len(),
|
||||
});
|
||||
};
|
||||
Ok(MaskedByte(inner))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::masked_byte::MASKED_BYTE_LEN;
|
||||
|
||||
use super::MaskedByte;
|
||||
use rand09::{Rng, RngCore, rng};
|
||||
|
||||
#[test]
|
||||
fn test_masking() {
|
||||
let mut mask: [u8; 256] = [0u8; 256];
|
||||
let mut wire_bytes: [u8; MASKED_BYTE_LEN];
|
||||
|
||||
// why not
|
||||
for i in 0..=u8::MAX {
|
||||
// gen mask
|
||||
rng().fill_bytes(&mut mask);
|
||||
let masked_byte = MaskedByte::new(i, &mask);
|
||||
wire_bytes = masked_byte.to_bytes();
|
||||
|
||||
let decoded_masked_byte = MaskedByte::from(wire_bytes);
|
||||
let output = decoded_masked_byte.unmask(&mask).unwrap();
|
||||
|
||||
assert_eq!(i, output);
|
||||
|
||||
// flip bit
|
||||
let mut with_flipped_bit = decoded_masked_byte.to_bytes();
|
||||
|
||||
let byte_idx: usize = rng().random_range(0..MASKED_BYTE_LEN);
|
||||
let bit_idx = rng().random_range(0..8);
|
||||
with_flipped_bit[byte_idx] ^= 1 << bit_idx;
|
||||
|
||||
let decoded_masked_byte = MaskedByte::from(with_flipped_bit);
|
||||
assert!(decoded_masked_byte.unmask(&mask).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoding() {
|
||||
let mut mask: [u8; 256] = [0u8; 256];
|
||||
|
||||
// gen mask
|
||||
rng().fill_bytes(&mut mask);
|
||||
let byte = rng().random();
|
||||
let masked_byte = MaskedByte::new(byte, &mask);
|
||||
let wire_bytes: [u8; MASKED_BYTE_LEN] = masked_byte.to_bytes();
|
||||
|
||||
// should succeed
|
||||
let decoded_masked_byte = MaskedByte::try_from(wire_bytes.as_slice()).unwrap();
|
||||
let output = decoded_masked_byte.unmask(&mask).unwrap();
|
||||
|
||||
assert_eq!(byte, output);
|
||||
|
||||
let empty_slice: &[u8] = &[];
|
||||
// should fail
|
||||
assert!(MaskedByte::try_from(empty_slice).is_err());
|
||||
|
||||
let mut wire_bytes_messy = Vec::from(wire_bytes);
|
||||
|
||||
// add more one more byte
|
||||
wire_bytes_messy.push(0x42);
|
||||
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN + 1);
|
||||
// should fail
|
||||
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
|
||||
|
||||
// pop the added byte
|
||||
_ = wire_bytes_messy.pop();
|
||||
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN);
|
||||
// should succeed
|
||||
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_ok());
|
||||
|
||||
// pop one more byte
|
||||
_ = wire_bytes_messy.pop();
|
||||
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN - 1);
|
||||
// should fail
|
||||
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::carrier::Carrier;
|
||||
use crate::context::{KKTContext, KKTMode, KKTRole};
|
||||
use crate::error::KKTError;
|
||||
use crate::frame::KKTFrame;
|
||||
use crate::keys::EncapsulationKey;
|
||||
use crate::masked_byte::{MASKED_BYTE_LEN, MaskedByte};
|
||||
use libcrux_chacha20poly1305::TAG_LEN;
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
|
||||
use nym_kkt_ciphersuite::{KEM, x25519};
|
||||
|
||||
pub struct KKTRequest {
|
||||
/// The plaintext part of the request
|
||||
pub(crate) plaintext: KKTRequestPlaintext,
|
||||
|
||||
/// Ciphertext of an initial request `KKTFrame`
|
||||
pub(crate) encrypted_frame: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KKTRequest {
|
||||
// the size of KKTRequest is the plaintext data followed by the frame and the encryption tag
|
||||
pub const fn size_excluding_payload(mode: KKTMode, kem: KEM) -> usize {
|
||||
KKTRequestPlaintext::SIZE
|
||||
+ KKTFrame::size_excluding_payload(KKTRole::Initiator, mode, kem)
|
||||
+ TAG_LEN
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.encrypted_frame.len() + KKTRequestPlaintext::SIZE
|
||||
}
|
||||
|
||||
pub fn into_bytes(mut self) -> Vec<u8> {
|
||||
let mut out = self.plaintext.to_bytes();
|
||||
out.append(&mut self.encrypted_frame);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
|
||||
if b.len() < x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: "the KKTRequest frame has invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
let plaintext =
|
||||
KKTRequestPlaintext::try_from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN])?;
|
||||
|
||||
Ok(KKTRequest {
|
||||
plaintext,
|
||||
encrypted_frame: b[x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN..].to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KKTRequestPlaintext {
|
||||
/// Ephemeral Diffie-Hellman public key of the initiator
|
||||
pub(crate) dh_pubkey: DHPublicKey,
|
||||
|
||||
/// Masked bytes representing the outer protocol version information
|
||||
pub(crate) masked_version_bytes: MaskedByte,
|
||||
}
|
||||
|
||||
impl KKTRequestPlaintext {
|
||||
pub const SIZE: usize = x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN;
|
||||
|
||||
pub(crate) fn new(
|
||||
initiator_pubkey: DHPublicKey,
|
||||
responder_pubkey: &DHPublicKey,
|
||||
outer_protocol_version: u8,
|
||||
) -> Self {
|
||||
let mask = Self::create_version_mask(&initiator_pubkey, responder_pubkey);
|
||||
let masked_version_bytes = MaskedByte::new(outer_protocol_version, &mask);
|
||||
KKTRequestPlaintext {
|
||||
dh_pubkey: initiator_pubkey,
|
||||
masked_version_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_request(
|
||||
self,
|
||||
carrier: &mut Carrier,
|
||||
frame: KKTFrame,
|
||||
) -> Result<KKTRequest, KKTError> {
|
||||
let frame_bytes = frame.try_to_bytes()?;
|
||||
let frame_ciphertext = carrier.encrypt(&frame_bytes)?;
|
||||
Ok(KKTRequest {
|
||||
plaintext: self,
|
||||
encrypted_frame: frame_ciphertext,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn create_version_mask(
|
||||
initiator_pubkey: &DHPublicKey,
|
||||
responder_pubkey: &DHPublicKey,
|
||||
) -> Vec<u8> {
|
||||
let mut mask = Vec::with_capacity(2 * x25519::PUBLIC_KEY_LENGTH);
|
||||
mask.extend_from_slice(initiator_pubkey.as_ref());
|
||||
mask.extend_from_slice(responder_pubkey.as_ref());
|
||||
mask
|
||||
}
|
||||
|
||||
fn create_carrier_ctx(
|
||||
masked_version: &MaskedByte,
|
||||
initiator_pubkey: &DHPublicKey,
|
||||
responder_pubkey: &DHPublicKey,
|
||||
) -> Vec<u8> {
|
||||
let mut context = Vec::new();
|
||||
context.extend_from_slice(masked_version.as_slice());
|
||||
context.extend_from_slice(crate::frame::KKT_CARRIER_CONTEXT);
|
||||
context.extend_from_slice(initiator_pubkey.as_ref());
|
||||
context.extend_from_slice(responder_pubkey.as_ref());
|
||||
context
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(Self::SIZE);
|
||||
out.extend_from_slice(self.dh_pubkey.as_ref());
|
||||
out.extend_from_slice(self.masked_version_bytes.as_slice());
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
|
||||
if b.len() != Self::SIZE {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: "the KKTRequest frame has invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
// SAFETY: we're using exactly 32 byte
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let dh_pubkey =
|
||||
DHPublicKey::from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH].try_into().unwrap());
|
||||
let masked_version_bytes = MaskedByte::try_from(&b[x25519::PUBLIC_KEY_LENGTH..])?;
|
||||
|
||||
Ok(KKTRequestPlaintext {
|
||||
dh_pubkey,
|
||||
masked_version_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn version_mask(&self, responder_pubkey: &DHPublicKey) -> Vec<u8> {
|
||||
Self::create_version_mask(&self.dh_pubkey, responder_pubkey)
|
||||
}
|
||||
|
||||
pub(crate) fn derive_initiator_carrier(
|
||||
&self,
|
||||
initiator_sk: &DHPrivateKey,
|
||||
responder_pubkey: &DHPublicKey,
|
||||
) -> Result<Carrier, KKTError> {
|
||||
let ctx = Self::create_carrier_ctx(
|
||||
&self.masked_version_bytes,
|
||||
&self.dh_pubkey,
|
||||
responder_pubkey,
|
||||
);
|
||||
|
||||
let shared_secret = initiator_sk
|
||||
.diffie_hellman(responder_pubkey)
|
||||
.map_err(KKTError::shared_secret_derivation_failure)?;
|
||||
|
||||
Ok(Carrier::from_secret_slice(
|
||||
shared_secret.as_ref(),
|
||||
&ctx,
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn derive_responder_carrier(
|
||||
&self,
|
||||
responder_keys: &DHKeyPair,
|
||||
) -> Result<Carrier, KKTError> {
|
||||
let ctx = Self::create_carrier_ctx(
|
||||
&self.masked_version_bytes,
|
||||
&self.dh_pubkey,
|
||||
&responder_keys.pk,
|
||||
);
|
||||
let shared_secret = responder_keys
|
||||
.sk()
|
||||
.diffie_hellman(&self.dh_pubkey)
|
||||
.map_err(KKTError::shared_secret_derivation_failure)?;
|
||||
Ok(Carrier::from_secret_slice(
|
||||
shared_secret.as_ref(),
|
||||
&ctx,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KKTRequestEncryptionResult {
|
||||
/// Derived carrier used for decrypting this frame and encrypting the response
|
||||
pub(crate) carrier: Carrier,
|
||||
|
||||
/// The underlying request that is going to get sent to the remote
|
||||
pub(crate) request: KKTRequest,
|
||||
}
|
||||
|
||||
pub struct DecryptedRequestFrame {
|
||||
/// Derived carrier used for decrypting this frame and encrypting the response
|
||||
pub(crate) carrier: Carrier,
|
||||
|
||||
/// The remote frame sent in the message
|
||||
pub(crate) remote_frame: KKTFrame,
|
||||
|
||||
/// The unmasked byte representing the outer protocol version sent by the initiator
|
||||
pub(crate) outer_protocol_version: u8,
|
||||
}
|
||||
|
||||
impl DecryptedRequestFrame {
|
||||
pub(crate) fn remote_context(&self) -> &KKTContext {
|
||||
self.remote_frame.context()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessedKKTRequest {
|
||||
pub response: KKTResponse,
|
||||
|
||||
/// The obtained encapsulation key of the remote
|
||||
pub remote_encapsulation_key: Option<EncapsulationKey>,
|
||||
|
||||
/// The KEM key requested in the original request
|
||||
pub requested_kem: KEM,
|
||||
|
||||
/// The unmasked byte representing the outer protocol version sent by the initiator
|
||||
pub outer_protocol_version: u8,
|
||||
|
||||
// Request payload data (Could be empty. Contents are unrelated to current KKT execution).
|
||||
pub request_payload: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct KKTResponse {
|
||||
/// Encrypted KKT frame that is going to be sent back to the initiator
|
||||
pub encrypted_frame: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KKTResponse {
|
||||
// the size of KKTRequest is the plaintext data followed by the frame and the encryption tag
|
||||
pub const fn size_excluding_payload(kem: KEM) -> usize {
|
||||
// `KKTMode` argument makes no difference for the Responder role
|
||||
KKTFrame::size_excluding_payload(KKTRole::Responder, KKTMode::OneWay, kem) + TAG_LEN
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.encrypted_frame.len()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> KKTResponse {
|
||||
KKTResponse {
|
||||
encrypted_frame: bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.encrypted_frame
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessedKKTResponse {
|
||||
/// The obtained encapsulation key of the remote
|
||||
pub encapsulation_key: EncapsulationKey,
|
||||
|
||||
/// Indicates whether responder was able to verify the initiator's kem key,
|
||||
pub verified_initiator_kem_key: bool,
|
||||
|
||||
/// Optional response payload (Could be empty. Contents are unrelated to current KKT execution).
|
||||
pub response_payload: Vec<u8>,
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Post-Quantum Re-Key Protocol
|
||||
|
||||
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
|
||||
/// We currently support MlKem768.
|
||||
///
|
||||
/// This protocol is safe if it runs under a trusted secure channel.
|
||||
///
|
||||
/// Bandwidth costs:
|
||||
/// Request (MlKem768): 1216 bytes
|
||||
/// Response (MlKem768): 1088 bytes
|
||||
use libcrux_kem::*;
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
/// Context string to be used with the Blake3 KDF.
|
||||
const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1";
|
||||
|
||||
pub struct RekeyInitiator {
|
||||
algorithm: Algorithm,
|
||||
decapsulation_key: PrivateKey,
|
||||
salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl Debug for RekeyInitiator {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let key_typ = match self.decapsulation_key {
|
||||
PrivateKey::X25519(_) => "x25519",
|
||||
PrivateKey::P256(_) => "p256",
|
||||
PrivateKey::MlKem512(_) => "ml512",
|
||||
PrivateKey::MlKem768(_) => "mlkem768",
|
||||
PrivateKey::X25519MlKem768Draft00(_) => "x25519-mlkem768",
|
||||
PrivateKey::XWingKemDraft06(_) => "xwing",
|
||||
PrivateKey::MlKem1024(_) => "ml1024",
|
||||
};
|
||||
|
||||
f.debug_struct("RekeyInitiator")
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("decapsulation_key", &key_typ)
|
||||
.field("salt", &self.salt)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RekeyInitiator {
|
||||
/// The Initiator generates an ephemeral KEM keypair and a 32-byte salt.
|
||||
/// The Initiator keeps the decapsulation key and generates a request message.
|
||||
/// The request message contains the salt and an encoding of the encapsulation key as follows
|
||||
/// salt encapsulation_key
|
||||
/// [0 ........ 32 | 32 .............. ]
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// kem: a KEM algorithm (we currently support MlKem768 only)
|
||||
///
|
||||
/// Outputs:
|
||||
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
|
||||
/// Vec<u8>: The request message as explained above. This is to be sent to the responder as-is.
|
||||
pub fn generate_request<R>(rng: &mut R, kem: KEM) -> Result<(RekeyInitiator, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let (algorithm, buffer_size) = match kem {
|
||||
// KEM::XWing => (Algorithm::XWingKemDraft06, 32 + xwing::PUBLIC_KEY_LENGTH),
|
||||
KEM::MlKem768 => (Algorithm::MlKem768, 32 + ml_kem768::PUBLIC_KEY_LENGTH),
|
||||
// We don't support McEliece because the keys are massive.
|
||||
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
|
||||
KEM::McEliece => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the Initiator's salt
|
||||
let mut salt = [0u8; 32];
|
||||
rng.fill_bytes(&mut salt);
|
||||
|
||||
// Create the buffer for the request message and copy the salt into it.
|
||||
let mut request_buffer = Vec::with_capacity(buffer_size);
|
||||
request_buffer.extend_from_slice(&salt);
|
||||
|
||||
// Generate the ephemeral KEM keypair based on the algorithm from the function's input.
|
||||
let (decapsulation_key, encapsulation_key) = key_gen(algorithm, rng)?;
|
||||
|
||||
// Append the encoding of the KEM encapsulation key to the initiator's randomness.
|
||||
request_buffer.extend(encapsulation_key.encode());
|
||||
|
||||
Ok((
|
||||
// The Initiator should store this until they use `RekeyInitiator::finalize`.
|
||||
RekeyInitiator {
|
||||
algorithm,
|
||||
decapsulation_key,
|
||||
salt,
|
||||
},
|
||||
// This is to be sent to the responder.
|
||||
request_buffer,
|
||||
))
|
||||
}
|
||||
|
||||
/// The Initiator will attempt to decapsulate the `pre_key` generated by the responder
|
||||
/// secret. This `pre_key` will be combined with the Initiator's previously generated salt
|
||||
/// as input to a Blake3 KDF call to generate the new shared secret.
|
||||
///
|
||||
/// This function fails if the ciphertext cannot be decoded or decapsulated.
|
||||
///
|
||||
/// Input:
|
||||
/// response_message: the responder's message which contains an encapsulation of `pre_key`.
|
||||
/// Output:
|
||||
/// [u8; 32]: the new shared secret.
|
||||
pub fn finalize(mut self, response_message: &[u8]) -> Result<[u8; 32], KKTError> {
|
||||
// Decode the responder's ciphertext.
|
||||
let ciphertext = Ct::decode(self.algorithm, response_message)?;
|
||||
// Decapsulate the `pre_key` using the Initiator's decapsulation key.
|
||||
let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?;
|
||||
|
||||
// Encode the `pre_key` into bytes
|
||||
let pre_key_bytes = pre_key.encode();
|
||||
|
||||
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &self.salt);
|
||||
|
||||
// Zeroize the Initiator's salt
|
||||
self.salt.zeroize();
|
||||
|
||||
// TODO: zeroize the decapsulation key
|
||||
|
||||
Ok(new_secret)
|
||||
}
|
||||
}
|
||||
|
||||
/// The responder parses the request message.
|
||||
/// The first 32 bytes are the Initiator's salt,
|
||||
/// and the remainder is the encoding of the public key.
|
||||
/// Given that XWing and MlKem768 have different key lengths,
|
||||
/// we could deduce the algorithm from that.
|
||||
///
|
||||
/// If the message is badly formatted, or the encapsulation received is invalid,
|
||||
/// this function will produce an error.
|
||||
///
|
||||
/// If everything is alright, the responder generates and encapsulates a key `pre_key` to send to the Initiator.
|
||||
/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's salt to obtain
|
||||
/// the new shared secret.
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// request_message: the Initiator's request message (contains the salt and encapsulation key)
|
||||
///
|
||||
/// Outputs:
|
||||
/// [u8; 32]: new shared secret
|
||||
/// Vec<u8>: response which contains an encapsulation of a secret value generated by the responder.
|
||||
/// This is to be sent back to the Initiator as-is.
|
||||
pub fn responder_process<R>(
|
||||
rng: &mut R,
|
||||
mut request_message: Vec<u8>,
|
||||
) -> Result<([u8; 32], Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
// Deduce the KEM algorithm from the message length
|
||||
let algorithm = match request_message.len().checked_sub(32) {
|
||||
//
|
||||
Some(num) => match num {
|
||||
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
|
||||
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
|
||||
// If message length is 1248 (32 + 1216) then the algorithm should be xwing
|
||||
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
|
||||
// We don't support McEliece because the keys are massive.
|
||||
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
|
||||
mceliece::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// We don't support X25519 because it's not post-quantum secure.
|
||||
x25519::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// Reject if the size does not match any of the above.
|
||||
_ => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "Unknown Algorithm",
|
||||
});
|
||||
}
|
||||
},
|
||||
// Reject if message length is less than 32.
|
||||
None => {
|
||||
return Err(KKTError::DecodingError {
|
||||
info: "Invalid rekey request: size is too small",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Split the message to get the Initiator's salt (first 32 bytes)
|
||||
// and the encoding of the Initiator's public key.
|
||||
let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32);
|
||||
|
||||
// Attempt to decode the Initiator's encapsulation key.
|
||||
let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?;
|
||||
|
||||
// Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`.
|
||||
let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?;
|
||||
// Encode the ciphertext into bytes to send back to the initiator.
|
||||
let message = ciphertext.encode();
|
||||
|
||||
// Encode the `pre_key` into bytes
|
||||
let pre_key_bytes = pre_key.encode();
|
||||
|
||||
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, remote_salt);
|
||||
|
||||
// Zeroize the Initiator's salt
|
||||
remote_salt.zeroize();
|
||||
|
||||
Ok((new_secret, message))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::KKTError;
|
||||
use crate::rekey::{RekeyInitiator, responder_process};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
|
||||
#[test]
|
||||
fn rekey_test() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let (rekey_state, request_message) =
|
||||
RekeyInitiator::generate_request(&mut rng, KEM::MlKem768).unwrap();
|
||||
|
||||
let (responder_secret, response_message) =
|
||||
responder_process(&mut rng, request_message).unwrap();
|
||||
|
||||
let initiator_secret = rekey_state.finalize(&response_message).unwrap();
|
||||
|
||||
assert_eq!(initiator_secret, responder_secret);
|
||||
|
||||
// mceliece should fail
|
||||
let err = RekeyInitiator::generate_request(&mut rng, KEM::McEliece).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
}
|
||||
.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::key_utils::validate_encapsulation_key;
|
||||
use crate::keys::{EncapsulationKey, KEMKeys};
|
||||
use crate::message::{KKTRequest, KKTResponse, ProcessedKKTRequest};
|
||||
use crate::{
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::KKTFrame,
|
||||
};
|
||||
use libcrux_psq::handshake::types::DHKeyPair;
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, KEMKeyDigests, SignatureScheme};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Representation of a KKT Responder
|
||||
pub struct KKTResponder<'a> {
|
||||
/// Long-term x25519 DH key pair of this Responder
|
||||
x25519_keypair: &'a DHKeyPair,
|
||||
|
||||
/// KEM keys of this responder
|
||||
kem_keys: &'a KEMKeys,
|
||||
|
||||
/// Digests of the initiator's kem key
|
||||
expected_initiator_kem_digests: &'a BTreeMap<KEM, KEMKeyDigests>,
|
||||
|
||||
/// List of supported Hash Functions by this Responder
|
||||
supported_hash_functions: Vec<HashFunction>,
|
||||
|
||||
/// List of supported Signature Schemes by this Responder
|
||||
supported_signature_schemes: Vec<SignatureScheme>,
|
||||
|
||||
/// List of supported outer (LP) protocol version by this Responder
|
||||
supported_outer_protocol_versions: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> KKTResponder<'a> {
|
||||
pub fn new(
|
||||
x25519_keypair: &'a DHKeyPair,
|
||||
kem_keys: &'a KEMKeys,
|
||||
expected_initiator_kem_digests: &'a BTreeMap<KEM, KEMKeyDigests>,
|
||||
supported_hash_functions: &[HashFunction],
|
||||
supported_signature_schemes: &[SignatureScheme],
|
||||
supported_outer_protocol_versions: &[u8],
|
||||
) -> Result<Self, KKTError> {
|
||||
if supported_hash_functions.is_empty() {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "Did not provide a supported HashFunction when instantiating a KKTResponder",
|
||||
});
|
||||
}
|
||||
|
||||
if supported_signature_schemes.is_empty() {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "Did not provide a supported SignatureScheme when instantiating a KKTResponder",
|
||||
});
|
||||
}
|
||||
|
||||
if supported_outer_protocol_versions.is_empty() {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "Did not provide a supported outer protocol version when instantiating a KKTResponder",
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
x25519_keypair,
|
||||
kem_keys,
|
||||
expected_initiator_kem_digests,
|
||||
supported_hash_functions: supported_hash_functions.to_vec(),
|
||||
supported_signature_schemes: supported_signature_schemes.to_vec(),
|
||||
supported_outer_protocol_versions: supported_outer_protocol_versions.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to retrieve expected KEM key hash of the initiator based on the received `Ciphersuite`
|
||||
pub(crate) fn expected_initiator_kem_digest(
|
||||
&self,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<&Vec<u8>, KKTError> {
|
||||
let kem = ciphersuite.kem();
|
||||
let hash_function = ciphersuite.hash_function();
|
||||
|
||||
self.expected_initiator_kem_digests
|
||||
.get(&kem)
|
||||
.ok_or(KKTError::NoKnownKEMKeyDigests { kem, hash_function })?
|
||||
.get(&hash_function)
|
||||
.ok_or(KKTError::NoKnownKEMKeyDigests { kem, hash_function })
|
||||
}
|
||||
|
||||
fn check_ciphersuite_compatiblity(
|
||||
&self,
|
||||
remote_ciphersuite: Ciphersuite,
|
||||
) -> Result<(), KKTError> {
|
||||
let r_hash = remote_ciphersuite.hash_function();
|
||||
let r_sig = remote_ciphersuite.signature_scheme();
|
||||
|
||||
if !self.supported_hash_functions.contains(&r_hash) {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported HashFunction",
|
||||
});
|
||||
}
|
||||
|
||||
if !self.supported_signature_schemes.contains(&r_sig) {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported SignatureScheme",
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// When this function fails, we do that silently (i.e. we don't generate a response to the initiator).
|
||||
|
||||
pub fn process_request(
|
||||
&self,
|
||||
request: KKTRequest,
|
||||
request_payload_len: usize,
|
||||
) -> Result<ProcessedKKTRequest, KKTError> {
|
||||
let processed_req = KKTFrame::decrypt_initiator_frame(
|
||||
self.x25519_keypair,
|
||||
request,
|
||||
&self.supported_outer_protocol_versions,
|
||||
request_payload_len,
|
||||
)?;
|
||||
|
||||
let remote_context = *processed_req.remote_context();
|
||||
|
||||
let remote_frame = processed_req.remote_frame;
|
||||
let request_payload = remote_frame.payload().to_vec();
|
||||
let mut carrier = processed_req.carrier;
|
||||
|
||||
self.check_ciphersuite_compatiblity(remote_context.ciphersuite())?;
|
||||
|
||||
let (local_context, remote_encapsulation_key) = match remote_context.mode() {
|
||||
KKTMode::OneWay => responder_ingest_message(None, remote_frame)?,
|
||||
KKTMode::Mutual => {
|
||||
let digest = self.expected_initiator_kem_digest(remote_context.ciphersuite())?;
|
||||
responder_ingest_message(Some(digest), remote_frame)?
|
||||
}
|
||||
};
|
||||
|
||||
let kem = local_context.ciphersuite().kem();
|
||||
let Some(kem_key) = self.kem_keys.encoded_encapsulation_key(kem) else {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported KEM",
|
||||
});
|
||||
};
|
||||
|
||||
// for now the response payload is empty
|
||||
let response_payload = Vec::new();
|
||||
|
||||
let frame = KKTFrame::new(local_context, kem_key, response_payload);
|
||||
|
||||
// encryption - responder frame
|
||||
let encrypted_frame = carrier.encrypt(&frame.try_to_bytes()?)?;
|
||||
Ok(ProcessedKKTRequest {
|
||||
response: KKTResponse { encrypted_frame },
|
||||
remote_encapsulation_key,
|
||||
requested_kem: remote_context.ciphersuite().kem(),
|
||||
outer_protocol_version: processed_req.outer_protocol_version,
|
||||
request_payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_ingest_message(
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey>), KKTError> {
|
||||
let remote_context = remote_frame.context();
|
||||
let mut own_context = remote_context.derive_responder_header()?;
|
||||
let cs = own_context.ciphersuite();
|
||||
|
||||
match remote_context.role() {
|
||||
KKTRole::Initiator => {
|
||||
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
|
||||
match own_context.mode() {
|
||||
KKTMode::OneWay => Ok((own_context, None)),
|
||||
KKTMode::Mutual => {
|
||||
let Some(expected_hash) = expected_hash else {
|
||||
own_context.update_status(KKTStatus::UnverifiedKEMKey);
|
||||
// we don't store an unverified key
|
||||
// changing the status notifies the initiator that we didn't
|
||||
return Ok((own_context, None));
|
||||
};
|
||||
|
||||
if !validate_encapsulation_key(
|
||||
cs.hash_function(),
|
||||
cs.hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
// The key does not match the hash obtained from the directory
|
||||
return Err(KKTError::MismatchedKEMHash);
|
||||
}
|
||||
let remote_key =
|
||||
EncapsulationKey::try_from_bytes(remote_frame.body(), cs.kem())?;
|
||||
Ok((own_context, Some(remote_key)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::{KKT_SESSION_ID_LEN, KKTFrame},
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
pub fn initiator_process<'a, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
|
||||
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0; KKT_SESSION_ID_LEN];
|
||||
// Generate Session ID
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => &encaps_key.encode(),
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(context.full_message_len() - context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&context_bytes);
|
||||
bytes_to_sign.extend_from_slice(body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok((
|
||||
context,
|
||||
KKTFrame::new(context_bytes, body, session_id, &signature),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn anonymous_initiator_process<R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?;
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[])))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
check_compatibility(own_context, remote_context)?;
|
||||
match remote_context.status() {
|
||||
KKTStatus::Ok => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
remote_context.full_message_len() - remote_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
let received_encapsulation_key = EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
remote_frame.body_ref(),
|
||||
)?;
|
||||
|
||||
match validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => Ok(received_encapsulation_key),
|
||||
|
||||
// The key does not match the hash obtained from the directory
|
||||
false => Err(KKTError::KEMError {
|
||||
info: "Hash of received encapsulation key does not match the value stored on the directory.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
_ => Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// todo: figure out how to handle errors using status codes
|
||||
|
||||
pub fn responder_ingest_message<'a>(
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: Option<&ed25519::PublicKey>,
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: &KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> {
|
||||
let own_context = remote_context.derive_responder_header()?;
|
||||
|
||||
match remote_context.role() {
|
||||
KKTRole::AnonymousInitiator => Ok((own_context, None)),
|
||||
|
||||
KKTRole::Initiator => {
|
||||
match remote_verification_key {
|
||||
Some(remote_verif_key) => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
own_context.full_message_len() - own_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.context_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
|
||||
match own_context.mode() {
|
||||
KKTMode::OneWay => Ok((own_context, None)),
|
||||
KKTMode::Mutual => {
|
||||
match expected_hash {
|
||||
Some(expected_hash) => {
|
||||
let received_encapsulation_key =
|
||||
EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
remote_frame.body_ref(),
|
||||
)?;
|
||||
if validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
Ok((
|
||||
own_context,
|
||||
Some(received_encapsulation_key),
|
||||
))
|
||||
}
|
||||
// The key does not match the hash obtained from the directory
|
||||
else {
|
||||
Err(KKTError::KEMError {
|
||||
info: "Hash of received encapsulation key does not match the value stored on the directory.",
|
||||
})
|
||||
}
|
||||
}
|
||||
None => Err(KKTError::FunctionInputError {
|
||||
info: "Expected hash of the remote encapsulation key is not provided.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
None => Err(KKTError::FunctionInputError {
|
||||
info: "Remote Signature Verification Key Not Provided",
|
||||
}),
|
||||
}
|
||||
}
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
session_id: KKTSessionId,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
) -> Result<KKTFrame, KKTError> {
|
||||
let body = encapsulation_key.encode();
|
||||
|
||||
let context_bytes = own_context.encode()?;
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&own_context.encode()?);
|
||||
bytes_to_sign.extend_from_slice(&body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok(KKTFrame::new(context_bytes, &body, session_id, &signature))
|
||||
}
|
||||
|
||||
fn check_compatibility(
|
||||
_own_context: &KKTContext,
|
||||
_remote_context: &KKTContext,
|
||||
) -> Result<(), KKTError> {
|
||||
// todo: check ciphersuite/context compatibility
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user