Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fa1b67c8d | |||
| 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 | |||
| 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
+134
-140
@@ -1351,10 +1351,11 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "classic-mceliece-rust"
|
||||
version = "3.2.0"
|
||||
source = "git+https://github.com/georgio/classic-mceliece-rust#f2f27048b621df103bbe64369a18174ffec04ae1"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62a9b6d27e553269a76625911aa8cf6afaa8659f1b0c85b410cb5f51a87183d9"
|
||||
dependencies = [
|
||||
"rand 0.9.2",
|
||||
"rand 0.8.5",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1563,11 +1564,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-models"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.5"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"pastey",
|
||||
"pastey 0.2.1",
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
@@ -3307,9 +3308,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86"
|
||||
checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc"
|
||||
dependencies = [
|
||||
"hax-lib-macros",
|
||||
"num-bigint",
|
||||
@@ -3318,9 +3319,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib-macros"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1"
|
||||
checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f"
|
||||
dependencies = [
|
||||
"hax-lib-macros-types",
|
||||
"proc-macro-error2",
|
||||
@@ -3331,9 +3332,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hax-lib-macros-types"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515"
|
||||
checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4120,8 +4121,10 @@ dependencies = [
|
||||
"nym-credential-verification",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-gateway",
|
||||
"nym-lp-transport",
|
||||
"nym-kkt",
|
||||
"nym-kkt-ciphersuite",
|
||||
"nym-lp",
|
||||
"nym-node",
|
||||
"nym-registration-client",
|
||||
"nym-test-utils",
|
||||
"nym-wireguard",
|
||||
@@ -4359,9 +4362,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
@@ -4450,10 +4453,21 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-aesgcm"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-intrinsics",
|
||||
"libcrux-platform",
|
||||
"libcrux-secrets",
|
||||
"libcrux-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-chacha20poly1305"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4464,8 +4478,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-curve25519"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4475,8 +4489,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ecdh"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-curve25519",
|
||||
"libcrux-p256",
|
||||
@@ -4486,8 +4500,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ed25519"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4499,15 +4513,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-hacl-rs"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-hkdf"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-hmac",
|
||||
@@ -4516,8 +4530,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-hmac"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4526,8 +4540,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-intrinsics"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"core-models",
|
||||
"hax-lib",
|
||||
@@ -4535,8 +4549,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-kem"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-curve25519",
|
||||
"libcrux-ecdh",
|
||||
@@ -4551,16 +4565,30 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-macros"
|
||||
version = "0.0.3"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ml-dsa"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"core-models",
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
"libcrux-macros",
|
||||
"libcrux-platform",
|
||||
"libcrux-sha3",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ml-kem"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
@@ -4574,8 +4602,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-p256"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4586,8 +4614,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-platform"
|
||||
version = "0.0.2"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.3"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -4595,7 +4623,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-poly1305"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4603,34 +4631,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-psq"
|
||||
version = "0.0.5"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"classic-mceliece-rust",
|
||||
"libcrux-aesgcm",
|
||||
"libcrux-chacha20poly1305",
|
||||
"libcrux-ecdh",
|
||||
"libcrux-ed25519",
|
||||
"libcrux-hkdf",
|
||||
"libcrux-hmac",
|
||||
"libcrux-kem",
|
||||
"libcrux-ml-dsa",
|
||||
"libcrux-ml-kem",
|
||||
"libcrux-sha2",
|
||||
"libcrux-traits",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-secrets"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.5"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-sha2"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4639,8 +4671,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-sha3"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
@@ -4650,8 +4682,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-traits"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux#f63bb67ead59297560edf523a3b29b21489c17ea"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-secrets",
|
||||
"rand 0.9.2",
|
||||
@@ -4954,7 +4986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -5075,7 +5107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3176f18d11a1ae46053e59ec89d46ba318ae1343615bd3f8c908bfc84edae35c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"pastey",
|
||||
"pastey 0.1.1",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
@@ -5314,7 +5346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.73"
|
||||
version = "1.1.75"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5422,7 +5454,6 @@ dependencies = [
|
||||
"nym-serde-helpers",
|
||||
"nym-test-utils",
|
||||
"nym-ticketbooks-merkle",
|
||||
"rand_chacha 0.3.1",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5560,7 +5591,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.70"
|
||||
version = "1.1.72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5643,7 +5674,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.72"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5689,6 +5720,7 @@ dependencies = [
|
||||
"clap",
|
||||
"comfy-table",
|
||||
"futures",
|
||||
"getrandom 0.3.3",
|
||||
"gloo-timers",
|
||||
"http-body-util",
|
||||
"humantime",
|
||||
@@ -6192,10 +6224,13 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"jwt-simple",
|
||||
"libcrux-curve25519",
|
||||
"libcrux-psq",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"nym-test-utils",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
@@ -6209,7 +6244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-data-observatory"
|
||||
version = "1.0.4"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -6357,7 +6392,6 @@ dependencies = [
|
||||
"bincode",
|
||||
"bip39",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"defguard_wireguard_rs",
|
||||
"fastrand",
|
||||
@@ -6377,9 +6411,7 @@ dependencies = [
|
||||
"nym-gateway-storage",
|
||||
"nym-id",
|
||||
"nym-ip-packet-router",
|
||||
"nym-kcp",
|
||||
"nym-lp",
|
||||
"nym-lp-transport",
|
||||
"nym-metrics",
|
||||
"nym-mixnet-client",
|
||||
"nym-network-defaults",
|
||||
@@ -6486,6 +6518,7 @@ dependencies = [
|
||||
"nym-validator-client",
|
||||
"pnet_packet",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"reqwest 0.13.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6790,15 +6823,16 @@ name = "nym-kkt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"blake3",
|
||||
"classic-mceliece-rust",
|
||||
"criterion",
|
||||
"libcrux-chacha20poly1305",
|
||||
"libcrux-ecdh",
|
||||
"libcrux-kem",
|
||||
"libcrux-ml-kem",
|
||||
"libcrux-psq",
|
||||
"num_enum",
|
||||
"nym-crypto",
|
||||
"nym-kkt-ciphersuite",
|
||||
"nym-kkt-context",
|
||||
"nym-pemstore",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"strum",
|
||||
@@ -6813,11 +6847,21 @@ dependencies = [
|
||||
"blake3",
|
||||
"libcrux-sha3",
|
||||
"num_enum",
|
||||
"semver 1.0.26",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-kkt-context"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"num_enum",
|
||||
"nym-kkt-ciphersuite",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-ledger"
|
||||
version = "1.20.4"
|
||||
@@ -6836,25 +6880,14 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"criterion",
|
||||
"dashmap",
|
||||
"libcrux-kem",
|
||||
"libcrux-psq",
|
||||
"libcrux-traits",
|
||||
"mock_instant",
|
||||
"num_enum",
|
||||
"nym-crypto",
|
||||
"nym-kkt",
|
||||
"nym-lp-common",
|
||||
"nym-lp-transport",
|
||||
"nym-kkt-ciphersuite",
|
||||
"nym-test-utils",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"snow",
|
||||
"thiserror 2.0.12",
|
||||
"tls_codec",
|
||||
"tokio",
|
||||
@@ -6886,6 +6919,7 @@ dependencies = [
|
||||
"nym-topology",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6897,19 +6931,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp-transport"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-test-utils",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-metrics"
|
||||
version = "1.20.4"
|
||||
@@ -7055,7 +7076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.71"
|
||||
version = "1.1.73"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -7105,13 +7126,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.25.0"
|
||||
version = "1.27.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"arrayref",
|
||||
"async-trait",
|
||||
"axum 0.7.9",
|
||||
"bincode",
|
||||
"bip39",
|
||||
"blake2 0.8.1",
|
||||
"bloomfilter",
|
||||
@@ -7126,6 +7148,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"csv",
|
||||
"cupid",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"hex",
|
||||
"hkdf",
|
||||
@@ -7145,6 +7168,7 @@ dependencies = [
|
||||
"nym-http-api-common",
|
||||
"nym-ip-packet-router",
|
||||
"nym-kkt",
|
||||
"nym-lp",
|
||||
"nym-metrics",
|
||||
"nym-mixnet-client",
|
||||
"nym-network-requester",
|
||||
@@ -7154,6 +7178,7 @@ dependencies = [
|
||||
"nym-noise-keys",
|
||||
"nym-nonexhaustive-delayqueue",
|
||||
"nym-pemstore",
|
||||
"nym-registration-common",
|
||||
"nym-sphinx-acknowledgements",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-forwarding",
|
||||
@@ -7163,6 +7188,7 @@ dependencies = [
|
||||
"nym-sphinx-types",
|
||||
"nym-statistics-common",
|
||||
"nym-task",
|
||||
"nym-test-utils",
|
||||
"nym-topology",
|
||||
"nym-types",
|
||||
"nym-validator-client",
|
||||
@@ -7172,7 +7198,7 @@ dependencies = [
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -7220,9 +7246,9 @@ dependencies = [
|
||||
"nym-http-api-client",
|
||||
"nym-kkt-ciphersuite",
|
||||
"nym-noise-keys",
|
||||
"nym-test-utils",
|
||||
"nym-upgrade-mode-check",
|
||||
"nym-wireguard-types",
|
||||
"rand_chacha 0.3.1",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7237,7 +7263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "1.1.2"
|
||||
version = "1.1.2-test"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -7256,7 +7282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.1.0"
|
||||
version = "4.1.0-test"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -7445,12 +7471,7 @@ dependencies = [
|
||||
"blake3",
|
||||
"chacha20",
|
||||
"chacha20poly1305",
|
||||
"criterion",
|
||||
"fastrand",
|
||||
"getrandom 0.2.16",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
"x25519-dalek",
|
||||
@@ -7496,6 +7517,7 @@ dependencies = [
|
||||
name = "nym-registration-client"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bytes",
|
||||
"futures",
|
||||
"nym-authenticator-client",
|
||||
@@ -7504,19 +7526,19 @@ dependencies = [
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ip-packet-client",
|
||||
"nym-kkt",
|
||||
"nym-lp",
|
||||
"nym-lp-transport",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-test-utils",
|
||||
"nym-validator-client",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typed-builder",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7650,7 +7672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.72"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -8018,6 +8040,7 @@ dependencies = [
|
||||
"futures",
|
||||
"nym-bin-common",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_chacha 0.9.0",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -8251,31 +8274,6 @@ dependencies = [
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-vpn-api-lib-wasm"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"getrandom 0.2.16",
|
||||
"js-sys",
|
||||
"nym-bin-common",
|
||||
"nym-compact-ecash",
|
||||
"nym-credential-proxy-requests",
|
||||
"nym-credentials",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ecash-time",
|
||||
"nym-wasm-utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tsify",
|
||||
"wasm-bindgen",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-wallet-types"
|
||||
version = "1.0.0"
|
||||
@@ -8335,9 +8333,7 @@ name = "nym-wasm-storage"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"getrandom 0.2.16",
|
||||
"indexed_db_futures",
|
||||
"js-sys",
|
||||
"nym-store-cipher",
|
||||
"nym-wasm-utils",
|
||||
"serde",
|
||||
@@ -8473,7 +8469,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.35"
|
||||
version = "0.1.37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -8829,6 +8825,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.8.5"
|
||||
@@ -14094,22 +14096,14 @@ dependencies = [
|
||||
name = "zknym-lib"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bs58",
|
||||
"getrandom 0.2.16",
|
||||
"js-sys",
|
||||
"nym-bin-common",
|
||||
"nym-compact-ecash",
|
||||
"nym-credentials",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"nym-wasm-utils",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.13.1",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tsify",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
|
||||
+18
-5
@@ -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"
|
||||
@@ -324,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"
|
||||
@@ -392,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" }
|
||||
@@ -436,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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,15 +9,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
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LpTransportError {
|
||||
#[error("the encoded packet is too long ({size} bytes)")]
|
||||
PacketTooBig { size: usize },
|
||||
|
||||
#[error("the encoded packet is too small ({size} bytes) to encode valid data")]
|
||||
PacketTooSmall { size: usize },
|
||||
|
||||
#[error("failed to establish connection with the remote host: {0}")]
|
||||
ConnectionFailure(String),
|
||||
|
||||
#[error("failed to configure the established connection: {0}")]
|
||||
ConnectionConfigFailure(String),
|
||||
|
||||
#[error("connection got closed before finishing the operation")]
|
||||
ConnectionClosed,
|
||||
|
||||
#[error("the received packet was malformed: {0}")]
|
||||
MalformedPacket(String),
|
||||
|
||||
#[error("failed to send bytes across the channel: {0}")]
|
||||
TransportSendFailure(String),
|
||||
|
||||
#[error("failed to receive bytes across the channel: {0}")]
|
||||
TransportReceiveFailure(String),
|
||||
}
|
||||
|
||||
impl LpTransportError {
|
||||
pub fn connection_failure(error: impl Into<String>) -> Self {
|
||||
LpTransportError::ConnectionFailure(error.into())
|
||||
}
|
||||
|
||||
pub fn connection_config(error: impl Into<String>) -> Self {
|
||||
LpTransportError::ConnectionConfigFailure(error.into())
|
||||
}
|
||||
|
||||
pub fn send_failure(error: std::io::Error) -> Self {
|
||||
if error.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||
return LpTransportError::ConnectionClosed;
|
||||
}
|
||||
LpTransportError::TransportSendFailure(error.to_string())
|
||||
}
|
||||
|
||||
pub fn receive_failure(error: std::io::Error) -> Self {
|
||||
if error.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||
return LpTransportError::ConnectionClosed;
|
||||
}
|
||||
LpTransportError::TransportReceiveFailure(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod error;
|
||||
pub mod traits;
|
||||
|
||||
pub use error::LpTransportError;
|
||||
|
||||
pub use traits::{LpHandshakeChannel, LpTransportChannel};
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::{EncryptedLpPacket, OuterHeader};
|
||||
use crate::transport::error::LpTransportError;
|
||||
use nym_kkt::context::KKTMode;
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::debug;
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
|
||||
pub const MAX_TRANSPORT_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
pub const MAX_HANDSHAKE_PACKET_SIZE: usize = 524287; // 524'160 for mceliece key + a bit of overhead for safety
|
||||
|
||||
/// Simple trait allowing sending bytes across.
|
||||
/// It is not concerned with encryption. It is up to the caller.
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpHandshakeChannel: Sized {
|
||||
/// Write all provided data and immediately flush the buffer
|
||||
async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError>;
|
||||
|
||||
/// Wrapper around `ReadExact` to return the `Vec<u8>` of `n` bytes directly
|
||||
async fn read_n_bytes(&mut self, n: usize) -> Result<Vec<u8>, LpTransportError>;
|
||||
|
||||
/// Send the provided handshake message on the connection
|
||||
async fn send_handshake_message<M: HandshakeMessage>(
|
||||
&mut self,
|
||||
message: M,
|
||||
_: KEM,
|
||||
) -> Result<(), LpTransportError> {
|
||||
self.write_all_and_flush(&message.into_bytes()).await
|
||||
}
|
||||
|
||||
/// Attempt to receive a handshake message of the provided type from the stream
|
||||
async fn receive_handshake_message<M: HandshakeMessage>(
|
||||
&mut self,
|
||||
expected_size: usize,
|
||||
) -> Result<M, LpTransportError> {
|
||||
let bytes = self.read_n_bytes(expected_size).await?;
|
||||
M::try_from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HandshakeMessage: Sized {
|
||||
/// Convert this message into bytes
|
||||
fn into_bytes(self) -> Vec<u8>;
|
||||
|
||||
/// Attempt to recover this message from the byte stream
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, LpTransportError>;
|
||||
|
||||
/// Expected size of this message based on the provided parameters
|
||||
fn expected_size(mode: KKTMode, expected_kem: KEM, payload_size: usize) -> usize;
|
||||
|
||||
/// Expected size of the response from the remote party.
|
||||
/// `None` if this is the final (PSQ msg2) message of the exchange
|
||||
fn response_size(&self, expected_kem: KEM) -> Option<usize>;
|
||||
}
|
||||
|
||||
async fn write_all_and_flush_async_write<W>(
|
||||
writer: &mut W,
|
||||
data: &[u8],
|
||||
) -> Result<(), LpTransportError>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
writer
|
||||
.write_all(data)
|
||||
.await
|
||||
.map_err(LpTransportError::send_failure)?;
|
||||
writer.flush().await.map_err(LpTransportError::send_failure)
|
||||
}
|
||||
|
||||
async fn read_n_bytes_async_read<R>(reader: &mut R, n: usize) -> Result<Vec<u8>, LpTransportError>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buf = vec![0u8; n];
|
||||
if n > MAX_HANDSHAKE_PACKET_SIZE {
|
||||
return Err(LpTransportError::PacketTooBig { size: n });
|
||||
}
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.await
|
||||
.map_err(LpTransportError::receive_failure)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransportChannel: Sized {
|
||||
async fn connect(endpoint: SocketAddr) -> Result<Self, LpTransportError>;
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> Result<(), LpTransportError>;
|
||||
|
||||
/// Sends a serialised and encrypted LP packet over the data stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `packet` - The encrypted LP packet to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn send_length_prefixed_transport_packet(
|
||||
&mut self,
|
||||
packet: &EncryptedLpPacket,
|
||||
) -> Result<(), LpTransportError>;
|
||||
|
||||
/// Receives an LP packet from a TCP stream with length-prefixed framing without additional parsing
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn receive_length_prefixed_transport_bytes(
|
||||
&mut self,
|
||||
) -> Result<Vec<u8>, LpTransportError>;
|
||||
|
||||
/// 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_length_prefixed_transport_packet(
|
||||
&mut self,
|
||||
) -> Result<EncryptedLpPacket, LpTransportError> {
|
||||
let mut bytes = self.receive_length_prefixed_transport_bytes().await?;
|
||||
|
||||
if bytes.len() < OuterHeader::SIZE {
|
||||
return Err(LpTransportError::PacketTooSmall { size: bytes.len() });
|
||||
}
|
||||
|
||||
// split it into the outer header and ciphertext
|
||||
let ciphertext = bytes.split_off(OuterHeader::SIZE);
|
||||
|
||||
// SAFETY: we just checked we have at least OuterHeader::SIZE bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let outer_header = OuterHeader::parse(&bytes).unwrap();
|
||||
|
||||
tracing::trace!(
|
||||
"Received LP packet ({} bytes + 4 byte length-prefix)",
|
||||
bytes.len()
|
||||
);
|
||||
Ok(EncryptedLpPacket::new(outer_header, ciphertext))
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_serialised_packet_async_write<W>(
|
||||
writer: &mut W,
|
||||
packet: &EncryptedLpPacket,
|
||||
) -> Result<(), LpTransportError>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet.encoded_length() as u32;
|
||||
writer
|
||||
.write_all(&len.to_le_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))
|
||||
.map_err(LpTransportError::send_failure)?;
|
||||
|
||||
// TODO: benchmark whether it'd be faster to concatenate all slices slices and
|
||||
// use a single `write_all` call
|
||||
|
||||
// Send the outer header
|
||||
writer
|
||||
.write_all(&packet.outer_header().to_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))
|
||||
.map_err(LpTransportError::send_failure)?;
|
||||
|
||||
// Send the actual packet data
|
||||
writer
|
||||
.write_all(packet.ciphertext())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))
|
||||
.map_err(LpTransportError::send_failure)?;
|
||||
|
||||
// Flush to ensure data is sent immediately
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))
|
||||
.map_err(LpTransportError::send_failure)?;
|
||||
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte length-prefix)",
|
||||
packet.encoded_length()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_length_prefixed_bytes_async_read<R>(
|
||||
reader: &mut R,
|
||||
) -> Result<Vec<u8>, LpTransportError>
|
||||
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}"))
|
||||
.map_err(LpTransportError::receive_failure)?;
|
||||
|
||||
let size = u32::from_le_bytes(len_buf) as usize;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
if size > MAX_TRANSPORT_PACKET_SIZE {
|
||||
return Err(LpTransportError::PacketTooBig { size });
|
||||
}
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; size];
|
||||
reader
|
||||
.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))
|
||||
.map_err(LpTransportError::receive_failure)?;
|
||||
|
||||
Ok(packet_buf)
|
||||
}
|
||||
|
||||
impl LpTransportChannel for TcpStream {
|
||||
async fn connect(endpoint: SocketAddr) -> Result<Self, LpTransportError> {
|
||||
TcpStream::connect(endpoint)
|
||||
.await
|
||||
.map_err(|err| LpTransportError::connection_failure(err.to_string()))
|
||||
}
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> Result<(), LpTransportError> {
|
||||
// Set TCP_NODELAY for low latency
|
||||
self.set_nodelay(nodelay)
|
||||
.map_err(|err| LpTransportError::connection_config(err.to_string()))
|
||||
}
|
||||
|
||||
async fn send_length_prefixed_transport_packet(
|
||||
&mut self,
|
||||
packet: &EncryptedLpPacket,
|
||||
) -> Result<(), LpTransportError> {
|
||||
send_serialised_packet_async_write(self, packet).await
|
||||
}
|
||||
|
||||
async fn receive_length_prefixed_transport_bytes(
|
||||
&mut self,
|
||||
) -> Result<Vec<u8>, LpTransportError> {
|
||||
receive_length_prefixed_bytes_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl LpTransportChannel for MockIOStream {
|
||||
async fn connect(_endpoint: SocketAddr) -> Result<Self, LpTransportError> {
|
||||
Ok(MockIOStream::default())
|
||||
}
|
||||
|
||||
fn set_no_delay(&mut self, _nodelay: bool) -> Result<(), LpTransportError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_length_prefixed_transport_packet(
|
||||
&mut self,
|
||||
packet: &EncryptedLpPacket,
|
||||
) -> Result<(), LpTransportError> {
|
||||
send_serialised_packet_async_write(self, packet).await
|
||||
}
|
||||
|
||||
async fn receive_length_prefixed_transport_bytes(
|
||||
&mut self,
|
||||
) -> Result<Vec<u8>, LpTransportError> {
|
||||
receive_length_prefixed_bytes_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl LpHandshakeChannel for MockIOStream {
|
||||
async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError> {
|
||||
write_all_and_flush_async_write(self, data).await
|
||||
}
|
||||
|
||||
async fn read_n_bytes(&mut self, n: usize) -> Result<Vec<u8>, LpTransportError> {
|
||||
read_n_bytes_async_read(self, n).await
|
||||
}
|
||||
}
|
||||
|
||||
impl LpHandshakeChannel for TcpStream {
|
||||
async fn write_all_and_flush(&mut self, data: &[u8]) -> Result<(), LpTransportError> {
|
||||
write_all_and_flush_async_write(self, data).await
|
||||
}
|
||||
|
||||
async fn read_n_bytes(&mut self, n: usize) -> Result<Vec<u8>, LpTransportError> {
|
||||
read_n_bytes_async_read(self, n).await
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ use nym_authenticator_requests::AuthenticatorVersion;
|
||||
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_ip_packet_requests::IpPair;
|
||||
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests, SignatureScheme};
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests};
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
pub use lp_messages::*;
|
||||
use nym_crypto::asymmetric::x25519::DHPublicKey;
|
||||
pub use serialisation::BincodeError;
|
||||
|
||||
mod lp_messages;
|
||||
@@ -56,9 +57,11 @@ pub struct WireguardConfiguration {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NymNodeLPInformation {
|
||||
pub address: SocketAddr,
|
||||
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
|
||||
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
|
||||
pub x25519: x25519::PublicKey,
|
||||
pub expected_kem_key_hashes: BTreeMap<KEM, KEMKeyDigests>,
|
||||
pub x25519: DHPublicKey,
|
||||
|
||||
// to be inferred from node's version
|
||||
pub ciphersuite: Ciphersuite,
|
||||
|
||||
/// Supported protocol version of the remote gateway.
|
||||
/// Included in case we have to downgrade our version.
|
||||
|
||||
@@ -21,7 +21,7 @@ thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
getrandom = { workspace = true, features = ["js"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -15,6 +15,7 @@ description = "Helpers, traits, and mock definitions for tests"
|
||||
anyhow = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand_chacha09 = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::traits::Timeboxed;
|
||||
use nym_bin_common::logging::tracing_subscriber::EnvFilter;
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
use nym_bin_common::logging::{default_tracing_fmt_layer, tracing_subscriber};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha09::rand_core::SeedableRng as SeedableRng09;
|
||||
use std::future::Future;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::error::Elapsed;
|
||||
|
||||
// 'current' rand crate
|
||||
pub use rand_chacha::ChaCha20Rng as DeterministicRng;
|
||||
pub use rand_chacha::rand_core::{CryptoRng, RngCore};
|
||||
|
||||
// rand09 compat
|
||||
pub use rand_chacha09::ChaChaRng as DeterministicRng09;
|
||||
pub use rand_chacha09::rand_core::{CryptoRng as CryptoRng09, RngCore as RngCore09};
|
||||
|
||||
pub fn leak<T>(val: T) -> &'static mut T {
|
||||
Box::leak(Box::new(val))
|
||||
}
|
||||
@@ -26,6 +36,35 @@ where
|
||||
tokio::spawn(async move { fut.timeboxed().await })
|
||||
}
|
||||
|
||||
pub struct DeterministicRng09Send(Arc<Mutex<DeterministicRng09>>);
|
||||
|
||||
impl DeterministicRng09Send {
|
||||
pub fn new(deterministic_rng09: DeterministicRng09) -> Self {
|
||||
Self(Arc::new(Mutex::new(deterministic_rng09)))
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoRng09 for DeterministicRng09Send {}
|
||||
|
||||
// unwraps are perfectly fine in test code
|
||||
impl RngCore09 for DeterministicRng09Send {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.0.lock().unwrap().next_u32()
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.0.lock().unwrap().next_u64()
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dst: &mut [u8]) {
|
||||
self.0.lock().unwrap().fill_bytes(dst)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deterministic_rng_09() -> DeterministicRng09 {
|
||||
seeded_rng_09([42u8; 32])
|
||||
}
|
||||
|
||||
pub fn deterministic_rng() -> DeterministicRng {
|
||||
seeded_rng([42u8; 32])
|
||||
}
|
||||
@@ -34,10 +73,18 @@ pub fn seeded_rng(seed: [u8; 32]) -> DeterministicRng {
|
||||
DeterministicRng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn seeded_rng_09(seed: [u8; 32]) -> DeterministicRng09 {
|
||||
DeterministicRng09::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn u64_seeded_rng(seed: u64) -> DeterministicRng {
|
||||
DeterministicRng::seed_from_u64(seed)
|
||||
}
|
||||
|
||||
pub fn u64_seeded_rng_09(seed: u64) -> DeterministicRng09 {
|
||||
DeterministicRng09::seed_from_u64(seed)
|
||||
}
|
||||
|
||||
// test logger to use during debugging
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn setup_test_logger() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
target_arch = "wasm32"
|
||||
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
|
||||
@@ -1,3 +1,4 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
target_arch = "wasm32"
|
||||
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user