Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1ba5acd72 | |||
| b7b238584d | |||
| 7ed5b5477b | |||
| 09506683ce | |||
| 98f1480ae8 | |||
| 05b6f5e282 | |||
| 5093450004 | |||
| f6bd511599 | |||
| e5c3f39a57 | |||
| 76f999fc88 | |||
| 2fce8c7ca3 | |||
| 468bd8b5d1 | |||
| 45022b1671 | |||
| 3b3c5beae4 | |||
| 650917e216 | |||
| c02adaa019 | |||
| d01c34263a | |||
| f247e028f2 | |||
| 20fe8dd028 | |||
| 89edabf796 | |||
| bf5352906f | |||
| 8eb9999876 | |||
| c0f582b336 | |||
| 133a855e01 | |||
| 98149dde87 | |||
| 5e733a5ebf | |||
| 5647ae6a41 | |||
| 4ed9d8fb7a | |||
| a2081af603 | |||
| 5b62fd76ba | |||
| 77a34fe3bf | |||
| 630c4922ac | |||
| 6edbece3ad | |||
| 8529a3c351 | |||
| 453e1cbe70 | |||
| 94a3599b4d | |||
| a6bc54461a | |||
| 4f0c40dab7 | |||
| 3eff6e5e3b | |||
| a519f4ccb8 | |||
| a3ba3bfc5a | |||
| 988df7cff7 | |||
| 260f8e9714 | |||
| d28d0ac39e | |||
| dce4d6b34b | |||
| bc47e9a1b2 | |||
| 3b693741b2 | |||
| cb277fe487 | |||
| 8bb29f4d07 | |||
| e753f24ed1 | |||
| c7cd962627 | |||
| 00467e4440 | |||
| f3d1000472 | |||
| 597aae1a20 | |||
| 40a3cd28b7 | |||
| a4950485d1 |
@@ -6,6 +6,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
env:
|
||||
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
|
||||
defaults:
|
||||
run:
|
||||
working-directory: documentation/docs
|
||||
@@ -41,6 +43,8 @@ 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
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: ci-build-vpn-api-wasm
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'common/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- '.github/workflows/ci-build-vpn-api-wasm.yml'
|
||||
|
||||
jobs:
|
||||
wasm:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '116'
|
||||
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli
|
||||
|
||||
- name: "Build"
|
||||
run: make
|
||||
working-directory: nym-credential-proxy/vpn-api-lib-wasm
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -15,6 +15,9 @@ env:
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-linux-latest
|
||||
timeout-minutes: 35
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -59,20 +62,60 @@ jobs:
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--allow-branch ${{ github.ref_name }} \
|
||||
--no-git-commit \
|
||||
--yes
|
||||
|
||||
- name: Preflight publish checks
|
||||
run: |
|
||||
python3 tools/internal/check_publish_preflight.py
|
||||
|
||||
# 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"
|
||||
set +e
|
||||
publish_status=1
|
||||
max_attempts=2
|
||||
attempt=1
|
||||
rm -f /tmp/publish-dry-run.log
|
||||
|
||||
# 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
|
||||
while [ "$attempt" -le "$max_attempts" ]; do
|
||||
echo "Dry-run publish attempt ${attempt}/${max_attempts}"
|
||||
cargo workspaces publish --dry-run --allow-dirty 2>&1 | tee /tmp/publish-dry-run.log
|
||||
publish_status=${PIPESTATUS[0]}
|
||||
|
||||
if [ "$publish_status" -eq 0 ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Retry once for interruption/runner issues.
|
||||
if [ "$attempt" -lt "$max_attempts" ] && \
|
||||
{ [ "$publish_status" -eq 130 ] || [ "$publish_status" -eq 137 ]; }; then
|
||||
echo "Publish dry-run interrupted (exit ${publish_status}), retrying in 10s..."
|
||||
sleep 10
|
||||
attempt=$((attempt + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
set -e
|
||||
|
||||
if grep -Eiq \
|
||||
"failed to verify manifest|failed to parse manifest|invalid Cargo.toml|error: package .* has no (description|license|repository)" \
|
||||
/tmp/publish-dry-run.log; then
|
||||
echo "Detected real packaging/manifest errors"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# In dry-run mode, non-zero publish status is expected due to
|
||||
# dependency-cascade failures against crates.io index.
|
||||
if [ "$publish_status" -ne 0 ]; then
|
||||
echo "Dry-run publish returned non-zero (${publish_status}) but no real manifest blockers were detected."
|
||||
fi
|
||||
|
||||
echo "Only expected dry-run dependency cascade errors detected (if any)."
|
||||
|
||||
# Show the list of packages published
|
||||
- name: Show package versions
|
||||
|
||||
@@ -17,6 +17,8 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -17,6 +17,8 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -15,6 +15,8 @@ env:
|
||||
jobs:
|
||||
version-bump:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
||||
@@ -51,25 +51,3 @@ 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,38 +93,3 @@ 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,38 +55,3 @@ 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,34 +24,3 @@ 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
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
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.2
|
||||
uses: mikefarah/yq@v4.52.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -25,6 +25,10 @@ jobs:
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Preflight publish checks
|
||||
run: |
|
||||
python3 tools/internal/check_publish_preflight.py
|
||||
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -4,51 +4,23 @@ 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.
|
||||
|
||||
### 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`
|
||||
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub
|
||||
Actions.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,126 +0,0 @@
|
||||
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();
|
||||
@@ -1,67 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -76,3 +76,4 @@ CLAUDE.md
|
||||
.claude/settings.json
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
|
||||
@@ -4,6 +4,82 @@ 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])
|
||||
|
||||
Generated
+1698
-1703
File diff suppressed because it is too large
Load Diff
+23
-9
@@ -74,7 +74,6 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-lp-common",
|
||||
"common/nym-kkt",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
@@ -129,7 +128,6 @@ members = [
|
||||
"nym-browser-extension/storage",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-credential-proxy/vpn-api-lib-wasm",
|
||||
"nym-data-observatory",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
@@ -173,8 +171,9 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
|
||||
# "nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite", "common/nym-kkt-context",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -274,6 +273,7 @@ futures = "0.3.31"
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
getrandom03 = { package = "getrandom", version = "=0.3.3" }
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
@@ -309,8 +309,10 @@ nix = "0.30.1"
|
||||
notify = "5.1.0"
|
||||
num_enum = "0.7.5"
|
||||
once_cell = "1.21.3"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
opentelemetry = "0.31.0"
|
||||
opentelemetry_sdk = "0.31.0"
|
||||
opentelemetry-otlp = "0.31.0"
|
||||
tonic = "0.14.4"
|
||||
parking_lot = "0.12.3"
|
||||
pem = "0.8"
|
||||
petgraph = "0.6.5"
|
||||
@@ -322,6 +324,7 @@ quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rayon = "1.5.1"
|
||||
@@ -368,9 +371,8 @@ tower = "0.5.2"
|
||||
tower-http = "0.6.6"
|
||||
tracing = "0.1.41"
|
||||
tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-subscriber = "0.3.20"
|
||||
tracing-tree = "0.2.2"
|
||||
tracing-indicatif = "0.3.9"
|
||||
tracing-test = "0.2.5"
|
||||
ts-rs = "10.1.0"
|
||||
@@ -391,6 +393,17 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-curve25519 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
@@ -435,7 +448,8 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
|
||||
@@ -104,11 +104,11 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
|
||||
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
||||
|
||||
sdk-wasm-build:
|
||||
$(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
# $(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/node-tester
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/full-nym-wasm
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
@@ -119,13 +119,14 @@ sdk-typescript-build:
|
||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
|
||||
|
||||
sdk-wasm-test:
|
||||
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
|
||||
sdk-wasm-lint:
|
||||
cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
$(MAKE) -C wasm/mix-fetch check-fmt
|
||||
|
||||
# Add to top-level targets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.72"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
+12
-12
@@ -513,9 +513,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -3067,9 +3067,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -4989,9 +4989,9 @@
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -6870,9 +6870,9 @@
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.72"
|
||||
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"
|
||||
|
||||
@@ -19,12 +19,15 @@ serde_json = { workspace = true, optional = true }
|
||||
|
||||
## tracing
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||
tracing-tree = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
|
||||
opentelemetry = { workspace = true, features = ["trace"], optional = true }
|
||||
|
||||
## otel-otlp (modern OTLP export to SigNoz/any OTLP collector)
|
||||
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
|
||||
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "trace", "tls-roots"], optional = true }
|
||||
tonic = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
@@ -35,13 +38,14 @@ default = []
|
||||
openapi = ["utoipa"]
|
||||
output_format = ["serde_json", "dep:clap"]
|
||||
bin_info_schema = ["schemars"]
|
||||
basic_tracing = ["dep:tracing", "tracing-subscriber"]
|
||||
tracing = [
|
||||
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
|
||||
otel-otlp = [
|
||||
"basic_tracing",
|
||||
"tracing-tree",
|
||||
"opentelemetry-jaeger",
|
||||
"tracing-opentelemetry",
|
||||
"opentelemetry",
|
||||
"dep:opentelemetry",
|
||||
"dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry-otlp",
|
||||
"dep:tracing-opentelemetry",
|
||||
"dep:tonic",
|
||||
]
|
||||
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
|
||||
models = []
|
||||
|
||||
@@ -4,16 +4,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
// Re-export tracing_subscriber for consumers that need to compose layers
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub use tracing_subscriber;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_tree;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -69,40 +62,106 @@ pub fn setup_tracing_logger() {
|
||||
build_tracing_logger().init()
|
||||
}
|
||||
|
||||
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason
|
||||
#[cfg(feature = "tracing")]
|
||||
#[macro_export]
|
||||
macro_rules! setup_tracing {
|
||||
($service_name: expr) => {
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
/// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
|
||||
///
|
||||
/// This produces a layer compatible with `tracing_subscriber::registry()` that
|
||||
/// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
|
||||
///
|
||||
/// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
|
||||
/// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
|
||||
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
|
||||
/// or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
|
||||
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
|
||||
/// sent as the `signoz-ingestion-key` gRPC metadata header on every export.
|
||||
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
|
||||
/// Attached as the `deployment.environment` OTel resource attribute.
|
||||
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
|
||||
/// Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
|
||||
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
|
||||
/// unbounded blocking if the collector is slow or unreachable.
|
||||
#[cfg(feature = "otel-otlp")]
|
||||
pub fn init_otel_layer<S>(
|
||||
service_name: &str,
|
||||
endpoint: &str,
|
||||
ingestion_key: Option<&str>,
|
||||
environment: &str,
|
||||
sample_ratio: f64,
|
||||
export_timeout_secs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
|
||||
opentelemetry_sdk::trace::SdkTracerProvider,
|
||||
),
|
||||
Box<dyn std::error::Error + Send + Sync>,
|
||||
>
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
{
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_otlp::WithTonicConfig;
|
||||
use opentelemetry_sdk::trace::Sampler;
|
||||
use std::time::Duration;
|
||||
|
||||
let registry = nym_bin_common::logging::tracing_subscriber::Registry::default()
|
||||
.with(nym_bin_common::logging::tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(
|
||||
nym_bin_common::logging::tracing_tree::HierarchicalLayer::new(4)
|
||||
.with_targets(true)
|
||||
.with_bracketed_fields(true),
|
||||
);
|
||||
// Validate endpoint URI early to fail with a clear message
|
||||
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
|
||||
return Err(format!(
|
||||
"invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let tracer = nym_bin_common::logging::opentelemetry_jaeger::new_collector_pipeline()
|
||||
.with_endpoint("http://44.199.230.10:14268/api/traces")
|
||||
.with_service_name($service_name)
|
||||
.with_isahc()
|
||||
.with_trace_config(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::config().with_sampler(
|
||||
nym_bin_common::logging::opentelemetry::sdk::trace::Sampler::TraceIdRatioBased(
|
||||
0.1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.install_batch(nym_bin_common::logging::opentelemetry::runtime::Tokio)
|
||||
.expect("Could not init tracer");
|
||||
let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
|
||||
|
||||
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
let mut builder = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(Duration::from_secs(export_timeout_secs));
|
||||
|
||||
registry.with(telemetry).init();
|
||||
};
|
||||
// Explicitly configure TLS when the endpoint uses HTTPS
|
||||
if endpoint.starts_with("https://") {
|
||||
builder =
|
||||
builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
|
||||
}
|
||||
|
||||
if let Some(key) = ingestion_key {
|
||||
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||
metadata.insert(
|
||||
"signoz-ingestion-key",
|
||||
key.parse()
|
||||
.map_err(|_| "invalid ingestion key format (value redacted)")?,
|
||||
);
|
||||
builder = builder.with_metadata(metadata);
|
||||
}
|
||||
|
||||
let exporter = builder
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;
|
||||
|
||||
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||
.with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
|
||||
.with_batch_exporter(exporter)
|
||||
.with_resource(
|
||||
opentelemetry_sdk::Resource::builder()
|
||||
.with_service_name(service_name.to_owned())
|
||||
.with_attribute(opentelemetry::KeyValue::new(
|
||||
"deployment.environment",
|
||||
environment.to_owned(),
|
||||
))
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
|
||||
let tracer = tracer_provider.tracer(service_name.to_owned());
|
||||
|
||||
Ok((
|
||||
tracing_opentelemetry::layer().with_tracer(tracer),
|
||||
tracer_provider,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn banner(crate_name: &str, crate_version: &str) -> String {
|
||||
|
||||
@@ -121,6 +121,10 @@ features = ["wasm-bindgen"]
|
||||
workspace = true
|
||||
features = ["full"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.getrandom03]
|
||||
workspace = true
|
||||
features = ["wasm_js"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
|
||||
@@ -15,3 +15,13 @@ pub(crate) fn get_time_now() -> Instant {
|
||||
pub(crate) fn new_interval_stream(polling_rate: Duration) -> IntervalStream {
|
||||
gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "Rust" fn __getrandom_v03_custom(
|
||||
dest: *mut u8,
|
||||
len: usize,
|
||||
) -> Result<(), getrandom03::Error> {
|
||||
let _ = dest;
|
||||
let _ = len;
|
||||
Err(getrandom03::Error::UNSUPPORTED)
|
||||
}
|
||||
|
||||
@@ -128,54 +128,95 @@ impl ManagedConnection {
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
debug!(
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "timeout",
|
||||
"failed to connect to {address} within {:?}",
|
||||
self.connection_timeout
|
||||
);
|
||||
|
||||
// we failed to connect - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Take whatever the receiver channel produces and put it on the connection.
|
||||
// We could have as well used conn.send_all(receiver.map(Ok)), but considering we don't care
|
||||
// about neither receiver nor the connection, it doesn't matter which one gets consumed
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!("Failed to forward packets to {address}: {err}");
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped",
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -272,16 +313,18 @@ impl SendWithoutResponse for Client {
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
debug!("establishing initial connection to {address}");
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
|
||||
// for sending for as soon as the connection is created
|
||||
debug!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "not_connected",
|
||||
"establishing initial connection to {address}"
|
||||
);
|
||||
self.make_connection(address, framed_packet);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
@@ -289,15 +332,24 @@ impl SendWithoutResponse for Client {
|
||||
));
|
||||
};
|
||||
|
||||
let channel_capacity = sender.channel.max_capacity();
|
||||
let channel_available = sender.channel.capacity();
|
||||
let channel_used = channel_capacity - channel_available;
|
||||
|
||||
let sending_res = sender.channel.try_send(framed_packet);
|
||||
drop(sender);
|
||||
|
||||
sending_res.map_err(|err| {
|
||||
match err {
|
||||
TrySendError::Full(_) => {
|
||||
debug!("Connection to {address} seems to not be able to handle all the traffic - dropping the current packet");
|
||||
// it's not a 'big' error, but we did not manage to send the packet
|
||||
// if the queue is full, we can't really do anything but to drop the packet
|
||||
warn!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "full_dropped",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"dropping packet: connection buffer to {address} is full ({channel_used}/{channel_capacity})"
|
||||
);
|
||||
io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"connection queue is full",
|
||||
@@ -305,11 +357,13 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
TrySendError::Closed(dropped) => {
|
||||
debug!(
|
||||
"Connection to {address} seems to be dead. attempting to re-establish it...",
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "closed_reconnecting",
|
||||
channel_capacity,
|
||||
channel_used,
|
||||
"connection to {address} dead, attempting re-establishment"
|
||||
);
|
||||
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue
|
||||
// it up to send it as soon as the connection is re-established
|
||||
self.make_connection(address, dropped);
|
||||
io::Error::new(
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
|
||||
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
|
||||
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
|
||||
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
@@ -273,18 +273,18 @@ impl<C, S> Client<C, S> {
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
// #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
|
||||
#[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
|
||||
pub async fn get_all_cached_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||
Ok(self.nym_api.get_all_described_nodes().await?)
|
||||
}
|
||||
|
||||
// pub async fn get_all_cached_described_nodes_v2(
|
||||
// &self,
|
||||
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
// Ok(self.nym_api.get_all_described_nodes_v2().await?)
|
||||
// }
|
||||
pub async fn get_all_cached_described_nodes_v2(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
Ok(self.nym_api.get_all_described_nodes_v2().await?)
|
||||
}
|
||||
|
||||
pub async fn get_all_cached_bonded_nym_nodes(
|
||||
&self,
|
||||
@@ -473,7 +473,7 @@ impl NymApiClient {
|
||||
Ok(self.nym_api.health().await?)
|
||||
}
|
||||
|
||||
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
pub async fn get_all_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||
@@ -495,29 +495,29 @@ impl NymApiClient {
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
// pub async fn get_all_described_nodes_v2(
|
||||
// &self,
|
||||
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
// let mut page = 0;
|
||||
// let mut descriptions = Vec::new();
|
||||
//
|
||||
// loop {
|
||||
// let mut res = self
|
||||
// .nym_api
|
||||
// .get_nodes_described_v2(Some(page), None)
|
||||
// .await?;
|
||||
//
|
||||
// descriptions.append(&mut res.data);
|
||||
// if descriptions.len() < res.pagination.total {
|
||||
// page += 1
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(descriptions)
|
||||
// }
|
||||
pub async fn get_all_described_nodes_v2(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_nodes_described_v2(Some(page), None)
|
||||
.await?;
|
||||
|
||||
descriptions.append(&mut res.data);
|
||||
if descriptions.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
pub async fn get_all_bonded_nym_nodes(
|
||||
&self,
|
||||
|
||||
@@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
@@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
// #[deprecated(note = "use .get_nodes_described_v2 instead")]
|
||||
#[deprecated(note = "use .get_nodes_described_v2 instead")]
|
||||
async fn get_nodes_described(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
@@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
// #[tracing::instrument(level = "debug", skip_all)]
|
||||
// async fn get_nodes_described_v2(
|
||||
// &self,
|
||||
// page: Option<u32>,
|
||||
// per_page: Option<u32>,
|
||||
// ) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
|
||||
// let mut params = Vec::new();
|
||||
//
|
||||
// if let Some(page) = page {
|
||||
// params.push(("page", page.to_string()))
|
||||
// }
|
||||
//
|
||||
// if let Some(per_page) = per_page {
|
||||
// params.push(("per_page", per_page.to_string()))
|
||||
// }
|
||||
//
|
||||
// self.get_json(
|
||||
// &[
|
||||
// routes::V2_API_VERSION,
|
||||
// routes::NYM_NODES_ROUTES,
|
||||
// routes::NYM_NODES_DESCRIBED,
|
||||
// ],
|
||||
// ¶ms,
|
||||
// )
|
||||
// .await
|
||||
// }
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_nodes_described_v2(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_DESCRIBED,
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||
self.get_rewarded_set().await
|
||||
@@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
// #[allow(deprecated)]
|
||||
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||
#[allow(deprecated)]
|
||||
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescriptionV1>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
@@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
// async fn (&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
|
||||
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
// let mut page = 0;
|
||||
// let mut descriptions = Vec::new();
|
||||
//
|
||||
// loop {
|
||||
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
|
||||
//
|
||||
// descriptions.append(&mut res.data);
|
||||
// if descriptions.len() < res.pagination.total {
|
||||
// page += 1
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(descriptions)
|
||||
// }
|
||||
async fn get_all_described_nodes_v2(&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.get_nodes_described_v2(Some(page), None).await?;
|
||||
|
||||
descriptions.append(&mut res.data);
|
||||
if descriptions.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_nym_nodes(
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct Args {
|
||||
}
|
||||
|
||||
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
||||
match client.get_all_cached_described_nodes().await {
|
||||
match client.get_all_cached_described_nodes_v2().await {
|
||||
Ok(res) => match args.identity_key {
|
||||
Some(identity_key) => {
|
||||
let node = res.iter().find(|node| {
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct Args {
|
||||
}
|
||||
|
||||
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
||||
match client.get_all_cached_described_nodes().await {
|
||||
match client.get_all_cached_described_nodes_v2().await {
|
||||
Ok(res) => match args.identity_key {
|
||||
Some(identity_key) => {
|
||||
let node = res.iter().find(|node| {
|
||||
|
||||
@@ -21,10 +21,13 @@ generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
hmac = { workspace = true, optional = true }
|
||||
jwt-simple = { workspace = true, optional = true }
|
||||
libcrux-psq = { workspace = true, optional = true }
|
||||
libcrux-curve25519 = { workspace = true, optional = true }
|
||||
cipher = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
|
||||
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
rand09 = { workspace = true, optional = true }
|
||||
serde_bytes = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
@@ -39,17 +42,18 @@ nym-pemstore = { workspace = true }
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
libcrux_x25519 = ["libcrux-psq", "libcrux-curve25519"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize", "rand09"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod serde_helpers;
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
|
||||
|
||||
/// Size of a X25519 private key
|
||||
pub const PRIVATE_KEY_SIZE: usize = 32;
|
||||
|
||||
@@ -45,6 +48,9 @@ pub enum KeyRecoveryError {
|
||||
#[source]
|
||||
source: bs58::decode::Error,
|
||||
},
|
||||
|
||||
#[error("the x25519 private key could not be converted to its PSQ representation")]
|
||||
IncompatiblePSQPrivateKey,
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
@@ -413,6 +419,88 @@ impl AsRef<[u8]> for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
// libcrux-psq conversion
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
impl TryFrom<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::*;
|
||||
@@ -421,6 +509,21 @@ mod tests {
|
||||
|
||||
fn assert_zeroize<T: Zeroize>() {}
|
||||
|
||||
#[test]
|
||||
fn test_key_conversion() {
|
||||
let dalek_kp = super::KeyPair::new(&mut rand::thread_rng());
|
||||
|
||||
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
|
||||
|
||||
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
|
||||
let libcrux_private_key =
|
||||
libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes)
|
||||
.unwrap();
|
||||
let libcrux_public_key = libcrux_private_key.to_public();
|
||||
|
||||
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_key_is_zeroized() {
|
||||
assert_zeroize::<PrivateKey>();
|
||||
|
||||
@@ -44,3 +44,25 @@ pub mod option_bs58_x25519_pubkey {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libcrux_x25519")]
|
||||
pub mod bs58_dh_public_key {
|
||||
use crate::asymmetric::x25519;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<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,3 +109,152 @@ impl DerivationMaterial {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod blake3 {
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
use blake3::Hasher;
|
||||
|
||||
use rand09::{RngCore, rng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub fn derive_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
salt: &[u8],
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = Hasher::new_derive_key(info);
|
||||
|
||||
for input_key in input_key_material {
|
||||
hasher.update(input_key);
|
||||
}
|
||||
|
||||
hasher.update(salt);
|
||||
|
||||
hasher.finalize().as_bytes().to_owned()
|
||||
}
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
derive_key_blake3_multi_input(info, &[input_key_material], salt)
|
||||
}
|
||||
|
||||
pub fn derive_fresh_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
) -> [u8; 32] {
|
||||
let mut salt = [0u8; 32];
|
||||
rng().fill_bytes(&mut salt);
|
||||
|
||||
let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt);
|
||||
|
||||
// Zeroize salt
|
||||
salt.zeroize();
|
||||
|
||||
derived_key
|
||||
}
|
||||
|
||||
/// Derives a fresh 32-byte key using Blake3's key derivation mode.
|
||||
/// The function calls a random number generator to generate a fresh salt.
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes());
|
||||
/// ```
|
||||
pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] {
|
||||
derive_fresh_key_blake3_multi_input(info, &[input_key_material])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "nym-kkt-ciphersuite"
|
||||
description = "Nym KKT ciphersuite"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -9,15 +10,17 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
|
||||
blake3 = { workspace = true, optional = true }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
|
||||
libcrux-sha3 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
digests = ["blake3", "libcrux-sha3"]
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
use crate::error::KKTCiphersuiteError;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use strum_macros::{Display, EnumIter, EnumString};
|
||||
|
||||
pub use strum::IntoEnumIterator;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub const DEFAULT_HASH_LEN: usize = 32;
|
||||
@@ -45,10 +47,13 @@ pub mod xwing {
|
||||
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
|
||||
}
|
||||
|
||||
pub type KEMKeyDigests = KeyDigests;
|
||||
pub type SigningKeyDigests = KeyDigests;
|
||||
pub type KEMKeyDigests = BTreeMap<HashFunction, Vec<u8>>;
|
||||
|
||||
pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
|
||||
pub mod node_compatibility {
|
||||
/// Indicates the initial version where kkt has been introduced
|
||||
/// 1.27.0 Raclette release
|
||||
pub const INTRODUCTION: semver::Version = semver::Version::new(1, 27, 0);
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
@@ -62,6 +67,8 @@ pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
@@ -204,23 +211,26 @@ impl SignatureScheme {
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Display,
|
||||
Default,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[repr(u8)]
|
||||
pub enum KEM {
|
||||
XWing = 0,
|
||||
// unsupported
|
||||
// XWing = 0,
|
||||
#[default]
|
||||
MlKem768 = 1,
|
||||
McEliece = 2,
|
||||
X25519 = 255,
|
||||
}
|
||||
|
||||
impl KEM {
|
||||
pub fn encapsulation_key_length(&self) -> usize {
|
||||
pub const fn encapsulation_key_length(&self) -> usize {
|
||||
match self {
|
||||
KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH,
|
||||
KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
|
||||
KEM::X25519 => x25519::PUBLIC_KEY_LENGTH,
|
||||
// KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
|
||||
KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH,
|
||||
}
|
||||
}
|
||||
@@ -238,6 +248,17 @@ pub struct Ciphersuite {
|
||||
signature_length: usize,
|
||||
}
|
||||
|
||||
impl Default for Ciphersuite {
|
||||
fn default() -> Self {
|
||||
Ciphersuite::new(
|
||||
KEM::MlKem768,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ciphersuite {
|
||||
pub fn new(
|
||||
kem: KEM,
|
||||
@@ -257,6 +278,51 @@ impl Ciphersuite {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine optimal `Ciphersuite` based on remote's node's version
|
||||
pub fn from_node_version(semver: semver::Version) -> Option<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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[package]
|
||||
name = "nym-lp-transport"
|
||||
version = "0.1.0"
|
||||
name = "nym-kkt-context"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -9,15 +8,14 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||
nym-test-utils = { path = "../test-utils", optional = true }
|
||||
tracing = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
io-mocks = ["nym-test-utils"]
|
||||
nym-kkt-ciphersuite = { path = "../nym-kkt-ciphersuite" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,13 +1,38 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2026 - 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)]
|
||||
@@ -15,11 +40,11 @@ pub enum KKTStatus {
|
||||
Ok = 0b0000_0000,
|
||||
InvalidRequestFormat = 0b0010_0000,
|
||||
InvalidResponseFormat = 0b0100_0000,
|
||||
InvalidSignature = 0b0110_0000,
|
||||
UnsupportedCiphersuite = 0b1000_0000,
|
||||
UnsupportedKKTVersion = 0b1010_0000,
|
||||
InvalidKey = 0b1100_0000,
|
||||
Timeout = 0b1110_0000,
|
||||
UnsupportedCiphersuite = 0b0110_0000,
|
||||
UnsupportedKKTVersion = 0b1000_0000,
|
||||
InvalidKey = 0b1010_0000,
|
||||
Timeout = 0b1100_0000,
|
||||
UnverifiedKEMKey = 0b1110_0000,
|
||||
}
|
||||
|
||||
impl Display for KKTStatus {
|
||||
@@ -28,10 +53,10 @@ impl Display for KKTStatus {
|
||||
KKTStatus::Ok => "Ok",
|
||||
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
|
||||
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
|
||||
KKTStatus::InvalidSignature => "Invalid Signature",
|
||||
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
|
||||
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
|
||||
KKTStatus::InvalidKey => "Invalid Key",
|
||||
KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key",
|
||||
KKTStatus::Timeout => "Timeout",
|
||||
})
|
||||
}
|
||||
@@ -43,7 +68,16 @@ impl Display for KKTStatus {
|
||||
pub enum KKTRole {
|
||||
Initiator = 0b0000_0000,
|
||||
Responder = 0b0000_0001,
|
||||
AnonymousInitiator = 0b0000_0010,
|
||||
}
|
||||
|
||||
impl KKTRole {
|
||||
pub const fn is_initiator(&self) -> bool {
|
||||
matches!(self, KKTRole::Initiator)
|
||||
}
|
||||
|
||||
pub const fn is_responder(&self) -> bool {
|
||||
matches!(self, KKTRole::Responder)
|
||||
}
|
||||
}
|
||||
|
||||
// bitmask used: 0b0001_1100
|
||||
@@ -54,6 +88,16 @@ pub enum KKTMode {
|
||||
Mutual = 0b0000_0100,
|
||||
}
|
||||
|
||||
impl KKTMode {
|
||||
pub const fn is_one_way(&self) -> bool {
|
||||
matches!(self, KKTMode::OneWay)
|
||||
}
|
||||
|
||||
pub const fn is_mutual(&self) -> bool {
|
||||
matches!(self, KKTMode::Mutual)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct KKTContext {
|
||||
version: u8,
|
||||
@@ -63,24 +107,20 @@ pub struct KKTContext {
|
||||
role: KKTRole,
|
||||
ciphersuite: Ciphersuite,
|
||||
}
|
||||
|
||||
impl KKTContext {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
|
||||
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Anonymous Initiator can only use OneWay mode",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Self {
|
||||
Self {
|
||||
version: KKT_VERSION,
|
||||
message_sequence: 0,
|
||||
status: KKTStatus::Ok,
|
||||
mode,
|
||||
role,
|
||||
ciphersuite,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTContextEncodingError> {
|
||||
let mut responder_header = *self;
|
||||
|
||||
responder_header.increment_message_sequence_count()?;
|
||||
@@ -89,12 +129,12 @@ impl KKTContext {
|
||||
Ok(responder_header)
|
||||
}
|
||||
|
||||
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> {
|
||||
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTContextEncodingError> {
|
||||
if self.message_sequence + 1 < (1 << 4) {
|
||||
self.message_sequence += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KKTError::MessageCountLimitReached)
|
||||
Err(KKTContextEncodingError::MessageCountLimitReached)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +158,10 @@ impl KKTContext {
|
||||
}
|
||||
|
||||
pub fn body_len(&self) -> usize {
|
||||
if self.status != KKTStatus::Ok
|
||||
|| (self.mode == KKTMode::OneWay
|
||||
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
|
||||
if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey)
|
||||
||
|
||||
// no payload
|
||||
(self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator)
|
||||
{
|
||||
0
|
||||
} else {
|
||||
@@ -128,37 +169,18 @@ impl KKTContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_len(&self) -> usize {
|
||||
match self.role {
|
||||
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
|
||||
KKTRole::AnonymousInitiator => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn header_len(&self) -> usize {
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
|
||||
pub const fn session_id_len(&self) -> usize {
|
||||
// note: if anyone decides to update this function and changes the constant value,
|
||||
// you will have to adjust encoding/decoding functions
|
||||
|
||||
// match self.role {
|
||||
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
|
||||
// It doesn't make sense to send a session_id if we send messages in the clear
|
||||
// KKTRole::AnonymousInitiator => 0,
|
||||
// }
|
||||
KKT_SESSION_ID_LEN
|
||||
}
|
||||
|
||||
pub fn full_message_len(&self) -> usize {
|
||||
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
|
||||
self.body_len() + self.header_len()
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> {
|
||||
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTContextEncodingError> {
|
||||
let mut header_bytes = [0u8; KKT_CONTEXT_LEN];
|
||||
if self.message_sequence >= 1 << 4 {
|
||||
return Err(KKTError::MessageCountLimitReached);
|
||||
return Err(KKTContextEncodingError::MessageCountLimitReached);
|
||||
}
|
||||
|
||||
let ciphersuite_bytes = self.ciphersuite.encode();
|
||||
@@ -175,15 +197,17 @@ impl KKTContext {
|
||||
Ok(header_bytes)
|
||||
}
|
||||
|
||||
pub fn try_decode(header_bytes: [u8; KKT_CONTEXT_LEN]) -> Result<Self, KKTError> {
|
||||
pub fn try_decode(
|
||||
header_bytes: [u8; KKT_CONTEXT_LEN],
|
||||
) -> Result<Self, KKTContextEncodingError> {
|
||||
let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4;
|
||||
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
|
||||
|
||||
// We only check if stuff is valid here, not necessarily if it's compatible
|
||||
|
||||
if kkt_version > KKT_VERSION {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Version: {kkt_version}"),
|
||||
return Err(KKTContextEncodingError::InvalidVersion {
|
||||
version: kkt_version,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,16 +215,15 @@ impl KKTContext {
|
||||
let raw_kkt_role = header_bytes[1] & 0b0000_0011;
|
||||
let raw_kkt_mode = header_bytes[1] & 0b0001_1100;
|
||||
|
||||
let status =
|
||||
KKTStatus::try_from(raw_kkt_status).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Status: {raw_kkt_status}"),
|
||||
})?;
|
||||
let role = KKTRole::try_from(raw_kkt_role).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Role: {raw_kkt_role}"),
|
||||
})?;
|
||||
let mode = KKTMode::try_from(raw_kkt_mode).map_err(|_| KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
|
||||
let status = KKTStatus::try_from(raw_kkt_status).map_err(|_| {
|
||||
KKTContextEncodingError::InvalidStatus {
|
||||
raw: raw_kkt_status,
|
||||
}
|
||||
})?;
|
||||
let role = KKTRole::try_from(raw_kkt_role)
|
||||
.map_err(|_| KKTContextEncodingError::InvalidRole { raw: raw_kkt_role })?;
|
||||
let mode = KKTMode::try_from(raw_kkt_mode)
|
||||
.map_err(|_| KKTContextEncodingError::InvalidMode { raw: raw_kkt_mode })?;
|
||||
|
||||
// SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
@@ -228,9 +251,8 @@ mod tests {
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
Ciphersuite::decode([1, 1, 0, 0]).unwrap(),
|
||||
);
|
||||
let encoded = valid_context.encode().unwrap();
|
||||
let decoded = KKTContext::try_decode(encoded).unwrap();
|
||||
|
||||
@@ -7,35 +7,30 @@ license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
blake3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
|
||||
nym-crypto = { path = "../crypto", features = ["hashing"] }
|
||||
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
|
||||
nym-kkt-context = { path = "../nym-kkt-context" }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-kem = { workspace = true }
|
||||
libcrux-ecdh = { workspace = true, features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { workspace = true }
|
||||
|
||||
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
|
||||
rand09 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
libcrux-psq = { workspace = true, features = ["classic-mceliece"] }
|
||||
libcrux-ml-kem = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.9.0"
|
||||
anyhow = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
// 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 rand09::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand09::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Encode Frame - Request",
|
||||
),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Decode Frame - Request",
|
||||
),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Encode Frame",
|
||||
),
|
||||
|b| b.iter(|| r_frame.to_bytes()),
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Encode Frame - Request",),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Decode Frame - Request",),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator OneWay: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Encode Frame",),
|
||||
|b| {
|
||||
b.iter(|| r_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator OneWay: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Encode Frame - Request",),
|
||||
|b| {
|
||||
b.iter(|| i_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Decode Frame - Request",),
|
||||
|b| {
|
||||
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator Mutual: Responder Generate Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Encode Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
r_frame.to_bytes();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{kem}, {hash_function} | Initiator Mutual: Initiator Ingest Response",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
kkt_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,188 @@
|
||||
use libcrux_chacha20poly1305::TAG_LEN;
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
// This is arbitrary
|
||||
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
|
||||
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
|
||||
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Carrier {
|
||||
tx_key: [u8; 32],
|
||||
rx_key: [u8; 32],
|
||||
tx_counter: u64,
|
||||
rx_counter: u64,
|
||||
}
|
||||
|
||||
pub enum CarrierRole {
|
||||
Initiator,
|
||||
Responder,
|
||||
}
|
||||
|
||||
fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> {
|
||||
match nonce.checked_add(1) {
|
||||
Some(incremented_nonce) => {
|
||||
*nonce = incremented_nonce;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(KKTError::AEADError {
|
||||
info: "Nonce maxed out.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_nonce_bytes(nonce: u64) -> [u8; 12] {
|
||||
let mut bytes = [0u8; 12];
|
||||
let nonce_bytes = nonce.to_le_bytes();
|
||||
bytes[4..].clone_from_slice(&nonce_bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
impl Carrier {
|
||||
fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
tx_key,
|
||||
rx_key,
|
||||
tx_counter: 1,
|
||||
rx_counter: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<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,
|
||||
b"kkt-carrier-v1",
|
||||
&as_nonce_bytes(self.tx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.tx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<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,
|
||||
b"kkt-carrier-v1",
|
||||
&as_nonce_bytes(self.rx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.rx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{carrier::Carrier, key_utils::generate_lp_keypair_x25519};
|
||||
use rand09::RngCore;
|
||||
|
||||
#[test]
|
||||
fn test_e2e() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate responder x25519 keys
|
||||
let r_x25519 = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
let mut context: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut context);
|
||||
|
||||
let ephemeral_keypair = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap();
|
||||
|
||||
let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap();
|
||||
|
||||
let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context, true);
|
||||
let mut r_carrier = Carrier::from_secret_slice(r_shared_secret.as_ref(), &context, false);
|
||||
|
||||
let test1 = b"test1: i>r #1";
|
||||
let ct1 = i_carrier.encrypt(test1).unwrap();
|
||||
let pt1 = r_carrier.decrypt(&ct1).unwrap();
|
||||
assert_eq!(pt1, test1);
|
||||
|
||||
let test2 = b"test2: r>i #1";
|
||||
let ct2 = i_carrier.encrypt(test2).unwrap();
|
||||
let pt2 = r_carrier.decrypt(&ct2).unwrap();
|
||||
assert_eq!(pt2, test2);
|
||||
let test3 = b"test3: i>r #2";
|
||||
|
||||
let ct3 = i_carrier.encrypt(test3).unwrap();
|
||||
let pt3 = r_carrier.decrypt(&ct3).unwrap();
|
||||
assert_eq!(pt3, test3);
|
||||
|
||||
let test4 = b"test4: i>r #3";
|
||||
let ct4 = i_carrier.encrypt(test4).unwrap();
|
||||
let pt4 = r_carrier.decrypt(&ct4).unwrap();
|
||||
assert_eq!(pt4, test4);
|
||||
|
||||
let test5 = b"test5: r>i #2";
|
||||
let ct5 = i_carrier.encrypt(test5).unwrap();
|
||||
let pt5 = r_carrier.decrypt(&ct5).unwrap();
|
||||
assert_eq!(pt5, test5);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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,253 +0,0 @@
|
||||
// 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 rand09::{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 rand09::{RngCore, SeedableRng, rng};
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
let mut rng = rng();
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let (session_secret_key, ephemeral_public_key) =
|
||||
KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key());
|
||||
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_x25519_keypair.private_key(),
|
||||
ephemeral_public_key.as_bytes().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption() {
|
||||
let mut rng = rng();
|
||||
|
||||
let mut secret_key = [0u8; DEFAULT_HASH_LEN];
|
||||
rng.fill_bytes(&mut secret_key);
|
||||
|
||||
let mut plaintext = vec![0; 100];
|
||||
rng.fill_bytes(&mut plaintext);
|
||||
|
||||
let mut nonce = [0; 12];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut aad = vec![0; 124];
|
||||
rng.fill_bytes(&mut aad);
|
||||
|
||||
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
|
||||
|
||||
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
|
||||
|
||||
assert_eq!(o_plaintext, plaintext)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([255, 1, 0, 0])?,
|
||||
)?;
|
||||
let dummy_frame = KKTFrame::new(
|
||||
valid_context.encode()?,
|
||||
&[2u8; 32],
|
||||
[3u8; KKT_SESSION_ID_LEN],
|
||||
&[4u8; 64],
|
||||
);
|
||||
|
||||
let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?;
|
||||
|
||||
let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?;
|
||||
|
||||
assert_eq!(dummy_frame, frame);
|
||||
assert_eq!(context, valid_context);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,18 @@
|
||||
|
||||
use crate::context::KKTStatus;
|
||||
use nym_kkt_ciphersuite::error::KKTCiphersuiteError;
|
||||
use nym_kkt_context::KKTContextEncodingError;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KKTError {
|
||||
#[error("Signature constructor error")]
|
||||
SigConstructorError,
|
||||
#[error("Signature verification error")]
|
||||
SigVerifError,
|
||||
#[error(transparent)]
|
||||
CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
|
||||
|
||||
#[error(transparent)]
|
||||
MaskedByteError(#[from] MaskedByteError),
|
||||
|
||||
#[error("KEM mapping failure: {}", info)]
|
||||
KEMMapping { info: &'static str },
|
||||
|
||||
@@ -33,9 +33,6 @@ pub enum KKTError {
|
||||
#[error("KKT Responder Flagged Error: {}", status)]
|
||||
ResponderFlaggedError { status: KKTStatus },
|
||||
|
||||
#[error("KKT Message Count Limit Reached")]
|
||||
MessageCountLimitReached,
|
||||
|
||||
#[error("PSQ KEM Error: {}", info)]
|
||||
KEMError { info: &'static str },
|
||||
|
||||
@@ -48,8 +45,40 @@ pub enum KKTError {
|
||||
#[error("{}", info)]
|
||||
AEADError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
DecodingError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
UnsupportedAlgorithm { info: &'static str },
|
||||
|
||||
#[error("Generic libcrux error")]
|
||||
LibcruxError,
|
||||
|
||||
#[error("failed to derive shared secret: {inner:?}")]
|
||||
SharedSecretDerivationFailure {
|
||||
inner: libcrux_psq::handshake::HandshakeError,
|
||||
},
|
||||
|
||||
#[error("the received encapsulation key hash does not match the expected value")]
|
||||
MismatchedKEMHash,
|
||||
|
||||
#[error(transparent)]
|
||||
MalformedContext(#[from] KKTContextEncodingError),
|
||||
}
|
||||
|
||||
impl KKTError {
|
||||
pub fn shared_secret_derivation_failure(inner: libcrux_psq::handshake::HandshakeError) -> Self {
|
||||
KKTError::SharedSecretDerivationFailure { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MaskedByteError {
|
||||
#[error("invalid Masked Byte Length: Expected({expected}), Actual({actual})")]
|
||||
InvalidLength { expected: usize, actual: usize },
|
||||
|
||||
#[error("failed to Unmask Byte")]
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for KKTError {
|
||||
|
||||
+114
-71
@@ -7,90 +7,158 @@
|
||||
// [2..=5] => Ciphersuite
|
||||
// [6] => Reserved
|
||||
|
||||
use crate::context::{KKTMode, KKTRole};
|
||||
use crate::message::{
|
||||
DecryptedRequestFrame, KKTRequest, KKTRequestEncryptionResult, KKTRequestPlaintext,
|
||||
};
|
||||
use crate::{
|
||||
context::{KKT_CONTEXT_LEN, KKTContext},
|
||||
error::KKTError,
|
||||
};
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
pub const KKT_SESSION_ID_LEN: usize = 16;
|
||||
|
||||
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
|
||||
pub(crate) const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF";
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct KKTFrame {
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
session_id: KKTSessionId,
|
||||
context: KKTContext,
|
||||
body: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
|
||||
// if message coming from anonymous initiator => body is empty, there is no signature.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
|
||||
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
|
||||
// if oneway and message coming from initiator => body is empty.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key.
|
||||
// if coming from responder => body has the responder's kem public key.
|
||||
|
||||
impl KKTFrame {
|
||||
pub fn new(
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
body: &[u8],
|
||||
session_id: [u8; KKT_SESSION_ID_LEN],
|
||||
signature: &[u8],
|
||||
) -> Self {
|
||||
pub fn new(context: KKTContext, body: &[u8], payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
context,
|
||||
body: Vec::from(body),
|
||||
session_id,
|
||||
signature: Vec::from(signature),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
pub fn context_ref(&self) -> &[u8] {
|
||||
|
||||
pub const fn size_excluding_payload(role: KKTRole, mode: KKTMode, kem: KEM) -> usize {
|
||||
match role {
|
||||
KKTRole::Initiator => {
|
||||
match mode {
|
||||
KKTMode::OneWay => {
|
||||
// if oneway and message coming from initiator => body is empty.
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
KKTMode::Mutual => {
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key.
|
||||
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
|
||||
}
|
||||
}
|
||||
}
|
||||
KKTRole::Responder => {
|
||||
// if coming from responder => body has the responder's kem public key.
|
||||
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.payload.len()
|
||||
+ Self::size_excluding_payload(
|
||||
self.context.role(),
|
||||
self.context.mode(),
|
||||
self.context.ciphersuite().kem(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &KKTContext {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn context(&self) -> Result<KKTContext, KKTError> {
|
||||
KKTContext::try_decode(self.context)
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
self.payload.as_ref()
|
||||
}
|
||||
|
||||
pub fn signature_ref(&self) -> &[u8] {
|
||||
&self.signature
|
||||
pub fn encrypt_initiator_frame<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 body_ref(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
pub fn session_id_ref(&self) -> &[u8] {
|
||||
&self.session_id
|
||||
}
|
||||
pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] {
|
||||
self.session_id
|
||||
pub fn body(self) -> Vec<u8> {
|
||||
self.body
|
||||
}
|
||||
|
||||
pub fn signature_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.signature
|
||||
}
|
||||
pub fn body_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.body
|
||||
}
|
||||
|
||||
pub fn session_id_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.session_id
|
||||
}
|
||||
|
||||
pub fn frame_length(&self) -> usize {
|
||||
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
|
||||
KKT_CONTEXT_LEN + self.body.len()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
pub fn try_to_bytes(&self) -> Result<Vec<u8>, KKTError> {
|
||||
let mut bytes = Vec::with_capacity(self.frame_length());
|
||||
bytes.extend_from_slice(&self.context);
|
||||
bytes.extend_from_slice(&self.context.encode()?);
|
||||
bytes.extend_from_slice(&self.body);
|
||||
bytes.extend_from_slice(&self.session_id);
|
||||
bytes.extend_from_slice(&self.signature);
|
||||
bytes
|
||||
bytes.extend_from_slice(&self.payload);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> {
|
||||
pub fn from_bytes(bytes: &[u8], payload_len: usize) -> Result<Self, KKTError> {
|
||||
let len = bytes.len();
|
||||
if bytes.len() < KKT_CONTEXT_LEN {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
@@ -105,7 +173,7 @@ impl KKTFrame {
|
||||
let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap();
|
||||
let context = KKTContext::try_decode(context_bytes)?;
|
||||
|
||||
if bytes.len() != context.full_message_len() {
|
||||
if bytes.len() != context.full_message_len() + payload_len {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Frame is shorter than expected: actual {len} != expected {}",
|
||||
@@ -115,7 +183,6 @@ impl KKTFrame {
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
let mut signature = Vec::new();
|
||||
|
||||
// decode body
|
||||
if context.body_len() > 0 {
|
||||
@@ -123,33 +190,9 @@ impl KKTFrame {
|
||||
body.extend_from_slice(body_bytes);
|
||||
}
|
||||
|
||||
let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN];
|
||||
// SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let session_id = session_bytes.try_into().unwrap();
|
||||
// decode payload. this could be empty.
|
||||
let payload: Vec<u8> = Vec::from(&bytes[KKT_CONTEXT_LEN + context.body_len()..]);
|
||||
|
||||
// // old code left for reference if session id becomes variable in length:
|
||||
// if context.session_id_len() > 0 {
|
||||
// session_id.extend_from_slice(
|
||||
// &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
// ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
|
||||
// );
|
||||
// }
|
||||
|
||||
// decode signature
|
||||
if context.signature_len() > 0 {
|
||||
let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN
|
||||
..KKT_CONTEXT_LEN
|
||||
+ context.body_len()
|
||||
+ KKT_SESSION_ID_LEN
|
||||
+ context.signature_len()];
|
||||
signature.extend_from_slice(signature_bytes);
|
||||
}
|
||||
|
||||
Ok((
|
||||
KKTFrame::new(context_bytes, &body, session_id, &signature),
|
||||
context,
|
||||
))
|
||||
Ok(KKTFrame::new(context, &body, payload))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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,
|
||||
match payload {
|
||||
Some(payload_vec) => payload_vec,
|
||||
None => Vec::with_capacity(0),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response(
|
||||
own_context: &KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<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,74 +1,35 @@
|
||||
use crate::ciphersuite::HashFunction;
|
||||
use std::collections::HashMap;
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use libcrux_ml_kem::mlkem768::MlKem768KeyPair;
|
||||
use libcrux_psq::handshake::types::DHKeyPair;
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
index: Option<u32>,
|
||||
) -> nym_crypto::asymmetric::ed25519::KeyPair
|
||||
pub fn generate_lp_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
|
||||
DHKeyPair::new(rng)
|
||||
}
|
||||
|
||||
pub fn generate_keypair_x25519<R>(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair
|
||||
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> MlKem768KeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
|
||||
let private_key = nym_crypto::asymmetric::x25519::PrivateKey::from_secret(secret_initiator);
|
||||
private_key.into()
|
||||
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng)
|
||||
}
|
||||
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_libcrux<R>(
|
||||
rng: &mut R,
|
||||
kem: crate::ciphersuite::KEM,
|
||||
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError>
|
||||
pub fn generate_keypair_mceliece<R>(rng: &mut R) -> libcrux_psq::classic_mceliece::KeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
match kem {
|
||||
crate::ciphersuite::KEM::MlKem768 => {
|
||||
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, rng)?)
|
||||
}
|
||||
crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
rng,
|
||||
)?),
|
||||
crate::ciphersuite::KEM::X25519 => {
|
||||
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?)
|
||||
}
|
||||
_ => Err(crate::error::KKTError::KEMError {
|
||||
info: "Key Generation Error: Unsupported Libcrux Algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_mceliece<'a, R>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
classic_mceliece_rust::SecretKey<'a>,
|
||||
classic_mceliece_rust::PublicKey<'a>,
|
||||
)
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||
(decapsulation_key, encapsulation_key)
|
||||
libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng)
|
||||
}
|
||||
|
||||
pub fn hash_key_bytes(
|
||||
hash_function: &HashFunction,
|
||||
hash_function: HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
) -> Vec<u8> {
|
||||
@@ -77,9 +38,9 @@ pub fn hash_key_bytes(
|
||||
|
||||
/// attempt to produce digests of the provided key using all known [HashFunction] with a default
|
||||
/// hash length where variable output is available
|
||||
pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests {
|
||||
pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests {
|
||||
use strum::IntoEnumIterator;
|
||||
let mut digests = HashMap::new();
|
||||
let mut digests = BTreeMap::new();
|
||||
for hash in HashFunction::iter() {
|
||||
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
|
||||
}
|
||||
@@ -93,7 +54,7 @@ fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
pub fn validate_encapsulation_key(
|
||||
hash_function: &HashFunction,
|
||||
hash_function: HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
@@ -104,20 +65,8 @@ pub fn validate_encapsulation_key(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn validate_key_bytes(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
) -> bool {
|
||||
compare_hashes(
|
||||
&hash_key_bytes(hash_function, hash_length, key_bytes),
|
||||
expected_hash_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hash_encapsulation_key(
|
||||
hash_function: &HashFunction,
|
||||
hash_function: HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
) -> Vec<u8> {
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
// 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,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Convenience wrappers around KKT protocol functions for easier integration.
|
||||
//!
|
||||
//! This module provides simplified APIs for the common use case of exchanging
|
||||
//! KEM public keys between a client (initiator) and gateway (responder).
|
||||
//!
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode},
|
||||
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
// Re-export core session functions for advanced use cases
|
||||
pub use crate::session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
};
|
||||
|
||||
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
|
||||
use crate::frame::KKTFrame;
|
||||
|
||||
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
|
||||
///
|
||||
/// This is the client-side operation that initiates a KKT exchange.
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
|
||||
/// * `KKTContext` - Context to use when validating the response
|
||||
/// * `Vec<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(
|
||||
/// &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 = rand09::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Generate responder's KEM keypair (X25519 for testing)
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// Create ciphersuite
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Hash the KEM key (simulating directory storage)
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway: Handle request
|
||||
let response_frame_ciphertext = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(ed25519_init.public_key()), // Authenticated
|
||||
ed25519_resp.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
&response_frame_ciphertext,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify we got the correct KEM key
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap();
|
||||
|
||||
// Gateway: Handle anonymous request
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&encrypted_request_bytes,
|
||||
None, // Anonymous - no verification key
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
&response_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Different keypair for wrong signature
|
||||
let mut wrong_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut wrong_secret);
|
||||
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_session_key, _context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway handles request but we provide WRONG verification key
|
||||
let result = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(wrong_keypair.public_key()), // Wrong key!
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
);
|
||||
|
||||
// Should fail signature verification
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame,
|
||||
Some(initiator_keypair.public_key()),
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
&response_frame,
|
||||
);
|
||||
|
||||
// Should fail hash validation
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
+184
-452
@@ -1,498 +1,230 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod ciphersuite;
|
||||
pub mod context;
|
||||
pub mod encryption;
|
||||
pub mod carrier;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod initiator;
|
||||
pub mod key_utils;
|
||||
// pub mod kkt;
|
||||
pub mod session;
|
||||
pub mod keys;
|
||||
pub mod masked_byte;
|
||||
pub mod message;
|
||||
pub mod rekey;
|
||||
pub mod responder;
|
||||
|
||||
// This must be less than 4 bits
|
||||
pub const KKT_VERSION: u8 = 1;
|
||||
const _: () = assert!(KKT_VERSION < 1 << 4);
|
||||
pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
|
||||
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME";
|
||||
pub use nym_kkt_context as context;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use rand09::RngCore;
|
||||
|
||||
use crate::keys::KEMKeys;
|
||||
use crate::{
|
||||
KKT_RESPONSE_AAD,
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
|
||||
encryption::{
|
||||
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
|
||||
encrypt_kkt_frame,
|
||||
},
|
||||
frame::KKTFrame,
|
||||
initiator::KKTInitiator,
|
||||
key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
|
||||
generate_keypair_x25519, hash_encapsulation_key,
|
||||
},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519,
|
||||
hash_encapsulation_key,
|
||||
},
|
||||
responder::KKTResponder,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
fn test_kkt_psq_e2e_encrypted_carrier() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
let mut payload: Vec<u8> = vec![0u8; 900_000];
|
||||
rng.fill_bytes(&mut payload);
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
let responder_x25519_keypair = generate_lp_keypair_x25519(&mut rng);
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
// generate kem public keys
|
||||
|
||||
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let responder_kem = KEMKeys::new(responder_mceliece_keypair, responder_mlkem_keypair);
|
||||
|
||||
let r_dir_hash_mlkem = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
);
|
||||
|
||||
let r_dir_hash_mceliece = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref(),
|
||||
);
|
||||
let initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let _i_dir_hash_mlkem = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
initiator_mlkem_keypair.public_key().as_slice(),
|
||||
);
|
||||
|
||||
let _i_dir_hash_mceliece = hash_encapsulation_key(
|
||||
hash_function,
|
||||
HashLength::Default.value(),
|
||||
initiator_mceliece_keypair.pk.as_ref(),
|
||||
);
|
||||
|
||||
let responder = KKTResponder::new(
|
||||
&responder_x25519_keypair,
|
||||
&responder_kem,
|
||||
&[
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
],
|
||||
&[SignatureScheme::Ed25519],
|
||||
&[1],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// OneWay - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
let result = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
assert_eq!(
|
||||
result.encapsulation_key.as_bytes(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
)
|
||||
}
|
||||
// Mutual - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
// if we keep unverified keys, this should change
|
||||
assert!(processed_request.remote_encapsulation_key.is_none());
|
||||
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.ml_kem768_encapsulation_key().as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
// decryption - responder frame
|
||||
// OneWay - McEliece
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref()
|
||||
)
|
||||
}
|
||||
// Mutual - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
Some(payload.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let processed_request = responder.process_request(request, payload.len()).unwrap();
|
||||
|
||||
assert_eq!(processed_request.request_payload, payload);
|
||||
|
||||
// if we keep unverified keys, this should change
|
||||
assert!(processed_request.remote_encapsulation_key.is_none());
|
||||
|
||||
let processed_response = initiator
|
||||
.process_response(processed_request.response, 0)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, r_context) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
assert_eq!(
|
||||
processed_response.encapsulation_key.as_bytes(),
|
||||
responder_kem.mc_eliece_encapsulation_key().as_ref()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
|
||||
|
||||
use crate::error::{
|
||||
MaskedByteError,
|
||||
MaskedByteError::{Failure, InvalidLength},
|
||||
};
|
||||
|
||||
pub const MASKED_BYTE_LEN: usize = 16;
|
||||
pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1";
|
||||
|
||||
const U8_RANGE: [u8; 256] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
|
||||
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
|
||||
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
|
||||
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
|
||||
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
|
||||
136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
|
||||
155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
|
||||
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192,
|
||||
193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
|
||||
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
|
||||
231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
|
||||
250, 251, 252, 253, 254, 255,
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MaskedByte([u8; MASKED_BYTE_LEN]);
|
||||
|
||||
impl MaskedByte {
|
||||
/// Mask a byte by hashing it with some mask.
|
||||
/// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte)
|
||||
pub fn new(byte: u8, mask: &[u8]) -> Self {
|
||||
let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(MASKED_BYTE_CONTEXT_STR);
|
||||
hasher.update(mask);
|
||||
// avoid zero update
|
||||
hasher.update(&[0xFF, byte]);
|
||||
hasher.finalize_xof_into(&mut output);
|
||||
|
||||
Self(output)
|
||||
}
|
||||
/// Unmasks a byte by trial hashing.
|
||||
/// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF).
|
||||
/// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max).
|
||||
/// If we find an `i` which yields back the hash input, then we found the masked byte.
|
||||
/// Otherwise, the function returns an error.
|
||||
pub fn unmask(&self, mask: &[u8]) -> Result<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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN);
|
||||
out.extend_from_slice(self.dh_pubkey.as_ref());
|
||||
out.extend_from_slice(self.masked_version_bytes.as_slice());
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<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(),
|
||||
});
|
||||
}
|
||||
// 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>,
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// 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 and XWing.
|
||||
///
|
||||
/// This protocol is safe if it runs under a trusted secure channel.
|
||||
///
|
||||
/// Bandwidth costs:
|
||||
/// Request (MlKem768): 1216 bytes
|
||||
/// Response (MlKem768): 1088 bytes
|
||||
/// Request (XWing): 1248 bytes
|
||||
/// Response (XWing): 1120 bytes
|
||||
use libcrux_kem::*;
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
/// Context string to be used with the Blake3 KDF.
|
||||
const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1";
|
||||
|
||||
pub struct RekeyInitiator {
|
||||
algorithm: Algorithm,
|
||||
decapsulation_key: PrivateKey,
|
||||
salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl Debug for RekeyInitiator {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let key_typ = match self.decapsulation_key {
|
||||
PrivateKey::X25519(_) => "x25519",
|
||||
PrivateKey::P256(_) => "p256",
|
||||
PrivateKey::MlKem512(_) => "ml512",
|
||||
PrivateKey::MlKem768(_) => "mlkem768",
|
||||
PrivateKey::X25519MlKem768Draft00(_) => "x25519-mlkem768",
|
||||
PrivateKey::XWingKemDraft06(_) => "xwing",
|
||||
PrivateKey::MlKem1024(_) => "ml1024",
|
||||
};
|
||||
|
||||
f.debug_struct("RekeyInitiator")
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("decapsulation_key", &key_typ)
|
||||
.field("salt", &self.salt)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RekeyInitiator {
|
||||
/// The Initiator generates an ephemeral KEM keypair and a 32-byte salt.
|
||||
/// The Initiator keeps the decapsulation key and generates a request message.
|
||||
/// The request message contains the salt and an encoding of the encapsulation key as follows
|
||||
/// salt encapsulation_key
|
||||
/// [0 ........ 32 | 32 .............. ]
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
|
||||
///
|
||||
/// Outputs:
|
||||
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
|
||||
/// Vec<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 MlKem768
|
||||
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
|
||||
// We don't support McEliece because the keys are massive.
|
||||
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
|
||||
mceliece::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// We don't support X25519 because it's not post-quantum secure.
|
||||
x25519::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// Reject if the size does not match any of the above.
|
||||
_ => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "Unknown Algorithm",
|
||||
});
|
||||
}
|
||||
},
|
||||
// Reject if message length is less than 32.
|
||||
None => {
|
||||
return Err(KKTError::DecodingError {
|
||||
info: "Invalid rekey request: size is too small",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Split the message to get the Initiator's salt (first 32 bytes)
|
||||
// and the encoding of the Initiator's public key.
|
||||
let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32);
|
||||
|
||||
// Attempt to decode the Initiator's encapsulation key.
|
||||
let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?;
|
||||
|
||||
// Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`.
|
||||
let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?;
|
||||
// Encode the ciphertext into bytes to send back to the initiator.
|
||||
let message = ciphertext.encode();
|
||||
|
||||
// Encode the `pre_key` into bytes
|
||||
let pre_key_bytes = pre_key.encode();
|
||||
|
||||
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, remote_salt);
|
||||
|
||||
// Zeroize the Initiator's salt
|
||||
remote_salt.zeroize();
|
||||
|
||||
Ok((new_secret, message))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::KKTError;
|
||||
use crate::rekey::{RekeyInitiator, responder_process};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
|
||||
#[test]
|
||||
fn rekey_test() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let (rekey_state, request_message) =
|
||||
RekeyInitiator::generate_request(&mut rng, KEM::MlKem768).unwrap();
|
||||
|
||||
let (responder_secret, response_message) =
|
||||
responder_process(&mut rng, request_message).unwrap();
|
||||
|
||||
let initiator_secret = rekey_state.finalize(&response_message).unwrap();
|
||||
|
||||
assert_eq!(initiator_secret, responder_secret);
|
||||
|
||||
// mceliece should fail
|
||||
let err = RekeyInitiator::generate_request(&mut rng, KEM::McEliece).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
}
|
||||
.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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, SignatureScheme};
|
||||
|
||||
/// Representation of a KKT Responder
|
||||
pub struct KKTResponder<'a> {
|
||||
/// Long-term x25519 DH key pair of this Responder
|
||||
x25519_keypair: &'a DHKeyPair,
|
||||
|
||||
/// KEM keys of this responder
|
||||
kem_keys: &'a KEMKeys,
|
||||
|
||||
/// List of supported Hash Functions by this Responder
|
||||
supported_hash_functions: Vec<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,
|
||||
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,
|
||||
supported_hash_functions: supported_hash_functions.to_vec(),
|
||||
supported_signature_schemes: supported_signature_schemes.to_vec(),
|
||||
supported_outer_protocol_versions: supported_outer_protocol_versions.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn check_ciphersuite_compatiblity(
|
||||
&self,
|
||||
remote_ciphersuite: Ciphersuite,
|
||||
) -> Result<(), KKTError> {
|
||||
let r_hash = remote_ciphersuite.hash_function();
|
||||
let r_sig = remote_ciphersuite.signature_scheme();
|
||||
|
||||
if !self.supported_hash_functions.contains(&r_hash) {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported HashFunction",
|
||||
});
|
||||
}
|
||||
|
||||
if !self.supported_signature_schemes.contains(&r_sig) {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported SignatureScheme",
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// When this function fails, we do that silently (i.e. we don't generate a response to the initiator).
|
||||
|
||||
pub fn process_request(
|
||||
&self,
|
||||
request: KKTRequest,
|
||||
request_payload_len: usize,
|
||||
) -> Result<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 => {
|
||||
// So we can either fetch the remote hash here using some async call to the directory,
|
||||
// which might make registration hang or accept the sent key then verify later.
|
||||
|
||||
// If we choose to not accept, the response's status will be KKTStatus::UnverifiedKEMKey.
|
||||
// The response would still contain the responder's encapsulation key.
|
||||
responder_ingest_message(None, remote_frame)?
|
||||
}
|
||||
};
|
||||
|
||||
let kem = local_context.ciphersuite().kem();
|
||||
let Some(kem_key) = self.kem_keys.encoded_encapsulation_key(kem) else {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported KEM",
|
||||
});
|
||||
};
|
||||
|
||||
// for now the response payload is empty
|
||||
let response_payload = Vec::with_capacity(0);
|
||||
|
||||
let frame = KKTFrame::new(local_context, kem_key, response_payload);
|
||||
|
||||
// encryption - responder frame
|
||||
let encrypted_frame = carrier.encrypt(&frame.try_to_bytes()?)?;
|
||||
Ok(ProcessedKKTRequest {
|
||||
response: KKTResponse { encrypted_frame },
|
||||
remote_encapsulation_key,
|
||||
requested_kem: remote_context.ciphersuite().kem(),
|
||||
outer_protocol_version: processed_req.outer_protocol_version,
|
||||
request_payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_ingest_message(
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: KKTFrame,
|
||||
) -> Result<(KKTContext, Option<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
|
||||
|
||||
// we could still keep it here and then verify later...
|
||||
// let received_encapsulation_key = EncapsulationKey::decode(
|
||||
// own_context.ciphersuite().kem(),
|
||||
// remote_frame.body_ref(),
|
||||
// )?;
|
||||
// Ok((own_context, Some(received_encapsulation_key)))
|
||||
//
|
||||
return Ok((own_context, None));
|
||||
};
|
||||
|
||||
if !validate_encapsulation_key(
|
||||
cs.hash_function(),
|
||||
cs.hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
// The key does not match the hash obtained from the directory
|
||||
return Err(KKTError::MismatchedKEMHash);
|
||||
}
|
||||
let remote_key =
|
||||
EncapsulationKey::try_from_bytes(remote_frame.body(), cs.kem())?;
|
||||
Ok((own_context, Some(remote_key)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::{KKT_SESSION_ID_LEN, KKTFrame},
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
pub fn initiator_process<'a, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
|
||||
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0; KKT_SESSION_ID_LEN];
|
||||
// Generate Session ID
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => &encaps_key.encode(),
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(context.full_message_len() - context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&context_bytes);
|
||||
bytes_to_sign.extend_from_slice(body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok((
|
||||
context,
|
||||
KKTFrame::new(context_bytes, body, session_id, &signature),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn anonymous_initiator_process<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: &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: &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(())
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
[package]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
@@ -1,139 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "io-mocks")]
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::debug;
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransport: Sized {
|
||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
||||
|
||||
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `packet_data` - The serialised LP packet to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
|
||||
|
||||
/// Receives an LP packet from a TCP stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
async fn send_serialised_packet_async_write<W>(
|
||||
writer: &mut W,
|
||||
packet_data: &[u8],
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
writer
|
||||
.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Send the actual packet data
|
||||
writer
|
||||
.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Flush to ensure data is sent immediately
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader
|
||||
.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
reader
|
||||
.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
}
|
||||
|
||||
impl LpTransport for TcpStream {
|
||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self> {
|
||||
TcpStream::connect(endpoint).await
|
||||
}
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()> {
|
||||
// Set TCP_NODELAY for low latency
|
||||
self.set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "io-mocks")]
|
||||
impl LpTransport for MockIOStream {
|
||||
async fn connect(_endpoint: SocketAddr) -> std::io::Result<Self> {
|
||||
Ok(MockIOStream::default())
|
||||
}
|
||||
|
||||
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
@@ -7,50 +7,36 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||
rand09 = { workspace = true }
|
||||
tls_codec = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-crypto = { path = "../crypto", features = ["hashing"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-lp-transport = { path = "../nym-lp-transport" }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||
"test-utils",
|
||||
] }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
tls_codec = { workspace = true }
|
||||
libcrux-psq = { workspace = true, features = ["test-utils"] }
|
||||
num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
|
||||
# needed for the 'mock 'feature
|
||||
nym-test-utils = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
#rand_chacha = "0.3"
|
||||
mock_instant = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
|
||||
|
||||
|
||||
[features]
|
||||
mock = ["nym-test-utils", "nym-crypto/rand"]
|
||||
mock = ["nym-test-utils"]
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
harness = false
|
||||
harness = false
|
||||
@@ -1,9 +1,8 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use nym_test_utils::helpers::u64_seeded_rng;
|
||||
use parking_lot::Mutex;
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
use nym_test_utils::helpers::deterministic_rng_09;
|
||||
use rand09::Rng;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_sequential");
|
||||
@@ -47,8 +46,8 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = u64_seeded_rng(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
let mut rng = deterministic_rng_09();
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.random_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut validator = validator.clone();
|
||||
@@ -75,19 +74,15 @@ fn bench_thread_safety(c: &mut Criterion) {
|
||||
BenchmarkId::new("thread_safe_validator", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default()));
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
let counters: Vec<u64> = (0..size).collect();
|
||||
|
||||
b.iter(|| {
|
||||
for &counter in &counters {
|
||||
let result = {
|
||||
let guard = validator.lock();
|
||||
black_box(guard.will_accept_branchless(counter))
|
||||
};
|
||||
let result = { black_box(validator.will_accept_branchless(counter)) };
|
||||
|
||||
if result.is_ok() {
|
||||
let mut guard = validator.lock();
|
||||
let _ = black_box(guard.mark_did_receive_branchless(counter));
|
||||
let _ = black_box(validator.mark_did_receive_branchless(counter));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -202,7 +197,7 @@ fn bench_concurrency_scaling(c: &mut Criterion) {
|
||||
let mut success_count = 0;
|
||||
for i in 0..100 {
|
||||
let counter = t * 1000 + i;
|
||||
let mut guard = validator_clone.lock();
|
||||
let mut guard = validator_clone.lock().unwrap();
|
||||
if guard.mark_did_receive_branchless(counter as u64).is_ok() {
|
||||
success_count += 1;
|
||||
}
|
||||
|
||||
+233
-1263
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ pub const DEFAULT_PSK_TTL_SECS: u64 = 3600;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LpConfig {
|
||||
/// KEM algorithm for PSQ key encapsulation.
|
||||
/// X25519 = classical (testing), MlKem768 = PQ, XWing = hybrid.
|
||||
/// Supported KEMs: MlKem768, McEliece
|
||||
#[serde(with = "kem_serde")]
|
||||
pub kem_algorithm: KEM,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct LpConfig {
|
||||
impl Default for LpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kem_algorithm: KEM::X25519,
|
||||
kem_algorithm: KEM::MlKem768,
|
||||
psk_ttl_secs: DEFAULT_PSK_TTL_SECS,
|
||||
enable_kkt: true,
|
||||
}
|
||||
@@ -55,10 +55,10 @@ mod kem_serde {
|
||||
S: Serializer,
|
||||
{
|
||||
match kem {
|
||||
KEM::X25519 => "X25519",
|
||||
KEM::MlKem768 => "MlKem768",
|
||||
KEM::XWing => "XWing",
|
||||
KEM::McEliece => "McEliece",
|
||||
KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")),
|
||||
KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
@@ -69,10 +69,10 @@ mod kem_serde {
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"X25519" => Ok(KEM::X25519),
|
||||
"MlKem768" => Ok(KEM::MlKem768),
|
||||
"XWing" => Ok(KEM::XWing),
|
||||
"McEliece" => Ok(KEM::McEliece),
|
||||
"X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")),
|
||||
"XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")),
|
||||
_ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))),
|
||||
}
|
||||
}
|
||||
|
||||
+64
-50
@@ -1,11 +1,16 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::MessageType;
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||
use crate::packet::MalformedLpPacketError;
|
||||
use crate::peer_config::LpReceiverIndex;
|
||||
use crate::replay::ReplayError;
|
||||
use crate::transport::LpTransportError;
|
||||
use libcrux_psq::handshake::HandshakeError;
|
||||
use libcrux_psq::handshake::builders::BuilderError;
|
||||
use libcrux_psq::session::SessionError;
|
||||
// use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::error::KKTError;
|
||||
use nym_kkt_ciphersuite::{HashFunction, KEM};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -13,33 +18,18 @@ pub enum LpError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Snow Error: {0}")]
|
||||
SnowKeyError(#[from] snow::Error),
|
||||
|
||||
#[error("Snow Pattern Error: {0}")]
|
||||
SnowPatternError(String),
|
||||
|
||||
#[error("Noise Protocol Error: {0}")]
|
||||
NoiseError(#[from] NoiseError),
|
||||
|
||||
#[error("Replay detected: {0}")]
|
||||
Replay(#[from] ReplayError),
|
||||
|
||||
#[error("Invalid packet format: {0}")]
|
||||
InvalidPacketFormat(String),
|
||||
|
||||
#[error("Invalid message type: {0}")]
|
||||
InvalidMessageType(u32),
|
||||
|
||||
#[error("Payload too large: {0}")]
|
||||
PayloadTooLarge(usize),
|
||||
|
||||
#[error("Insufficient buffer size provided")]
|
||||
InsufficientBufferSize,
|
||||
|
||||
#[error("Attempted operation on closed session")]
|
||||
SessionClosed,
|
||||
|
||||
#[error("There already exists an LP session with receiver index {0}")]
|
||||
DuplicateSessionId(LpReceiverIndex),
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
@@ -52,15 +42,12 @@ pub enum LpError {
|
||||
#[error("Deserialization error: {0}")]
|
||||
DeserializationError(String),
|
||||
|
||||
#[error("KKT protocol error: {0}")]
|
||||
KKTError(String),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidBase58String(#[from] bs58::decode::Error),
|
||||
|
||||
/// Session ID from incoming packet does not match any known session.
|
||||
#[error("Received packet with unknown session ID: {0}")]
|
||||
UnknownSessionId(u32),
|
||||
UnknownSessionId(LpReceiverIndex),
|
||||
|
||||
/// Invalid state transition attempt in the state machine.
|
||||
#[error("Invalid input '{input}' for current state '{state}'")]
|
||||
@@ -75,27 +62,14 @@ pub enum LpError {
|
||||
LpSessionProcessing,
|
||||
|
||||
/// State machine not found.
|
||||
#[error("State machine not found for lp_id: {lp_id}")]
|
||||
StateMachineNotFound { lp_id: u32 },
|
||||
#[error("State machine not found for lp_id: {0}")]
|
||||
StateMachineNotFound(LpReceiverIndex),
|
||||
|
||||
/// Ed25519 to X25519 conversion error.
|
||||
#[error("Ed25519 key conversion error: {0}")]
|
||||
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
|
||||
|
||||
/// Outer AEAD authentication tag verification failed.
|
||||
#[error("AEAD authentication tag verification failed")]
|
||||
AeadTagMismatch,
|
||||
|
||||
/// Received an LP packet with an incompatible, future, version
|
||||
#[error("incompatible LP packet version. got: {got}, highest supported: {highest_supported}")]
|
||||
IncompatibleFuturePacketVersion { got: u8, highest_supported: u8 },
|
||||
|
||||
/// Received an LP packet with an incompatible, legacy, version
|
||||
#[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")]
|
||||
IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 },
|
||||
|
||||
#[error("attempted to create an LP responder without providing a valid KEM key")]
|
||||
ResponderWithMissingKEMKey,
|
||||
// /// Ed25519 to X25519 conversion error.
|
||||
// #[error("Ed25519 key conversion error: {0}")]
|
||||
// Ed25519RecoveryError(#[from] Ed25519RecoveryError),
|
||||
#[error("attempted to create an LP responder without providing a valid KEM keys")]
|
||||
ResponderWithMissingKEMKeys,
|
||||
|
||||
#[error(
|
||||
"there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function"
|
||||
@@ -113,16 +87,56 @@ pub enum LpError {
|
||||
#[from]
|
||||
source: KKTError,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
MalformedPacket(#[from] MalformedLpPacketError),
|
||||
|
||||
#[error("version {version} is not supported")]
|
||||
UnsupportedVersion { version: u8 },
|
||||
|
||||
#[error("failed to build PSQ responder: {inner:?}")]
|
||||
PSQResponderBuilderFailure { inner: BuilderError },
|
||||
|
||||
#[error("failed to build PSQ initiator: {inner:?}")]
|
||||
PSQInitiatorBuilderFailure { inner: BuilderError },
|
||||
|
||||
#[error("failed to complete the PSQ handshake: {inner:?}")]
|
||||
PSQHandshakeFailure { inner: HandshakeError },
|
||||
|
||||
#[error("failed to run the PSQ session: {inner:?}")]
|
||||
PSQSessionFailure { inner: SessionError },
|
||||
|
||||
#[error("failed to derive a transport channel: {inner:?}")]
|
||||
TransportDerivationFailure { inner: SessionError },
|
||||
|
||||
#[error("the initiator authenticator is not available after ingesting PSQ msg1")]
|
||||
MissingInitiatorAuthenticator,
|
||||
|
||||
#[error("transport failure: {0}")]
|
||||
TransportFailure(#[from] LpTransportError),
|
||||
|
||||
#[error("the current session is not in transport state")]
|
||||
NotInTransport,
|
||||
}
|
||||
|
||||
impl LpError {
|
||||
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
|
||||
Self::KKTPSQHandshake(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
|
||||
Self::KKTPSQHandshake(format!(
|
||||
"received unexpected response, got: {got:?}, expected: {expected:?}"
|
||||
))
|
||||
impl From<HandshakeError> for LpError {
|
||||
fn from(handshake_error: HandshakeError) -> Self {
|
||||
Self::PSQHandshakeFailure {
|
||||
inner: handshake_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionError> for LpError {
|
||||
fn from(session_error: SessionError) -> Self {
|
||||
Self::PSQSessionFailure {
|
||||
inner: session_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,492 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! KKT (Key Encapsulation Transport) orchestration for nym-lp sessions.
|
||||
//!
|
||||
//! This module provides functions to perform KKT key exchange before establishing
|
||||
//! an nym-lp session. The KKT protocol allows secure distribution of post-quantum
|
||||
//! KEM public keys, which are then used with PSQ to derive a strong pre-shared key
|
||||
//! for the Noise protocol.
|
||||
//!
|
||||
//! # Protocol Flow
|
||||
//!
|
||||
//! 1. **Client (Initiator)**:
|
||||
//! - Calls `create_request()` to generate a KKT request
|
||||
//! - Sends `LpMessage::KKTRequest` to gateway
|
||||
//! - Receives `LpMessage::KKTResponse` from gateway
|
||||
//! - Calls `process_response()` to validate and extract gateway's KEM key
|
||||
//!
|
||||
//! 2. **Gateway (Responder)**:
|
||||
//! - Receives `LpMessage::KKTRequest` from client
|
||||
//! - Calls `handle_request()` to validate request and generate response
|
||||
//! - Sends `LpMessage::KKTResponse` to client
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use nym_lp::kkt_orchestrator::{create_request, process_response, handle_request};
|
||||
//! use nym_lp::message::{KKTRequestData, KKTResponseData};
|
||||
//! use nym_kkt::ciphersuite::{Ciphersuite, KEM, HashFunction, SignatureScheme, EncapsulationKey};
|
||||
//!
|
||||
//! // Setup ciphersuite
|
||||
//! let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
//! KEM::X25519,
|
||||
//! HashFunction::Blake3,
|
||||
//! SignatureScheme::Ed25519,
|
||||
//! None,
|
||||
//! ).unwrap();
|
||||
//!
|
||||
//! // Client: Create request
|
||||
//! let (session_secret, client_context, request_data) = create_request(
|
||||
//! ciphersuite,
|
||||
//! &client_signing_key,
|
||||
//! &responder_dh_public_key
|
||||
//! ).unwrap();
|
||||
//!
|
||||
//! // Gateway: Handle request
|
||||
//! let response_data = handle_request(
|
||||
//! &request_data,
|
||||
//! Some(&client_verification_key),
|
||||
//! &gateway_signing_key,
|
||||
//! &gateway_dh_private_key,
|
||||
//! &gateway_kem_public_key,
|
||||
//! ).unwrap();
|
||||
//!
|
||||
//! // Client: Process response
|
||||
//! let gateway_kem_key = process_response(
|
||||
//! client_context,
|
||||
//! &session_secret,
|
||||
//! &gateway_verification_key,
|
||||
//! &expected_key_hash,
|
||||
//! &response_data,
|
||||
//! ).unwrap();
|
||||
//! ```
|
||||
|
||||
use crate::LpError;
|
||||
use crate::message::{KKTRequestData, KKTResponseData};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, EncapsulationKey};
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::KKTSessionSecret;
|
||||
use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response};
|
||||
|
||||
/// Creates a KKT request to obtain the responder's KEM public key.
|
||||
///
|
||||
/// This is called by the **client (initiator)** to begin the KKT exchange.
|
||||
/// The returned context must be used when processing the response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Gateway's x25519 public key (from directory)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTSessionSecret` - Session secret key to encrypt/decrypt KKT messages for this session
|
||||
/// * `KKTContext` - Context to use when validating the response
|
||||
/// * `KKTRequestData` - Serialized KKT request frame to send to gateway
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `LpError::KKTError` if KKT request generation fails.
|
||||
pub fn create_request(
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_public_key: &x25519::PublicKey,
|
||||
) -> Result<(KKTSessionSecret, KKTContext, KKTRequestData), LpError> {
|
||||
// Note: Uses rand 0.9's thread_rng() to match nym-kkt's rand version
|
||||
let mut rng = rand09::rng();
|
||||
let (session_secret, context, request_bytes) =
|
||||
request_kem_key(&mut rng, ciphersuite, signing_key, responder_dh_public_key)
|
||||
.map_err(|e| LpError::KKTError(e.to_string()))?;
|
||||
|
||||
Ok((session_secret, context, KKTRequestData(request_bytes)))
|
||||
}
|
||||
|
||||
/// Processes a KKT response and extracts the responder's KEM public key.
|
||||
///
|
||||
/// This is called by the **client (initiator)** after receiving a KKT response
|
||||
/// from the gateway. It verifies the signature and validates the key hash.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context from the initial `create_request()` call
|
||||
/// * `session_secret` - The KKT session secret key from the initial `create_request()` call
|
||||
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
|
||||
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
|
||||
/// * `response_data` - Serialized KKT response frame from responder
|
||||
///
|
||||
/// # Returns
|
||||
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `LpError::KKTError` if:
|
||||
/// - Response deserialization fails
|
||||
/// - Signature verification fails
|
||||
/// - Key hash doesn't match expected value
|
||||
pub fn process_response<'a>(
|
||||
mut context: KKTContext,
|
||||
session_secret: &KKTSessionSecret,
|
||||
responder_vk: &ed25519::PublicKey,
|
||||
expected_key_hash: &[u8],
|
||||
response_data: &KKTResponseData,
|
||||
) -> Result<EncapsulationKey<'a>, LpError> {
|
||||
validate_kem_response(
|
||||
&mut context,
|
||||
session_secret,
|
||||
responder_vk,
|
||||
expected_key_hash,
|
||||
&response_data.0,
|
||||
)
|
||||
.map_err(|e| LpError::KKTError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Handles a KKT request and generates a signed response with the responder's KEM key.
|
||||
///
|
||||
/// This is called by the **gateway (responder)** when receiving a KKT request
|
||||
/// from a client. It validates the request signature (if authenticated) and
|
||||
/// responds with the gateway's KEM public key, signed for authenticity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_data` - Serialized KKT request frame from initiator
|
||||
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
|
||||
/// * `responder_signing_key` - Gateway's Ed25519 signing key
|
||||
/// * `responder_dh_private_key` - Gateway's x25519 private key
|
||||
/// * `responder_kem_key` - Gateway's KEM public key to send
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTResponseData` - Signed response frame containing the KEM public key
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `LpError::KKTError` if:
|
||||
/// - Request deserialization fails
|
||||
/// - Signature verification fails (if authenticated)
|
||||
/// - Response generation fails
|
||||
pub fn handle_request<'a>(
|
||||
request_data: &KKTRequestData,
|
||||
initiator_vk: Option<&ed25519::PublicKey>,
|
||||
responder_signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_private_key: &x25519::PrivateKey,
|
||||
responder_kem_key: &EncapsulationKey<'a>,
|
||||
) -> Result<KKTResponseData, LpError> {
|
||||
let mut rng = rand09::rng();
|
||||
// Handle the request and generate response
|
||||
let response_bytes = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_data.0,
|
||||
initiator_vk,
|
||||
responder_signing_key,
|
||||
responder_dh_private_key,
|
||||
responder_kem_key,
|
||||
)
|
||||
.map_err(|e| LpError::KKTError(e.to_string()))?;
|
||||
|
||||
Ok(KKTResponseData(response_bytes))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme};
|
||||
use nym_kkt::key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519,
|
||||
hash_encapsulation_key,
|
||||
};
|
||||
use rand09::RngCore;
|
||||
|
||||
#[test]
|
||||
fn test_kkt_roundtrip_authenticated() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
// Generate responder's KEM keypair (X25519 for testing)
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// Create ciphersuite
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Hash the KEM key (simulating directory storage)
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Client: Create request
|
||||
let (session_secret, context, request_data) = create_request(
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
responder_x25519.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway: Handle request
|
||||
let response_data = handle_request(
|
||||
&request_data,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
responder_x25519.private_key(),
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client: Process response
|
||||
let obtained_key = process_response(
|
||||
context,
|
||||
&session_secret,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&key_hash,
|
||||
&response_data,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify we got the correct KEM key
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_kkt_roundtrip_anonymous() {
|
||||
// let mut rng = rand09::rng();
|
||||
|
||||
// // Only responder has keys (anonymous initiator)
|
||||
// // Generate Ed25519 keypairs for both parties
|
||||
|
||||
// let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
// let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
// let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
// let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
// KEM::X25519,
|
||||
// HashFunction::Blake3,
|
||||
// SignatureScheme::Ed25519,
|
||||
// None,
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// let key_hash = hash_encapsulation_key(
|
||||
// &ciphersuite.hash_function(),
|
||||
// ciphersuite.hash_len(),
|
||||
// &responder_kem_key.encode(),
|
||||
// );
|
||||
|
||||
// // Anonymous initiator - use anonymous_initiator_process directly
|
||||
// use nym_kkt::kkt::anonymous_initiator_process;
|
||||
// let (mut context, request_frame) =
|
||||
// anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
// let request_data = KKTRequestData(request_frame.to_bytes());
|
||||
|
||||
// // Gateway: Handle anonymous request
|
||||
// let response_data = handle_request(
|
||||
// &request_data,
|
||||
// None,
|
||||
// responder_ed25519_keypair.private_key(),
|
||||
// &responder_x25519_sk,
|
||||
// &responder_kem_key,
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// // Initiator: Validate response
|
||||
// let obtained_key = initiator_ingest_response(
|
||||
// &mut context,
|
||||
// responder_ed25519_keypair.public_key(),
|
||||
// &key_hash,
|
||||
// &response_data.0,
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
// Different keypair for wrong signature
|
||||
let mut wrong_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut wrong_secret);
|
||||
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_session_secret, _context, request_data) = create_request(
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
responder_x25519.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway handles request but we provide WRONG verification key
|
||||
let result = handle_request(
|
||||
&request_data,
|
||||
Some(wrong_keypair.public_key()), // Wrong key!
|
||||
responder_ed25519_keypair.private_key(),
|
||||
responder_x25519.private_key(),
|
||||
&responder_kem_key,
|
||||
);
|
||||
|
||||
// Should fail signature verification
|
||||
assert!(result.is_err());
|
||||
if let Err(LpError::KKTError(_)) = result {
|
||||
// Expected
|
||||
} else {
|
||||
panic!("Expected KKTError");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let (init, resp) = mock_peers();
|
||||
let responder_kem_key = resp.encapsulate_kem_key().unwrap();
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_secret, context, request_data) = create_request(
|
||||
ciphersuite,
|
||||
init.ed25519.private_key(),
|
||||
resp.x25519.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response_data = handle_request(
|
||||
&request_data,
|
||||
Some(init.ed25519.public_key()),
|
||||
resp.ed25519.private_key(),
|
||||
resp.x25519.private_key(),
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = process_response(
|
||||
context,
|
||||
&session_secret,
|
||||
resp.ed25519.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
&response_data,
|
||||
);
|
||||
|
||||
// Should fail hash validation
|
||||
assert!(result.is_err());
|
||||
if let Err(LpError::KKTError(_)) = result {
|
||||
// Expected
|
||||
} else {
|
||||
panic!("Expected KKTError");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_request_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// Create malformed request data (invalid bytes)
|
||||
let malformed_request = KKTRequestData(vec![0xFF; 100]);
|
||||
|
||||
let result = handle_request(
|
||||
&malformed_request,
|
||||
None,
|
||||
responder_ed25519_keypair.private_key(),
|
||||
responder_x25519.private_key(),
|
||||
&responder_kem_key,
|
||||
);
|
||||
|
||||
// Should fail to parse
|
||||
assert!(result.is_err());
|
||||
if let Err(LpError::KKTError(_)) = result {
|
||||
// Expected
|
||||
} else {
|
||||
panic!("Expected KKTError");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_response_rejected() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
let responder_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (session_secret, context, _request_data) = create_request(
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
responder_x25519.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create malformed response data
|
||||
let malformed_response = KKTResponseData(vec![0xFF; 100]);
|
||||
let key_hash = [0u8; 32];
|
||||
|
||||
let result = process_response(
|
||||
context,
|
||||
&session_secret,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&key_hash,
|
||||
&malformed_response,
|
||||
);
|
||||
|
||||
// Should fail to parse
|
||||
assert!(result.is_err());
|
||||
if let Err(LpError::KKTError(_)) = result {
|
||||
// Expected
|
||||
} else {
|
||||
panic!("Expected KKTError");
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-311
@@ -2,32 +2,40 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod codec;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
// pub mod kkt_orchestrator;
|
||||
pub mod message;
|
||||
pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod peer;
|
||||
pub mod psk;
|
||||
mod psq;
|
||||
pub mod peer_config;
|
||||
pub mod psq;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
pub mod session_manager;
|
||||
pub mod state_machine;
|
||||
pub mod transport;
|
||||
|
||||
pub use config::LpConfig;
|
||||
pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||
pub use nym_kkt_ciphersuite::{
|
||||
Ciphersuite, HashFunction, HashLength, KEM, KEMKeyDigests, SignatureScheme,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
use nym_test_utils::helpers::u64_seeded_rng_09;
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
use crate::psq::{PSQ_MSG2_SIZE, initiator, psq_msg1_size, responder};
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
use crate::session::PersistentSessionBinding;
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
use libcrux_psq::{Channel, IntoSession};
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub struct SessionsMock {
|
||||
@@ -37,118 +45,103 @@ pub struct SessionsMock {
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl SessionsMock {
|
||||
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
|
||||
pub fn mock_seeded_post_handshake(seed: u64, kem: KEM) -> SessionsMock {
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use crate::peer_config::LpReceiverIndex;
|
||||
use rand09::Rng;
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
let init_remote = init.as_remote();
|
||||
let salt = [42u8; 32];
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
|
||||
let mut init_rng = u64_seeded_rng_09(seed);
|
||||
let resp_rng = u64_seeded_rng_09(seed + 1);
|
||||
|
||||
let receiver_index: LpReceiverIndex = init_rng.random();
|
||||
|
||||
let kem_keys = resp.kem_keypairs.as_ref().unwrap();
|
||||
|
||||
// skip KKT by just deriving the kem key locally
|
||||
let kem_keys = resp.kem_psq.as_ref().unwrap();
|
||||
let encapsulation_key = kem_keys.encapsulation_key(kem).unwrap();
|
||||
let enc_key = encapsulation_key.clone();
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
let initiator_ciphersuite =
|
||||
initiator::build_psq_ciphersuite(&init, &resp_remote, &enc_key).unwrap();
|
||||
let mut initiator =
|
||||
initiator::build_psq_principal(init_rng, 1, initiator_ciphersuite).unwrap();
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem).unwrap();
|
||||
let mut responder =
|
||||
responder::build_psq_principal(resp_rng, 1, responder_ciphersuite).unwrap();
|
||||
|
||||
// INIT -> RESP: PSQ MSG1
|
||||
let psq_initiator = crate::psk::psq_initiator_create_message(
|
||||
init.x25519.private_key(),
|
||||
&resp_remote.x25519_public,
|
||||
&encapsulation_key,
|
||||
init.ed25519.private_key(),
|
||||
init.ed25519.public_key(),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
// run PSQ
|
||||
let mut payload_buf_responder = vec![0u8; 4096];
|
||||
let mut payload_buf_initiator = vec![0u8; 4096];
|
||||
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
|
||||
// Send first message
|
||||
let mut buf = vec![0u8; psq_msg1_size(kem)];
|
||||
let len_i = initiator.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(len_i, buf.len());
|
||||
|
||||
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(init.x25519().private_key().as_bytes())
|
||||
.remote_public_key(resp_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_initiator()
|
||||
// Read first message
|
||||
let (_, _) = responder
|
||||
.read_message(&buf, &mut payload_buf_responder)
|
||||
.unwrap();
|
||||
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
|
||||
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
let psq_responder = crate::psk::psq_responder_process_message(
|
||||
resp.x25519.private_key(),
|
||||
&init_remote.x25519_public,
|
||||
(&decapsulation_key, &encapsulation_key),
|
||||
&init_remote.ed25519_public,
|
||||
&psq_payload,
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
// Get the authenticator out here, so we can deserialize the session later.
|
||||
let Some(initiator_authenticator) = responder.initiator_authenticator() else {
|
||||
panic!("No initiator authenticator found")
|
||||
};
|
||||
|
||||
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(resp.x25519().private_key().as_bytes())
|
||||
.remote_public_key(init_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_responder()
|
||||
// Respond
|
||||
let mut buf = [0u8; PSQ_MSG2_SIZE];
|
||||
let len_r = responder.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(len_r, buf.len());
|
||||
|
||||
// Finalize on registration initiator
|
||||
let (_, _) = initiator
|
||||
.read_message(&buf, &mut payload_buf_initiator)
|
||||
.unwrap();
|
||||
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
|
||||
noise_protocol_resp.read_message(&noise_msg1).unwrap();
|
||||
|
||||
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
|
||||
noise_protocol_init.read_message(&noise_msg2).unwrap();
|
||||
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
assert!(initiator.is_handshake_finished());
|
||||
assert!(responder.is_handshake_finished());
|
||||
|
||||
assert!(noise_protocol_init.is_handshake_finished());
|
||||
|
||||
noise_protocol_resp.read_message(&noise_msg3).unwrap();
|
||||
assert!(noise_protocol_resp.is_handshake_finished());
|
||||
let binding = PersistentSessionBinding {
|
||||
initiator_authenticator,
|
||||
responder_ecdh_pk: resp_remote.x25519_public,
|
||||
responder_pq_pk: Some(encapsulation_key),
|
||||
};
|
||||
|
||||
SessionsMock {
|
||||
initiator: LpSession::new(
|
||||
session_id,
|
||||
initiator.into_session().unwrap(),
|
||||
binding.clone(),
|
||||
receiver_index,
|
||||
1,
|
||||
outer_aead_key.clone(),
|
||||
init,
|
||||
resp_remote,
|
||||
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
noise_protocol_init,
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
responder: LpSession::new(
|
||||
session_id,
|
||||
responder.into_session().unwrap(),
|
||||
binding,
|
||||
receiver_index,
|
||||
1,
|
||||
outer_aead_key,
|
||||
resp,
|
||||
init_remote,
|
||||
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
noise_protocol_resp,
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_post_handshake(kem: KEM) -> SessionsMock {
|
||||
Self::mock_seeded_post_handshake(1, kem)
|
||||
}
|
||||
|
||||
// we just need a dummy 'valid' session for simpler tests
|
||||
pub fn mock_initiator() -> LpSession {
|
||||
Self::mock_post_handshake(1234).initiator
|
||||
Self::mock_post_handshake(KEM::default()).initiator
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
let sessions = SessionsMock::mock_post_handshake(69);
|
||||
let sessions = SessionsMock::mock_post_handshake(KEM::default());
|
||||
(sessions.initiator, sessions.responder)
|
||||
}
|
||||
|
||||
@@ -156,224 +149,3 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
pub fn mock_session_for_test() -> LpSession {
|
||||
SessionsMock::mock_initiator()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{LpError, SessionsMock, mock_session_for_test};
|
||||
use bytes::BytesMut;
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let mut session = mock_session_for_test();
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42, // Matches session's sending_index assumption for this test
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet1.header.counter)
|
||||
.expect("Initial packet failed replay check");
|
||||
|
||||
// Mark received (simulating successful processing)
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet1.header.counter)
|
||||
.expect("Failed to mark initial packet received");
|
||||
|
||||
// === Packet 2 (Counter 0 - Replay, should fail check) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 0, // Same counter as before (replay)
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error, got {:?}", e),
|
||||
}
|
||||
// Do not mark received as it failed validation
|
||||
|
||||
// === Packet 3 (Counter 1 - Should succeed) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 1, // Incremented counter
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet3.header.counter)
|
||||
.expect("Packet 3 failed replay check");
|
||||
|
||||
// Mark received
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet3.header.counter)
|
||||
.expect("Failed to mark packet 3 received");
|
||||
|
||||
// Verify validator state directly on the session
|
||||
let state = session.current_packet_cnt();
|
||||
assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
|
||||
assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
// Create session manager
|
||||
let mut local_manager = SessionManager::new();
|
||||
let mut remote_manager = SessionManager::new();
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let local_session = sessions.initiator;
|
||||
let remote_session = sessions.responder;
|
||||
|
||||
// Create a session via manager
|
||||
let _ = local_manager.create_session_state_machine(local_session);
|
||||
let _ = remote_manager.create_session_state_machine(remote_session);
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
|
||||
// Process via SessionManager method (which should handle checks + marking)
|
||||
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
|
||||
// that encapsulates parse -> check -> process_noise -> mark.
|
||||
// For now, we simulate the steps using the retrieved session.
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 mark failed");
|
||||
|
||||
// === Packet 2 (Counter 1 - Should succeed on same session) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 1,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 mark failed");
|
||||
|
||||
// === Packet 3 (Counter 0 - Replay, should fail check) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0, // Replay of first packet
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error for packet 3, got {:?}", e),
|
||||
}
|
||||
// Do not mark received
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,892 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClientHelloData {
|
||||
/// Client-proposed receiver index for session identification (4 bytes)
|
||||
/// Auto-generated randomly by the client
|
||||
pub receiver_index: u32,
|
||||
/// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key
|
||||
pub client_lp_public_key: x25519::PublicKey,
|
||||
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
|
||||
pub client_ed25519_public_key: ed25519::PublicKey,
|
||||
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
|
||||
pub salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl ClientHelloData {
|
||||
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
||||
pub const LEN: usize = 100;
|
||||
|
||||
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
|
||||
LpPacket::new(
|
||||
LpHeader::new(
|
||||
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
|
||||
0, // counter starts at 0
|
||||
protocol_version,
|
||||
),
|
||||
LpMessage::ClientHello(self),
|
||||
)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
Self::LEN
|
||||
}
|
||||
|
||||
fn generate_receiver_index() -> u32 {
|
||||
loop {
|
||||
let candidate = rand::random();
|
||||
if candidate != BOOTSTRAP_RECEIVER_IDX {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new ClientHelloData with fresh salt.
|
||||
///
|
||||
/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_lp_public_key` - Client's x25519 public key (derived from Ed25519)
|
||||
/// * `client_ed25519_public_key` - Client's Ed25519 public key (for PSQ authentication)
|
||||
pub fn new_with_fresh_salt(
|
||||
client_lp_public_key: x25519::PublicKey,
|
||||
client_ed25519_public_key: ed25519::PublicKey,
|
||||
timestamp: u64,
|
||||
) -> Self {
|
||||
// Generate salt: timestamp + nonce
|
||||
let mut salt = [0u8; 32];
|
||||
|
||||
// First 8 bytes: current timestamp as u64 little-endian
|
||||
salt[..8].copy_from_slice(×tamp.to_le_bytes());
|
||||
|
||||
// Last 24 bytes: random nonce
|
||||
rand::thread_rng().fill_bytes(&mut salt[8..]);
|
||||
|
||||
Self {
|
||||
receiver_index: Self::generate_receiver_index(), // Auto-generate random receiver index
|
||||
client_lp_public_key,
|
||||
client_ed25519_public_key,
|
||||
salt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the timestamp from the salt.
|
||||
///
|
||||
/// # Returns
|
||||
/// Unix timestamp in seconds
|
||||
pub fn extract_timestamp(&self) -> u64 {
|
||||
let mut timestamp_bytes = [0u8; 8];
|
||||
timestamp_bytes.copy_from_slice(&self.salt[..8]);
|
||||
u64::from_le_bytes(timestamp_bytes)
|
||||
}
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.receiver_index);
|
||||
dst.put_slice(self.client_lp_public_key.as_bytes());
|
||||
dst.put_slice(self.client_ed25519_public_key.as_bytes());
|
||||
dst.put_slice(&self.salt);
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Self, LpError> {
|
||||
if b.len() != Self::LEN {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Expected {} bytes to deserialise ClientHelloData. got {}",
|
||||
Self::LEN,
|
||||
b.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// SAFETY: we checked for valid byte lengths
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let client_lp_public_key_bytes = b[4..36].try_into().unwrap();
|
||||
let client_ed25519_public_key_bytes = b[36..68].try_into().unwrap();
|
||||
|
||||
Ok(ClientHelloData {
|
||||
receiver_index: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
|
||||
client_lp_public_key: x25519::PublicKey::from_byte_array(client_lp_public_key_bytes),
|
||||
client_ed25519_public_key: ed25519::PublicKey::from_byte_array(
|
||||
client_ed25519_public_key_bytes,
|
||||
)?,
|
||||
salt: b[68..].try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to construct remote peer information based on the data provided in this packet.
|
||||
pub fn to_remote_peer(&self) -> LpRemotePeer {
|
||||
LpRemotePeer::new(self.client_ed25519_public_key, self.client_lp_public_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum MessageType {
|
||||
Busy = 0x0000,
|
||||
Handshake = 0x0001,
|
||||
EncryptedData = 0x0002,
|
||||
ClientHello = 0x0003,
|
||||
KKTRequest = 0x0004,
|
||||
KKTResponse = 0x0005,
|
||||
ForwardPacket = 0x0006,
|
||||
/// Receiver index collision - client should retry with new index
|
||||
Collision = 0x0007,
|
||||
/// Acknowledgment - gateway confirms receipt of message
|
||||
Ack = 0x0008,
|
||||
/// Subsession request - client initiates subsession creation
|
||||
SubsessionRequest = 0x0009,
|
||||
/// Subsession KK1 - first message of Noise KK handshake
|
||||
SubsessionKK1 = 0x000A,
|
||||
/// Subsession KK2 - second message of Noise KK handshake
|
||||
SubsessionKK2 = 0x000B,
|
||||
/// Subsession ready - subsession established confirmation
|
||||
SubsessionReady = 0x000C,
|
||||
/// Subsession abort - race winner tells loser to become responder
|
||||
SubsessionAbort = 0x000D,
|
||||
/// General error
|
||||
Error = 0x00FF,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
pub(crate) fn from_u32(value: u32) -> Option<Self> {
|
||||
MessageType::try_from(value).ok()
|
||||
}
|
||||
|
||||
pub fn to_u32(&self) -> u32 {
|
||||
u32::from(*self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandshakeData(pub Vec<u8>);
|
||||
|
||||
impl HandshakeData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(HandshakeData(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EncryptedDataPayload(pub Vec<u8>);
|
||||
|
||||
impl EncryptedDataPayload {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(EncryptedDataPayload(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// KKT request frame data (serialized KKTFrame bytes)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KKTRequestData(pub Vec<u8>);
|
||||
|
||||
impl KKTRequestData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(KKTRequestData(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// KKT response frame data (serialized KKTFrame bytes)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KKTResponseData(pub Vec<u8>);
|
||||
|
||||
impl KKTResponseData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(KKTResponseData(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// General human-readable error message
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorPacketData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorPacketData {
|
||||
pub(crate) fn new(message: impl Into<String>) -> Self {
|
||||
ErrorPacketData {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// length-encoding + message
|
||||
4 + self.message.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.message.len() as u32);
|
||||
dst.put_slice(self.message.as_bytes());
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ErrorPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||
if bytes[4..].len() != message_len {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
|
||||
bytes.len(),
|
||||
4 + message_len
|
||||
)));
|
||||
}
|
||||
|
||||
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
|
||||
|
||||
Ok(ErrorPacketData { message })
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardPacketData {
|
||||
/// Target gateway's Ed25519 identity (32 bytes)
|
||||
pub target_gateway_identity: [u8; 32],
|
||||
|
||||
/// Target gateway's LP address (IP:port string)
|
||||
pub target_lp_address: SocketAddr,
|
||||
|
||||
/// Complete inner LP packet bytes (serialized LpPacket)
|
||||
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
||||
pub inner_packet_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ForwardPacketData {
|
||||
pub fn new(
|
||||
target_gateway_identity: ed25519::PublicKey,
|
||||
target_lp_address: SocketAddr,
|
||||
inner_packet_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
ForwardPacketData {
|
||||
target_gateway_identity: target_gateway_identity.to_bytes(),
|
||||
target_lp_address,
|
||||
inner_packet_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// 32 bytes target gateway identity
|
||||
// +
|
||||
// 1 byte length of target lp address type
|
||||
// +
|
||||
// {4,16} target_lp_address IPv{4,6}
|
||||
// +
|
||||
// 2 bytes target_lp_address port
|
||||
// +
|
||||
// 4 bytes of length of inner packet bytes
|
||||
// +
|
||||
// inner_packet_bytes.len()
|
||||
match self.target_lp_address {
|
||||
SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
|
||||
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
|
||||
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
|
||||
};
|
||||
|
||||
dst.put_slice(&self.target_gateway_identity);
|
||||
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
|
||||
dst.put_slice(&ip_bytes); // IP bytes
|
||||
dst.put_u16_le(self.target_lp_address.port()); // Port
|
||||
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
|
||||
dst.put_slice(&self.inner_packet_bytes);
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = BytesMut::new();
|
||||
self.encode(&mut buf);
|
||||
buf.into()
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
// smallest possible packet with ipv4 and empty data
|
||||
if bytes.len() < 43 {
|
||||
// 32 + 1 + 4 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let target_gateway_identity = bytes[0..32].try_into().unwrap();
|
||||
let target_lp_address_is_ipv6 = bytes[32] != 0;
|
||||
|
||||
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
|
||||
// IPv6, first check we have actually enough bytes
|
||||
// smallest possible packet with ipv6 and empty data
|
||||
if bytes.len() < 55 {
|
||||
// 32 + 1 + 16 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[49], bytes[50]]);
|
||||
(SocketAddr::new(ipv6, port), 51)
|
||||
} else {
|
||||
// IPv4. Length check done at the start
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[37], bytes[38]]);
|
||||
(SocketAddr::new(ipv4, port), 39)
|
||||
};
|
||||
|
||||
let inner_packet_bytes_len = u32::from_le_bytes([
|
||||
bytes[next_index],
|
||||
bytes[next_index + 1],
|
||||
bytes[next_index + 2],
|
||||
bytes[next_index + 3],
|
||||
]);
|
||||
if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
|
||||
bytes[next_index + 4..].len()
|
||||
)));
|
||||
}
|
||||
let inner_packet_bytes = bytes[next_index + 4..].to_vec();
|
||||
|
||||
Ok(ForwardPacketData {
|
||||
target_gateway_identity,
|
||||
target_lp_address,
|
||||
inner_packet_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Subsession KK1 message - first message of Noise KK handshake
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubsessionKK1Data {
|
||||
/// Noise KK first message payload (ephemeral key + encrypted static)
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SubsessionKK1Data {
|
||||
fn len(&self) -> usize {
|
||||
self.payload.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.payload);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(SubsessionKK1Data {
|
||||
payload: bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Subsession KK2 message - second message of Noise KK handshake
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubsessionKK2Data {
|
||||
/// Noise KK second message payload (ephemeral key + encrypted response)
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SubsessionKK2Data {
|
||||
fn len(&self) -> usize {
|
||||
self.payload.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.payload);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(SubsessionKK2Data {
|
||||
payload: bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Subsession ready confirmation with new session index
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubsessionReadyData {
|
||||
/// New subsession's receiver index for routing
|
||||
pub receiver_index: u32,
|
||||
}
|
||||
|
||||
impl SubsessionReadyData {
|
||||
pub const LEN: usize = 4;
|
||||
|
||||
fn len(&self) -> usize {
|
||||
Self::LEN
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.receiver_index);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() != 4 {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Expected 4 bytes to deserialise SubsessionReadyData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
Ok(SubsessionReadyData {
|
||||
receiver_index: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LpMessage {
|
||||
Busy,
|
||||
Handshake(HandshakeData),
|
||||
EncryptedData(EncryptedDataPayload),
|
||||
ClientHello(ClientHelloData),
|
||||
KKTRequest(KKTRequestData),
|
||||
KKTResponse(KKTResponseData),
|
||||
ForwardPacket(ForwardPacketData),
|
||||
/// Receiver index collision - client should retry with new receiver_index
|
||||
Collision,
|
||||
/// Acknowledgment - gateway confirms receipt of message
|
||||
Ack,
|
||||
/// Subsession request - client initiates subsession creation (empty, signal only)
|
||||
SubsessionRequest,
|
||||
/// Subsession KK1 - first message of Noise KK handshake
|
||||
SubsessionKK1(SubsessionKK1Data),
|
||||
/// Subsession KK2 - second message of Noise KK handshake
|
||||
SubsessionKK2(SubsessionKK2Data),
|
||||
/// Subsession ready - subsession established confirmation
|
||||
SubsessionReady(SubsessionReadyData),
|
||||
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
||||
SubsessionAbort,
|
||||
/// An error has occurred
|
||||
Error(ErrorPacketData),
|
||||
}
|
||||
|
||||
impl From<HandshakeData> for LpMessage {
|
||||
fn from(value: HandshakeData) -> Self {
|
||||
LpMessage::Handshake(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptedDataPayload> for LpMessage {
|
||||
fn from(value: EncryptedDataPayload) -> Self {
|
||||
LpMessage::EncryptedData(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientHelloData> for LpMessage {
|
||||
fn from(value: ClientHelloData) -> Self {
|
||||
LpMessage::ClientHello(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTRequestData> for LpMessage {
|
||||
fn from(value: KKTRequestData) -> Self {
|
||||
LpMessage::KKTRequest(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTResponseData> for LpMessage {
|
||||
fn from(value: KKTResponseData) -> Self {
|
||||
LpMessage::KKTResponse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ForwardPacketData> for LpMessage {
|
||||
fn from(value: ForwardPacketData) -> Self {
|
||||
LpMessage::ForwardPacket(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK1Data> for LpMessage {
|
||||
fn from(value: SubsessionKK1Data) -> Self {
|
||||
LpMessage::SubsessionKK1(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK2Data> for LpMessage {
|
||||
fn from(value: SubsessionKK2Data) -> Self {
|
||||
LpMessage::SubsessionKK2(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionReadyData> for LpMessage {
|
||||
fn from(value: SubsessionReadyData) -> Self {
|
||||
LpMessage::SubsessionReady(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LpMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LpMessage::Busy => write!(f, "Busy"),
|
||||
LpMessage::Handshake(_) => write!(f, "Handshake"),
|
||||
LpMessage::EncryptedData(_) => write!(f, "EncryptedData"),
|
||||
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
|
||||
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
|
||||
LpMessage::KKTResponse(_) => write!(f, "KKTResponse"),
|
||||
LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"),
|
||||
LpMessage::Collision => write!(f, "Collision"),
|
||||
LpMessage::Ack => write!(f, "Ack"),
|
||||
LpMessage::SubsessionRequest => write!(f, "SubsessionRequest"),
|
||||
LpMessage::SubsessionKK1(_) => write!(f, "SubsessionKK1"),
|
||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||
LpMessage::Error(_) => write!(f, "Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LpMessage {
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
match self {
|
||||
LpMessage::Busy => &[],
|
||||
LpMessage::Handshake(payload) => payload.0.as_slice(),
|
||||
LpMessage::EncryptedData(payload) => payload.0.as_slice(),
|
||||
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
|
||||
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
|
||||
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::Collision => &[],
|
||||
LpMessage::Ack => &[],
|
||||
LpMessage::SubsessionRequest => &[],
|
||||
LpMessage::SubsessionKK1(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionAbort => &[],
|
||||
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
LpMessage::Busy => true,
|
||||
LpMessage::Handshake(payload) => payload.0.is_empty(),
|
||||
LpMessage::EncryptedData(payload) => payload.0.is_empty(),
|
||||
LpMessage::ClientHello(_) => false, // Always has data
|
||||
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
|
||||
LpMessage::KKTResponse(payload) => payload.0.is_empty(),
|
||||
LpMessage::ForwardPacket(_) => false, // Always has data
|
||||
LpMessage::Collision => true,
|
||||
LpMessage::Ack => true,
|
||||
LpMessage::SubsessionRequest => true, // Empty signal
|
||||
LpMessage::SubsessionKK1(_) => false, // Always has payload
|
||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||
LpMessage::SubsessionAbort => true, // Empty signal
|
||||
LpMessage::Error(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
LpMessage::Busy => 0,
|
||||
LpMessage::Handshake(payload) => payload.len(),
|
||||
LpMessage::EncryptedData(payload) => payload.len(),
|
||||
LpMessage::ClientHello(payload) => payload.len(),
|
||||
LpMessage::KKTRequest(payload) => payload.len(),
|
||||
LpMessage::KKTResponse(payload) => payload.len(),
|
||||
LpMessage::ForwardPacket(payload) => payload.len(),
|
||||
LpMessage::Collision => 0,
|
||||
LpMessage::Ack => 0,
|
||||
LpMessage::SubsessionRequest => 0,
|
||||
LpMessage::SubsessionKK1(payload) => payload.len(),
|
||||
LpMessage::SubsessionKK2(payload) => payload.len(),
|
||||
LpMessage::SubsessionReady(payload) => payload.len(),
|
||||
LpMessage::SubsessionAbort => 0,
|
||||
LpMessage::Error(payload) => payload.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> MessageType {
|
||||
match self {
|
||||
LpMessage::Busy => MessageType::Busy,
|
||||
LpMessage::Handshake(_) => MessageType::Handshake,
|
||||
LpMessage::EncryptedData(_) => MessageType::EncryptedData,
|
||||
LpMessage::ClientHello(_) => MessageType::ClientHello,
|
||||
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
|
||||
LpMessage::KKTResponse(_) => MessageType::KKTResponse,
|
||||
LpMessage::ForwardPacket(_) => MessageType::ForwardPacket,
|
||||
LpMessage::Collision => MessageType::Collision,
|
||||
LpMessage::Ack => MessageType::Ack,
|
||||
LpMessage::SubsessionRequest => MessageType::SubsessionRequest,
|
||||
LpMessage::SubsessionKK1(_) => MessageType::SubsessionKK1,
|
||||
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
||||
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
||||
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
||||
LpMessage::Error(_) => MessageType::Error,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_content(&self, dst: &mut BytesMut) {
|
||||
match self {
|
||||
LpMessage::Busy => { /* No content */ }
|
||||
LpMessage::Handshake(payload) => payload.encode(dst),
|
||||
LpMessage::EncryptedData(payload) => payload.encode(dst),
|
||||
LpMessage::ClientHello(data) => data.encode(dst),
|
||||
LpMessage::KKTRequest(payload) => payload.encode(dst),
|
||||
LpMessage::KKTResponse(payload) => payload.encode(dst),
|
||||
LpMessage::ForwardPacket(data) => data.encode(dst),
|
||||
LpMessage::Collision => { /* No content */ }
|
||||
LpMessage::Ack => { /* No content */ }
|
||||
LpMessage::SubsessionRequest => { /* No content - signal only */ }
|
||||
LpMessage::SubsessionKK1(data) => data.encode(dst),
|
||||
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
||||
LpMessage::SubsessionReady(data) => data.encode(dst),
|
||||
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
||||
LpMessage::Error(data) => data.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse message from its type and content bytes.
|
||||
///
|
||||
/// Used when decrypting outer-encrypted packets where the message type
|
||||
/// was encrypted along with the content.
|
||||
pub fn decode_content(content: &[u8], message_type: MessageType) -> Result<Self, LpError> {
|
||||
match message_type {
|
||||
MessageType::Busy => {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::Busy)
|
||||
}
|
||||
MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData::decode(content)?)),
|
||||
MessageType::EncryptedData => Ok(LpMessage::EncryptedData(
|
||||
EncryptedDataPayload::decode(content)?,
|
||||
)),
|
||||
MessageType::ClientHello => {
|
||||
Ok(LpMessage::ClientHello(ClientHelloData::decode(content)?))
|
||||
}
|
||||
MessageType::KKTRequest => Ok(LpMessage::KKTRequest(KKTRequestData::decode(content)?)),
|
||||
MessageType::KKTResponse => {
|
||||
Ok(LpMessage::KKTResponse(KKTResponseData::decode(content)?))
|
||||
}
|
||||
MessageType::ForwardPacket => Ok(LpMessage::ForwardPacket(ForwardPacketData::decode(
|
||||
content,
|
||||
)?)),
|
||||
MessageType::Collision => {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::Collision)
|
||||
}
|
||||
MessageType::Ack => {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::Ack)
|
||||
}
|
||||
MessageType::SubsessionRequest => {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::SubsessionRequest)
|
||||
}
|
||||
MessageType::SubsessionKK1 => Ok(LpMessage::SubsessionKK1(SubsessionKK1Data::decode(
|
||||
content,
|
||||
)?)),
|
||||
MessageType::SubsessionKK2 => Ok(LpMessage::SubsessionKK2(SubsessionKK2Data::decode(
|
||||
content,
|
||||
)?)),
|
||||
MessageType::SubsessionReady => Ok(LpMessage::SubsessionReady(
|
||||
SubsessionReadyData::decode(content)?,
|
||||
)),
|
||||
MessageType::SubsessionAbort => {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::SubsessionAbort)
|
||||
}
|
||||
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait for improving readability to return error if bytes content is not empty
|
||||
trait EnsureEmptyContent {
|
||||
fn ensure_empty(&self) -> Result<(), LpError>;
|
||||
}
|
||||
|
||||
impl EnsureEmptyContent for &[u8] {
|
||||
fn ensure_empty(&self) -> Result<(), LpError> {
|
||||
if !self.is_empty() {
|
||||
return Err(LpError::InvalidPayloadSize {
|
||||
expected: 0,
|
||||
actual: self.len(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::*;
|
||||
use crate::LpPacket;
|
||||
use crate::packet::{LpHeader, TRAILER_LEN};
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
let message = LpMessage::EncryptedData(EncryptedDataPayload(vec![11u8; 124]));
|
||||
|
||||
let resp_header = LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 0,
|
||||
counter: 0,
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: resp_header,
|
||||
message,
|
||||
trailer: [80; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Just print packet for debug, will be captured in test output
|
||||
println!("{packet:?}");
|
||||
|
||||
// Verify message type
|
||||
assert!(matches!(packet.message.typ(), MessageType::EncryptedData));
|
||||
|
||||
// Verify correct data in message
|
||||
match &packet.message {
|
||||
LpMessage::EncryptedData(data) => {
|
||||
assert_eq!(*data, EncryptedDataPayload(vec![11u8; 124]));
|
||||
}
|
||||
_ => panic!("Wrong message type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_generation() {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let ed25519 = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519 = ed25519.to_x25519();
|
||||
|
||||
let client_key = *x25519.public_key();
|
||||
let client_ed25519_key = *ed25519.public_key();
|
||||
let hello1 =
|
||||
ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
|
||||
let hello2 =
|
||||
ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
|
||||
|
||||
// Different salts should be generated
|
||||
assert_ne!(hello1.salt, hello2.salt);
|
||||
|
||||
// But timestamps should be very close (within 1 second)
|
||||
let ts1 = hello1.extract_timestamp();
|
||||
let ts2 = hello2.extract_timestamp();
|
||||
assert!((ts1 as i64 - ts2 as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_timestamp_extraction() {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
let mut rng = rand::thread_rng();
|
||||
let ed25519 = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519 = ed25519.to_x25519();
|
||||
|
||||
let client_key = *x25519.public_key();
|
||||
let client_ed25519_key = *ed25519.public_key();
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
|
||||
|
||||
let timestamp = hello.extract_timestamp();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Timestamp should be within 1 second of now
|
||||
assert!((timestamp as i64 - now as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_format() {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
let mut rng = rand::thread_rng();
|
||||
let ed25519 = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519 = ed25519.to_x25519();
|
||||
|
||||
let client_key = *x25519.public_key();
|
||||
let client_ed25519_key = *ed25519.public_key();
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
|
||||
|
||||
// First 8 bytes should be non-zero timestamp
|
||||
let timestamp_bytes = &hello.salt[..8];
|
||||
assert_ne!(timestamp_bytes, &[0u8; 8]);
|
||||
|
||||
// Salt should be 32 bytes total
|
||||
assert_eq!(hello.salt.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Sans-IO Noise protocol state machine, adapted from noise-psq.
|
||||
|
||||
use snow::{TransportState, params::NoiseParams};
|
||||
use thiserror::Error;
|
||||
|
||||
// --- Error Definition ---
|
||||
|
||||
/// Errors related to the Noise protocol state machine.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NoiseError {
|
||||
#[error("encountered a Noise decryption error")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("encountered a Noise Protocol error - {0}")]
|
||||
ProtocolError(snow::Error),
|
||||
|
||||
#[error("operation is invalid in the current protocol state")]
|
||||
IncorrectStateError,
|
||||
|
||||
#[error("attempted transport mode operation without real PSK injection")]
|
||||
PskNotInjected,
|
||||
|
||||
#[error("Other Noise-related error: {0}")]
|
||||
Other(String),
|
||||
|
||||
#[error("session is read-only after demotion")]
|
||||
SessionReadOnly,
|
||||
}
|
||||
|
||||
impl From<snow::Error> for NoiseError {
|
||||
fn from(err: snow::Error) -> Self {
|
||||
match err {
|
||||
snow::Error::Decrypt => NoiseError::DecryptionError,
|
||||
err => NoiseError::ProtocolError(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Protocol State and Structs ---
|
||||
|
||||
/// Represents the possible states of the Noise protocol machine.
|
||||
#[derive(Debug)]
|
||||
pub enum NoiseProtocolState {
|
||||
/// The protocol is currently performing the handshake.
|
||||
/// Contains the Snow handshake state.
|
||||
Handshaking(Box<snow::HandshakeState>),
|
||||
|
||||
/// The handshake is complete, and the protocol is in transport mode.
|
||||
/// Contains the Snow transport state.
|
||||
Transport(TransportState),
|
||||
|
||||
/// The protocol has encountered an unrecoverable error.
|
||||
/// Stores the error description.
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
/// The core sans-io Noise protocol state machine.
|
||||
#[derive(Debug)]
|
||||
pub struct NoiseProtocol {
|
||||
state: NoiseProtocolState,
|
||||
// We might need buffers for incoming/outgoing data later if we add internal buffering
|
||||
// read_buffer: Vec<u8>,
|
||||
// write_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents the outcome of processing received bytes via `read_message`.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ReadResult {
|
||||
/// A handshake or transport message was successfully processed, but yielded no application data
|
||||
/// and did not complete the handshake.
|
||||
NoOp,
|
||||
/// A complete application data message was decrypted.
|
||||
DecryptedData(Vec<u8>),
|
||||
/// The handshake successfully completed during this read operation.
|
||||
HandshakeComplete,
|
||||
// NOTE: NeedMoreBytes variant removed as read_message expects full frames.
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
impl NoiseProtocol {
|
||||
pub fn params() -> NoiseParams {
|
||||
// SAFETY: the hardcoded pattern must be valid
|
||||
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
|
||||
#[allow(clippy::unwrap_used)]
|
||||
crate::NOISE_PATTERN.parse().unwrap()
|
||||
}
|
||||
|
||||
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
||||
///
|
||||
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
||||
pub fn new(initial_state: snow::HandshakeState) -> Self {
|
||||
NoiseProtocol {
|
||||
state: NoiseProtocolState::Handshaking(Box::new(initial_state)),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_handshake_state<'a>(
|
||||
local_private_key: &'a [u8],
|
||||
remote_public_key: &'a [u8],
|
||||
psk: &'a [u8],
|
||||
) -> snow::Builder<'a> {
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params = NoiseProtocol::params();
|
||||
|
||||
snow::Builder::new(noise_params)
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key)
|
||||
.psk(psk_index, psk)
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_initiator(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_initiator()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_responder()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Processes a single, complete incoming Noise message frame.
|
||||
///
|
||||
/// Assumes the caller handles buffering and framing to provide one full message.
|
||||
/// Returns the result of processing the message.
|
||||
pub fn read_message(&mut self, input: &[u8]) -> Result<ReadResult, NoiseError> {
|
||||
// Allocate a buffer large enough for the maximum possible Noise message size.
|
||||
// TODO: Consider reusing a buffer for efficiency.
|
||||
let mut buffer = vec![0u8; 65535]; // Max Noise message size
|
||||
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Handshaking(handshake_state) => {
|
||||
match handshake_state.read_message(input, &mut buffer) {
|
||||
Ok(_) => {
|
||||
if handshake_state.is_handshake_finished() {
|
||||
// Transition to Transport state.
|
||||
let current_state = std::mem::replace(
|
||||
&mut self.state,
|
||||
// Temporary placeholder needed for mem::replace
|
||||
NoiseProtocolState::Failed(
|
||||
NoiseError::IncorrectStateError.to_string(),
|
||||
),
|
||||
);
|
||||
if let NoiseProtocolState::Handshaking(state_to_convert) = current_state
|
||||
{
|
||||
match state_to_convert.into_transport_mode() {
|
||||
Ok(transport_state) => {
|
||||
self.state = NoiseProtocolState::Transport(transport_state);
|
||||
Ok(ReadResult::HandshakeComplete)
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Should be unreachable
|
||||
let err = NoiseError::IncorrectStateError;
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
} else {
|
||||
// Handshake continues
|
||||
Ok(ReadResult::NoOp)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Transport(transport_state) => {
|
||||
match transport_state.read_message(input, &mut buffer) {
|
||||
Ok(len) => Ok(ReadResult::DecryptedData(buffer[..len].to_vec())),
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Failed(_) => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if there are pending handshake messages to send.
|
||||
///
|
||||
/// If in Handshaking state and it's our turn, generates the message.
|
||||
/// Transitions state to Transport if the handshake completes after this message.
|
||||
/// Returns `None` if not in Handshaking state or not our turn.
|
||||
pub fn get_bytes_to_send(&mut self) -> Option<Result<Vec<u8>, NoiseError>> {
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Handshaking(handshake_state) => {
|
||||
if handshake_state.is_my_turn() {
|
||||
let mut buffer = vec![0u8; 65535];
|
||||
match handshake_state.write_message(&[], &mut buffer) {
|
||||
// Empty payload for handshake msg
|
||||
Ok(len) => {
|
||||
if handshake_state.is_handshake_finished() {
|
||||
// Transition to Transport state.
|
||||
let current_state = std::mem::replace(
|
||||
&mut self.state,
|
||||
NoiseProtocolState::Failed(
|
||||
NoiseError::IncorrectStateError.to_string(),
|
||||
),
|
||||
);
|
||||
if let NoiseProtocolState::Handshaking(state_to_convert) =
|
||||
current_state
|
||||
{
|
||||
match state_to_convert.into_transport_mode() {
|
||||
Ok(transport_state) => {
|
||||
self.state =
|
||||
NoiseProtocolState::Transport(transport_state);
|
||||
Some(Ok(buffer[..len].to_vec())) // Return final handshake msg
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state =
|
||||
NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Should be unreachable
|
||||
let err = NoiseError::IncorrectStateError;
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
} else {
|
||||
// Handshake continues
|
||||
Some(Ok(buffer[..len].to_vec()))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not our turn
|
||||
None
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Transport(_) | NoiseProtocolState::Failed(_) => {
|
||||
// No handshake messages to send in these states
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypts an application data payload for sending during the Transport phase.
|
||||
///
|
||||
/// Returns the ciphertext (payload + 16-byte tag).
|
||||
/// Errors if not in Transport state or encryption fails.
|
||||
pub fn write_message(&mut self, payload: &[u8]) -> Result<Vec<u8>, NoiseError> {
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Transport(transport_state) => {
|
||||
let mut buffer = vec![0u8; payload.len() + 16]; // Payload + tag
|
||||
match transport_state.write_message(payload, &mut buffer) {
|
||||
Ok(len) => Ok(buffer[..len].to_vec()),
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Handshaking(_) | NoiseProtocolState::Failed(_) => {
|
||||
Err(NoiseError::IncorrectStateError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the protocol is in the transport phase (handshake complete).
|
||||
pub fn is_transport(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Transport(_))
|
||||
}
|
||||
|
||||
/// Returns true if the protocol has failed.
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Failed(_))
|
||||
}
|
||||
|
||||
/// Check if the handshake has finished and the protocol is in transport mode.
|
||||
pub fn is_handshake_finished(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Transport(_))
|
||||
}
|
||||
|
||||
/// Inject a PSK into the Noise HandshakeState.
|
||||
///
|
||||
/// This allows dynamic PSK injection after HandshakeState construction,
|
||||
/// which is required for PSQ (Post-Quantum Secure PSK) integration where
|
||||
/// the PSK is derived during the handshake process.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `index` - PSK index (typically 3 for XKpsk3 pattern)
|
||||
/// * `psk` - The pre-shared key bytes to inject
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if:
|
||||
/// - Not in handshake state
|
||||
/// - The underlying snow library rejects the PSK
|
||||
pub fn set_psk(&mut self, index: u8, psk: &[u8]) -> Result<(), NoiseError> {
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Handshaking(handshake_state) => {
|
||||
handshake_state
|
||||
.set_psk(index as usize, psk)
|
||||
.map_err(NoiseError::ProtocolError)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use crate::message::{LpMessage, MessageType};
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_lp_common::format_debug_bytes;
|
||||
use parking_lot::Mutex;
|
||||
use std::fmt::Write;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_HEADER_LEN: usize = 8;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const IP_HEADER_LEN: usize = 40; // v4 - 20, v6 - 40
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const MTU: usize = 1500;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_OVERHEAD: usize = UDP_HEADER_LEN + IP_HEADER_LEN;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const TRAILER_LEN: usize = 16;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_PAYLOAD_SIZE: usize = MTU - UDP_OVERHEAD - TRAILER_LEN;
|
||||
|
||||
pub mod version {
|
||||
/// The current version of the Lewes Protocol that is put into each new constructed header.
|
||||
pub const CURRENT: u8 = 1;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LpPacket {
|
||||
pub(crate) header: LpHeader,
|
||||
pub(crate) message: LpMessage,
|
||||
pub(crate) trailer: [u8; TRAILER_LEN],
|
||||
}
|
||||
|
||||
impl Debug for LpPacket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", format_debug_bytes(&self.debug_bytes())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl LpPacket {
|
||||
pub fn new(header: LpHeader, message: LpMessage) -> Self {
|
||||
Self {
|
||||
header,
|
||||
message,
|
||||
trailer: [0; TRAILER_LEN],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> MessageType {
|
||||
self.message.typ()
|
||||
}
|
||||
|
||||
/// Compute a hash of the message payload
|
||||
///
|
||||
/// This can be used for message integrity verification or deduplication
|
||||
pub fn hash_payload(&self) -> [u8; 32] {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
// Include message type and content in the hash
|
||||
buffer.put_slice(&(self.message.typ() as u16).to_le_bytes());
|
||||
self.message.encode_content(&mut buffer);
|
||||
|
||||
hasher.update(&buffer);
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
pub fn hash_payload_hex(&self) -> String {
|
||||
let hash = self.hash_payload();
|
||||
hash.iter()
|
||||
.fold(String::with_capacity(hash.len() * 2), |mut acc, byte| {
|
||||
let _ = write!(acc, "{:02x}", byte);
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &LpMessage {
|
||||
&self.message
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &LpHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = BytesMut::new();
|
||||
self.encode(&mut bytes);
|
||||
bytes.freeze().to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
self.header.encode(dst);
|
||||
|
||||
dst.put_slice(&(self.message.typ() as u16).to_le_bytes());
|
||||
self.message.encode_content(dst);
|
||||
|
||||
dst.put_slice(&self.trailer)
|
||||
}
|
||||
|
||||
/// Validate packet counter against a replay protection validator
|
||||
///
|
||||
/// This performs a quick check to see if the packet counter is valid before
|
||||
/// any expensive processing is done.
|
||||
pub fn validate_counter(
|
||||
&self,
|
||||
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
|
||||
) -> Result<(), LpError> {
|
||||
let guard = validator.lock();
|
||||
guard.will_accept_branchless(self.header.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark packet as received in the replay protection validator
|
||||
///
|
||||
/// This should be called after a packet has been successfully processed.
|
||||
pub fn mark_received(
|
||||
&self,
|
||||
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
|
||||
) -> Result<(), LpError> {
|
||||
let mut guard = validator.lock();
|
||||
guard.mark_did_receive_branchless(self.header.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Session ID used for ClientHello bootstrap packets before session is established.
|
||||
///
|
||||
/// When a client first connects, it sends a ClientHello packet with receiver_idx=0
|
||||
/// because neither side can compute the deterministic session ID yet (requires
|
||||
/// both parties' X25519 keys). After ClientHello is processed, both sides derive
|
||||
/// the same session ID from their keys, and all subsequent packets use that ID.
|
||||
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
|
||||
|
||||
/// Outer header (12 bytes) - always cleartext, used for routing.
|
||||
///
|
||||
/// This is the first 12 bytes of every LP packet, containing only the fields
|
||||
/// needed for session lookup (receiver_idx) and replay protection (counter).
|
||||
/// For encrypted packets, this is the AAD (additional authenticated data).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OuterHeader {
|
||||
pub receiver_idx: u32,
|
||||
pub counter: u64,
|
||||
}
|
||||
|
||||
impl OuterHeader {
|
||||
pub const SIZE: usize = 12; // receiver_idx(4) + counter(8)
|
||||
|
||||
pub fn new(receiver_idx: u32, counter: u64) -> Self {
|
||||
Self {
|
||||
receiver_idx,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
Ok(Self {
|
||||
receiver_idx: u32::from_le_bytes(src[0..4].try_into().unwrap()),
|
||||
counter: u64::from_le_bytes(src[4..12].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> [u8; Self::SIZE] {
|
||||
let mut buf = [0u8; Self::SIZE];
|
||||
buf[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes());
|
||||
buf[4..12].copy_from_slice(&self.counter.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Encode directly into a BytesMut buffer
|
||||
pub fn encode_into(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.receiver_idx.to_le_bytes());
|
||||
dst.put_slice(&self.counter.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal LP header representation containing all logical header fields.
|
||||
///
|
||||
/// **Note**: This struct represents the LOGICAL header, not the wire format.
|
||||
/// On the wire, packets use the unified format where:
|
||||
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
|
||||
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
|
||||
///
|
||||
/// The `LpHeader::encode()` method outputs the old logical format for debug purposes only.
|
||||
/// Use `serialize_lp_packet()` in codec.rs for actual wire serialization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpHeader {
|
||||
pub protocol_version: u8,
|
||||
pub reserved: [u8; 3],
|
||||
pub receiver_idx: u32,
|
||||
pub counter: u64,
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub const SIZE: usize = 16;
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
// protocol version
|
||||
dst.put_u8(self.protocol_version);
|
||||
|
||||
// reserved
|
||||
dst.put_slice(&self.reserved);
|
||||
|
||||
// sender index
|
||||
dst.put_slice(&self.receiver_idx.to_le_bytes());
|
||||
|
||||
// counter
|
||||
dst.put_slice(&self.counter.to_le_bytes());
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
|
||||
let protocol_version = src[0];
|
||||
|
||||
// Ensure we are using compatible protocol
|
||||
// right now only support a single version
|
||||
if protocol_version > version::CURRENT {
|
||||
return Err(LpError::IncompatibleFuturePacketVersion {
|
||||
got: protocol_version,
|
||||
highest_supported: version::CURRENT,
|
||||
});
|
||||
}
|
||||
|
||||
if protocol_version < version::CURRENT {
|
||||
return Err(LpError::IncompatibleLegacyPacketVersion {
|
||||
got: protocol_version,
|
||||
lowest_supported: version::CURRENT,
|
||||
});
|
||||
}
|
||||
|
||||
// skip reserved bytes, but log if they're different from the expected zeroes
|
||||
let reserved = [src[1], src[2], src[3]];
|
||||
if reserved != [0u8; 3] {
|
||||
warn!("received non-zero reserved bytes. got: {reserved:?}");
|
||||
}
|
||||
|
||||
let mut receiver_idx_bytes = [0u8; 4];
|
||||
receiver_idx_bytes.copy_from_slice(&src[4..8]);
|
||||
let receiver_idx = u32::from_le_bytes(receiver_idx_bytes);
|
||||
|
||||
let mut counter_bytes = [0u8; 8];
|
||||
counter_bytes.copy_from_slice(&src[8..16]);
|
||||
let counter = u64::from_le_bytes(counter_bytes);
|
||||
|
||||
Ok(LpHeader {
|
||||
protocol_version,
|
||||
reserved,
|
||||
receiver_idx,
|
||||
counter,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the counter value from the header
|
||||
pub fn counter(&self) -> u64 {
|
||||
self.counter
|
||||
}
|
||||
|
||||
/// Get the sender index from the header
|
||||
pub fn receiver_idx(&self) -> u32 {
|
||||
self.receiver_idx
|
||||
}
|
||||
}
|
||||
|
||||
// subsequent data: MessageType || Data
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MalformedLpPacketError {
|
||||
#[error("failed to deserialise received data: {0}")]
|
||||
DeserialisationFailure(String),
|
||||
|
||||
#[error("provided insufficient data to fully deserialise the struct")]
|
||||
InsufficientData,
|
||||
|
||||
#[error("{0} is not a valid LpDataKind")]
|
||||
InvalidLpDataKind(u16),
|
||||
|
||||
#[error("invalid payload size: expected {expected}, got {actual}")]
|
||||
InvalidPayloadSize { expected: usize, actual: usize },
|
||||
|
||||
/// Received an LP packet with an incompatible, future, version
|
||||
#[error("incompatible LP packet version. got: {got}, highest supported: {highest_supported}")]
|
||||
IncompatibleFuturePacketVersion { got: u8, highest_supported: u8 },
|
||||
|
||||
/// Received an LP packet with an incompatible, legacy, version
|
||||
#[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")]
|
||||
IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 },
|
||||
}
|
||||
|
||||
impl MalformedLpPacketError {
|
||||
pub fn invalid_data_kind(message_type: u16) -> Self {
|
||||
MalformedLpPacketError::InvalidLpDataKind(message_type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::version;
|
||||
use crate::{packet::error::MalformedLpPacketError, peer_config::LpReceiverIndex};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use tracing::warn;
|
||||
|
||||
/// Outer header (12 bytes) - always cleartext, used for routing.
|
||||
///
|
||||
/// This is the first 12 bytes of every LP packet, containing only the fields
|
||||
/// needed for session lookup (receiver_idx) and replay protection (counter).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OuterHeader {
|
||||
pub receiver_idx: LpReceiverIndex,
|
||||
pub counter: u64,
|
||||
}
|
||||
|
||||
impl OuterHeader {
|
||||
pub const SIZE: usize = 12; // receiver_idx(4) + counter(8)
|
||||
|
||||
pub fn new(receiver_idx: LpReceiverIndex, counter: u64) -> Self {
|
||||
Self {
|
||||
receiver_idx,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, MalformedLpPacketError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(MalformedLpPacketError::InsufficientData);
|
||||
}
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Ok(Self {
|
||||
receiver_idx: LpReceiverIndex::from_le_bytes(src[0..4].try_into().unwrap()),
|
||||
counter: u64::from_le_bytes(src[4..12].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; Self::SIZE] {
|
||||
let mut bytes = [0u8; Self::SIZE];
|
||||
bytes[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes());
|
||||
bytes[4..12].copy_from_slice(&self.counter.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Encode directly into a BytesMut buffer
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.receiver_idx.to_le_bytes());
|
||||
dst.put_slice(&self.counter.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// InnerHeader header (8 bytes) - encrypted, used for message parsing
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct InnerHeader {
|
||||
pub protocol_version: u8,
|
||||
pub reserved: [u8; 3],
|
||||
}
|
||||
|
||||
impl InnerHeader {
|
||||
pub const SIZE: usize = 4; // protocol_version(1) + reserved(3)
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
// protocol version
|
||||
dst.put_u8(self.protocol_version);
|
||||
|
||||
// reserved
|
||||
dst.put_slice(&self.reserved);
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, MalformedLpPacketError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(MalformedLpPacketError::InsufficientData);
|
||||
}
|
||||
|
||||
let protocol_version = src[0];
|
||||
|
||||
// Ensure we are using compatible protocol
|
||||
// right now only support a single version
|
||||
if protocol_version > version::CURRENT {
|
||||
return Err(MalformedLpPacketError::IncompatibleFuturePacketVersion {
|
||||
got: protocol_version,
|
||||
highest_supported: version::CURRENT,
|
||||
});
|
||||
}
|
||||
|
||||
if protocol_version < version::CURRENT {
|
||||
return Err(MalformedLpPacketError::IncompatibleLegacyPacketVersion {
|
||||
got: protocol_version,
|
||||
lowest_supported: version::CURRENT,
|
||||
});
|
||||
}
|
||||
|
||||
// skip reserved bytes, but log if they're different from the expected zeroes
|
||||
let reserved = [src[1], src[2], src[3]];
|
||||
if reserved != [0u8; 3] {
|
||||
warn!("received non-zero reserved bytes. got: {reserved:?}");
|
||||
}
|
||||
|
||||
Ok(InnerHeader {
|
||||
protocol_version,
|
||||
reserved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal LP header representation containing all logical header fields.
|
||||
///
|
||||
/// **Note**: This struct represents the LOGICAL header, not the wire format.
|
||||
/// On the wire, packets use the unified format where:
|
||||
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
|
||||
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LpHeader {
|
||||
pub outer: OuterHeader,
|
||||
pub inner: InnerHeader,
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub fn new(receiver_idx: LpReceiverIndex, counter: u64, protocol_version: u8) -> Self {
|
||||
Self {
|
||||
outer: OuterHeader {
|
||||
receiver_idx,
|
||||
counter,
|
||||
},
|
||||
inner: InnerHeader {
|
||||
protocol_version,
|
||||
reserved: [0u8; 3],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) {
|
||||
self.outer.encode(dst);
|
||||
self.inner.encode(dst);
|
||||
}
|
||||
|
||||
/// Get the counter value from the header
|
||||
pub fn counter(&self) -> u64 {
|
||||
self.outer.counter
|
||||
}
|
||||
|
||||
/// Get the sender index from the header
|
||||
pub fn receiver_idx(&self) -> LpReceiverIndex {
|
||||
self.outer.receiver_idx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::error::MalformedLpPacketError;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LpMessageHeader {
|
||||
pub kind: LpMessageType,
|
||||
pub message_attributes: [u8; 14],
|
||||
}
|
||||
|
||||
impl LpMessageHeader {
|
||||
pub const SIZE: usize = 16; // message_kind(2) + message_attributes(14)
|
||||
|
||||
pub fn new(kind: LpMessageType, message_attributes: [u8; 14]) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
message_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_attributes(kind: LpMessageType) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
message_attributes: [0; 14],
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode directly into a BytesMut buffer
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u16_le(self.kind as u16);
|
||||
dst.put_slice(&self.message_attributes);
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, MalformedLpPacketError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(MalformedLpPacketError::InsufficientData);
|
||||
}
|
||||
let raw_kind = u16::from_le_bytes([src[0], src[1]]);
|
||||
|
||||
let kind = LpMessageType::try_from(raw_kind)
|
||||
.map_err(|_| MalformedLpPacketError::invalid_data_kind(raw_kind))?;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let message_attributes = src[2..16].try_into().unwrap();
|
||||
Ok(Self {
|
||||
kind,
|
||||
message_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent application data being sent in Transport mode
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LpMessage {
|
||||
pub header: LpMessageHeader,
|
||||
pub content: Bytes,
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for LpMessage {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.content
|
||||
}
|
||||
}
|
||||
|
||||
impl LpMessage {
|
||||
pub fn new(kind: LpMessageType, content: impl Into<Bytes>) -> Self {
|
||||
Self {
|
||||
header: LpMessageHeader::new_no_attributes(kind),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
self.header.encode(dst);
|
||||
|
||||
dst.put_slice(&self.content);
|
||||
}
|
||||
|
||||
pub fn decode(src: &[u8]) -> Result<Self, MalformedLpPacketError> {
|
||||
let header = LpMessageHeader::parse(src)?;
|
||||
let content = src[LpMessageHeader::SIZE..].to_vec().into();
|
||||
|
||||
Ok(Self { header, content })
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> LpMessageType {
|
||||
self.header.kind
|
||||
}
|
||||
|
||||
pub fn new_opaque(content: impl Into<Bytes>) -> Self {
|
||||
Self::new(LpMessageType::Opaque, content)
|
||||
}
|
||||
|
||||
pub fn new_registration(data: impl Into<Bytes>) -> Self {
|
||||
Self::new(LpMessageType::Registration, data)
|
||||
}
|
||||
|
||||
pub fn new_forward(data: impl Into<Bytes>) -> Self {
|
||||
Self::new(LpMessageType::Forward, data)
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
LpMessageHeader::SIZE + self.content.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent kind of application data being sent in Transport mode
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum LpMessageType {
|
||||
Opaque = 0,
|
||||
Registration = 1,
|
||||
Forward = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ExpectedResponseSize {
|
||||
/// We've sent a handshake message and expect response of predefined size
|
||||
Handshake(u32),
|
||||
|
||||
/// We've sent a transport message and the response is length-prefixed
|
||||
Transport,
|
||||
}
|
||||
|
||||
impl ExpectedResponseSize {
|
||||
pub fn to_bytes(&self) -> [u8; 4] {
|
||||
// there are no empty handshake messages, so we use 0 bytes to indicate Transport variant
|
||||
match self {
|
||||
ExpectedResponseSize::Handshake(size) => size.to_le_bytes(),
|
||||
ExpectedResponseSize::Transport => [0u8; 4],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(b: [u8; 4]) -> Self {
|
||||
let size = u32::from_le_bytes(b);
|
||||
if size == 0 {
|
||||
ExpectedResponseSize::Transport
|
||||
} else {
|
||||
ExpectedResponseSize::Handshake(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ForwardPacketData {
|
||||
/// Target gateway's LP address (IP:port string)
|
||||
pub target_lp_address: SocketAddr,
|
||||
|
||||
/// Indication of the expected size of the response
|
||||
/// to allow the proxy to read correct data from the stream
|
||||
pub expected_response_size: ExpectedResponseSize,
|
||||
|
||||
/// Complete inner LP packet bytes (serialized LpPacket)
|
||||
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
||||
pub inner_packet_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ForwardPacketData {
|
||||
pub fn new(
|
||||
target_lp_address: SocketAddr,
|
||||
expected_response_size: ExpectedResponseSize,
|
||||
inner_packet_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
ForwardPacketData {
|
||||
target_lp_address,
|
||||
expected_response_size,
|
||||
inner_packet_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
// 0 || [4B ipv4] || [2B port] || [4B res size] || [4B plen] || payload
|
||||
// 1 || [16B ipv6] || [2B port] || [4B res size] || [4B plen] || payload
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
|
||||
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
|
||||
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
|
||||
};
|
||||
|
||||
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
|
||||
dst.put_slice(&ip_bytes); // IP bytes
|
||||
dst.put_u16_le(self.target_lp_address.port()); // Port
|
||||
dst.put_slice(&self.expected_response_size.to_bytes());
|
||||
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
|
||||
dst.put_slice(&self.inner_packet_bytes);
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = BytesMut::new();
|
||||
self.encode(&mut buf);
|
||||
buf.into()
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Self, MalformedLpPacketError> {
|
||||
// smallest possible packet with ipv4 and empty data
|
||||
if b.len() < 15 {
|
||||
// 1 + 4 + 2 + 4 + 4 + 0
|
||||
return Err(MalformedLpPacketError::DeserialisationFailure(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData. got {}",
|
||||
b.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let target_lp_address_is_ipv6 = b[0] != 0;
|
||||
|
||||
let (target_lp_address, i) = if target_lp_address_is_ipv6 {
|
||||
// IPv6, first check we have actually enough bytes
|
||||
// smallest possible packet with ipv6 and empty data
|
||||
if b.len() < 27 {
|
||||
// 1 + 16 + 2 + 4 + 4+ 0
|
||||
return Err(MalformedLpPacketError::DeserialisationFailure(format!(
|
||||
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
|
||||
b.len()
|
||||
)));
|
||||
}
|
||||
// Ipv6Addr::from_octets is not available until 1.91 so we have to use
|
||||
// the slightly less obvious u128 conversion
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv6 = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(
|
||||
b[1..17].try_into().unwrap(),
|
||||
)));
|
||||
let port = u16::from_le_bytes([b[17], b[18]]);
|
||||
(SocketAddr::new(ipv6, port), 19)
|
||||
} else {
|
||||
// IPv4. Length check done at the start
|
||||
|
||||
// Ipv4Addr::from_octets is not available until 1.91
|
||||
let ipv4 = IpAddr::V4(Ipv4Addr::new(b[1], b[2], b[3], b[4]));
|
||||
let port = u16::from_le_bytes([b[5], b[6]]);
|
||||
(SocketAddr::new(ipv4, port), 7)
|
||||
};
|
||||
|
||||
let expected_response_size_bytes = [b[i], b[i + 1], b[i + 2], b[i + 3]];
|
||||
let inner_packet_bytes_len = u32::from_le_bytes([b[i + 4], b[i + 5], b[i + 6], b[i + 7]]);
|
||||
|
||||
if b[i + 8..].len() != inner_packet_bytes_len as usize {
|
||||
return Err(MalformedLpPacketError::DeserialisationFailure(format!(
|
||||
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
|
||||
b[i + 8..].len()
|
||||
)));
|
||||
}
|
||||
let inner_packet_bytes = b[i + 8..].to_vec();
|
||||
|
||||
Ok(ForwardPacketData {
|
||||
target_lp_address,
|
||||
expected_response_size: ExpectedResponseSize::from_bytes(expected_response_size_bytes),
|
||||
inner_packet_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::utils::format_debug_bytes;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
pub use error::MalformedLpPacketError;
|
||||
pub use header::{InnerHeader, LpHeader, OuterHeader};
|
||||
pub use message::{ForwardPacketData, LpMessage};
|
||||
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod message;
|
||||
pub mod replay;
|
||||
pub mod utils;
|
||||
|
||||
pub mod version {
|
||||
/// The current version of the Lewes Protocol that is put into each new constructed header.
|
||||
pub const CURRENT: u8 = 1;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_HEADER_LEN: usize = 8;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const IP_HEADER_LEN: usize = 40; // v4 - 20, v6 - 40
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const MTU: usize = 1500;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_OVERHEAD: usize = UDP_HEADER_LEN + IP_HEADER_LEN;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_PAYLOAD_SIZE: usize = MTU - UDP_OVERHEAD;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EncryptedLpPacket {
|
||||
// The outer header that's sent in plaintext
|
||||
pub(crate) outer_header: OuterHeader,
|
||||
|
||||
// The ciphertext containing the inner header and the payload
|
||||
pub(crate) ciphertext: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EncryptedLpPacket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", format_debug_bytes(&self.debug_bytes())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptedLpPacket {
|
||||
pub fn new(outer_header: OuterHeader, ciphertext: Vec<u8>) -> EncryptedLpPacket {
|
||||
EncryptedLpPacket {
|
||||
outer_header,
|
||||
ciphertext,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encoded_length(&self) -> usize {
|
||||
OuterHeader::SIZE + self.ciphertext.len()
|
||||
}
|
||||
|
||||
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = BytesMut::new();
|
||||
self.encode(&mut bytes);
|
||||
bytes.freeze().to_vec()
|
||||
}
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
self.outer_header.encode(dst);
|
||||
dst.put_slice(&self.ciphertext)
|
||||
}
|
||||
|
||||
pub fn ciphertext(&self) -> &[u8] {
|
||||
&self.ciphertext
|
||||
}
|
||||
|
||||
pub fn outer_header(&self) -> OuterHeader {
|
||||
self.outer_header
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct LpPacket {
|
||||
pub(crate) header: LpHeader,
|
||||
pub(crate) message: LpMessage,
|
||||
}
|
||||
|
||||
impl Debug for LpPacket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", format_debug_bytes(&self.debug_bytes())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl LpPacket {
|
||||
pub fn new(header: LpHeader, message: LpMessage) -> Self {
|
||||
Self { header, message }
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &LpMessage {
|
||||
&self.message
|
||||
}
|
||||
|
||||
pub fn into_message(self) -> LpMessage {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &LpHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = BytesMut::new();
|
||||
self.dbg_encode(&mut bytes);
|
||||
bytes.freeze().to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) {
|
||||
self.header.dbg_encode(dst);
|
||||
self.message.encode(dst)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::{LpError, packet::LpPacket, replay::ReceivingKeyCounterValidator};
|
||||
|
||||
pub trait LpPacketReplayExt {
|
||||
/// Validate packet counter against a replay protection validator
|
||||
///
|
||||
/// This performs a quick check to see if the packet counter is valid before
|
||||
/// any expensive processing is done.
|
||||
fn validate_counter(&self, validator: &ReceivingKeyCounterValidator) -> Result<(), LpError>;
|
||||
|
||||
/// Mark packet as received in the replay protection validator
|
||||
///
|
||||
/// This should be called after a packet has been successfully processed.
|
||||
fn mark_received(&self, validator: &mut ReceivingKeyCounterValidator) -> Result<(), LpError>;
|
||||
}
|
||||
|
||||
impl LpPacketReplayExt for LpPacket {
|
||||
/// Validate packet counter against a replay protection validator
|
||||
///
|
||||
/// This performs a quick check to see if the packet counter is valid before
|
||||
/// any expensive processing is done.
|
||||
fn validate_counter(&self, validator: &ReceivingKeyCounterValidator) -> Result<(), LpError> {
|
||||
validator.will_accept_branchless(self.header().outer.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark packet as received in the replay protection validator
|
||||
///
|
||||
/// This should be called after a packet has been successfully processed.
|
||||
fn mark_received(&self, validator: &mut ReceivingKeyCounterValidator) -> Result<(), LpError> {
|
||||
validator.mark_did_receive_branchless(self.header().outer.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::Write;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
pub fn format_debug_bytes(bytes: &[u8]) -> Result<String, fmt::Error> {
|
||||
let mut out = String::new();
|
||||
+63
-92
@@ -1,102 +1,77 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ClientHelloData, LpError};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use std::collections::HashMap;
|
||||
use crate::LpError;
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
pub use nym_kkt::key_utils::{
|
||||
generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519,
|
||||
};
|
||||
pub use nym_kkt::keys::KEMKeys;
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct LpLocalPeer {
|
||||
/// Local Ed25519 keys for PSQ authentication
|
||||
pub(crate) ed25519: Arc<ed25519::KeyPair>,
|
||||
pub(crate) ciphersuite: Ciphersuite,
|
||||
|
||||
/// Local x25519 keys (Noise static key)
|
||||
pub(crate) x25519: Arc<x25519::KeyPair>,
|
||||
pub(crate) x25519: Arc<DHKeyPair>,
|
||||
|
||||
/// Local KEM key used for PSQ
|
||||
pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>,
|
||||
/// Local KEM keys used for PSQ
|
||||
pub(crate) kem_keypairs: Option<KEMKeys>,
|
||||
}
|
||||
|
||||
impl LpLocalPeer {
|
||||
pub fn new(ed25519: Arc<ed25519::KeyPair>, x25519: Arc<x25519::KeyPair>) -> Self {
|
||||
pub fn new(ciphersuite: Ciphersuite, x25519: Arc<DHKeyPair>) -> Self {
|
||||
LpLocalPeer {
|
||||
ed25519,
|
||||
ciphersuite,
|
||||
x25519,
|
||||
kem_psq: None,
|
||||
kem_keypairs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
|
||||
ClientHelloData::new_with_fresh_salt(
|
||||
*self.x25519().public_key(),
|
||||
*self.ed25519().public_key(),
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
|
||||
self.kem_psq = Some(key);
|
||||
pub fn with_kem_keys(mut self, kem_keys: KEMKeys) -> Self {
|
||||
self.kem_keypairs = Some(kem_keys);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ed25519(&self) -> &Arc<ed25519::KeyPair> {
|
||||
&self.ed25519
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> &Arc<x25519::KeyPair> {
|
||||
pub fn x25519(&self) -> &Arc<DHKeyPair> {
|
||||
&self.x25519
|
||||
}
|
||||
|
||||
/// Returns the reference to the KEM Public key of the peer (if available).
|
||||
pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
|
||||
self.kem_psq
|
||||
.as_ref()
|
||||
.map(|kp| kp.public_key())
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)
|
||||
}
|
||||
|
||||
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
|
||||
#[doc(hidden)]
|
||||
pub fn as_remote(&self) -> LpRemotePeer {
|
||||
let expected_kem_key_digests = match &self.kem_psq {
|
||||
None => HashMap::new(),
|
||||
Some(kem_keys) => {
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(
|
||||
KEM::X25519,
|
||||
nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()),
|
||||
);
|
||||
digests
|
||||
}
|
||||
};
|
||||
|
||||
let mut expected_signing_key_digests = HashMap::new();
|
||||
expected_signing_key_digests.insert(
|
||||
SignatureScheme::Ed25519,
|
||||
nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()),
|
||||
);
|
||||
let expected_kem_key_digests = self
|
||||
.kem_keypairs
|
||||
.as_ref()
|
||||
.map(|k| k.encapsulation_keys_digests())
|
||||
.unwrap_or_default();
|
||||
|
||||
LpRemotePeer {
|
||||
ed25519_public: *self.ed25519.public_key(),
|
||||
x25519_public: *self.x25519.public_key(),
|
||||
x25519_public: self.x25519.pk,
|
||||
expected_kem_key_digests,
|
||||
expected_signing_key_digests,
|
||||
}
|
||||
}
|
||||
|
||||
// this is only exposed in tests as ideally we should be storing the proper types to begin with
|
||||
#[cfg(test)]
|
||||
pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey<'_>> {
|
||||
let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes();
|
||||
let libcrux_pk =
|
||||
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?;
|
||||
pub fn ciphersuite(&self) -> Ciphersuite {
|
||||
self.ciphersuite
|
||||
}
|
||||
}
|
||||
|
||||
Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk))
|
||||
impl Debug for LpLocalPeer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LpLocalPeer")
|
||||
.field("ciphersuite", &self.ciphersuite)
|
||||
.field("x25519", &self.x25519.pk)
|
||||
.field("kem_keypairs", &self.kem_keypairs)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,45 +79,31 @@ impl LpLocalPeer {
|
||||
/// encapsulating all the known information and keys.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpRemotePeer {
|
||||
/// Remote Ed25519 public key for PSQ authentication
|
||||
pub(crate) ed25519_public: ed25519::PublicKey,
|
||||
|
||||
/// Remote X25519 public key (Noise static key)
|
||||
pub(crate) x25519_public: x25519::PublicKey,
|
||||
pub(crate) x25519_public: DHPublicKey,
|
||||
|
||||
/// Expected digests of the remote's KEM key
|
||||
pub(crate) expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
|
||||
|
||||
/// Expected digests of the remote's signing key
|
||||
pub(crate) expected_signing_key_digests: HashMap<SignatureScheme, SigningKeyDigests>,
|
||||
pub(crate) expected_kem_key_digests: BTreeMap<KEM, KEMKeyDigests>,
|
||||
}
|
||||
|
||||
impl LpRemotePeer {
|
||||
pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self {
|
||||
pub fn new(x25519_public: DHPublicKey) -> Self {
|
||||
LpRemotePeer {
|
||||
ed25519_public,
|
||||
x25519_public,
|
||||
expected_kem_key_digests: Default::default(),
|
||||
expected_signing_key_digests: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519(&self) -> ed25519::PublicKey {
|
||||
self.ed25519_public
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> x25519::PublicKey {
|
||||
self.x25519_public
|
||||
pub fn x25519(&self) -> &DHPublicKey {
|
||||
&self.x25519_public
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_key_digests(
|
||||
mut self,
|
||||
expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
|
||||
expected_signing_key_digests: HashMap<SignatureScheme, SigningKeyDigests>,
|
||||
expected_kem_key_digests: BTreeMap<KEM, KEMKeyDigests>,
|
||||
) -> Self {
|
||||
self.expected_kem_key_digests = expected_kem_key_digests;
|
||||
self.expected_signing_key_digests = expected_signing_key_digests;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -168,30 +129,40 @@ impl LpRemotePeer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DHPublicKey> for LpRemotePeer {
|
||||
fn from(value: DHPublicKey) -> Self {
|
||||
LpRemotePeer {
|
||||
x25519_public: value,
|
||||
expected_kem_key_digests: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peer() -> LpLocalPeer {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng_09();
|
||||
random_peer(&mut rng)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
let x25519 = Arc::new(ed25519.to_x25519());
|
||||
let kem_psq = Some(x25519.clone());
|
||||
pub fn random_peer<R: rand09::CryptoRng + rand09::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let x25519 = Arc::new(nym_kkt::key_utils::generate_lp_keypair_x25519(rng));
|
||||
|
||||
LpLocalPeer {
|
||||
ed25519,
|
||||
ciphersuite: Ciphersuite::default(),
|
||||
|
||||
x25519,
|
||||
kem_psq,
|
||||
kem_keypairs: Some(KEMKeys::new(
|
||||
nym_kkt::key_utils::generate_keypair_mceliece(rng),
|
||||
nym_kkt::key_utils::generate_keypair_mlkem(rng),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng_09();
|
||||
|
||||
(random_peer(&mut rng), random_peer(&mut rng))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use libcrux_psq::handshake::types::Authenticator;
|
||||
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3_multi_input;
|
||||
use nym_kkt::keys::EncapsulationKey;
|
||||
use rand09::{self, CryptoRng, Rng};
|
||||
use tls_codec::Serialize;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub type LpReceiverIndex = u32;
|
||||
|
||||
pub const MAX_HOPS: u8 = 16;
|
||||
pub const LP_PEER_CONFIG_SIZE: usize = 20;
|
||||
|
||||
const SEED_LEN: usize = 16;
|
||||
const CONFIG_LEN: usize = 1;
|
||||
const FILLER_LEN: usize = LP_PEER_CONFIG_SIZE - SEED_LEN - CONFIG_LEN;
|
||||
|
||||
const RECEIVER_INDEX_DERIVATION_CONTEXT: &str = "LP_PEER_CONFIG_RECEIVER_INDEX_DERIVATION_V1";
|
||||
|
||||
// 20 bytes
|
||||
#[derive(PartialEq)]
|
||||
pub struct LpPeerConfig {
|
||||
// The first 4 fields will be packed in one u8
|
||||
// with 1 bit left at the end
|
||||
|
||||
// Determine the hop id.
|
||||
// Should be 0 if node_initiator is true
|
||||
// Should be > 1 if is_exit is true
|
||||
hop_id: u8,
|
||||
|
||||
// Determine if the recipient should be an exit node
|
||||
is_exit: bool,
|
||||
|
||||
// Determine if we are establishing a node<>node connection
|
||||
// Should be false if is_exit is true
|
||||
node_initiator: bool,
|
||||
|
||||
// Enable censorship resistance countermeasures
|
||||
censorship_resistance: bool,
|
||||
|
||||
// If we add more config params later, we can use this
|
||||
filler: [u8; FILLER_LEN],
|
||||
|
||||
seed: [u8; SEED_LEN],
|
||||
}
|
||||
|
||||
impl LpPeerConfig {
|
||||
/// Creates a new client to entry config.
|
||||
/// Sets `hop_id` to 0.
|
||||
/// Input: censorship_resistance flag to enable censorship resistance features.
|
||||
pub fn new_client_to_entry<R>(rng: &mut R, censorship_resistance: bool) -> Self
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
Self::build(
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
censorship_resistance,
|
||||
rng.random(),
|
||||
rng.random(),
|
||||
)
|
||||
}
|
||||
/// Creates a new client to exit config.
|
||||
/// Inputs:
|
||||
/// hop_id: this value must be in the range (1..=15). This function returns an error if this is not the case.
|
||||
/// censorship_resistance flag to enable censorship resistance features.
|
||||
pub fn new_client_to_exit<R>(
|
||||
rng: &mut R,
|
||||
hop_id: u8,
|
||||
censorship_resistance: bool,
|
||||
) -> Result<Self, LpError>
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
Self::new(rng, hop_id, true, false, censorship_resistance)
|
||||
}
|
||||
/// Creates a new client to an intermediate node config.
|
||||
/// Inputs:
|
||||
/// hop_id: this value must be in the range (1..=14). This function returns an error if this is not the case.
|
||||
/// censorship_resistance flag to enable censorship resistance features.
|
||||
pub fn new_client_to_intermediate<R>(
|
||||
rng: &mut R,
|
||||
hop_id: u8,
|
||||
censorship_resistance: bool,
|
||||
) -> Result<Self, LpError>
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
if hop_id == 0 || hop_id == 15 {
|
||||
Err(LpError::Internal(format!(
|
||||
"An intermediate hop cannot be the first or last hop. Requested hop id {hop_id}"
|
||||
)))
|
||||
} else {
|
||||
Self::new(rng, hop_id, false, false, censorship_resistance)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new node to node config.
|
||||
/// Censorship resistance features are disabled by default between nodes.
|
||||
pub fn new_node_to_node<R>(rng: &mut R) -> Result<Self, LpError>
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
// no need for censorship resistance between nodes (for now)
|
||||
// hop_id between nodes is 0
|
||||
Self::new(rng, 0, false, true, false)
|
||||
}
|
||||
|
||||
pub fn new<R>(
|
||||
rng: &mut R,
|
||||
hop_id: u8,
|
||||
is_exit: bool,
|
||||
node_initiator: bool,
|
||||
censorship_resistance: bool,
|
||||
) -> Result<Self, LpError>
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
{
|
||||
Self::build_checked(
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
rng.random(),
|
||||
rng.random(),
|
||||
)
|
||||
}
|
||||
fn build(
|
||||
hop_id: u8,
|
||||
is_exit: bool,
|
||||
node_initiator: bool,
|
||||
censorship_resistance: bool,
|
||||
seed: [u8; SEED_LEN],
|
||||
filler: [u8; FILLER_LEN],
|
||||
) -> Self {
|
||||
Self {
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
filler,
|
||||
seed,
|
||||
}
|
||||
}
|
||||
fn build_checked(
|
||||
hop_id: u8,
|
||||
is_exit: bool,
|
||||
node_initiator: bool,
|
||||
censorship_resistance: bool,
|
||||
seed: [u8; SEED_LEN],
|
||||
filler: [u8; FILLER_LEN],
|
||||
) -> Result<Self, LpError> {
|
||||
if node_initiator && is_exit {
|
||||
Err(LpError::Internal(
|
||||
"A node cannot establish an exit node for itself.".into(),
|
||||
))
|
||||
} else if node_initiator && hop_id != 0 {
|
||||
Err(LpError::Internal(
|
||||
"Hop id in node to node connections must be zero.".into(),
|
||||
))
|
||||
} else if !node_initiator && hop_id >= MAX_HOPS {
|
||||
Err(LpError::Internal(format!(
|
||||
"Requested hop index ({}) is greater than the allowed maximum {}.",
|
||||
hop_id,
|
||||
MAX_HOPS - 1
|
||||
)))
|
||||
} else if !node_initiator && is_exit && hop_id == 0 {
|
||||
Err(LpError::Internal(
|
||||
"Hop id for exit node cannot be zero.".into(),
|
||||
))
|
||||
} else if !node_initiator && !is_exit && hop_id == 15 {
|
||||
Err(LpError::Internal(
|
||||
"The hop with id 15 must be an exit node.".into(),
|
||||
))
|
||||
} else {
|
||||
Ok(Self::build(
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
seed,
|
||||
filler,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hop_id(&self) -> u8 {
|
||||
self.hop_id
|
||||
}
|
||||
|
||||
pub fn seed(&self) -> &[u8; SEED_LEN] {
|
||||
&self.seed
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> [u8; LP_PEER_CONFIG_SIZE] {
|
||||
let mut output_bytes: [u8; LP_PEER_CONFIG_SIZE] = [0u8; LP_PEER_CONFIG_SIZE];
|
||||
output_bytes[0..4].copy_from_slice(self.pack_config().as_slice());
|
||||
output_bytes[4..].copy_from_slice(&self.seed);
|
||||
output_bytes
|
||||
}
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() != LP_PEER_CONFIG_SIZE {
|
||||
Err(LpError::DeserializationError(format!(
|
||||
"Invalid Lp Config Length ({}), expected ({})",
|
||||
bytes.len(),
|
||||
LP_PEER_CONFIG_SIZE
|
||||
)))
|
||||
} else {
|
||||
let (hop_id, is_exit, node_initiator, censorship_resistance) =
|
||||
Self::unpack_first_byte(bytes[0]);
|
||||
|
||||
let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN];
|
||||
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
|
||||
|
||||
let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN];
|
||||
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
|
||||
|
||||
Self::build_checked(
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
seed,
|
||||
filler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_config(&self) -> [u8; 4] {
|
||||
[
|
||||
self.pack_first_byte(),
|
||||
self.filler[0],
|
||||
self.filler[1],
|
||||
self.filler[2],
|
||||
]
|
||||
}
|
||||
|
||||
fn pack_first_byte(&self) -> u8 {
|
||||
let mut byte = self.hop_id;
|
||||
|
||||
// Set the 5th bit to determine if the node is an exit node
|
||||
if self.is_exit {
|
||||
byte |= 0b0001_0000;
|
||||
}
|
||||
// Set the 6th bit to determine if we're establishing a node to node connection
|
||||
if self.node_initiator {
|
||||
byte |= 0b0010_0000;
|
||||
}
|
||||
// Set the 7th bit to determine if we should use censorship resistance measures
|
||||
if self.censorship_resistance {
|
||||
byte |= 0b0100_0000;
|
||||
}
|
||||
|
||||
// There will be 1 free bit at the end
|
||||
|
||||
byte
|
||||
}
|
||||
|
||||
fn unpack_first_byte(byte: u8) -> (u8, bool, bool, bool) {
|
||||
// extract 4 bits
|
||||
let hop_id = byte & 0b0000_1111;
|
||||
|
||||
// extract 5th bit
|
||||
let is_exit = (byte & 0b0001_0000) >> 4 == 1;
|
||||
// extract 6th bit
|
||||
let node_initiator = (byte & 0b0010_0000) >> 5 == 1;
|
||||
// extract 7th bit
|
||||
let censorship_resistance = (byte & 0b0100_0000) >> 6 == 1;
|
||||
|
||||
// If we need to use the last bit, we can add something here
|
||||
(hop_id, is_exit, node_initiator, censorship_resistance)
|
||||
}
|
||||
|
||||
pub fn is_client_entry(&self) -> bool {
|
||||
self.hop_id == 0 && !self.is_exit && !self.node_initiator
|
||||
}
|
||||
|
||||
pub fn is_client_intermediate_node(&self) -> bool {
|
||||
self.hop_id > 0 && !self.is_exit && !self.node_initiator
|
||||
}
|
||||
|
||||
pub fn is_client_exit(&self) -> bool {
|
||||
self.hop_id > 0 && self.is_exit && !self.node_initiator
|
||||
}
|
||||
|
||||
pub fn is_node_to_node(&self) -> bool {
|
||||
self.hop_id == 0 && !self.is_exit && self.node_initiator
|
||||
}
|
||||
|
||||
// This returns a LpReceiverIndex made out of the first 4 bytes from
|
||||
// KDF(RECEIVER_INDEX_DERIVATION_CONTEXT, initiator_pub_key || responder_kem_key, seed)
|
||||
pub fn derive_receiver_index(
|
||||
&self,
|
||||
initiator_public_key: &Authenticator,
|
||||
responder_kem_pk: &EncapsulationKey,
|
||||
) -> Result<LpReceiverIndex, LpError> {
|
||||
let initiator_public_key = initiator_public_key.tls_serialize_detached().map_err(|_| {
|
||||
LpError::Internal(
|
||||
"Failed to serialize initiator public key when computing receiver index".into(),
|
||||
)
|
||||
})?;
|
||||
let mut h = derive_key_blake3_multi_input(
|
||||
RECEIVER_INDEX_DERIVATION_CONTEXT,
|
||||
&[initiator_public_key.as_slice(), responder_kem_pk.as_bytes()],
|
||||
self.seed(),
|
||||
);
|
||||
let index = LpReceiverIndex::from_le_bytes([h[0], h[1], h[2], h[3]]);
|
||||
h.zeroize();
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::peer_config::LpPeerConfig;
|
||||
|
||||
#[test]
|
||||
fn test_pack_config() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Node to node, no censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0010_0000;
|
||||
let conf = LpPeerConfig::new(&mut rng, 0, false, true, false).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(
|
||||
conf_bytes[0],
|
||||
LpPeerConfig::new_node_to_node(&mut rng)
|
||||
.unwrap()
|
||||
.serialize()[0]
|
||||
);
|
||||
assert!(conf.is_node_to_node());
|
||||
}
|
||||
|
||||
// Node to node, with censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0110_0000;
|
||||
let conf = LpPeerConfig::new(&mut rng, 0, false, true, true).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert!(conf.is_node_to_node());
|
||||
}
|
||||
|
||||
// Client to Entry, no censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0000_0000;
|
||||
let conf = LpPeerConfig::new(&mut rng, 0, false, false, false).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
let conf_alt_first_byte =
|
||||
LpPeerConfig::new_client_to_entry(&mut rng, false).serialize()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(conf_bytes[0], conf_alt_first_byte);
|
||||
assert!(conf.is_client_entry())
|
||||
}
|
||||
|
||||
// Client to Entry, with censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0100_0000;
|
||||
let conf = LpPeerConfig::new(&mut rng, 0, false, false, true).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
let conf_alt_first_byte =
|
||||
LpPeerConfig::new_client_to_entry(&mut rng, true).serialize()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(conf_bytes[0], conf_alt_first_byte);
|
||||
assert!(conf.is_client_entry());
|
||||
}
|
||||
|
||||
// Client to Exit(exit hop = 1), with censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0101_0001;
|
||||
let conf = LpPeerConfig::new(&mut rng, 1, true, false, true).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
let conf_alt_first_byte = LpPeerConfig::new_client_to_exit(&mut rng, 1, true)
|
||||
.unwrap()
|
||||
.serialize()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(conf_bytes[0], conf_alt_first_byte);
|
||||
assert!(conf.is_client_exit());
|
||||
}
|
||||
|
||||
// Client to Exit(exit hop = 2), without censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0001_0010;
|
||||
let conf = LpPeerConfig::new(&mut rng, 2, true, false, false).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
let conf_alt_first_byte = LpPeerConfig::new_client_to_exit(&mut rng, 2, false)
|
||||
.unwrap()
|
||||
.serialize()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(conf_bytes[0], conf_alt_first_byte);
|
||||
assert!(conf.is_client_exit());
|
||||
}
|
||||
// Client to Intermediate (hop_id = 14), without censorship resistance
|
||||
{
|
||||
let expected_conf = 0b0000_1110;
|
||||
let conf = LpPeerConfig::new(&mut rng, 14, false, false, false).unwrap();
|
||||
let conf_bytes = conf.serialize();
|
||||
let deserialized_conf_first_byte = LpPeerConfig::deserialize(&conf_bytes)
|
||||
.unwrap()
|
||||
.pack_config()[0];
|
||||
let conf_alt_first_byte = LpPeerConfig::new_client_to_intermediate(&mut rng, 14, false)
|
||||
.unwrap()
|
||||
.serialize()[0];
|
||||
|
||||
assert_eq!(expected_conf, conf_bytes[0]);
|
||||
assert_eq!(expected_conf, deserialized_conf_first_byte);
|
||||
assert_eq!(conf_bytes[0], conf_alt_first_byte);
|
||||
assert!(conf.is_client_intermediate_node());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_failures() {
|
||||
let mut rng = rand09::rng();
|
||||
// Hop with id 15 must be an exit node
|
||||
assert!(LpPeerConfig::new(&mut rng, 15, false, false, false).is_err());
|
||||
|
||||
// intermediate hop cannot be the first hop
|
||||
assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 0, false).is_err());
|
||||
// intermediate hop cannot be the last hop
|
||||
assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 15, false).is_err());
|
||||
|
||||
// Hop with id 0 must be an entry node
|
||||
assert!(LpPeerConfig::new_client_to_intermediate(&mut rng, 0, false).is_err());
|
||||
assert!(LpPeerConfig::new_client_to_exit(&mut rng, 0, false).is_err());
|
||||
assert!(LpPeerConfig::new(&mut rng, 0, true, false, false).is_err());
|
||||
|
||||
// cannot be node to node with hop_id > 0
|
||||
assert!(LpPeerConfig::new(&mut rng, 1, false, true, false).is_err());
|
||||
|
||||
// cannot be node to node and exit at the same time
|
||||
assert!(LpPeerConfig::new(&mut rng, 0, true, true, false).is_err());
|
||||
|
||||
// cannot have hop_id greater than 15
|
||||
// this is a valid config
|
||||
assert!(LpPeerConfig::new(&mut rng, 0, false, false, false).is_ok());
|
||||
// this is a valid config
|
||||
assert!(LpPeerConfig::new(&mut rng, 14, false, false, false).is_ok());
|
||||
// this is a valid config
|
||||
assert!(LpPeerConfig::new(&mut rng, 15, true, false, false).is_ok());
|
||||
// these are not valid configs
|
||||
assert!(LpPeerConfig::new(&mut rng, 16, false, false, false).is_err());
|
||||
assert!(LpPeerConfig::new(&mut rng, 16, true, false, false).is_err());
|
||||
assert!(LpPeerConfig::new(&mut rng, 240, false, false, false).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,792 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! PSK (Pre-Shared Key) derivation for LP sessions using Blake3 KDF.
|
||||
//!
|
||||
//! This module implements identity-bound PSK derivation where both client and gateway
|
||||
//! derive the same PSK from their LP keypairs.
|
||||
//!
|
||||
//! PSQ is embedded in Noise (not separate protocol) because:
|
||||
//! 1. Single round-trip: PSQ ciphertext piggybacks on Noise handshake messages
|
||||
//! 2. PSK binding: Noise XKpsk3 pattern authenticates both ECDH and PSQ-derived PSK
|
||||
//! 3. Simpler state machine: No separate PSQ negotiation phase needed
|
||||
//! 4. Atomic security: Session establishment either succeeds fully or fails completely
|
||||
//!
|
||||
//! Two approaches are supported:
|
||||
//! - **Legacy ECDH-only** (`derive_psk`) - Simple but no post-quantum security
|
||||
//! - **PSQ-enhanced** (`derive_psk_with_psq_*`) - Combines ECDH with post-quantum KEM
|
||||
//!
|
||||
//! ## Error Handling Strategy
|
||||
//!
|
||||
//! **PSQ failures always abort the handshake cleanly with no retry or fallback.**
|
||||
//!
|
||||
//! ### Rationale
|
||||
//!
|
||||
//! PSQ errors indicate:
|
||||
//! - **Authentication failures** (CredError) - Potential attack or misconfiguration
|
||||
//! - **Timing failures** (TimestampElapsed) - Replay attacks or clock skew
|
||||
//! - **Crypto failures** (CryptoError) - Library bugs or hardware faults
|
||||
//! - **Serialization failures** (Serialization) - Protocol violations or corruption
|
||||
//!
|
||||
//! None of these are transient errors that benefit from retry. Falling back to
|
||||
//! ECDH-only PSK would silently degrade post-quantum security.
|
||||
//!
|
||||
//! ### Error Recovery Behavior
|
||||
//!
|
||||
//! On any PSQ error:
|
||||
//! 1. Function returns `Err(LpError)` immediately
|
||||
//! 2. Session state remains unchanged (dummy PSK, clean Noise state)
|
||||
//! 3. Handshake aborts - caller must start fresh connection
|
||||
//! 4. Error is logged with diagnostic context
|
||||
//!
|
||||
//! ### State Guarantees on Error
|
||||
//!
|
||||
//! - **`psq_state`**: Remains in `NotStarted` (initiator) or `ResponderWaiting` (responder)
|
||||
//! - **Noise `HandshakeState`**: PSK slot 3 = dummy `[0u8; 32]` (not modified on error)
|
||||
//! - **No partial data**: All allocations are stack-local to failed function
|
||||
//! - **No cleanup needed**: No state was mutated
|
||||
|
||||
use crate::LpError;
|
||||
use libcrux_psq::v1::cred::{Authenticator, Ed25519};
|
||||
use libcrux_psq::v1::impls::X25519 as PsqX25519;
|
||||
use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder};
|
||||
use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use std::time::Duration;
|
||||
use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait};
|
||||
|
||||
/// Context string for Blake3 KDF domain separation (PSQ-enhanced).
|
||||
const PSK_PSQ_CONTEXT: &str = "nym-lp-psk-psq-v1";
|
||||
|
||||
/// Session context for PSQ protocol.
|
||||
const PSQ_SESSION_CONTEXT: &[u8] = b"nym-lp-psq-session";
|
||||
|
||||
/// Context string for subsession PSK derivation.
|
||||
const SUBSESSION_PSK_CONTEXT: &str = "lp-subsession-psk-v1";
|
||||
|
||||
/// Result from PSQ initiator message creation.
|
||||
///
|
||||
/// Contains all outputs needed for session establishment:
|
||||
/// - `psk`: Final derived PSK for Noise handshake (ECDH || K_pq || salt → Blake3)
|
||||
/// - `payload`: Serialized PSQ message to send to responder
|
||||
/// - `pq_shared_secret`: Raw K_pq from KEM encapsulation (for subsession derivation)
|
||||
#[derive(Debug)]
|
||||
pub struct PsqInitiatorResult {
|
||||
/// Final PSK for Noise XKpsk3 handshake
|
||||
pub psk: [u8; 32],
|
||||
/// Serialized PSQ payload to embed in handshake message
|
||||
pub payload: Vec<u8>,
|
||||
/// Raw PQ shared secret (K_pq) before KDF combination.
|
||||
/// Used for deriving subsession PSKs to preserve PQ protection.
|
||||
pub pq_shared_secret: [u8; 32],
|
||||
}
|
||||
|
||||
/// Result from PSQ responder message processing.
|
||||
///
|
||||
/// Contains all outputs needed for session establishment:
|
||||
/// - `psk`: Final derived PSK for Noise handshake (matches initiator's)
|
||||
/// - `psk_handle`: Encrypted PSK handle (ctxt_B) to send back to initiator
|
||||
/// - `pq_shared_secret`: Raw K_pq from KEM decapsulation (for subsession derivation)
|
||||
#[derive(Debug)]
|
||||
pub struct PsqResponderResult {
|
||||
/// Final PSK for Noise XKpsk3 handshake
|
||||
pub psk: [u8; 32],
|
||||
/// Encrypted PSK handle (ctxt_B) from PSQ responder message
|
||||
pub psk_handle: Vec<u8>,
|
||||
/// Raw PQ shared secret (K_pq) before KDF combination.
|
||||
/// Used for deriving subsession PSKs to preserve PQ protection.
|
||||
pub pq_shared_secret: [u8; 32],
|
||||
}
|
||||
|
||||
/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Initiator side.
|
||||
///
|
||||
/// This function combines classical ECDH with post-quantum KEM to provide forward secrecy
|
||||
/// and HNDL (Harvest-Now, Decrypt-Later) resistance.
|
||||
///
|
||||
/// # Formula
|
||||
/// ```text
|
||||
/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public)
|
||||
/// (psq_psk, ct) = PSQ_Encapsulate(remote_kem_public, session_context)
|
||||
/// psk = Blake3_derive_key(
|
||||
/// context="nym-lp-psk-psq-v1",
|
||||
/// input=ecdh_secret || psq_psk || salt
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_x25519_private` - Initiator's X25519 private key (for Noise)
|
||||
/// * `remote_x25519_public` - Responder's X25519 public key (for Noise)
|
||||
/// * `remote_kem_public` - Responder's KEM public key (obtained via KKT)
|
||||
/// * `salt` - 32-byte salt for session binding
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok((psk, ciphertext))` - PSK and ciphertext to send to responder
|
||||
/// * `Err(LpError)` - If PSQ encapsulation fails
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Client side (after KKT exchange)
|
||||
/// let (psk, ciphertext) = derive_psk_with_psq_initiator(
|
||||
/// client_x25519_private,
|
||||
/// gateway_x25519_public,
|
||||
/// &gateway_kem_key, // from KKT
|
||||
/// &salt
|
||||
/// )?;
|
||||
/// // Send ciphertext to gateway
|
||||
/// ```
|
||||
pub fn derive_psk_with_psq_initiator(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
remote_kem_public: &EncapsulationKey,
|
||||
salt: &[u8; 32],
|
||||
) -> Result<([u8; 32], Vec<u8>), LpError> {
|
||||
// Step 1: Classical ECDH for baseline security
|
||||
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
|
||||
|
||||
// Step 2: PSQ encapsulation for post-quantum security
|
||||
// KEM algorithm migration path:
|
||||
// - X25519: Current default for testing/compatibility (no HNDL resistance)
|
||||
// - MlKem768: Future production default (NIST PQ Level 3, HNDL resistant)
|
||||
// - XWing: Maximum security option (hybrid X25519 + ML-KEM)
|
||||
// Migration: Update LpConfig.kem_algorithm, no protocol changes needed.
|
||||
// KKT protocol adapts automatically to different KEM key sizes.
|
||||
let kem_pk = match remote_kem_public {
|
||||
EncapsulationKey::X25519(pk) => pk,
|
||||
_ => {
|
||||
return Err(LpError::KKTError(
|
||||
"Only X25519 KEM is currently supported for PSQ".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut rng = rand09::rng();
|
||||
let (psq_psk, ciphertext) =
|
||||
PsqX25519::encapsulate_psq(kem_pk, PSQ_SESSION_CONTEXT, &mut rng)
|
||||
.map_err(|e| LpError::Internal(format!("PSQ encapsulation failed: {:?}", e)))?;
|
||||
|
||||
// Step 3: Combine ECDH + PSQ via Blake3 KDF
|
||||
let mut combined = Vec::with_capacity(64 + psq_psk.len());
|
||||
combined.extend_from_slice(&ecdh_secret);
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Serialize ciphertext using TLS encoding for transport
|
||||
let ct_bytes = ciphertext
|
||||
.tls_serialize_detached()
|
||||
.map_err(|e| LpError::Internal(format!("Ciphertext serialization failed: {:?}", e)))?;
|
||||
|
||||
Ok((final_psk, ct_bytes))
|
||||
}
|
||||
|
||||
/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Responder side.
|
||||
///
|
||||
/// This function decapsulates the ciphertext from the initiator and combines it with
|
||||
/// ECDH to derive the same PSK.
|
||||
///
|
||||
/// # Formula
|
||||
/// ```text
|
||||
/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public)
|
||||
/// psq_psk = PSQ_Decapsulate(local_kem_keypair, ciphertext, session_context)
|
||||
/// psk = Blake3_derive_key(
|
||||
/// context="nym-lp-psk-psq-v1",
|
||||
/// input=ecdh_secret || psq_psk || salt
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_x25519_private` - Responder's X25519 private key (for Noise)
|
||||
/// * `remote_x25519_public` - Initiator's X25519 public key (for Noise)
|
||||
/// * `local_kem_keypair` - Responder's KEM keypair (decapsulation key, public key)
|
||||
/// * `ciphertext` - PSQ ciphertext from initiator
|
||||
/// * `salt` - 32-byte salt for session binding
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(psk)` - Derived PSK
|
||||
/// * `Err(LpError)` - If PSQ decapsulation fails
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Gateway side (after receiving ciphertext)
|
||||
/// let psk = derive_psk_with_psq_responder(
|
||||
/// gateway_x25519_private,
|
||||
/// client_x25519_public,
|
||||
/// (&gateway_kem_sk, &gateway_kem_pk),
|
||||
/// &ciphertext, // from client
|
||||
/// &salt
|
||||
/// )?;
|
||||
/// ```
|
||||
pub fn derive_psk_with_psq_responder(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
|
||||
ciphertext: &[u8],
|
||||
salt: &[u8; 32],
|
||||
) -> Result<[u8; 32], LpError> {
|
||||
// Step 1: Classical ECDH for baseline security
|
||||
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
|
||||
|
||||
// Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
|
||||
let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
|
||||
(DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk),
|
||||
_ => {
|
||||
return Err(LpError::KKTError(
|
||||
"Only X25519 KEM is currently supported for PSQ".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3: Deserialize ciphertext using TLS decoding
|
||||
let ct = PsqCiphertext::<PsqX25519>::tls_deserialize(&mut &ciphertext[..])
|
||||
.map_err(|e| LpError::Internal(format!("Ciphertext deserialization failed: {:?}", e)))?;
|
||||
|
||||
// Step 4: PSQ decapsulation for post-quantum security
|
||||
let psq_psk = PsqX25519::decapsulate_psq(kem_sk, kem_pk, &ct, PSQ_SESSION_CONTEXT)
|
||||
.map_err(|e| LpError::Internal(format!("PSQ decapsulation failed: {:?}", e)))?;
|
||||
|
||||
// Step 5: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator)
|
||||
let mut combined = Vec::with_capacity(64 + psq_psk.len());
|
||||
combined.extend_from_slice(&ecdh_secret);
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
Ok(final_psk)
|
||||
}
|
||||
|
||||
/// PSQ protocol wrapper for initiator (client) side.
|
||||
///
|
||||
/// Creates a PSQ initiator message with Ed25519 authentication, following the protocol:
|
||||
/// 1. Encapsulate PSK using responder's KEM key
|
||||
/// 2. Derive PSK and AEAD keys from K_pq
|
||||
/// 3. Sign the encapsulation with Ed25519
|
||||
/// 4. AEAD encrypt (timestamp || signature || public_key)
|
||||
///
|
||||
/// Returns (PSK, serialized_payload) where payload includes enc_pq and encrypted auth data.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_x25519_private` - Client's X25519 private key (for hybrid ECDH)
|
||||
/// * `remote_x25519_public` - Gateway's X25519 public key (for hybrid ECDH)
|
||||
/// * `remote_kem_public` - Gateway's PQ KEM public key (from KKT)
|
||||
/// * `client_ed25519_sk` - Client's Ed25519 signing key
|
||||
/// * `client_ed25519_pk` - Client's Ed25519 public key (credential)
|
||||
/// * `salt` - Session salt
|
||||
/// * `session_context` - Context bytes for PSQ (e.g., b"nym-lp-psq-session")
|
||||
///
|
||||
/// # Returns
|
||||
/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret
|
||||
pub fn psq_initiator_create_message(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
remote_kem_public: &EncapsulationKey,
|
||||
client_ed25519_sk: &ed25519::PrivateKey,
|
||||
client_ed25519_pk: &ed25519::PublicKey,
|
||||
salt: &[u8; 32],
|
||||
session_context: &[u8],
|
||||
) -> Result<PsqInitiatorResult, LpError> {
|
||||
// Step 1: Classical ECDH for baseline security
|
||||
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
|
||||
|
||||
// Step 2: PSQ v1 with Ed25519 authentication
|
||||
// Extract X25519 KEM key from EncapsulationKey
|
||||
let kem_pk = match remote_kem_public {
|
||||
EncapsulationKey::X25519(pk) => pk,
|
||||
_ => {
|
||||
return Err(LpError::KKTError(
|
||||
"Only X25519 KEM is currently supported for PSQ".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert nym Ed25519 keys to libcrux format
|
||||
type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey;
|
||||
let ed25519_sk_bytes = client_ed25519_sk.to_bytes();
|
||||
let ed25519_pk_bytes = client_ed25519_pk.to_bytes();
|
||||
let ed25519_verification_key = Ed25519VerificationKey::from_bytes(ed25519_pk_bytes);
|
||||
|
||||
// Use PSQ v1 API with Ed25519 authentication
|
||||
let mut rng = rand09::rng();
|
||||
let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqX25519>(
|
||||
session_context,
|
||||
Duration::from_secs(3600), // 1 hour expiry
|
||||
kem_pk,
|
||||
&ed25519_sk_bytes,
|
||||
&ed25519_verification_key,
|
||||
&mut rng,
|
||||
)
|
||||
.map_err(|e| {
|
||||
tracing::error!(
|
||||
"PSQ initiator failed - KEM encapsulation or signing error: {:?}",
|
||||
e
|
||||
);
|
||||
LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e))
|
||||
})?;
|
||||
|
||||
// Extract PSQ shared secret (unregistered PSK) - this is K_pq
|
||||
let psq_psk = state.unregistered_psk();
|
||||
|
||||
// pq_shared_secret is the raw K_pq from KEM encapsulation.
|
||||
// Store it for subsession derivation before it's combined with ECDH.
|
||||
let pq_shared_secret: [u8; 32] = *psq_psk;
|
||||
|
||||
// Step 3: Combine ECDH + PSQ via Blake3 KDF
|
||||
let mut combined = Vec::with_capacity(64 + psq_psk.len());
|
||||
combined.extend_from_slice(&ecdh_secret);
|
||||
combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32]
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Serialize InitiatorMsg with TLS encoding for transport
|
||||
let msg_bytes = initiator_msg
|
||||
.tls_serialize_detached()
|
||||
.map_err(|e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e)))?;
|
||||
|
||||
Ok(PsqInitiatorResult {
|
||||
psk: final_psk,
|
||||
payload: msg_bytes,
|
||||
pq_shared_secret,
|
||||
})
|
||||
}
|
||||
|
||||
/// PSQ protocol wrapper for responder (gateway) side.
|
||||
///
|
||||
/// Processes a PSQ initiator message, verifies authentication, and derives PSK.
|
||||
/// Follows the protocol:
|
||||
/// 1. Decapsulate to get K_pq
|
||||
/// 2. Derive AEAD keys and verify encrypted auth data
|
||||
/// 3. Verify Ed25519 signature
|
||||
/// 4. Check timestamp validity
|
||||
/// 5. Derive PSK
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_x25519_private` - Gateway's X25519 private key (for hybrid ECDH)
|
||||
/// * `remote_x25519_public` - Client's X25519 public key (for hybrid ECDH)
|
||||
/// * `local_kem_keypair` - Gateway's PQ KEM keypair
|
||||
/// * `initiator_ed25519_pk` - Client's Ed25519 public key (for signature verification)
|
||||
/// * `psq_payload` - Serialized PSQ payload from initiator
|
||||
/// * `salt` - Session salt (must match initiator's)
|
||||
/// * `session_context` - Context bytes for PSQ
|
||||
///
|
||||
/// # Returns
|
||||
/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret
|
||||
pub fn psq_responder_process_message(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
|
||||
initiator_ed25519_pk: &ed25519::PublicKey,
|
||||
psq_payload: &[u8],
|
||||
salt: &[u8; 32],
|
||||
session_context: &[u8],
|
||||
) -> Result<PsqResponderResult, LpError> {
|
||||
// Step 1: Classical ECDH for baseline security
|
||||
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
|
||||
|
||||
// Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
|
||||
let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
|
||||
(DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk),
|
||||
_ => {
|
||||
return Err(LpError::KKTError(
|
||||
"Only X25519 KEM is currently supported for PSQ".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3: Deserialize InitiatorMsg using TLS decoding
|
||||
let initiator_msg = InitiatorMsg::<PsqX25519>::tls_deserialize(&mut &psq_payload[..])
|
||||
.map_err(|e| LpError::Internal(format!("InitiatorMsg deserialization failed: {:?}", e)))?;
|
||||
|
||||
// Step 4: Convert nym Ed25519 public key to libcrux VerificationKey format
|
||||
type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey;
|
||||
let initiator_ed25519_pk_bytes = initiator_ed25519_pk.to_bytes();
|
||||
let initiator_verification_key = Ed25519VerificationKey::from_bytes(initiator_ed25519_pk_bytes);
|
||||
|
||||
// Step 5: PSQ v1 responder processing with Ed25519 verification
|
||||
let (registered_psk, responder_msg) = Responder::send::<Ed25519, PsqX25519>(
|
||||
b"nym-lp-handle", // PSK storage handle
|
||||
Duration::from_secs(3600), // 1 hour expiry (must match initiator)
|
||||
session_context, // Must match initiator's session_context
|
||||
kem_pk, // Responder's public key
|
||||
kem_sk, // Responder's secret key
|
||||
&initiator_verification_key, // Initiator's Ed25519 public key for verification
|
||||
&initiator_msg, // InitiatorMsg to verify and process
|
||||
)
|
||||
.map_err(|e| {
|
||||
use libcrux_psq::v1::Error as PsqError;
|
||||
match e {
|
||||
PsqError::CredError => {
|
||||
tracing::warn!(
|
||||
"PSQ responder auth failure - invalid Ed25519 signature (potential attack)"
|
||||
);
|
||||
}
|
||||
PsqError::TimestampElapsed | PsqError::RegistrationError => {
|
||||
tracing::warn!(
|
||||
"PSQ responder timing failure - TTL expired (potential replay attack)"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("PSQ responder failed - {:?}", e);
|
||||
}
|
||||
}
|
||||
LpError::Internal(format!("PSQ v1 responder send failed: {:?}", e))
|
||||
})?;
|
||||
|
||||
// Extract the PSQ PSK from the registered PSK - this is K_pq
|
||||
let psq_psk = registered_psk.psk;
|
||||
|
||||
// pq_shared_secret is the raw K_pq from KEM decapsulation.
|
||||
// Store it for subsession derivation before it's combined with ECDH.
|
||||
let pq_shared_secret: [u8; 32] = psq_psk;
|
||||
|
||||
// Step 6: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator)
|
||||
let mut combined = Vec::with_capacity(64 + psq_psk.len());
|
||||
combined.extend_from_slice(&ecdh_secret);
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Step 7: Serialize ResponderMsg (contains ctxt_B - encrypted PSK handle)
|
||||
use tls_codec::Serialize;
|
||||
let responder_msg_bytes = responder_msg
|
||||
.tls_serialize_detached()
|
||||
.map_err(|e| LpError::Internal(format!("ResponderMsg serialization failed: {:?}", e)))?;
|
||||
|
||||
Ok(PsqResponderResult {
|
||||
psk: final_psk,
|
||||
psk_handle: responder_msg_bytes,
|
||||
pq_shared_secret,
|
||||
})
|
||||
}
|
||||
|
||||
/// Derive subsession PSK from parent's PQ shared secret.
|
||||
///
|
||||
/// Uses Blake3 KDF with domain separation to derive unique PSK for each subsession.
|
||||
/// This preserves PQ protection: subsession keys inherit quantum resistance from
|
||||
/// parent's KEM shared secret (K_pq).
|
||||
///
|
||||
/// # Security Model
|
||||
///
|
||||
/// Subsessions use Noise KKpsk0 pattern where:
|
||||
/// - Both parties already know each other's static X25519 keys (from parent session)
|
||||
/// - PSK provides PQ protection by deriving from parent's K_pq
|
||||
/// - Each subsession gets unique PSK via index parameter (prevents key reuse)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pq_shared_secret` - Parent session's K_pq (32 bytes from KEM)
|
||||
/// * `subsession_index` - Monotonic index for this subsession (prevents reuse)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte PSK for Noise KKpsk0 handshake
|
||||
pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] {
|
||||
nym_crypto::kdf::derive_key_blake3(
|
||||
SUBSESSION_PSK_CONTEXT,
|
||||
pq_shared_secret,
|
||||
&subsession_index.to_le_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::thread_rng;
|
||||
|
||||
fn generate_x25519_keypair() -> x25519::KeyPair {
|
||||
x25519::KeyPair::new(&mut thread_rng())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psk_derivation_is_symmetric() {
|
||||
let keypair_1 = generate_x25519_keypair();
|
||||
let keypair_2 = generate_x25519_keypair();
|
||||
let salt = [2u8; 32];
|
||||
|
||||
let mut rng = &mut rand09::rng();
|
||||
let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
let dec_key = DecapsulationKey::X25519(_kem_sk);
|
||||
|
||||
// Client derives PSK
|
||||
let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway derives PSK from their perspective
|
||||
let gateway_psk = derive_psk_with_psq_responder(
|
||||
keypair_2.private_key(),
|
||||
keypair_1.public_key(),
|
||||
(&dec_key, &enc_key),
|
||||
&ciphertext,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client_psk, gateway_psk,
|
||||
"Both sides should derive identical PSK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_psks() {
|
||||
let keypair_1 = generate_x25519_keypair();
|
||||
let keypair_2 = generate_x25519_keypair();
|
||||
|
||||
let salt1 = [1u8; 32];
|
||||
let salt2 = [2u8; 32];
|
||||
let mut rng = &mut rand09::rng();
|
||||
let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let psk1 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt1,
|
||||
)
|
||||
.unwrap();
|
||||
let psk2 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_keys_produce_different_psks() {
|
||||
let keypair_1 = generate_x25519_keypair();
|
||||
let keypair_2 = generate_x25519_keypair();
|
||||
let keypair_3 = generate_x25519_keypair();
|
||||
let salt = [3u8; 32];
|
||||
|
||||
let mut rng = &mut rand09::rng();
|
||||
let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let psk1 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
let psk2 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_3.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(
|
||||
psk1, psk2,
|
||||
"Different remote keys should produce different PSKs"
|
||||
);
|
||||
}
|
||||
|
||||
// PSQ-enhanced PSK tests
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey, KEM};
|
||||
use nym_kkt::key_utils::generate_keypair_libcrux;
|
||||
|
||||
#[test]
|
||||
fn test_psq_derivation_deterministic() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate X25519 keypairs for Noise
|
||||
let client_keypair = generate_x25519_keypair();
|
||||
let gateway_keypair = generate_x25519_keypair();
|
||||
|
||||
// Generate KEM keypair for PSQ
|
||||
let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
let dec_key = DecapsulationKey::X25519(kem_sk);
|
||||
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// Derive PSK twice with same inputs (initiator side)
|
||||
let (_psk1, ct1) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_psk2, _ct2) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// PSKs will be different due to randomness in PSQ, but ciphertexts too
|
||||
// This test verifies the function is deterministic given the SAME ciphertext
|
||||
let psk_responder1 = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
(&dec_key, &enc_key),
|
||||
&ct1,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let psk_responder2 = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
(&dec_key, &enc_key),
|
||||
&ct1, // Same ciphertext
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
psk_responder1, psk_responder2,
|
||||
"Same ciphertext should produce same PSK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_derivation_symmetric() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate X25519 keypairs for Noise
|
||||
let client_keypair = generate_x25519_keypair();
|
||||
let gateway_keypair = generate_x25519_keypair();
|
||||
|
||||
// Generate KEM keypair for PSQ
|
||||
let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
let dec_key = DecapsulationKey::X25519(kem_sk);
|
||||
|
||||
let salt = [2u8; 32];
|
||||
|
||||
// Client derives PSK (initiator)
|
||||
let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway derives PSK from ciphertext (responder)
|
||||
let gateway_psk = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
(&dec_key, &enc_key),
|
||||
&ciphertext,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client_psk, gateway_psk,
|
||||
"Both sides should derive identical PSK via PSQ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_kem_keys_different_psk() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let client_keypair = generate_x25519_keypair();
|
||||
let gateway_keypair = generate_x25519_keypair();
|
||||
|
||||
// Two different KEM keypairs
|
||||
let (_, kem_pk1) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let (_, kem_pk2) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
|
||||
let enc_key1 = EncapsulationKey::X25519(kem_pk1);
|
||||
let enc_key2 = EncapsulationKey::X25519(kem_pk2);
|
||||
|
||||
let salt = [3u8; 32];
|
||||
|
||||
let (psk1, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key1,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (psk2, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key2,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(
|
||||
psk1, psk2,
|
||||
"Different KEM keys should produce different PSKs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_psk_output_length() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let client_keypair = generate_x25519_keypair();
|
||||
let gateway_keypair = generate_x25519_keypair();
|
||||
|
||||
let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let salt = [4u8; 32];
|
||||
|
||||
let (psk, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psk.len(), 32, "PSQ PSK should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_different_salts_different_psks() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let client_keypair = generate_x25519_keypair();
|
||||
let gateway_keypair = generate_x25519_keypair();
|
||||
|
||||
let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let salt1 = [1u8; 32];
|
||||
let salt2 = [2u8; 32];
|
||||
|
||||
let (psk1, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (psk2, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
&enc_key,
|
||||
&salt2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size};
|
||||
use crate::transport::{LpTransportError, traits::HandshakeMessage};
|
||||
use nym_kkt::context::KKTMode;
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub struct KKTRequest(nym_kkt::message::KKTRequest);
|
||||
|
||||
impl From<nym_kkt::message::KKTRequest> for KKTRequest {
|
||||
fn from(request: nym_kkt::message::KKTRequest) -> Self {
|
||||
KKTRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTRequest> for nym_kkt::message::KKTRequest {
|
||||
fn from(request: KKTRequest) -> Self {
|
||||
request.0
|
||||
}
|
||||
}
|
||||
|
||||
impl HandshakeMessage for KKTRequest {
|
||||
fn into_bytes(self) -> Vec<u8> {
|
||||
self.0.into_bytes()
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, LpTransportError> {
|
||||
Ok(KKTRequest(
|
||||
nym_kkt::message::KKTRequest::try_from_bytes(&bytes)
|
||||
.map_err(|err| LpTransportError::MalformedPacket(err.to_string()))?,
|
||||
))
|
||||
}
|
||||
|
||||
fn expected_size(mode: KKTMode, expected_kem: KEM, payload_size: usize) -> usize {
|
||||
nym_kkt::message::KKTRequest::size_excluding_payload(mode, expected_kem) + payload_size
|
||||
}
|
||||
|
||||
fn response_size(&self, expected_kem: KEM) -> Option<usize> {
|
||||
Some(nym_kkt::message::KKTResponse::size_excluding_payload(
|
||||
expected_kem,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KKTResponse(nym_kkt::message::KKTResponse);
|
||||
|
||||
impl From<nym_kkt::message::KKTResponse> for KKTResponse {
|
||||
fn from(request: nym_kkt::message::KKTResponse) -> Self {
|
||||
KKTResponse(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTResponse> for nym_kkt::message::KKTResponse {
|
||||
fn from(request: KKTResponse) -> Self {
|
||||
request.0
|
||||
}
|
||||
}
|
||||
|
||||
impl HandshakeMessage for KKTResponse {
|
||||
fn into_bytes(self) -> Vec<u8> {
|
||||
self.0.into_bytes()
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, LpTransportError> {
|
||||
Ok(KKTResponse(nym_kkt::message::KKTResponse::from_bytes(
|
||||
bytes,
|
||||
)))
|
||||
}
|
||||
|
||||
fn expected_size(_: KKTMode, expected_kem: KEM, payload_size: usize) -> usize {
|
||||
nym_kkt::message::KKTResponse::size_excluding_payload(expected_kem) + payload_size
|
||||
}
|
||||
|
||||
fn response_size(&self, expected_kem: KEM) -> Option<usize> {
|
||||
Some(psq_msg1_size(expected_kem))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQMsg1(Vec<u8>);
|
||||
|
||||
impl Deref for PSQMsg1 {
|
||||
type Target = Vec<u8>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PSQMsg1 {
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
PSQMsg1(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl HandshakeMessage for PSQMsg1 {
|
||||
fn into_bytes(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, LpTransportError> {
|
||||
Ok(PSQMsg1(bytes))
|
||||
}
|
||||
|
||||
fn expected_size(_: KKTMode, expected_kem: KEM, payload_size: usize) -> usize {
|
||||
psq_msg1_size(expected_kem) + payload_size
|
||||
}
|
||||
|
||||
fn response_size(&self, _: KEM) -> Option<usize> {
|
||||
Some(PSQ_MSG2_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQMsg2(Vec<u8>);
|
||||
|
||||
impl Deref for PSQMsg2 {
|
||||
type Target = Vec<u8>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PSQMsg2 {
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
PSQMsg2(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl HandshakeMessage for PSQMsg2 {
|
||||
fn into_bytes(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, LpTransportError> {
|
||||
Ok(PSQMsg2(bytes))
|
||||
}
|
||||
|
||||
fn expected_size(_: KKTMode, _: KEM, payload_size: usize) -> usize {
|
||||
PSQ_MSG2_SIZE + payload_size
|
||||
}
|
||||
|
||||
fn response_size(&self, _: KEM) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,12 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||
use crate::{LpError, LpPacket};
|
||||
use bytes::BytesMut;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use libcrux_psq::handshake::ciphersuites::CiphersuiteName;
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(not(test))]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
|
||||
.map(|d| d.as_secs())
|
||||
}
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransportHandshakeExt: LpTransport {
|
||||
// the outer key is temporary until the algorithm is changed with psqv2
|
||||
async fn receive_packet(
|
||||
&mut self,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let raw = self.receive_raw_packet().await?;
|
||||
parse_lp_packet(&raw, outer_key)
|
||||
}
|
||||
|
||||
async fn send_packet(
|
||||
&mut self,
|
||||
packet: LpPacket,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let mut packet_buf = BytesMut::new();
|
||||
|
||||
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
|
||||
self.send_serialised_packet(&packet_buf).await?;
|
||||
Ok(())
|
||||
pub(crate) fn kem_to_ciphersuite(kem: KEM) -> CiphersuiteName {
|
||||
match kem {
|
||||
KEM::MlKem768 => CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256,
|
||||
KEM::McEliece => CiphersuiteName::X25519_CLASSICMCELIECE_X25519_AESGCM128_HKDFSHA256,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
|
||||
|
||||
+300
-351
@@ -1,391 +1,340 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTRequestData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_initiator_create_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
|
||||
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::peer_config::LpPeerConfig;
|
||||
use crate::psq::handshake_message::{PSQMsg1, PSQMsg2};
|
||||
use crate::psq::helpers::kem_to_ciphersuite;
|
||||
use crate::psq::{
|
||||
AAD_INITIATOR_INNER_V1, AAD_INITIATOR_OUTER_V1, InitiatorData, PSQ_MSG2_SIZE,
|
||||
PSQHandshakeState, SESSION_CONTEXT_V1, handshake_message, psq_msg1_size,
|
||||
};
|
||||
use crate::session::PersistentSessionBinding;
|
||||
use crate::transport::traits::LpHandshakeChannel;
|
||||
use crate::{LpError, LpSession};
|
||||
use libcrux_psq::handshake::RegistrationInitiator;
|
||||
use libcrux_psq::handshake::builders::{
|
||||
CiphersuiteBuilder, InitiatorCiphersuite, PrincipalBuilder,
|
||||
};
|
||||
use libcrux_psq::handshake::types::Authenticator;
|
||||
use libcrux_psq::{Channel, IntoSession};
|
||||
use nym_kkt::initiator::KKTInitiator;
|
||||
use nym_kkt::keys::EncapsulationKey;
|
||||
use nym_kkt::message::{KKTRequest, KKTResponse};
|
||||
use rand09::SeedableRng;
|
||||
use tracing::debug;
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
pub struct PSQHandshakeStateInitiator<'a, S> {
|
||||
pub(super) inner_state: PSQHandshakeState<'a, S>,
|
||||
pub(super) initiator_data: InitiatorData,
|
||||
}
|
||||
|
||||
pub(crate) fn build_psq_principal<R>(
|
||||
rng: R,
|
||||
version: u8,
|
||||
ciphersuite: InitiatorCiphersuite,
|
||||
) -> Result<RegistrationInitiator<R>, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
R: rand09::CryptoRng,
|
||||
{
|
||||
/// Generate and send client hello to the responder
|
||||
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
let (ctx, inner_aad, outer_aad) = match version {
|
||||
1 => (
|
||||
SESSION_CONTEXT_V1,
|
||||
AAD_INITIATOR_INNER_V1,
|
||||
AAD_INITIATOR_OUTER_V1,
|
||||
),
|
||||
other => return Err(LpError::UnsupportedVersion { version: other }),
|
||||
};
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
let timestamp = current_timestamp()?;
|
||||
PrincipalBuilder::new(rng)
|
||||
.outer_aad(outer_aad)
|
||||
.inner_aad(inner_aad)
|
||||
.context(ctx)
|
||||
.build_registration_initiator(ciphersuite)
|
||||
.map_err(|inner| LpError::PSQInitiatorBuilderFailure { inner })
|
||||
}
|
||||
|
||||
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
|
||||
self.connection
|
||||
.send_packet(client_hello_data.into_lp_packet(protocol), None)
|
||||
.await?;
|
||||
Ok(client_hello_data)
|
||||
}
|
||||
|
||||
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
|
||||
/// whether the request has been successful or whether there has been a collision in receiver
|
||||
/// index requiring a retry
|
||||
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
|
||||
match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Ack => Ok(true),
|
||||
LpMessage::Collision => Ok(false),
|
||||
other => {
|
||||
// TODO: retry on collision
|
||||
Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub(crate) fn build_psq_ciphersuite<'a>(
|
||||
init: &'a LpLocalPeer,
|
||||
responder: &'a LpRemotePeer,
|
||||
kem_key: &'a EncapsulationKey,
|
||||
) -> Result<InitiatorCiphersuite<'a>, LpError> {
|
||||
let psq_ciphersuite = kem_to_ciphersuite(kem_key.kem());
|
||||
|
||||
let builder = CiphersuiteBuilder::new(psq_ciphersuite)
|
||||
.longterm_x25519_keys(init.x25519())
|
||||
.peer_longterm_x25519_pk(responder.x25519());
|
||||
|
||||
match kem_key {
|
||||
EncapsulationKey::McEliece(kem_key) => builder.peer_longterm_cmc_pk(kem_key),
|
||||
EncapsulationKey::MlKem768(kem_key) => builder.peer_longterm_mlkem_pk(kem_key),
|
||||
}
|
||||
.build_initiator_ciphersuite()
|
||||
.map_err(|inner| LpError::PSQInitiatorBuilderFailure { inner })
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeStateInitiator<'a, S>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
/// Attempt to send KKT request to begin the handshake
|
||||
pub(crate) async fn send_kkt_request(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
async fn send_kkt_request(&mut self, request: KKTRequest) -> Result<(), LpError> {
|
||||
let kem = self.inner_state.local_peer.ciphersuite.kem();
|
||||
|
||||
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
|
||||
let (session_secret, encrypted_frame) =
|
||||
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
|
||||
let lp_message = KKTRequestData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((kkt_context, session_secret))
|
||||
}
|
||||
|
||||
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
|
||||
/// the received encapsulation key
|
||||
pub(crate) async fn receive_kkt_response(
|
||||
&mut self,
|
||||
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<EncapsulationKey<'static>, LpError> {
|
||||
let kkt_response = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTResponse(response) => response,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTResponse,
|
||||
));
|
||||
}
|
||||
};
|
||||
debug!("received KKT response");
|
||||
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
|
||||
|
||||
let (response_frame, remote_context) =
|
||||
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
|
||||
let encapsulation_key = initiator_ingest_response(
|
||||
&kkt_context,
|
||||
&response_frame,
|
||||
&remote_context,
|
||||
&remote_peer.ed25519_public,
|
||||
&expected_kem_key_digest,
|
||||
)?;
|
||||
Ok(encapsulation_key)
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send initial PSQ msg1
|
||||
pub(crate) async fn send_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
let session_id = u32::from_le_bytes(*session_id_bytes);
|
||||
|
||||
let psq_initiator = psq_initiator_create_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
encapsulation_key,
|
||||
self.local_peer.ed25519.private_key(),
|
||||
self.local_peer.ed25519.public_key(),
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
// prepare noise state and msg1
|
||||
let mut noise_protocol = NoiseProtocol::build_new_initiator(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
|
||||
// prepare noise msg1
|
||||
let noise_msg1 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
|
||||
let psq_len = psq_payload.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
|
||||
combined.extend_from_slice(&psq_len.to_le_bytes());
|
||||
combined.extend_from_slice(&psq_payload);
|
||||
combined.extend_from_slice(&noise_msg1);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate received PSQ msg2
|
||||
pub(crate) async fn receive_psq_responder_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg2 = match self
|
||||
self.inner_state
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
|
||||
if psq_msg2.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
|
||||
if psq_msg2.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
// Extract and "store" the PSK handle
|
||||
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg2[2 + handle_len..];
|
||||
|
||||
// *sigh* ignore the message
|
||||
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
|
||||
.send_handshake_message::<handshake_message::KKTRequest>(request.into(), kem)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send final PSQ msg3
|
||||
pub(crate) async fn send_final_psq_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
/// Attempt to receive a KKT response to the previously sent request
|
||||
async fn receive_kkt_response(&mut self) -> Result<KKTResponse, LpError> {
|
||||
// no response payload
|
||||
let packet_len =
|
||||
KKTResponse::size_excluding_payload(self.inner_state.local_peer.ciphersuite.kem());
|
||||
|
||||
let noise_msg3 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
|
||||
|
||||
let lp_message = HandshakeData::new(noise_msg3).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
let resp = self
|
||||
.inner_state
|
||||
.connection
|
||||
.receive_handshake_message::<handshake_message::KKTResponse>(packet_len)
|
||||
.await?;
|
||||
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(resp.into())
|
||||
}
|
||||
|
||||
/// Receive final ACK that indicates finalisation of the handshake
|
||||
pub(crate) async fn receive_final_ack(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Ack => Ok(()),
|
||||
other => Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete_as_initiator_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
pub async fn complete_handshake(self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
// 0. retrieve the expected kem key hash. if we don't know it,
|
||||
// there's no point in even trying to start the handshake
|
||||
let Some(remote_peer) = self.remote_peer.take() else {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
|
||||
));
|
||||
};
|
||||
let mut rng = rand09::rngs::StdRng::from_os_rng();
|
||||
self.complete_handshake_with_rng(&mut rng).await
|
||||
}
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
// and keep retrying until we manage to establish a receiver index without collisions
|
||||
let mut attempt = 0;
|
||||
let client_hello_data = loop {
|
||||
attempt += 1;
|
||||
pub async fn complete_handshake_with_rng<R>(mut self, rng: &mut R) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
R: rand09::CryptoRng,
|
||||
{
|
||||
let ciphersuite = self.inner_state.local_peer.ciphersuite();
|
||||
let kem = ciphersuite.kem();
|
||||
|
||||
debug!("sending client hello");
|
||||
let client_hello = self
|
||||
.send_client_hello()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?;
|
||||
if self
|
||||
.receive_client_hello_ack()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?
|
||||
{
|
||||
debug!("received client hello ACK");
|
||||
break client_hello;
|
||||
}
|
||||
debug!("received client hello collision");
|
||||
let lp_peer_config = LpPeerConfig::new_client_to_entry(rng, false);
|
||||
|
||||
// TODO: make it configurable
|
||||
if attempt > 3 {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake(
|
||||
"failed to establish receiver index without collision",
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
// 1. retrieve the expected kem key hash. if we don't know it,
|
||||
let dir_hash = self
|
||||
.initiator_data
|
||||
.remote_peer
|
||||
.expected_kem_key_hash(ciphersuite)?;
|
||||
|
||||
// 2. prepare and send KKT request
|
||||
let (mut initiator, kkt_request) = KKTInitiator::generate_one_way_request(
|
||||
rng,
|
||||
ciphersuite,
|
||||
self.initiator_data.remote_peer.x25519(),
|
||||
&dir_hash,
|
||||
self.initiator_data.protocol_version,
|
||||
Some(Vec::from(lp_peer_config.serialize())),
|
||||
)?;
|
||||
// derive the receiver index from the request
|
||||
// let receiver_index = kkt_request
|
||||
|
||||
// 3. prepare and send KKT request
|
||||
debug!("sending KKT request");
|
||||
let kkt_data = self
|
||||
.send_kkt_request(session_id, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
self.send_kkt_request(kkt_request).await?;
|
||||
|
||||
// 4. receive and process KKT response
|
||||
let encapsulation_key = self
|
||||
.receive_kkt_response(kkt_data, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
// 3. receive and process KKT response
|
||||
let raw_response = self.receive_kkt_response().await?;
|
||||
debug!("received KKT response");
|
||||
|
||||
// 5. prepare and send PSQ msg1
|
||||
debug!("sending PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
|
||||
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
// the responder does not send a payload
|
||||
let response = initiator.process_response(raw_response, 0)?;
|
||||
|
||||
// 6. receive and process PSQ msg2
|
||||
debug!("received PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
// 4. generate and send PSQ request
|
||||
let protocol = self.initiator_data.protocol_version;
|
||||
let conn = self.inner_state.connection;
|
||||
|
||||
// note: the clone is cheap due to internal Arcs
|
||||
let encapsulation_key = response.encapsulation_key.clone();
|
||||
|
||||
// build the PSQ initiator
|
||||
let initiator_ciphersuite = build_psq_ciphersuite(
|
||||
&self.inner_state.local_peer,
|
||||
&self.initiator_data.remote_peer,
|
||||
&response.encapsulation_key,
|
||||
)?;
|
||||
|
||||
let mut psq_initiator = build_psq_principal(rng, protocol, initiator_ciphersuite)?;
|
||||
|
||||
// PSQ msg 1 send
|
||||
let mut buf = vec![0u8; psq_msg1_size(kem)];
|
||||
// annoyingly `RegistrationInitiator` has to write into unresizable `&mut [u8]`...
|
||||
let n = psq_initiator.write_message(&[], &mut buf)?;
|
||||
debug!("sending PSQ handshake msg");
|
||||
if n != buf.len() {
|
||||
return Err(LpError::Internal(
|
||||
"unexpected changes in PSQ msg1 size".to_string(),
|
||||
));
|
||||
}
|
||||
let msg = PSQMsg1::new(buf);
|
||||
conn.send_handshake_message(msg, kem).await?;
|
||||
|
||||
// 5. receive and process PSQ response
|
||||
let psq_msg: PSQMsg2 = conn.receive_handshake_message(PSQ_MSG2_SIZE).await?;
|
||||
debug!("received PSQ handshake msg");
|
||||
psq_initiator.read_message(&psq_msg, &mut [])?;
|
||||
|
||||
if !psq_initiator.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"handshake not finished after receiving psq response",
|
||||
));
|
||||
}
|
||||
|
||||
// 7. prepare and send PSQ msg3
|
||||
debug!("sending PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
let initiator_authenticator = Authenticator::Dh(self.inner_state.local_peer.x25519().pk);
|
||||
|
||||
// 8. receive final ACK and finalise
|
||||
debug!("received final ACK");
|
||||
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
let receiver_index =
|
||||
lp_peer_config.derive_receiver_index(&initiator_authenticator, &encapsulation_key)?;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
let binding = PersistentSessionBinding {
|
||||
initiator_authenticator,
|
||||
responder_ecdh_pk: self.initiator_data.remote_peer.x25519_public,
|
||||
responder_pq_pk: Some(encapsulation_key),
|
||||
};
|
||||
|
||||
// TODO: missing: receive counter check
|
||||
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_initiator_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
let psq_session = psq_initiator.into_session()?;
|
||||
LpSession::new(psq_session, binding, receiver_index, protocol)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codec::{decrypt_data, encrypt_data};
|
||||
use crate::peer::mock_peers;
|
||||
use crate::peer_config::LP_PEER_CONFIG_SIZE;
|
||||
use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size, responder};
|
||||
use nym_kkt::context::KKTMode;
|
||||
use nym_kkt::responder::KKTResponder;
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, IntoEnumIterator, KEM, SignatureScheme};
|
||||
use nym_test_utils::helpers::{DeterministicRng09Send, u64_seeded_rng_09};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, Timeboxed};
|
||||
|
||||
#[tokio::test]
|
||||
async fn initiator_test_plain() -> anyhow::Result<()> {
|
||||
for kem in KEM::iter() {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
|
||||
let (mut init, mut resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let ciphersuite = Ciphersuite::default().with_kem(kem);
|
||||
init.ciphersuite = ciphersuite;
|
||||
resp.ciphersuite = ciphersuite;
|
||||
let initiator_data = InitiatorData::new(1, resp_remote);
|
||||
|
||||
let handshake_init =
|
||||
PSQHandshakeState::new(conn_init, init).as_initiator(initiator_data);
|
||||
|
||||
let mut init_rng = DeterministicRng09Send::new(u64_seeded_rng_09(1));
|
||||
|
||||
let init_fut = tokio::spawn(async move {
|
||||
handshake_init
|
||||
.complete_handshake_with_rng(&mut init_rng)
|
||||
.timeboxed()
|
||||
.await
|
||||
});
|
||||
|
||||
// responder:
|
||||
let supported_sigs = [SignatureScheme::Ed25519];
|
||||
let supported_hash = [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::Shake256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::SHA256,
|
||||
];
|
||||
let resp_keys = resp.kem_keypairs.as_ref().unwrap();
|
||||
let responder_x25519_keypair = resp.x25519();
|
||||
|
||||
let kkt_responder = KKTResponder::new(
|
||||
responder_x25519_keypair,
|
||||
resp_keys,
|
||||
&supported_hash,
|
||||
&supported_sigs,
|
||||
&[1],
|
||||
)?;
|
||||
|
||||
// 1. read KKT request
|
||||
let raw_kkt_req: handshake_message::KKTRequest = conn_resp
|
||||
.receive_handshake_message(
|
||||
KKTRequest::size_excluding_payload(KKTMode::OneWay, kem) + LP_PEER_CONFIG_SIZE,
|
||||
)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
let req = raw_kkt_req.into();
|
||||
|
||||
// 2. process
|
||||
let processed_req = kkt_responder.process_request(req, LP_PEER_CONFIG_SIZE)?;
|
||||
conn_resp
|
||||
.send_handshake_message::<handshake_message::KKTResponse>(
|
||||
processed_req.response.into(),
|
||||
kem,
|
||||
)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
|
||||
// 3. read PSQ req
|
||||
let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem)?;
|
||||
let mut responder =
|
||||
responder::build_psq_principal(rand09::rng(), 1, responder_ciphersuite)?;
|
||||
let response_len = psq_msg1_size(kem);
|
||||
|
||||
let msg: PSQMsg1 = conn_resp
|
||||
.receive_handshake_message(response_len)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
responder.read_message(&msg, &mut []).unwrap();
|
||||
|
||||
// 4 send PSQ response
|
||||
let mut buf = vec![0u8; PSQ_MSG2_SIZE];
|
||||
let n = responder.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(n, buf.len());
|
||||
let msg = PSQMsg2::new(buf);
|
||||
conn_resp
|
||||
.send_handshake_message(msg, kem)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
|
||||
assert!(responder.is_handshake_finished());
|
||||
|
||||
let mut session_init = init_fut.await???;
|
||||
|
||||
let mut r_transport = responder.into_session().unwrap();
|
||||
|
||||
// test serialization, deserialization
|
||||
let channel_i = session_init.active_transport();
|
||||
let mut channel_r = r_transport.transport_channel().unwrap();
|
||||
|
||||
assert_eq!(channel_i.identifier(), channel_r.identifier());
|
||||
|
||||
let app_data_i = b"Derived session hey".as_slice();
|
||||
let app_data_r = b"Derived session ho".as_slice();
|
||||
|
||||
let ct_i = encrypt_data(app_data_i, channel_i)?;
|
||||
let pt_r = decrypt_data(&ct_i, &mut channel_r)?;
|
||||
|
||||
assert_eq!(app_data_i, pt_r);
|
||||
|
||||
let ct_r = encrypt_data(app_data_r, &mut channel_r)?;
|
||||
let pt_i = decrypt_data(&ct_r, channel_i)?;
|
||||
|
||||
assert_eq!(app_data_r, pt_i);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+290
-258
@@ -1,172 +1,108 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::ErrorPacketData;
|
||||
use crate::packet::LpHeader;
|
||||
use crate::packet::version;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use nym_kkt::ciphersuite::Ciphersuite;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use tracing::debug;
|
||||
use crate::transport::traits::LpHandshakeChannel;
|
||||
use nym_kkt_ciphersuite::{HashFunction, IntoEnumIterator, KEM, SignatureScheme};
|
||||
|
||||
pub(crate) mod handshake_message;
|
||||
mod helpers;
|
||||
mod initiator;
|
||||
mod responder;
|
||||
pub mod initiator;
|
||||
pub mod responder;
|
||||
|
||||
pub(crate) struct IntermediateHandshakeFailure {
|
||||
/// Session id established during exchange if we managed to derive it
|
||||
session_id: Option<u32>,
|
||||
pub use initiator::PSQHandshakeStateInitiator;
|
||||
pub use responder::PSQHandshakeStateResponder;
|
||||
|
||||
/// Protocol version established during the exchange
|
||||
protocol_version: Option<u8>,
|
||||
pub(crate) const AAD_INITIATOR_OUTER_V1: &[u8] = b"NYM-PQ-AAD-INIT-OUTER-V1";
|
||||
pub(crate) const AAD_INITIATOR_INNER_V1: &[u8] = b"NYM-PQ-AAD-INIT-INNER-V1";
|
||||
pub(crate) const AAD_RESPONDER_V1: &[u8] = b"NYM-PQ-AAD-RESP-V1";
|
||||
pub(crate) const SESSION_CONTEXT_V1: &[u8] = b"NYM-PQ-SESSION-CONTEXT-V1";
|
||||
|
||||
/// Outer aead key established during exchange if we managed to derive it
|
||||
outer_aead_key: Option<OuterAeadKey>,
|
||||
|
||||
/// The error source
|
||||
source: LpError,
|
||||
}
|
||||
|
||||
impl IntermediateHandshakeFailure {
|
||||
fn plain(source: LpError) -> IntermediateHandshakeFailure {
|
||||
IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: None,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
}
|
||||
/// Size of the first (initiator) PSQ message including all serialisation overheads if no additional payload has been attached
|
||||
pub(crate) fn psq_msg1_size(kem: KEM) -> usize {
|
||||
match kem {
|
||||
KEM::MlKem768 => 1247,
|
||||
KEM::McEliece => 315,
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of the second (responder) PSQ message including all serialisation overheads if no additional payload has been attached
|
||||
pub(crate) const PSQ_MSG2_SIZE: usize = 70;
|
||||
|
||||
pub struct PSQHandshakeState<'a, S> {
|
||||
/// The underlying connection established for the handshake
|
||||
connection: &'a mut S,
|
||||
|
||||
/// Protocol version used for the exchange.
|
||||
/// either known implicitly through the directory (initiator)
|
||||
/// or established through client hello (responder)
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Ciphersuite selected for the KKT/PSQ exchange
|
||||
ciphersuite: Ciphersuite,
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
local_peer: LpLocalPeer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InitiatorData {
|
||||
/// Protocol version used for the exchange known implicitly through the directory
|
||||
pub protocol_version: u8,
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
remote_peer: Option<LpRemotePeer>,
|
||||
pub remote_peer: LpRemotePeer,
|
||||
}
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: u64,
|
||||
impl InitiatorData {
|
||||
pub fn new(protocol_version: u8, remote_peer: LpRemotePeer) -> Self {
|
||||
InitiatorData {
|
||||
protocol_version,
|
||||
remote_peer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResponderData {
|
||||
/// List of supported Hash Functions by this Responder
|
||||
pub supported_hash_functions: Vec<HashFunction>,
|
||||
|
||||
/// List of supported Signature Schemes by this Responder
|
||||
pub supported_signature_schemes: Vec<SignatureScheme>,
|
||||
|
||||
/// List of supported outer (LP) protocol version by this Responder
|
||||
pub supported_outer_protocol_versions: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for ResponderData {
|
||||
fn default() -> Self {
|
||||
// by default all schemes are supported
|
||||
ResponderData {
|
||||
supported_hash_functions: HashFunction::iter().collect(),
|
||||
supported_signature_schemes: SignatureScheme::iter().collect(),
|
||||
supported_outer_protocol_versions: vec![version::CURRENT],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
|
||||
pub fn new(connection: &'a mut S, local_peer: LpLocalPeer) -> Self {
|
||||
PSQHandshakeState {
|
||||
connection,
|
||||
protocol_version: None,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer: None,
|
||||
sending_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
|
||||
self.protocol_version = Some(protocol_version);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
|
||||
self.remote_peer = Some(remote_peer);
|
||||
self
|
||||
}
|
||||
|
||||
fn protocol_version(&self) -> Result<u8, LpError> {
|
||||
self.protocol_version
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
|
||||
}
|
||||
|
||||
/// Generates the next counter value for outgoing packets.
|
||||
pub fn next_counter(&mut self) -> u64 {
|
||||
let counter = self.sending_counter;
|
||||
self.sending_counter += 1;
|
||||
counter
|
||||
}
|
||||
|
||||
pub fn next_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
message: LpMessage,
|
||||
) -> LpPacket {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(session_id, counter, protocol_version);
|
||||
LpPacket::new(header, message)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_send_error_packet(
|
||||
&mut self,
|
||||
err: IntermediateHandshakeFailure,
|
||||
) -> LpError {
|
||||
// if session_id is not known, we can't send the packet back (with the current design)
|
||||
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
|
||||
return err.source;
|
||||
};
|
||||
if let Err(err) = self
|
||||
.send_error_packet(
|
||||
session_id,
|
||||
protocol,
|
||||
err.source.to_string(),
|
||||
err.outer_aead_key.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to send back error response: {err}")
|
||||
pub fn as_initiator(self, initiator_data: InitiatorData) -> PSQHandshakeStateInitiator<'a, S> {
|
||||
PSQHandshakeStateInitiator {
|
||||
initiator_data,
|
||||
inner_state: self,
|
||||
}
|
||||
err.source
|
||||
}
|
||||
|
||||
/// Attempt to send an error packet
|
||||
pub(crate) async fn send_error_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
msg: impl Into<String>,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError> {
|
||||
let packet = self.next_packet(
|
||||
session_id,
|
||||
protocol_version,
|
||||
LpMessage::Error(ErrorPacketData::new(msg)),
|
||||
);
|
||||
self.connection.send_packet(packet, outer_aead_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive a packet from connection, explicitly checking for an error response
|
||||
/// and returning corresponding message if received
|
||||
pub(crate) async fn receive_non_error(
|
||||
&mut self,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError> {
|
||||
let packet = self.connection.receive_packet(outer_aead_key).await?;
|
||||
|
||||
match &packet.message {
|
||||
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
|
||||
"remote error: {}",
|
||||
error_packet.message
|
||||
))),
|
||||
_ => Ok(packet),
|
||||
pub fn as_responder(self, responder_data: ResponderData) -> PSQHandshakeStateResponder<'a, S> {
|
||||
PSQHandshakeStateResponder {
|
||||
responder_data,
|
||||
inner_state: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,167 +110,263 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codec::{decrypt_data, encrypt_data};
|
||||
use crate::peer::mock_peers;
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
|
||||
use mock_instant::thread_local::MockClock;
|
||||
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use crate::peer_config::{LP_PEER_CONFIG_SIZE, LpPeerConfig};
|
||||
use libcrux_psq::handshake::types::Authenticator;
|
||||
use libcrux_psq::session::{Session, SessionBinding};
|
||||
use libcrux_psq::{Channel, IntoSession};
|
||||
use nym_kkt::initiator::KKTInitiator;
|
||||
use nym_kkt::responder::KKTResponder;
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme};
|
||||
use nym_test_utils::helpers::{
|
||||
DeterministicRng09Send, deterministic_rng_09, u64_seeded_rng_09,
|
||||
};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn extract_error(conn: &mut MockIOStream) -> String {
|
||||
let packet = conn.receive_packet(None).await.unwrap();
|
||||
match packet.message {
|
||||
LpMessage::Error(error) => error.message,
|
||||
_ => panic!("non error packet"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn e2e_psq_handshake() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
for kem in KEM::iter() {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
let ciphersuite = Ciphersuite::default().with_kem(kem);
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (mut init, mut resp) = mock_peers();
|
||||
init.ciphersuite = ciphersuite;
|
||||
resp.ciphersuite = ciphersuite;
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
let handshake_init = PSQHandshakeState::new(conn_init, init)
|
||||
.as_initiator(InitiatorData::new(1, resp_remote));
|
||||
let handshake_resp =
|
||||
PSQHandshakeState::new(conn_resp, resp).as_responder(ResponderData::default());
|
||||
|
||||
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
|
||||
let init_rng = DeterministicRng09Send::new(u64_seeded_rng_09(1));
|
||||
let resp_rng = DeterministicRng09Send::new(u64_seeded_rng_09(2));
|
||||
|
||||
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
|
||||
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
|
||||
// similarly leak the rngs to get the static lifetimes
|
||||
let init_rng = init_rng.leak();
|
||||
let resp_rng = resp_rng.leak();
|
||||
|
||||
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||
let init_fut = handshake_init
|
||||
.complete_handshake_with_rng(init_rng)
|
||||
.spawn_timeboxed();
|
||||
let resp_fut = handshake_resp
|
||||
.complete_handshake_with_rng(resp_rng)
|
||||
.spawn_timeboxed();
|
||||
|
||||
let session_init = session_init???;
|
||||
let session_resp = session_resp???;
|
||||
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||
|
||||
assert_eq!(session_init.id(), session_resp.id());
|
||||
assert_eq!(
|
||||
session_init.outer_aead_key().as_bytes(),
|
||||
session_resp.outer_aead_key().as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
session_init.pq_shared_secret().as_bytes(),
|
||||
session_resp.pq_shared_secret().as_bytes()
|
||||
);
|
||||
let mut session_init = session_init???;
|
||||
let mut session_resp = session_resp???;
|
||||
|
||||
assert_eq!(session_init.receiver_index(), session_resp.receiver_index());
|
||||
|
||||
assert_eq!(
|
||||
session_init.session_identifier(),
|
||||
session_resp.session_identifier()
|
||||
);
|
||||
|
||||
// test serialization, deserialization
|
||||
let channel_i = session_init.active_transport();
|
||||
let channel_r = session_resp.active_transport();
|
||||
|
||||
assert_eq!(channel_i.identifier(), channel_r.identifier());
|
||||
|
||||
let app_data_i = b"Derived session hey".as_slice();
|
||||
let app_data_r = b"Derived session ho".as_slice();
|
||||
|
||||
let ct_i = encrypt_data(app_data_i, channel_i)?;
|
||||
let pt_r = decrypt_data(&ct_i, channel_r)?;
|
||||
|
||||
assert_eq!(app_data_i, pt_r);
|
||||
|
||||
let ct_r = encrypt_data(app_data_r, channel_r)?;
|
||||
let pt_i = decrypt_data(&ct_r, channel_i)?;
|
||||
|
||||
assert_eq!(app_data_r, pt_i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
// plain test without any wrappers
|
||||
#[test]
|
||||
fn e2e_test_plain() {
|
||||
let mut rng = deterministic_rng_09();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
for kem in KEM::iter() {
|
||||
// SETUP START:
|
||||
let protocol_version = 1;
|
||||
let (mut init, resp) = mock_peers();
|
||||
init.ciphersuite = Ciphersuite::default().with_kem(kem);
|
||||
let resp_remote = resp.as_remote();
|
||||
let dir_hash = resp_remote.expected_kem_key_hash(init.ciphersuite).unwrap();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
let resp_keys = resp.kem_keypairs.as_ref().unwrap();
|
||||
let responder_x25519_keypair = resp.x25519();
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let client_hello = handshake_init.send_client_hello().await?;
|
||||
let LpMessage::ClientHello(received_client_hello) =
|
||||
conn_resp.receive_packet(None).await?.message
|
||||
else {
|
||||
panic!("wrong message type");
|
||||
};
|
||||
assert_eq!(client_hello, received_client_hello);
|
||||
Ok(())
|
||||
}
|
||||
let supported_sigs = [SignatureScheme::Ed25519];
|
||||
let supported_hash = [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::Shake256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::SHA256,
|
||||
];
|
||||
let kkt_responder = KKTResponder::new(
|
||||
responder_x25519_keypair,
|
||||
resp_keys,
|
||||
&supported_hash,
|
||||
&supported_sigs,
|
||||
&[protocol_version],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// essentially make sure you can't accidentally trigger the handshake as the responder
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
// SETUP END
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (_, resp) = mock_peers();
|
||||
let lp_peer_config = LpPeerConfig::new_client_to_entry(&mut rng, false);
|
||||
|
||||
// as initiator
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
// OneWay - MlKem
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
init.ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&dir_hash,
|
||||
protocol_version,
|
||||
Some(Vec::from(lp_peer_config.serialize())),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let sending_res = handshake_resp.send_client_hello().await;
|
||||
assert!(sending_res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
let processed_req = kkt_responder
|
||||
.process_request(request, LP_PEER_CONFIG_SIZE)
|
||||
.unwrap();
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
|
||||
let current_time = Duration::from_secs(10000);
|
||||
MockClock::set_system_time(current_time);
|
||||
let response = initiator
|
||||
.process_response(processed_req.response, 0)
|
||||
.unwrap();
|
||||
let encapsulation_key = response.encapsulation_key;
|
||||
|
||||
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
|
||||
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
|
||||
let mut payload_buf_responder = vec![0u8; 4096];
|
||||
let mut payload_buf_initiator = vec![0u8; 4096];
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let initiator_ciphersuite =
|
||||
initiator::build_psq_ciphersuite(&init, &resp_remote, &encapsulation_key).unwrap();
|
||||
let mut initiator = initiator::build_psq_principal(
|
||||
rand09::rng(),
|
||||
protocol_version,
|
||||
initiator_ciphersuite,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// TOO OLD
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
let responder_ciphersuite = responder::build_psq_ciphersuite(&resp, kem).unwrap();
|
||||
let mut responder = responder::build_psq_principal(
|
||||
rand09::rng(),
|
||||
protocol_version,
|
||||
responder_ciphersuite,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
|
||||
// Send first message
|
||||
let mut buf = vec![0u8; psq_msg1_size(kem)];
|
||||
let len_i = initiator.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(len_i, buf.len());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_old.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
assert!(err.to_string().contains("too old"));
|
||||
// Read first message
|
||||
let (_, _) = responder
|
||||
.read_message(&buf, &mut payload_buf_responder)
|
||||
.unwrap();
|
||||
|
||||
// TOO RECENT
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
// Get the authenticator out here, so we can deserialize the session later.
|
||||
let Some(initiator_authenticator) = responder.initiator_authenticator() else {
|
||||
panic!("No initiator authenticator found")
|
||||
};
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
|
||||
// Respond
|
||||
let mut buf = [0u8; PSQ_MSG2_SIZE];
|
||||
let len_r = responder.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(len_r, buf.len());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
// Finalize on registration initiator
|
||||
let (len_i_deserialized, _) = initiator
|
||||
.read_message(&buf, &mut payload_buf_initiator)
|
||||
.unwrap();
|
||||
|
||||
assert!(err.to_string().contains("too future"));
|
||||
Ok(())
|
||||
// We read the same amount of data.
|
||||
assert_eq!(len_r, len_i_deserialized);
|
||||
|
||||
// Ready for transport mode
|
||||
assert!(initiator.is_handshake_finished());
|
||||
assert!(responder.is_handshake_finished());
|
||||
|
||||
let i_transport = initiator.into_session().unwrap();
|
||||
let r_transport = responder.into_session().unwrap();
|
||||
|
||||
// test serialization, deserialization
|
||||
let mut session_storage = vec![0u8; 4096];
|
||||
i_transport
|
||||
.serialize(
|
||||
&mut session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &Authenticator::Dh(init.x25519().pk),
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut i_transport = Session::deserialize(
|
||||
&session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &Authenticator::Dh(init.x25519().pk),
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
r_transport
|
||||
.serialize(
|
||||
&mut session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &initiator_authenticator,
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut r_transport = Session::deserialize(
|
||||
&session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &initiator_authenticator,
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(encapsulation_key.as_pq_encapsulation_key()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut channel_i = i_transport.transport_channel().unwrap();
|
||||
let mut channel_r = r_transport.transport_channel().unwrap();
|
||||
|
||||
assert_eq!(channel_i.identifier(), channel_r.identifier());
|
||||
|
||||
let app_data_i = b"Derived session hey".as_slice();
|
||||
let app_data_r = b"Derived session ho".as_slice();
|
||||
|
||||
let ct_i = encrypt_data(app_data_i, &mut channel_i).unwrap();
|
||||
let pt_r = decrypt_data(&ct_i, &mut channel_r).unwrap();
|
||||
|
||||
assert_eq!(app_data_i, pt_r);
|
||||
|
||||
let ct_r = encrypt_data(app_data_r, &mut channel_r).unwrap();
|
||||
let pt_i = decrypt_data(&ct_r, &mut channel_i).unwrap();
|
||||
|
||||
assert_eq!(app_data_r, pt_i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+312
-422
@@ -1,461 +1,351 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTResponseData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_responder_process_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
|
||||
use nym_kkt::frame::KKTSessionId;
|
||||
use nym_kkt::session::{responder_ingest_message, responder_process};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use std::time::Duration;
|
||||
use crate::peer::LpLocalPeer;
|
||||
use crate::peer_config::{LP_PEER_CONFIG_SIZE, LpPeerConfig};
|
||||
use crate::psq::handshake_message::{PSQMsg1, PSQMsg2};
|
||||
use crate::psq::helpers::kem_to_ciphersuite;
|
||||
use crate::psq::{
|
||||
AAD_RESPONDER_V1, PSQ_MSG2_SIZE, PSQHandshakeState, ResponderData, SESSION_CONTEXT_V1,
|
||||
handshake_message, psq_msg1_size,
|
||||
};
|
||||
use crate::session::PersistentSessionBinding;
|
||||
use crate::transport::traits::{HandshakeMessage, LpHandshakeChannel};
|
||||
use crate::{LpError, LpSession};
|
||||
use libcrux_psq::handshake::Responder;
|
||||
use libcrux_psq::handshake::builders::{
|
||||
CiphersuiteBuilder, PrincipalBuilder, ResponderCiphersuite,
|
||||
};
|
||||
use libcrux_psq::{Channel, IntoSession};
|
||||
use nym_kkt::context::KKTMode;
|
||||
use nym_kkt::message::{KKTRequest, KKTResponse, ProcessedKKTRequest};
|
||||
use nym_kkt::responder::KKTResponder;
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use rand09::SeedableRng;
|
||||
use tracing::debug;
|
||||
|
||||
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
|
||||
|
||||
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
|
||||
fn validate_client_hello_timestamp(
|
||||
client_timestamp: u64,
|
||||
tolerance: Duration,
|
||||
) -> Result<(), LpError> {
|
||||
let now = current_timestamp()?;
|
||||
|
||||
let age = now.abs_diff(client_timestamp);
|
||||
if age > tolerance.as_secs() {
|
||||
let direction = if now >= client_timestamp {
|
||||
"old"
|
||||
} else {
|
||||
"future"
|
||||
};
|
||||
|
||||
return Err(LpError::kkt_psq_handshake(format!(
|
||||
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
|
||||
tolerance.as_secs()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub struct PSQHandshakeStateResponder<'a, S> {
|
||||
pub(super) inner_state: PSQHandshakeState<'a, S>,
|
||||
pub(super) responder_data: ResponderData,
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
pub(crate) fn build_psq_principal<R>(
|
||||
rng: R,
|
||||
version: u8,
|
||||
ciphersuite: ResponderCiphersuite,
|
||||
) -> Result<Responder<R>, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
R: rand09::CryptoRng,
|
||||
{
|
||||
pub(crate) fn encapsulated_kem_keys(
|
||||
&self,
|
||||
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
|
||||
let kem_keys = self
|
||||
let (ctx, aad) = match version {
|
||||
1 => (SESSION_CONTEXT_V1, AAD_RESPONDER_V1),
|
||||
other => return Err(LpError::UnsupportedVersion { version: other }),
|
||||
};
|
||||
|
||||
PrincipalBuilder::new(rng)
|
||||
.context(ctx)
|
||||
.outer_aad(aad)
|
||||
.recent_keys_upper_bound(30)
|
||||
.build_responder(ciphersuite)
|
||||
.map_err(|inner| LpError::PSQResponderBuilderFailure { inner })
|
||||
}
|
||||
|
||||
pub(crate) fn build_psq_ciphersuite(
|
||||
peer: &LpLocalPeer,
|
||||
kem: KEM,
|
||||
) -> Result<ResponderCiphersuite<'_>, LpError> {
|
||||
let Some(kem_keys) = peer.kem_keypairs.as_ref() else {
|
||||
return Err(LpError::ResponderWithMissingKEMKeys);
|
||||
};
|
||||
|
||||
let psq_ciphersuite = kem_to_ciphersuite(kem);
|
||||
let builder = CiphersuiteBuilder::new(psq_ciphersuite).longterm_x25519_keys(peer.x25519());
|
||||
|
||||
match kem {
|
||||
KEM::MlKem768 => builder
|
||||
.longterm_mlkem_encapsulation_key(kem_keys.ml_kem768_encapsulation_key())
|
||||
.longterm_mlkem_decapsulation_key(kem_keys.ml_kem768_decapsulation_key()),
|
||||
KEM::McEliece => builder
|
||||
.longterm_cmc_encapsulation_key(kem_keys.mc_eliece_encapsulation_key())
|
||||
.longterm_cmc_decapsulation_key(kem_keys.mc_eliece_decapsulation_key()),
|
||||
}
|
||||
.build_responder_ciphersuite()
|
||||
.map_err(|inner| LpError::PSQResponderBuilderFailure { inner })
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeStateResponder<'a, S>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
/// Attempt to receive a KKT request from a one-way client
|
||||
async fn receive_one_way_kkt_request(&mut self) -> Result<KKTRequest, LpError> {
|
||||
let packet_len = KKTRequest::size_excluding_payload(
|
||||
KKTMode::OneWay,
|
||||
self.inner_state.local_peer.ciphersuite.kem(),
|
||||
) + LP_PEER_CONFIG_SIZE;
|
||||
|
||||
let req = self
|
||||
.inner_state
|
||||
.connection
|
||||
.receive_handshake_message::<handshake_message::KKTRequest>(packet_len)
|
||||
.await?;
|
||||
|
||||
Ok(req.into())
|
||||
}
|
||||
|
||||
/// Attempt to process the received KKT request
|
||||
fn process_kkt_request(&self, kkt_request: KKTRequest) -> Result<ProcessedKKTRequest, LpError> {
|
||||
let kem_keys = &self
|
||||
.inner_state
|
||||
.local_peer
|
||||
.kem_psq
|
||||
.kem_keypairs
|
||||
.as_ref()
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)?;
|
||||
.ok_or(LpError::ResponderWithMissingKEMKeys)?;
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
Ok((dec_key, enc_key))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate ClientHello
|
||||
pub(crate) async fn receive_client_hello(
|
||||
&mut self,
|
||||
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
|
||||
let client_hello_packet = self.receive_non_error(None).await?;
|
||||
let client_hello = match client_hello_packet.message {
|
||||
LpMessage::ClientHello(client_hello) => client_hello,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::ClientHello,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
validate_client_hello_timestamp(
|
||||
client_hello.extract_timestamp(),
|
||||
DEFAULT_TIMESTAMP_TOLERANCE,
|
||||
)?;
|
||||
|
||||
// TODO: somehow check for collision
|
||||
|
||||
// set version and remote peer information
|
||||
self.protocol_version = Some(client_hello_packet.header.protocol_version);
|
||||
let remote_peer = LpRemotePeer::new(
|
||||
client_hello.client_ed25519_public_key,
|
||||
client_hello.client_lp_public_key,
|
||||
);
|
||||
|
||||
Ok((client_hello, remote_peer))
|
||||
}
|
||||
|
||||
/// Send client hello ACK
|
||||
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection.send_packet(ack, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a KKT request
|
||||
pub(crate) async fn receive_kkt_request(
|
||||
&mut self,
|
||||
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
|
||||
let kkt_request = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTRequest(request) => request.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (session_secret, request_frame, remote_context) =
|
||||
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
|
||||
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
|
||||
|
||||
Ok((context, session_secret, request_frame.session_id()))
|
||||
let processed_req = KKTResponder::new(
|
||||
&self.inner_state.local_peer.x25519,
|
||||
kem_keys,
|
||||
&self.responder_data.supported_hash_functions,
|
||||
&self.responder_data.supported_signature_schemes,
|
||||
&self.responder_data.supported_outer_protocol_versions,
|
||||
)?
|
||||
.process_request(kkt_request, LP_PEER_CONFIG_SIZE)?;
|
||||
Ok(processed_req)
|
||||
}
|
||||
|
||||
/// Attempt to send KKT response to the previously received request
|
||||
pub(crate) async fn send_kkt_response(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let response_frame = responder_process(
|
||||
&kkt_context,
|
||||
kkt_session_id,
|
||||
self.local_peer.ed25519().private_key(),
|
||||
encapsulation_key,
|
||||
)?;
|
||||
let encrypted_frame = encrypt_kkt_frame(
|
||||
&mut rng(),
|
||||
&session_secret,
|
||||
&response_frame,
|
||||
KKT_RESPONSE_AAD,
|
||||
)?;
|
||||
let lp_message = KKTResponseData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
async fn send_kkt_response(&mut self, response: KKTResponse, kem: KEM) -> Result<(), LpError> {
|
||||
self.inner_state
|
||||
.connection
|
||||
.send_handshake_message::<handshake_message::KKTResponse>(response.into(), kem)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a PSQ msg1 request
|
||||
pub(crate) async fn receive_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
|
||||
let psq_msg1 = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
|
||||
if psq_msg1.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
|
||||
if psq_msg1.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let psq_payload = &psq_msg1[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg1[2 + handle_len..];
|
||||
|
||||
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
|
||||
let psq_responder = psq_responder_process_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
local_kem_keypair,
|
||||
&remote_peer.ed25519_public,
|
||||
psq_payload,
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
|
||||
let psk = psq_responder.psk;
|
||||
let psk_handle = psq_responder.psk_handle;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
let mut noise_protocol = NoiseProtocol::build_new_responder(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
noise_protocol.read_message(noise_payload)?;
|
||||
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
psk_handle,
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to prepare and generate a responder PSQ msg2
|
||||
pub(crate) async fn send_psq_responder_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
psk_handle: &[u8],
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let msg2 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
|
||||
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
|
||||
let handle_len = psk_handle.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
|
||||
combined.extend_from_slice(&handle_len.to_le_bytes());
|
||||
combined.extend_from_slice(psk_handle);
|
||||
combined.extend_from_slice(&msg2);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process final PSQ msg3
|
||||
pub(crate) async fn receive_final_psq_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg3 = match self
|
||||
async fn receive_psq_initiator_message(&mut self, kem: KEM) -> Result<Vec<u8>, LpError> {
|
||||
let packet_len = psq_msg1_size(kem);
|
||||
let msg: PSQMsg1 = self
|
||||
.inner_state
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
noise_protocol.read_message(&psq_msg3)?;
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send final ACK to indicate finalisation of the handshake
|
||||
pub(crate) async fn send_final_ack(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection
|
||||
.send_packet(ack, Some(outer_aead_key))
|
||||
.receive_handshake_message(packet_len)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(msg.into_bytes())
|
||||
}
|
||||
|
||||
async fn complete_as_responder_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
pub async fn complete_handshake(self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
// 1. receive and validate ClientHello
|
||||
let (client_hello_data, remote_peer) =
|
||||
self.receive_client_hello()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received client hello");
|
||||
let mut rng = rand09::rngs::StdRng::from_os_rng();
|
||||
self.complete_handshake_with_rng(&mut rng).await
|
||||
}
|
||||
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 2. send ack
|
||||
debug!("sending client hello ACK");
|
||||
self.send_client_hello_ack(session_id)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 3. receive and process KKT request
|
||||
let kkt_data =
|
||||
self.receive_kkt_request()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
pub async fn complete_handshake_with_rng<R>(mut self, rng: &mut R) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
R: rand09::CryptoRng,
|
||||
{
|
||||
// 1. receive and process KKTRequest
|
||||
let kkt_request = self.receive_one_way_kkt_request().await?;
|
||||
debug!("received KKT request");
|
||||
|
||||
// TEMP: 'derive' KEM keys
|
||||
let (dec_key, enc_key) =
|
||||
self.encapsulated_kem_keys()
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
let processed_req = self.process_kkt_request(kkt_request)?;
|
||||
let kem = processed_req.requested_kem;
|
||||
|
||||
// 4. prepare and send KKT response
|
||||
let lp_peer_config = LpPeerConfig::deserialize(&processed_req.request_payload)?;
|
||||
|
||||
// 2. send back the KKTResponse
|
||||
debug!("sending KKT response");
|
||||
self.send_kkt_response(session_id, kkt_data, &enc_key)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
self.send_kkt_response(processed_req.response, kem).await?;
|
||||
|
||||
// 5. receive and process PSQ msg1
|
||||
debug!("received PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
|
||||
.receive_psq_initiator_message(
|
||||
&remote_peer,
|
||||
(&dec_key, &enc_key),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
// 3. receive and process PSQ request
|
||||
let raw_psq1 = self.receive_psq_initiator_message(kem).await?;
|
||||
debug!("received PSQ handshake msg");
|
||||
|
||||
// 6. prepare and send PSQ msg2
|
||||
debug!("sending PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.send_psq_responder_message(
|
||||
session_id,
|
||||
&psk_handle,
|
||||
&outer_aead_key,
|
||||
&mut noise_protocol,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
// construct the responder and process the message
|
||||
let responder_ciphersuite = build_psq_ciphersuite(&self.inner_state.local_peer, kem)?;
|
||||
let version = processed_req.outer_protocol_version;
|
||||
let mut psq_responder = build_psq_principal(rng, version, responder_ciphersuite)?;
|
||||
psq_responder.read_message(&raw_psq1, &mut [])?;
|
||||
|
||||
let initiator_authenticator = psq_responder
|
||||
.initiator_authenticator()
|
||||
.ok_or(LpError::MissingInitiatorAuthenticator)?;
|
||||
|
||||
// 4. send PSQ response
|
||||
let conn = self.inner_state.connection;
|
||||
|
||||
let mut buf = vec![0u8; PSQ_MSG2_SIZE];
|
||||
psq_responder.write_message(&[], &mut buf)?;
|
||||
debug!("sending PSQ handshake msg");
|
||||
conn.send_handshake_message(PSQMsg2::new(buf), kem).await?;
|
||||
|
||||
if !psq_responder.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"handshake not finished after receiving psq response",
|
||||
));
|
||||
}
|
||||
|
||||
// 7. receive and process PSQ msg3
|
||||
debug!("received PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
// SAFETY: we have completed the exchange so this key MUST HAVE been present
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let kem_key = self
|
||||
.inner_state
|
||||
.local_peer
|
||||
.kem_keypairs
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.encapsulation_key(kem)
|
||||
.unwrap();
|
||||
|
||||
// 8. [optionally] send ACK to finalise
|
||||
debug!("sending final ACK");
|
||||
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
let receiver_index =
|
||||
lp_peer_config.derive_receiver_index(&initiator_authenticator, &kem_key)?;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
let binding = PersistentSessionBinding {
|
||||
initiator_authenticator,
|
||||
responder_ecdh_pk: self.inner_state.local_peer.x25519().pk,
|
||||
responder_pq_pk: Some(kem_key),
|
||||
};
|
||||
|
||||
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_responder_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
let psq_session = psq_responder.into_session()?;
|
||||
LpSession::new(
|
||||
psq_session,
|
||||
binding,
|
||||
receiver_index,
|
||||
processed_req.outer_protocol_version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codec::{decrypt_data, encrypt_data};
|
||||
use crate::peer::mock_peers;
|
||||
use crate::peer_config::LpPeerConfig;
|
||||
use crate::psq::initiator;
|
||||
use nym_kkt::initiator::KKTInitiator;
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, IntoEnumIterator};
|
||||
use nym_test_utils::helpers::{
|
||||
DeterministicRng09Send, deterministic_rng_09, u64_seeded_rng_09,
|
||||
};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, Timeboxed};
|
||||
|
||||
#[tokio::test]
|
||||
async fn responder_test_plain() -> anyhow::Result<()> {
|
||||
for kem in KEM::iter() {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// SETUP START:
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
|
||||
let (mut init, mut resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let ciphersuite = Ciphersuite::default().with_kem(kem);
|
||||
init.ciphersuite = ciphersuite;
|
||||
resp.ciphersuite = ciphersuite;
|
||||
|
||||
let responder_data = ResponderData::default();
|
||||
let handshake_resp =
|
||||
PSQHandshakeState::new(conn_resp, resp).as_responder(responder_data);
|
||||
|
||||
let mut resp_rng = DeterministicRng09Send::new(u64_seeded_rng_09(2));
|
||||
let resp_fut = tokio::spawn(async move {
|
||||
handshake_resp
|
||||
.complete_handshake_with_rng(&mut resp_rng)
|
||||
.timeboxed()
|
||||
.await
|
||||
});
|
||||
|
||||
// initiator:
|
||||
|
||||
let mut rng = deterministic_rng_09();
|
||||
let dir_hash = resp_remote.expected_kem_key_hash(init.ciphersuite)?;
|
||||
|
||||
let lp_peer_config = LpPeerConfig::new_client_to_entry(&mut rng, false);
|
||||
|
||||
// OneWay - MlKem
|
||||
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
init.ciphersuite,
|
||||
&resp_remote.x25519_public,
|
||||
&dir_hash,
|
||||
1,
|
||||
Some(Vec::from(lp_peer_config.serialize())),
|
||||
)?;
|
||||
|
||||
// 1. send kkt request
|
||||
conn_init
|
||||
.send_handshake_message::<handshake_message::KKTRequest>(request.into(), kem)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
|
||||
// 2. receive KKT response
|
||||
let response_len = KKTResponse::size_excluding_payload(kem);
|
||||
let resp: handshake_message::KKTResponse = conn_init
|
||||
.receive_handshake_message(response_len)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
let kkt_response = resp.into();
|
||||
|
||||
let response = initiator.process_response(kkt_response, 0)?;
|
||||
let encapsulation_key = response.encapsulation_key;
|
||||
|
||||
let initiator_ciphersuite =
|
||||
initiator::build_psq_ciphersuite(&init, &resp_remote, &encapsulation_key)?;
|
||||
let mut initiator =
|
||||
initiator::build_psq_principal(rand09::rng(), 1, initiator_ciphersuite)?;
|
||||
|
||||
// 3. send PSQ msg1
|
||||
// Send first message
|
||||
let mut buf = vec![0u8; psq_msg1_size(kem)];
|
||||
let n = initiator.write_message(&[], &mut buf).unwrap();
|
||||
assert_eq!(n, buf.len());
|
||||
let msg = PSQMsg1::new(buf);
|
||||
conn_init
|
||||
.send_handshake_message(msg, kem)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
|
||||
// 4. receive PSQ msg2
|
||||
let msg: PSQMsg2 = conn_init
|
||||
.receive_handshake_message(PSQ_MSG2_SIZE)
|
||||
.timeboxed()
|
||||
.await??;
|
||||
initiator.read_message(&msg, &mut []).unwrap();
|
||||
|
||||
assert!(initiator.is_handshake_finished());
|
||||
|
||||
let mut session_resp = resp_fut.await???;
|
||||
|
||||
let mut i_transport = initiator.into_session().unwrap();
|
||||
|
||||
// test serialization, deserialization
|
||||
let mut channel_i = i_transport.transport_channel().unwrap();
|
||||
let channel_r = session_resp.active_transport();
|
||||
|
||||
assert_eq!(channel_i.identifier(), channel_r.identifier());
|
||||
|
||||
let app_data_i = b"Derived session hey".as_slice();
|
||||
let app_data_r = b"Derived session ho".as_slice();
|
||||
|
||||
let ct_i = encrypt_data(app_data_i, &mut channel_i)?;
|
||||
let pt_r = decrypt_data(&ct_i, channel_r)?;
|
||||
|
||||
assert_eq!(app_data_i, pt_r);
|
||||
|
||||
let ct_r = encrypt_data(app_data_r, channel_r)?;
|
||||
let pt_i = decrypt_data(&ct_r, &mut channel_i)?;
|
||||
|
||||
assert_eq!(app_data_r, pt_i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,16 @@ const N_WORDS: usize = 16;
|
||||
/// Total number of bits in the bitmap
|
||||
const N_BITS: usize = WORD_SIZE * N_WORDS;
|
||||
|
||||
/// Current packet count statistics
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct PacketCount {
|
||||
/// the next expected counter value
|
||||
pub next: u64,
|
||||
|
||||
/// the total number of received packets
|
||||
pub received: u64,
|
||||
}
|
||||
|
||||
/// Validator for receiving key counters to prevent replay attacks.
|
||||
///
|
||||
/// This structure maintains a bitmap of received packets and validates
|
||||
@@ -205,11 +215,14 @@ impl ReceivingKeyCounterValidator {
|
||||
|
||||
/// Returns the current packet count statistics.
|
||||
///
|
||||
/// Returns a tuple of `(next, receive_cnt)` where:
|
||||
/// Returns a struct consisting of `(next, receive_cnt)` where:
|
||||
/// - `next` is the next expected counter value
|
||||
/// - `receive_cnt` is the total number of received packets
|
||||
pub fn current_packet_cnt(&self) -> (u64, u64) {
|
||||
(self.next, self.receive_cnt)
|
||||
pub fn current_packet_cnt(&self) -> PacketCount {
|
||||
PacketCount {
|
||||
next: self.next,
|
||||
received: self.receive_cnt,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -481,7 +494,10 @@ mod tests {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Initial state
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
let PacketCount {
|
||||
next,
|
||||
received: count,
|
||||
} = validator.current_packet_cnt();
|
||||
assert_eq!(next, 0);
|
||||
assert_eq!(count, 0);
|
||||
|
||||
@@ -490,21 +506,30 @@ mod tests {
|
||||
assert!(validator.mark_did_receive_branchless(1).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(2).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
let PacketCount {
|
||||
next,
|
||||
received: count,
|
||||
} = validator.current_packet_cnt();
|
||||
assert_eq!(next, 3);
|
||||
assert_eq!(count, 3);
|
||||
|
||||
// After an out of order packet
|
||||
assert!(validator.mark_did_receive_branchless(10).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
let PacketCount {
|
||||
next,
|
||||
received: count,
|
||||
} = validator.current_packet_cnt();
|
||||
assert_eq!(next, 11);
|
||||
assert_eq!(count, 4);
|
||||
|
||||
// After a packet from the past (within window)
|
||||
assert!(validator.mark_did_receive_branchless(5).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
let PacketCount {
|
||||
next,
|
||||
received: count,
|
||||
} = validator.current_packet_cnt();
|
||||
assert_eq!(next, 11); // Next doesn't change
|
||||
assert_eq!(count, 5); // Count increases
|
||||
}
|
||||
@@ -553,7 +578,7 @@ mod tests {
|
||||
assert!(validator.mark_did_receive_branchless(first_jump).is_ok());
|
||||
|
||||
// Verify next counter is updated
|
||||
let (next, _) = validator.current_packet_cnt();
|
||||
let PacketCount { next, .. } = validator.current_packet_cnt();
|
||||
assert_eq!(next, first_jump + 1);
|
||||
|
||||
// Second large jump, even further ahead
|
||||
@@ -561,7 +586,7 @@ mod tests {
|
||||
assert!(validator.mark_did_receive_branchless(second_jump).is_ok());
|
||||
|
||||
// Verify next counter is updated again
|
||||
let (next, _) = validator.current_packet_cnt();
|
||||
let PacketCount { next, .. } = validator.current_packet_cnt();
|
||||
assert_eq!(next, second_jump + 1);
|
||||
|
||||
// Test packets within the new window
|
||||
@@ -726,10 +751,10 @@ mod tests {
|
||||
|
||||
// Check final state of the validator
|
||||
let final_state = validator.lock().unwrap();
|
||||
let (_next, receive_cnt) = final_state.current_packet_cnt();
|
||||
let count = final_state.current_packet_cnt();
|
||||
|
||||
// Verify that the received count matches our successful operations
|
||||
assert_eq!(receive_cnt, total_successes as u64);
|
||||
assert_eq!(count.received, total_successes as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+190
-607
@@ -4,218 +4,177 @@
|
||||
//! Session management for the Lewes Protocol.
|
||||
//!
|
||||
//! This module implements session management functionality, including replay protection
|
||||
//! and Noise protocol state handling.
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::EncryptedDataPayload;
|
||||
// noiserm
|
||||
use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult};
|
||||
use crate::packet::LpHeader;
|
||||
use crate::codec::{decrypt_lp_packet, encrypt_lp_packet};
|
||||
use crate::packet::{EncryptedLpPacket, LpHeader, LpMessage, LpPacket};
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::psk::derive_subsession_psk;
|
||||
use crate::psq::PSQHandshakeState;
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use parking_lot::Mutex;
|
||||
use snow::Builder;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
use crate::peer_config::LpReceiverIndex;
|
||||
use crate::psq::{
|
||||
InitiatorData, PSQHandshakeState, PSQHandshakeStateInitiator, PSQHandshakeStateResponder,
|
||||
ResponderData,
|
||||
};
|
||||
use crate::replay::validator::PacketCount;
|
||||
use crate::transport::LpHandshakeChannel;
|
||||
use crate::{LpError, replay::ReceivingKeyCounterValidator};
|
||||
use libcrux_psq::handshake::types::{Authenticator, DHPublicKey};
|
||||
use libcrux_psq::session::{Session, SessionBinding};
|
||||
use nym_kkt::keys::EncapsulationKey;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
/// PQ shared secret wrapper with automatic memory zeroization.
|
||||
/// Ensures K_pq is cleared from memory when dropped.
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct PqSharedSecret([u8; 32]);
|
||||
|
||||
impl PqSharedSecret {
|
||||
pub fn new(secret: [u8; 32]) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PqSharedSecret {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PqSharedSecret")
|
||||
.field("secret", &"<redacted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
pub type SessionId = [u8; 32];
|
||||
|
||||
/// A session in the Lewes Protocol, handling connection state with Noise.
|
||||
///
|
||||
/// Sessions manage connection state, including LP replay protection.
|
||||
/// Each session has a unique receiving index and sending index for connection identification.
|
||||
#[derive(Debug)]
|
||||
pub struct LpSession {
|
||||
/// Id of the established session
|
||||
session_id: u32,
|
||||
/// The underlying established session
|
||||
psq_session: Session,
|
||||
|
||||
/// The public key material bound to the underlying session. Used for serialisation.
|
||||
session_binding: PersistentSessionBinding,
|
||||
|
||||
/// The current active transport channel
|
||||
// In the future it might get split between UDP and TCP transports
|
||||
active_transport: libcrux_psq::session::Transport,
|
||||
|
||||
/// Look-up index established during the initial KKT exchange
|
||||
receiver_index: LpReceiverIndex,
|
||||
|
||||
/// Negotiated protocol version from handshake.
|
||||
/// Set during handshake completion from the ClientHello/ServerHello packet header.
|
||||
/// Used for future version negotiation and compatibility checks.
|
||||
version: u8,
|
||||
|
||||
/// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake).
|
||||
outer_aead_key: OuterAeadKey,
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
local_peer: LpLocalPeer,
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
remote_peer: LpRemotePeer,
|
||||
|
||||
// TODO: ALL BELOW maybe not needed after all?
|
||||
/// Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation.
|
||||
/// Stored after PSQ handshake completes for subsession PSK derivation.
|
||||
pq_shared_secret: PqSharedSecret,
|
||||
|
||||
/// Noise protocol state machine
|
||||
noise_state: NoiseProtocol,
|
||||
protocol_version: u8,
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: u64,
|
||||
|
||||
/// Validator for incoming packet counters to prevent replay attacks
|
||||
receiving_counter: ReceivingKeyCounterValidator,
|
||||
}
|
||||
|
||||
/// Monotonically increasing counter for subsession indices.
|
||||
/// Each subsession gets a unique index to ensure unique PSK derivation.
|
||||
/// Uses u64 to make overflow practically impossible (~585k years at 1M/sec).
|
||||
subsession_counter: u64,
|
||||
/// Wraps public key material that is bound to a session.
|
||||
#[derive(Clone)]
|
||||
pub struct PersistentSessionBinding {
|
||||
/// The initiator's authenticator value, i.e. a long-term DH public value or signature verification key.
|
||||
pub initiator_authenticator: Authenticator,
|
||||
|
||||
/// True if this session has been demoted to read-only mode.
|
||||
/// Demoted sessions can still receive/decrypt but cannot send/encrypt.
|
||||
read_only: bool,
|
||||
/// The responder's long term DH public value.
|
||||
pub responder_ecdh_pk: DHPublicKey,
|
||||
|
||||
/// ID of the successor session that replaced this one.
|
||||
/// Set when demote() is called.
|
||||
successor_session_id: Option<u32>,
|
||||
/// The responder's long term PQ-KEM public key (if any).
|
||||
pub responder_pq_pk: Option<EncapsulationKey>,
|
||||
}
|
||||
|
||||
impl Debug for PersistentSessionBinding {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PersistentSessionBinding")
|
||||
.field("initiator_authenticator", &"<initiator_authenticator>")
|
||||
.field("responder_ecdh_pk", &self.responder_ecdh_pk)
|
||||
.field("responder_pq_pk", &self.responder_pq_pk)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PersistentSessionBinding> for SessionBinding<'a> {
|
||||
fn from(value: &'a PersistentSessionBinding) -> Self {
|
||||
SessionBinding {
|
||||
initiator_authenticator: &value.initiator_authenticator,
|
||||
responder_ecdh_pk: &value.responder_ecdh_pk,
|
||||
responder_pq_pk: value
|
||||
.responder_pq_pk
|
||||
.as_ref()
|
||||
.map(|k| k.as_pq_encapsulation_key()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for LpSession {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LpSession")
|
||||
.field("session_id", &self.psq_session.identifier())
|
||||
.field("session_binding", &self.session_binding)
|
||||
.field("active_transport_id", &self.active_transport.identifier())
|
||||
.field("protocol_version", &self.protocol_version)
|
||||
.field("sending_counter", &self.sending_counter)
|
||||
.field("receiving_counter", &self.receiving_counter)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl LpSession {
|
||||
/// Creates a new session after completed KTT/PSQ exchange
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session_id` - Session identifier
|
||||
/// * `version` - Protocol version to attach in all `LpPacket`s
|
||||
/// * `outer_aead_key` - Outer AEAD key for packet encryption
|
||||
/// * `local_peer` - This side's LP peer's keys
|
||||
/// * `remote_peer` - The remote's LP peer's keys
|
||||
/// * `pq_shared_secret` - Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation.
|
||||
/// * `noise_state` - Noise protocol state machine
|
||||
pub fn new(
|
||||
session_id: u32,
|
||||
version: u8,
|
||||
outer_aead_key: OuterAeadKey,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
pq_shared_secret: PqSharedSecret,
|
||||
noise_state: NoiseProtocol,
|
||||
) -> Self {
|
||||
LpSession {
|
||||
session_id,
|
||||
version,
|
||||
outer_aead_key,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_state,
|
||||
mut psq_session: Session,
|
||||
session_binding: PersistentSessionBinding,
|
||||
receiver_index: LpReceiverIndex,
|
||||
protocol_version: u8,
|
||||
) -> Result<Self, LpError> {
|
||||
// attempt to derive initial transport
|
||||
let transport = psq_session
|
||||
.transport_channel()
|
||||
.map_err(|inner| LpError::TransportDerivationFailure { inner })?;
|
||||
|
||||
Ok(LpSession {
|
||||
psq_session,
|
||||
session_binding,
|
||||
active_transport: transport,
|
||||
receiver_index,
|
||||
protocol_version,
|
||||
sending_counter: 0,
|
||||
receiving_counter: Default::default(),
|
||||
subsession_counter: 0,
|
||||
read_only: false,
|
||||
successor_session_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an instance of `Ciphersuite` using hardcoded defaults.
|
||||
/// This is a temporary workaround until values can be properly inferred
|
||||
/// from reported version
|
||||
pub fn default_ciphersuite() -> Ciphersuite {
|
||||
Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to create `PSQHandshakeState` for the handshake initiator
|
||||
pub fn complete_as_initiator<S>(
|
||||
pub fn psq_handshake_initiator<S>(
|
||||
connection: &'_ mut S,
|
||||
ciphersuite: Ciphersuite,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
remote_protocol_version: u8,
|
||||
) -> PSQHandshakeState<'_, S>
|
||||
) -> PSQHandshakeStateInitiator<'_, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
PSQHandshakeState::new(connection, ciphersuite, local_peer)
|
||||
.with_protocol_version(remote_protocol_version)
|
||||
.with_remote_peer(remote_peer)
|
||||
PSQHandshakeState::new(connection, local_peer)
|
||||
.as_initiator(InitiatorData::new(remote_protocol_version, remote_peer))
|
||||
}
|
||||
|
||||
/// Helper function to create `PSQHandshakeState` for the handshake responder
|
||||
pub fn psq_handshake_responder<S>(
|
||||
connection: &'_ mut S,
|
||||
ciphersuite: Ciphersuite,
|
||||
local_peer: LpLocalPeer,
|
||||
) -> PSQHandshakeState<'_, S>
|
||||
) -> PSQHandshakeStateResponder<'_, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
S: LpHandshakeChannel + Unpin,
|
||||
{
|
||||
PSQHandshakeState::new(connection, ciphersuite, local_peer)
|
||||
PSQHandshakeState::new(connection, local_peer).as_responder(ResponderData::default())
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.session_id
|
||||
pub fn session_binding(&self) -> &PersistentSessionBinding {
|
||||
&self.session_binding
|
||||
}
|
||||
|
||||
pub fn active_transport(&mut self) -> &mut libcrux_psq::session::Transport {
|
||||
&mut self.active_transport
|
||||
}
|
||||
|
||||
pub fn session_identifier(&self) -> &[u8; 32] {
|
||||
self.psq_session.identifier()
|
||||
}
|
||||
|
||||
pub fn receiver_index(&self) -> LpReceiverIndex {
|
||||
self.receiver_index
|
||||
}
|
||||
|
||||
/// Returns the negotiated protocol version from the handshake.
|
||||
///
|
||||
/// Set during `LpSession` creation after sending / receiving `ClientHelloData`
|
||||
pub fn negotiated_version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Returns the local X25519 public key.
|
||||
///
|
||||
/// This is used for KKT protocol when the responder needs to send their
|
||||
/// KEM public key in the KKT response.
|
||||
pub fn local_x25519_public(&self) -> x25519::PublicKey {
|
||||
*self.local_peer.x25519.public_key()
|
||||
}
|
||||
|
||||
/// Returns the remote ed25519 public key.
|
||||
pub fn remote_ed25519_public(&self) -> ed25519::PublicKey {
|
||||
self.remote_peer.ed25519_public
|
||||
}
|
||||
|
||||
/// Returns the remote X25519 public key.
|
||||
///
|
||||
/// Used for tie-breaking in simultaneous subsession initiation.
|
||||
/// Lower key loses and becomes responder.
|
||||
pub fn remote_x25519_public(&self) -> &x25519::PublicKey {
|
||||
&self.remote_peer.x25519_public
|
||||
}
|
||||
|
||||
/// Returns the outer AEAD key for packet encryption/decryption.
|
||||
pub fn outer_aead_key(&self) -> &OuterAeadKey {
|
||||
&self.outer_aead_key
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
pub fn next_packet(&mut self, message: LpMessage) -> Result<LpPacket, LpError> {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(self.id(), counter, self.version);
|
||||
let header = LpHeader::new(self.receiver_index(), counter, self.protocol_version);
|
||||
let packet = LpPacket::new(header, message);
|
||||
Ok(packet)
|
||||
}
|
||||
@@ -274,508 +233,132 @@ impl LpSession {
|
||||
/// A tuple containing:
|
||||
/// * The next expected counter value for incoming packets
|
||||
/// * The total number of received packets
|
||||
pub fn current_packet_cnt(&self) -> (u64, u64) {
|
||||
pub fn current_packet_cnt(&self) -> PacketCount {
|
||||
self.receiving_counter.current_packet_cnt()
|
||||
}
|
||||
|
||||
/// Returns the PQ shared secret (K_pq).
|
||||
///
|
||||
/// This is the raw KEM output from PSQ before Blake3 KDF combination.
|
||||
/// Used for deriving subsession PSKs to maintain PQ protection.
|
||||
pub fn pq_shared_secret(&self) -> &PqSharedSecret {
|
||||
&self.pq_shared_secret
|
||||
}
|
||||
|
||||
/// Gets the next subsession index and increments the counter.
|
||||
///
|
||||
/// Each subsession requires a unique index to ensure unique PSK derivation.
|
||||
/// The index is monotonically increasing per session.
|
||||
pub fn next_subsession_index(&mut self) -> u64 {
|
||||
let next = self.subsession_counter;
|
||||
self.subsession_counter += 1;
|
||||
next
|
||||
}
|
||||
|
||||
/// Returns true if this session is in read-only mode.
|
||||
///
|
||||
/// Read-only sessions have been demoted after a subsession was promoted.
|
||||
/// They can still decrypt incoming messages but cannot encrypt outgoing ones.
|
||||
pub fn is_read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
|
||||
/// Demotes this session to read-only mode after a subsession replaces it.
|
||||
///
|
||||
/// After demotion:
|
||||
/// - `encrypt_data()` will return `NoiseError::SessionReadOnly`
|
||||
/// - `decrypt_data()` still works (to drain in-flight messages)
|
||||
/// - Session should be cleaned up after TTL expires
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `successor_idx` - The receiver index of the session that replaced this one
|
||||
pub fn demote(&mut self, successor_idx: u32) {
|
||||
self.successor_session_id = Some(successor_idx);
|
||||
self.read_only = true;
|
||||
}
|
||||
|
||||
/// Returns the successor session ID if this session was demoted.
|
||||
pub fn successor_session_id(&self) -> Option<u32> {
|
||||
self.successor_session_id
|
||||
}
|
||||
|
||||
/// Encrypts application data payload using the established Noise transport session.
|
||||
/// Encrypts a produced application using the established transport session
|
||||
/// and produce an `EncryptedLpPacket`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payload` - The application data to encrypt.
|
||||
/// * `data` - plaintext data to encrypt
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the encrypted Noise message ciphertext.
|
||||
/// * `Err(NoiseError)` if the session is not in transport mode or encryption fails.
|
||||
pub fn encrypt_data(&mut self, payload: &[u8]) -> Result<LpMessage, NoiseError> {
|
||||
// Check if session is read-only (demoted)
|
||||
if self.read_only {
|
||||
return Err(NoiseError::SessionReadOnly);
|
||||
}
|
||||
|
||||
let payload = self.noise_state.write_message(payload)?;
|
||||
Ok(LpMessage::EncryptedData(EncryptedDataPayload(payload)))
|
||||
/// * `Ok(EncryptedLpPacket)` containing the encrypted message ciphertext.
|
||||
/// * `Err(LpError)` if the session is not in transport mode or encryption fails.
|
||||
pub(crate) fn encrypt_application_data(
|
||||
&mut self,
|
||||
data: LpMessage,
|
||||
) -> Result<EncryptedLpPacket, LpError> {
|
||||
let packet = self.next_packet(data)?;
|
||||
encrypt_lp_packet(packet, &mut self.active_transport)
|
||||
}
|
||||
|
||||
/// Decrypts an incoming Noise message containing application data.
|
||||
/// Decrypts an incoming LpPacket
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `noise_ciphertext` - The encrypted Noise message received from the peer.
|
||||
/// * `ciphertext` - The encrypted packet
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the decrypted application data payload.
|
||||
/// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data.
|
||||
pub fn decrypt_data(&mut self, noise_ciphertext: &LpMessage) -> Result<Vec<u8>, NoiseError> {
|
||||
let payload = noise_ciphertext.payload();
|
||||
|
||||
match self.noise_state.read_message(payload)? {
|
||||
ReadResult::DecryptedData(data) => Ok(data),
|
||||
_ => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new subsession using Noise KKpsk0 pattern.
|
||||
///
|
||||
/// KKpsk0 reuses parent's static X25519 keys (both parties know each other from parent session).
|
||||
/// PSK is derived from parent's PQ shared secret, preserving quantum resistance.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `subsession_index` - Unique index for this subsession (use `next_subsession_index()`)
|
||||
/// * `is_initiator` - True if this side initiates the subsession handshake
|
||||
///
|
||||
/// # Returns
|
||||
/// `SubsessionHandshake` ready for KK1/KK2 message exchange
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns error if parent handshake not complete
|
||||
/// * Returns error if PQ shared secret not available
|
||||
pub fn create_subsession(
|
||||
&self,
|
||||
subsession_index: u64,
|
||||
is_initiator: bool,
|
||||
) -> Result<SubsessionHandshake, LpError> {
|
||||
// Get PQ shared secret
|
||||
let pq_secret = self.pq_shared_secret();
|
||||
|
||||
// Derive subsession PSK from parent's PQ shared secret
|
||||
let subsession_psk = derive_subsession_psk(pq_secret.as_bytes(), subsession_index);
|
||||
|
||||
// Build KKpsk0 handshake
|
||||
// Pattern: Noise_KKpsk0_25519_ChaChaPoly_SHA256
|
||||
// Both parties already know each other's static keys from parent session
|
||||
let pattern_name = "Noise_KKpsk0_25519_ChaChaPoly_SHA256";
|
||||
let params = pattern_name.parse()?;
|
||||
|
||||
let local_key_bytes = self.local_peer.x25519.private_key().to_bytes();
|
||||
let remote_key_bytes = self.remote_x25519_public().to_bytes();
|
||||
|
||||
let builder = Builder::new(params)
|
||||
.local_private_key(&local_key_bytes)
|
||||
.remote_public_key(&remote_key_bytes)
|
||||
.psk(0, &subsession_psk); // PSK at position 0 for KKpsk0
|
||||
|
||||
let handshake_state = if is_initiator {
|
||||
builder.build_initiator().map_err(LpError::SnowKeyError)?
|
||||
} else {
|
||||
builder.build_responder().map_err(LpError::SnowKeyError)?
|
||||
};
|
||||
|
||||
Ok(SubsessionHandshake {
|
||||
index: subsession_index,
|
||||
noise_state: Mutex::new(NoiseProtocol::new(handshake_state)),
|
||||
is_initiator,
|
||||
local_peer: self.local_peer.clone(),
|
||||
remote_peer: self.remote_peer.clone(),
|
||||
pq_shared_secret: self.pq_shared_secret.clone(),
|
||||
subsession_psk,
|
||||
negotiated_version: self.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Subsession created via Noise KKpsk0 handshake tunneled through parent session.
|
||||
///
|
||||
/// Subsessions provide fresh session keys while inheriting PQ protection from parent's
|
||||
/// ML-KEM shared secret. After handshake completes, the subsession can be promoted
|
||||
/// to replace the parent session.
|
||||
///
|
||||
/// # Lifecycle
|
||||
/// 1. Parent calls `create_subsession()` to get `SubsessionHandshake`
|
||||
/// 2. Initiator calls `prepare_message()` to get KK1
|
||||
/// 3. KK1 sent through parent session (encrypted tunnel)
|
||||
/// 4. Responder calls `process_message(kk1)` to process KK1
|
||||
/// 5. Responder calls `prepare_message()` to get KK2
|
||||
/// 6. KK2 sent through parent session
|
||||
/// 7. Initiator calls `process_message(kk2)` to complete handshake
|
||||
/// 8. Both call `is_complete()` to verify
|
||||
#[derive(Debug)]
|
||||
pub struct SubsessionHandshake {
|
||||
/// Subsession index (unique per parent session)
|
||||
pub index: u64,
|
||||
/// Noise KKpsk0 handshake state
|
||||
noise_state: Mutex<NoiseProtocol>,
|
||||
/// Is this side the initiator?
|
||||
is_initiator: bool,
|
||||
|
||||
// Key material inherited from parent session for into_session() conversion
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
local_peer: LpLocalPeer,
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
remote_peer: LpRemotePeer,
|
||||
|
||||
/// PQ shared secret inherited from parent (for creating further subsessions)
|
||||
pq_shared_secret: PqSharedSecret,
|
||||
|
||||
/// Subsession PSK (for deriving outer AEAD key)
|
||||
subsession_psk: [u8; 32],
|
||||
|
||||
/// Negotiated protocol version from handshake.
|
||||
negotiated_version: u8,
|
||||
}
|
||||
|
||||
impl SubsessionHandshake {
|
||||
/// Prepares the next KK handshake message (KK1 or KK2 depending on role/state).
|
||||
///
|
||||
/// # Returns
|
||||
/// Noise handshake message bytes to send through parent session tunnel.
|
||||
pub fn prepare_message(&self) -> Result<Vec<u8>, LpError> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
noise_state
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::Internal("Not our turn to send".into()))?
|
||||
.map_err(LpError::NoiseError)
|
||||
}
|
||||
|
||||
/// Processes a received KK handshake message (KK1 or KK2).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - Noise handshake message received through parent session tunnel.
|
||||
///
|
||||
/// # Returns
|
||||
/// Any payload embedded in the handshake message (usually empty for KK).
|
||||
pub fn process_message(&self, message: &[u8]) -> Result<Vec<u8>, LpError> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
let result = noise_state
|
||||
.read_message(message)
|
||||
.map_err(LpError::NoiseError)?;
|
||||
match result {
|
||||
ReadResult::HandshakeComplete | ReadResult::NoOp => Ok(vec![]),
|
||||
ReadResult::DecryptedData(data) => Ok(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the handshake is complete (ready for transport mode).
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.noise_state.lock().is_handshake_finished()
|
||||
}
|
||||
|
||||
/// Returns whether this side is the initiator.
|
||||
pub fn is_initiator(&self) -> bool {
|
||||
self.is_initiator
|
||||
}
|
||||
|
||||
/// Returns the subsession index.
|
||||
pub fn subsession_index(&self) -> u64 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// Convert completed subsession handshake into a full LpSession.
|
||||
///
|
||||
/// This consumes the SubsessionHandshake and creates a new LpSession
|
||||
/// that can be used as a replacement for the parent session.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `receiver_index` - New receiver index for the promoted session
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns error if handshake is not complete
|
||||
pub fn into_session(self, receiver_index: u32) -> Result<LpSession, LpError> {
|
||||
if !self.is_complete() {
|
||||
return Err(LpError::Internal(
|
||||
"Cannot convert incomplete subsession to session".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Extract the noise state (now in transport mode)
|
||||
let noise_state = self.noise_state.into_inner();
|
||||
|
||||
// Derive outer AEAD key from the subsession PSK
|
||||
let outer_key = OuterAeadKey::from_psk(&self.subsession_psk);
|
||||
|
||||
Ok(LpSession {
|
||||
// noiserm
|
||||
session_id: receiver_index,
|
||||
noise_state,
|
||||
sending_counter: 0,
|
||||
receiving_counter: ReceivingKeyCounterValidator::new(0),
|
||||
local_peer: self.local_peer,
|
||||
remote_peer: self.remote_peer,
|
||||
outer_aead_key: outer_key,
|
||||
pq_shared_secret: self.pq_shared_secret,
|
||||
subsession_counter: 0,
|
||||
read_only: false,
|
||||
successor_session_id: None,
|
||||
version: self.negotiated_version,
|
||||
})
|
||||
/// * `Ok(LpPacket)` containing the decrypted application data payload.
|
||||
/// * `Err(LpError)` if the session is not in transport mode, decryption fails, or the message is not data.
|
||||
pub(crate) fn decrypt_packet(
|
||||
&mut self,
|
||||
packet: EncryptedLpPacket,
|
||||
) -> Result<LpPacket, LpError> {
|
||||
decrypt_lp_packet(packet, &mut self.active_transport)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{SessionsMock, replay::ReplayError, sessions_for_tests};
|
||||
use rand::thread_rng;
|
||||
|
||||
// Helper function to generate keypairs for tests
|
||||
fn generate_x25519_keypair() -> x25519::KeyPair {
|
||||
x25519::KeyPair::new(&mut thread_rng())
|
||||
}
|
||||
use crate::{ReplayError, SessionsMock};
|
||||
use nym_kkt_ciphersuite::{IntoEnumIterator, KEM};
|
||||
|
||||
#[test]
|
||||
fn test_session_creation() {
|
||||
let mut session = sessions_for_tests().0;
|
||||
for kem in KEM::iter() {
|
||||
let mut session = SessionsMock::mock_post_handshake(kem).responder;
|
||||
|
||||
// Initial counter should be zero
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 0);
|
||||
// Initial counter should be zero
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 0);
|
||||
|
||||
// Counter should increment
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 1);
|
||||
// Counter should increment
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: These tests are obsolete after removing optional KEM parameters.
|
||||
// PSQ now always runs using X25519 keys internally converted to KEM format.
|
||||
// The new tests at the end of this file (test_psq_*) cover PSQ integration.
|
||||
/*
|
||||
#[test]
|
||||
fn test_session_creation_with_psq_state_initiator() {
|
||||
// OLD API - REMOVED
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_creation_with_psq_state_responder() {
|
||||
// OLD API - REMOVED
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_sequential() {
|
||||
let mut session = sessions_for_tests().1;
|
||||
for kem in KEM::iter() {
|
||||
let mut session = SessionsMock::mock_post_handshake(kem).responder;
|
||||
|
||||
// Sequential counters should be accepted
|
||||
assert!(session.receiving_counter_quick_check(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
// Sequential counters should be accepted
|
||||
assert!(session.receiving_counter_quick_check(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
|
||||
assert!(session.receiving_counter_quick_check(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
assert!(session.receiving_counter_quick_check(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
|
||||
// Duplicates should be rejected
|
||||
assert!(session.receiving_counter_quick_check(0).is_err());
|
||||
let err = session.receiving_counter_mark(0).unwrap_err();
|
||||
match err {
|
||||
LpError::Replay(replay_error) => {
|
||||
assert!(matches!(replay_error, ReplayError::DuplicateCounter));
|
||||
// Duplicates should be rejected
|
||||
assert!(session.receiving_counter_quick_check(0).is_err());
|
||||
let err = session.receiving_counter_mark(0).unwrap_err();
|
||||
match err {
|
||||
LpError::Replay(replay_error) => {
|
||||
assert!(matches!(replay_error, ReplayError::DuplicateCounter));
|
||||
}
|
||||
_ => panic!("Expected replay error"),
|
||||
}
|
||||
_ => panic!("Expected replay error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_out_of_order() {
|
||||
let mut session = sessions_for_tests().1;
|
||||
for kem in KEM::iter() {
|
||||
let mut session = SessionsMock::mock_post_handshake(kem).responder;
|
||||
|
||||
// Receive packets in order
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(2).is_ok());
|
||||
// Receive packets in order
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(2).is_ok());
|
||||
|
||||
// Skip ahead
|
||||
assert!(session.receiving_counter_mark(10).is_ok());
|
||||
// Skip ahead
|
||||
assert!(session.receiving_counter_mark(10).is_ok());
|
||||
|
||||
// Can still receive out-of-order packets within window
|
||||
assert!(session.receiving_counter_quick_check(5).is_ok());
|
||||
assert!(session.receiving_counter_mark(5).is_ok());
|
||||
// Can still receive out-of-order packets within window
|
||||
assert!(session.receiving_counter_quick_check(5).is_ok());
|
||||
assert!(session.receiving_counter_mark(5).is_ok());
|
||||
|
||||
// But duplicates are still rejected
|
||||
assert!(session.receiving_counter_quick_check(5).is_err());
|
||||
assert!(session.receiving_counter_mark(5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packet_stats() {
|
||||
let mut session = sessions_for_tests().1;
|
||||
|
||||
// Initial stats
|
||||
let (next, received) = session.current_packet_cnt();
|
||||
assert_eq!(next, 0);
|
||||
assert_eq!(received, 0);
|
||||
|
||||
// After receiving packets
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
|
||||
let (next, received) = session.current_packet_cnt();
|
||||
assert_eq!(next, 2);
|
||||
assert_eq!(received, 2);
|
||||
}
|
||||
|
||||
/*
|
||||
// These tests remain commented as they rely on the old mock crypto functions
|
||||
#[test]
|
||||
fn test_mock_crypto() {
|
||||
let mut session = create_test_session(true);
|
||||
let data = [1, 2, 3, 4, 5];
|
||||
let mut encrypted = [0; 5];
|
||||
let mut decrypted = [0; 5];
|
||||
|
||||
// Mock encrypt should copy the data
|
||||
// let encrypted_len = session.encrypt_packet(&data, &mut encrypted).unwrap(); // Removed method
|
||||
// assert_eq!(encrypted_len, 5);
|
||||
// assert_eq!(encrypted, data);
|
||||
|
||||
// Mock decrypt should copy the data
|
||||
// let decrypted_len = session.decrypt_packet(&encrypted, &mut decrypted).unwrap(); // Removed method
|
||||
// assert_eq!(decrypted_len, 5);
|
||||
// assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_crypto_buffer_too_small() {
|
||||
let mut session = create_test_session(true);
|
||||
let data = [1, 2, 3, 4, 5];
|
||||
let mut too_small = [0; 3];
|
||||
|
||||
// Should fail with buffer too small
|
||||
// let result = session.encrypt_packet(&data, &mut too_small); // Removed method
|
||||
// assert!(result.is_err());
|
||||
// match result.unwrap_err() {
|
||||
// LpError::InsufficientBufferSize => {} // Error type might change
|
||||
// _ => panic!("Expected InsufficientBufferSize error"),
|
||||
// }
|
||||
}
|
||||
*/
|
||||
|
||||
/// Test that X25519 keys are correctly converted to KEM format
|
||||
#[test]
|
||||
fn test_x25519_to_kem_conversion() {
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
|
||||
let initiator_keys = generate_x25519_keypair();
|
||||
let responder_keys = generate_x25519_keypair();
|
||||
|
||||
// Verify we can convert X25519 public key to KEM format (as done in session.rs)
|
||||
let x25519_public_bytes = responder_keys.public_key().as_bytes();
|
||||
let libcrux_public_key =
|
||||
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, x25519_public_bytes)
|
||||
.expect("X25519 public key should convert to libcrux PublicKey");
|
||||
|
||||
let _kem_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
// Verify we can convert X25519 private key to KEM format
|
||||
let x25519_private_bytes = initiator_keys.private_key().to_bytes();
|
||||
let _libcrux_private_key =
|
||||
libcrux_kem::PrivateKey::decode(libcrux_kem::Algorithm::X25519, &x25519_private_bytes)
|
||||
.expect("X25519 private key should convert to libcrux PrivateKey");
|
||||
|
||||
// Successful conversion is sufficient - actual encapsulation is tested in psk.rs
|
||||
// (libcrux_kem::PrivateKey is an enum with no len() method, conversion success is enough)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_demote_sets_read_only() {
|
||||
let sessions = SessionsMock::mock_post_handshake(12345);
|
||||
let mut session = sessions.initiator;
|
||||
|
||||
// Initially not read-only
|
||||
assert!(!session.is_read_only());
|
||||
assert!(session.successor_session_id().is_none());
|
||||
|
||||
// Demote the session
|
||||
session.demote(99999);
|
||||
|
||||
// Now read-only with successor
|
||||
assert!(session.is_read_only());
|
||||
assert_eq!(session.successor_session_id(), Some(99999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_fails_after_demotion() {
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let mut initiator_session = sessions.initiator;
|
||||
|
||||
// Encryption works before demotion
|
||||
let plaintext = b"Hello before demotion";
|
||||
assert!(initiator_session.encrypt_data(plaintext).is_ok());
|
||||
|
||||
// Demote the session
|
||||
initiator_session.demote(99999);
|
||||
|
||||
// Encryption fails after demotion
|
||||
let result = initiator_session.encrypt_data(plaintext);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
NoiseError::SessionReadOnly => {
|
||||
// Expected
|
||||
}
|
||||
e => panic!("Expected SessionReadOnly error, got: {:?}", e),
|
||||
// But duplicates are still rejected
|
||||
assert!(session.receiving_counter_quick_check(5).is_err());
|
||||
assert!(session.receiving_counter_mark(5).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_works_after_demotion() {
|
||||
// --- Setup Handshake ---
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let mut initiator_session = sessions.initiator;
|
||||
let mut responder_session = sessions.responder;
|
||||
fn test_packet_stats() {
|
||||
for kem in KEM::iter() {
|
||||
let mut session = SessionsMock::mock_post_handshake(kem).responder;
|
||||
|
||||
// Responder encrypts a message
|
||||
let plaintext = b"Message to demoted initiator";
|
||||
let ciphertext = responder_session
|
||||
.encrypt_data(plaintext)
|
||||
.expect("Encryption failed");
|
||||
// Initial stats
|
||||
let packet_count = session.current_packet_cnt();
|
||||
assert_eq!(packet_count.next, 0);
|
||||
assert_eq!(packet_count.received, 0);
|
||||
|
||||
// Demote the initiator session
|
||||
initiator_session.demote(99999);
|
||||
assert!(initiator_session.is_read_only());
|
||||
// After receiving packets
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
|
||||
// Decryption still works on demoted session (drain in-flight)
|
||||
let decrypted = initiator_session
|
||||
.decrypt_data(&ciphertext)
|
||||
.expect("Decryption should work on demoted session");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
let packet_count = session.current_packet_cnt();
|
||||
assert_eq!(packet_count.next, 2);
|
||||
assert_eq!(packet_count.received, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,17 +6,20 @@
|
||||
//! This module implements session lifecycle management functionality, handling
|
||||
//! creation, retrieval, and storage of sessions.
|
||||
|
||||
use crate::packet::{EncryptedLpPacket, LpMessage};
|
||||
use crate::peer_config::LpReceiverIndex;
|
||||
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||
use crate::{LpError, LpSession, LpStateMachine};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use crate::replay::validator::PacketCount;
|
||||
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions,
|
||||
/// ensuring proper thread-safety for concurrent access.
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
state_machines: HashMap<u32, LpStateMachine>,
|
||||
state_machines: HashMap<LpReceiverIndex, LpStateMachine>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
@@ -35,62 +38,47 @@ impl SessionManager {
|
||||
|
||||
pub fn process_input(
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
lp_id: LpReceiverIndex,
|
||||
input: LpInput,
|
||||
) -> Result<Option<LpAction>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
||||
}
|
||||
|
||||
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
pub fn send_data(
|
||||
&mut self,
|
||||
lp_id: LpReceiverIndex,
|
||||
data: LpMessage,
|
||||
) -> Result<LpAction, LpError> {
|
||||
self.process_input(lp_id, LpInput::SendData(data))?
|
||||
.ok_or(LpError::NotInTransport)
|
||||
}
|
||||
|
||||
pub fn receive_packet(
|
||||
&mut self,
|
||||
lp_id: LpReceiverIndex,
|
||||
packet: EncryptedLpPacket,
|
||||
) -> Result<Option<LpAction>, LpError> {
|
||||
self.process_input(lp_id, LpInput::ReceivePacket(packet))
|
||||
}
|
||||
|
||||
pub fn closed(&self, lp_id: LpReceiverIndex) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
|
||||
}
|
||||
|
||||
pub fn transport(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
pub fn transport(&self, lp_id: LpReceiverIndex) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Transport)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_state_machine_id(&self, lp_id: u32) -> Result<u32, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.id())?
|
||||
fn get_state_machine_id(&self, lp_id: LpReceiverIndex) -> Result<LpReceiverIndex, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.receiver_index())?
|
||||
}
|
||||
|
||||
pub fn get_state(&self, lp_id: u32) -> Result<LpStateBare, LpError> {
|
||||
pub fn get_state(&self, lp_id: LpReceiverIndex) -> Result<LpStateBare, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.bare_state()))?
|
||||
}
|
||||
|
||||
pub fn receiving_counter_quick_check(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.receiving_counter_quick_check(counter)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?.receiving_counter_mark(counter)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
|
||||
}
|
||||
|
||||
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?
|
||||
.decrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?
|
||||
.encrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn current_packet_cnt(&self, lp_id: u32) -> Result<(u64, u64), LpError> {
|
||||
pub fn current_packet_cnt(&self, lp_id: LpReceiverIndex) -> Result<PacketCount, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
||||
}
|
||||
|
||||
@@ -98,43 +86,54 @@ impl SessionManager {
|
||||
self.state_machines.len()
|
||||
}
|
||||
|
||||
pub fn state_machine_exists(&self, lp_id: u32) -> bool {
|
||||
pub fn state_machine_exists(&self, lp_id: LpReceiverIndex) -> bool {
|
||||
self.state_machines.contains_key(&lp_id)
|
||||
}
|
||||
|
||||
pub fn with_state_machine<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
pub fn with_state_machine<F, R>(&self, lp_id: LpReceiverIndex, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&LpStateMachine) -> R,
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
Err(LpError::StateMachineNotFound(lp_id))
|
||||
}
|
||||
// self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution
|
||||
}
|
||||
|
||||
// For mutable access (like running process_input)
|
||||
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
pub fn with_state_machine_mut<F, R>(
|
||||
&mut self,
|
||||
lp_id: LpReceiverIndex,
|
||||
f: F,
|
||||
) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
Err(LpError::StateMachineNotFound(lp_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
|
||||
let receiver_index = lp_session.id();
|
||||
pub fn create_session_state_machine(
|
||||
&mut self,
|
||||
lp_session: LpSession,
|
||||
) -> Result<LpReceiverIndex, LpError> {
|
||||
let session_id = lp_session.receiver_index();
|
||||
|
||||
if self.state_machines.contains_key(&session_id) {
|
||||
return Err(LpError::DuplicateSessionId(session_id));
|
||||
}
|
||||
|
||||
let sm = LpStateMachine::new(lp_session);
|
||||
self.state_machines.insert(receiver_index, sm);
|
||||
receiver_index
|
||||
self.state_machines.insert(session_id, sm);
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
/// Method to remove a state machine
|
||||
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
|
||||
pub fn remove_state_machine(&mut self, lp_id: LpReceiverIndex) -> bool {
|
||||
let removed = self.state_machines.remove(&lp_id);
|
||||
|
||||
removed.is_some()
|
||||
@@ -145,21 +144,21 @@ impl SessionManager {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{SessionsMock, mock_session_for_test};
|
||||
use nym_kkt_ciphersuite::{IntoEnumIterator, KEM};
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_get() {
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let local_session = mock_session_for_test();
|
||||
let id = local_session.id();
|
||||
let id = local_session.receiver_index();
|
||||
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
let sm_1_id = manager.create_session_state_machine(local_session).unwrap();
|
||||
assert_eq!(sm_1_id, id);
|
||||
|
||||
let retrieved = manager.state_machine_exists(id);
|
||||
assert!(retrieved);
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
let not_found = manager.state_machine_exists(123);
|
||||
assert!(!not_found);
|
||||
}
|
||||
|
||||
@@ -167,8 +166,7 @@ mod tests {
|
||||
fn test_session_manager_remove() {
|
||||
let mut manager = SessionManager::new();
|
||||
let local_session = mock_session_for_test();
|
||||
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
let sm_1_id = manager.create_session_state_machine(local_session).unwrap();
|
||||
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
@@ -180,24 +178,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sessions() {
|
||||
let mut manager = SessionManager::new();
|
||||
let session1 = SessionsMock::mock_post_handshake(123).initiator;
|
||||
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||
let session3 = SessionsMock::mock_post_handshake(125).initiator;
|
||||
for kem in KEM::iter() {
|
||||
let mut manager = SessionManager::new();
|
||||
let session1 = SessionsMock::mock_seeded_post_handshake(123, kem).initiator;
|
||||
let session2 = SessionsMock::mock_seeded_post_handshake(124, kem).initiator;
|
||||
let session3 = SessionsMock::mock_seeded_post_handshake(125, kem).initiator;
|
||||
|
||||
let sm_1 = manager.create_session_state_machine(session1);
|
||||
let sm_2 = manager.create_session_state_machine(session2);
|
||||
let sm_3 = manager.create_session_state_machine(session3);
|
||||
let sm_1 = manager.create_session_state_machine(session1).unwrap();
|
||||
let sm_2 = manager.create_session_state_machine(session2).unwrap();
|
||||
let sm_3 = manager.create_session_state_machine(session3).unwrap();
|
||||
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
|
||||
let retrieved1 = manager.get_state_machine_id(sm_1).unwrap();
|
||||
let retrieved2 = manager.get_state_machine_id(sm_2).unwrap();
|
||||
let retrieved3 = manager.get_state_machine_id(sm_3).unwrap();
|
||||
let retrieved1 = manager.get_state_machine_id(sm_1).unwrap();
|
||||
let retrieved2 = manager.get_state_machine_id(sm_2).unwrap();
|
||||
let retrieved3 = manager.get_state_machine_id(sm_3).unwrap();
|
||||
|
||||
assert_eq!(retrieved1, sm_1);
|
||||
assert_eq!(retrieved2, sm_2);
|
||||
assert_eq!(retrieved3, sm_3);
|
||||
assert_eq!(retrieved1, sm_1);
|
||||
assert_eq!(retrieved2, sm_2);
|
||||
assert_eq!(retrieved3, sm_3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
|
||||
let sesion = mock_session_for_test();
|
||||
|
||||
let sm = manager.create_session_state_machine(sesion);
|
||||
let sm = manager.create_session_state_machine(sesion).unwrap();
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
|
||||
+171
-995
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user