Compare commits

..

92 Commits

Author SHA1 Message Date
mfahampshire 8b2e074ca7 check 2026-02-25 13:27:12 +00:00
benedettadavico a2081af603 . 2026-02-24 12:02:35 +01:00
benedettadavico 5b62fd76ba update changelog 2026-02-24 11:29:04 +01:00
benedettadavico a4950485d1 bump versions 2026-02-13 09:04:15 +01:00
benedetta davico d93d25ebae Merge pull request #6387 from nymtech/dependabot/npm_and_yarn/documentation/docs/next-16.1.5
build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs
2026-02-11 17:04:39 +01:00
benedetta davico ae0ab69bd2 Merge pull request #6405 from nymtech/dependabot/npm_and_yarn/eslint-9.26.0
build(deps-dev): bump eslint from 8.57.1 to 9.26.0
2026-02-11 17:03:31 +01:00
Jędrzej Stuczyński 4897cb0ce4 feat: introduce on-disk cache persistance for major nym-api caches (#6302)
This includes:
- mixnet contract cache
- described nodes cache
- nodes annotations cache (performance)

those changes include taking some code developed for the purposes of #6277
2026-02-11 15:57:47 +00:00
benedetta davico 46b9d5374b Merge pull request #6271 from nymtech/bugfix/data-observatory
Fix migrations in the Data Observatory
2026-02-11 16:02:43 +01:00
Jack Wampler e7fcaa980f HTTP & DNS Improvements (#6423)
* Improve HTTP use of connection pooling (#6375)

* add swap to system resolver instead of fallback (#6376)

* add header tracking outer host name used in stealth requests (#6389)

* Rotate urls on parse failure (#6383)

* Add shared settings for stealth policy across HTTP clients (#6388)

* Better controls for global interaction w/ static DNS (#6374)
2026-02-11 07:04:53 -07:00
mfahampshire 5fc2936d3f Max/quick patch docs (#6447)
* patch missing file + remove gitignore config

* patch missing file + remove gitignore config
2026-02-11 11:52:55 +00:00
benedetta davico 3d59a72ee8 Merge pull request #6444 from nymtech/changelog-v2026.3
Update changelog for v2026.3-parmigiano
2026-02-11 12:02:10 +01:00
Jędrzej Stuczyński bb694855d5 Lp/stateless handshake (#6437)
* perform KKT/PSQ handshake outside of LPStateMachine

* initiator

* responder

* concurrent test

* remove KTT/PSQ from the LpStateMachine

* adjusted gateway's Handler to accomodate new changes

* filling in placehlders

* fixed imports in nym-kkt crate

* naming

* clippy and moved more placeholder tests

* split up the initiator side of the PSQ

* split up the responder side of the PSQ

* additional helpers

* addressing review comments

* additional tests and explicit Error message
2026-02-10 17:20:54 +00:00
dependabot[bot] 9cb2655e7d build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts (#6416)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.6.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.6.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:29:24 +00:00
dependabot[bot] 0c3efe67fb build(deps): bump bytes from 1.10.1 to 1.11.1 in /nym-wallet (#6413)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:28:48 +00:00
dependabot[bot] ca5ad94420 build(deps): bump bytes from 1.11.0 to 1.11.1 (#6414)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:28:35 +00:00
dependabot[bot] 220c64100d build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs
Bumps [next](https://github.com/vercel/next.js) from 15.5.9 to 16.1.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.9...v16.1.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 16:08:44 +00:00
dependabot[bot] 505a19e32f build(deps-dev): bump eslint from 8.57.1 to 9.26.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.1 to 9.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/v9.26.0/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.1...v9.26.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.26.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 16:08:26 +00:00
dependabot[bot] b75839461b build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs (#6345)
Bumps [diff](https://github.com/kpdecker/jsdiff) from 5.2.0 to 5.2.2.
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/v5.2.0...v5.2.2)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 5.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:07:35 +00:00
dependabot[bot] 4e4e0df721 build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs (#6325)
Bumps [undici](https://github.com/nodejs/undici) from 6.21.3 to 6.23.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.3...v6.23.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.23.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:07:08 +00:00
dependabot[bot] c3520b575f build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs (#6332)
Bumps [h3](https://github.com/h3js/h3) from 1.15.4 to 1.15.5.
- [Release notes](https://github.com/h3js/h3/releases)
- [Changelog](https://github.com/h3js/h3/blob/v1.15.5/CHANGELOG.md)
- [Commits](https://github.com/h3js/h3/compare/v1.15.4...v1.15.5)

---
updated-dependencies:
- dependency-name: h3
  dependency-version: 1.15.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:06:48 +00:00
dependabot[bot] c7a466860e build(deps): bump h3 from 1.15.4 to 1.15.5 (#6339)
Bumps [h3](https://github.com/h3js/h3) from 1.15.4 to 1.15.5.
- [Release notes](https://github.com/h3js/h3/releases)
- [Changelog](https://github.com/h3js/h3/blob/v1.15.5/CHANGELOG.md)
- [Commits](https://github.com/h3js/h3/compare/v1.15.4...v1.15.5)

---
updated-dependencies:
- dependency-name: h3
  dependency-version: 1.15.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 16:06:31 +00:00
Jędrzej Stuczyński 956df22d86 Chore/revert 6433 (#6445)
* Revert "build(deps): bump time from 0.3.41 to 0.3.47 in /nym-wallet (#6433)"

This reverts commit fd47ebfad0.

* chore: revert #6433 due to rust version incompatibility
2026-02-10 16:05:45 +00:00
dependabot[bot] 0ca122c56b build(deps): bump qs and express (#6307)
Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.11.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.11.0...v6.14.1)

Updates `express` from 4.19.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...v4.22.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:58:50 +00:00
dependabot[bot] 492eb22d74 build(deps): bump qs and express in /wasm/mix-fetch/internal-dev (#6308)
Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.13.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1)

Updates `express` from 4.21.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:58:33 +00:00
dependabot[bot] 9513eb458b build(deps): bump rsa from 0.9.8 to 0.9.10 (#6311)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.8 to 0.9.10.
- [Changelog](https://github.com/RustCrypto/RSA/blob/v0.9.10/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.8...v0.9.10)

---
updated-dependencies:
- dependency-name: rsa
  dependency-version: 0.9.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:58:08 +00:00
dependabot[bot] 8bca0698ee build(deps): bump lodash-es in /documentation/docs (#6350)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:56:15 +00:00
dependabot[bot] 8e278866c7 build(deps): bump lodash (#6351)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:55:56 +00:00
dependabot[bot] ff93657609 build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs (#6353)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:55:29 +00:00
dependabot[bot] d46e967b5b build(deps): bump lodash in /sdk/typescript/packages/nodejs-client (#6354)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:55:12 +00:00
dependabot[bot] 1219dcf874 build(deps-dev): bump lodash in /sdk/typescript/codegen/contract-clients (#6359)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:54:54 +00:00
dependabot[bot] a7068ea421 build(deps): bump lodash-es from 4.17.21 to 4.17.23 (#6360)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:54:34 +00:00
dependabot[bot] 5dc6546f1c build(deps): bump lodash from 4.17.21 to 4.17.23 (#6369)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:53:44 +00:00
dependabot[bot] 5f2bc60c2c build(deps): bump next in /nym-node-status-api/nym-node-status-ui (#6385)
Bumps [next](https://github.com/vercel/next.js) from 15.4.10 to 16.1.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.10...v16.1.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:53:24 +00:00
dependabot[bot] 195c75d293 build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 (#6407)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.50.1 to 4.52.2.
- [Release notes](https://github.com/mikefarah/yq/releases)
- [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt)
- [Commits](https://github.com/mikefarah/yq/compare/v4.50.1...v4.52.2)

---
updated-dependencies:
- dependency-name: mikefarah/yq
  dependency-version: 4.52.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:51:45 +00:00
dependabot[bot] f9827f5dd4 build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 (#6415)
Bumps @isaacs/brace-expansion from 5.0.0 to 5.0.1.

---
updated-dependencies:
- dependency-name: "@isaacs/brace-expansion"
  dependency-version: 5.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:51:19 +00:00
dependabot[bot] b92dd2f264 build(deps-dev): bump webpack (#6428)
Bumps [webpack](https://github.com/webpack/webpack) from 5.76.0 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.76.0...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:50:47 +00:00
dependabot[bot] 8e792b7b93 build(deps-dev): bump webpack in /wasm/zknym-lib/internal-dev (#6429)
Bumps [webpack](https://github.com/webpack/webpack) from 5.77.0 to 5.104.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.77.0...v5.104.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.104.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:50:27 +00:00
dependabot[bot] 061840c47c build(deps-dev): bump webpack (#6430)
Bumps [webpack](https://github.com/webpack/webpack) from 5.94.0 to 5.104.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.94.0...v5.104.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.104.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:50:01 +00:00
dependabot[bot] 93834bcf28 build(deps-dev): bump webpack in /wasm/mix-fetch/internal-dev (#6431)
Bumps [webpack](https://github.com/webpack/webpack) from 5.98.0 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.98.0...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:49:42 +00:00
dependabot[bot] 89ab2630cd build(deps-dev): bump webpack from 5.102.1 to 5.104.1 (#6432)
Bumps [webpack](https://github.com/webpack/webpack) from 5.102.1 to 5.104.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.1...v5.104.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.104.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:49:15 +00:00
dependabot[bot] fd47ebfad0 build(deps): bump time from 0.3.41 to 0.3.47 in /nym-wallet (#6433)
Bumps [time](https://github.com/time-rs/time) from 0.3.41 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.41...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:48:56 +00:00
dependabot[bot] b0d01ec12a build(deps-dev): bump webpack in /wasm/client/internal-dev (#6435)
Bumps [webpack](https://github.com/webpack/webpack) from 5.98.0 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.98.0...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 15:48:30 +00:00
Merve 470282612b Update changelog.mdx 2026-02-10 16:34:31 +03:00
Merve bb24b5e91d Update changelog.mdx 2026-02-10 16:23:52 +03:00
Merve 4222a7b684 Update changelog.mdx 2026-02-10 15:50:01 +03:00
merve 73bc746cd6 Update changelog for v2026.3-parmigiano 2026-02-10 15:31:06 +03:00
mfahampshire 32402d64e8 remove doubled and bring up to date (#6443)
* remove doubled and bring up to date

* update readme

* update build
2026-02-10 12:00:02 +00:00
benedetta davico 681b0d17b5 Merge pull request #6442 from nymtech/master
syncing
2026-02-10 12:22:41 +01:00
benedetta davico 068ee7d2b7 Merge pull request #6440 from nymtech/release/2026.3-parmigiano
Release/2026.3 parmigiano
2026-02-10 12:07:01 +01:00
benedetta davico 39cfd532a8 Merge pull request #6439 from nymtech/release/2026.3-parmigiano
Release/2026.3 parmigiano
2026-02-10 12:06:52 +01:00
mfahampshire 613d496133 Add Lychee linkchecker for inter-doc links (#6438)
* Add Lychee linkchecker for inter-doc links

* Fix path to linkcheckr CI

* Try fix path again

* Update lychee config

* Fix broken links

* Add Lychee usage info to readme
2026-02-10 10:59:17 +00:00
benedettadavico 1ecb457c66 update changelog 2026-02-10 10:30:45 +01:00
Sachin Kamath 49faa13855 chore: update chain watcher workflow to hosted runner 2026-02-09 14:38:35 +05:30
Sachin Kamath 51e5e7825d chore: update chain watcher push workflow 2026-02-09 14:32:28 +05:30
Sachin Kamath ded23a6271 chore: add headers to coingecko fetch 2026-02-09 13:45:39 +05:30
Jędrzej Stuczyński 801dcdda1e do not run LP (#6422) 2026-02-06 08:41:19 +00:00
Jędrzej Stuczyński e2d29f184d Merge pull request #6424 from nymtech/release/2026.3-parmigiano
Syncing parmigiano and develop
2026-02-05 16:49:37 +00:00
Jędrzej Stuczyński a151a03181 Lp/ip pool fixes (#6412)
* squashing Lp/ip pool fixes#6412

removed unused imports

gateway probe fixes

PSK injection + test fixes

cleanup minus PSK injection

combine with lp reg

moved authenticator peer registration to centralised location

bugfix: ensure IpPool never allocates gateway ip

ip pool allocation tests

* review fixes

* test fixes
2026-02-05 14:47:37 +00:00
Simon Wicky b19e82d4f7 revert mixnet-based client fautly changes from LP (#6420) 2026-02-05 14:43:18 +01:00
Simon Wicky 88a4633bc4 [LP fix] Registration client with fallback (#6419)
* don't start mixnet client for lp reg, with fallback

* tweaks

* add logging
2026-02-05 10:51:37 +01:00
dynco-nym 660eff45dc Endpoint for exit GW IPs (#6418) 2026-02-04 22:14:46 +01:00
Simon Wicky d4882ca276 [LP-fix] expose wg psk for the vpn-client (#6411)
* expose wg psk for the vpn-client :
store socket addr of nested session as socketaddr

* probe fix

* nits
2026-02-04 09:35:44 +01:00
mfahampshire cfcf804b47 Max/crates publishing tweaks (#6343)
* add semver validator action

* update runner

* update runner with sed for old version in CI

* Add no commit to publish for the moment

* fix version bump command

* configure git bot

* error check

* make dryrun less opaque

* Reintroduce error check - keep logging dryrun in for debug (commented
out)

* fix grep check

* bring non-dry-run to parity

* add node for npx semver check to action

* updated sed command

* revert erroneous version bump

* added semver check to publish workflow

* allow from other branches

* allow from other branches again

* publishing guide

* update publication runner

* Release 1.20.3

nym-api-requests@1.20.3
nym-async-file-watcher@1.20.3
nym-authenticator-requests@1.20.3
nym-bandwidth-controller@1.20.3
nym-bin-common@1.20.3
nym-cache@1.20.3
nym-cli-commands@1.20.3
nym-client-core@1.20.3
nym-client-core-config-types@1.20.3
nym-client-core-gateways-storage@1.20.3
nym-client-core-surb-storage@1.20.3
nym-client-websocket-requests@1.20.3
nym-coconut-dkg-common@1.20.3
nym-common@1.20.3
nym-compact-ecash@1.20.3
nym-config@1.20.3
nym-contracts-common@1.20.3
nym-contracts-common-testing@1.20.3
nym-cpp-ffi@1.20.3
nym-credential-proxy-lib@1.20.3
nym-credential-proxy-requests@1.20.3
nym-credential-storage@1.20.3
nym-credential-utils@1.20.3
nym-credential-verification@1.20.3
nym-credentials@1.20.3
nym-credentials-interface@1.20.3
nym-crypto@1.20.3
nym-dkg@1.20.3
nym-ecash-contract-common@1.20.3
nym-ecash-signer-check@1.20.3
nym-ecash-signer-check-types@1.20.3
nym-ecash-time@1.20.3
nym-exit-policy@1.20.3
nym-ffi-shared@1.20.3
nym-gateway-client@1.20.3
nym-gateway-requests@1.20.3
nym-gateway-stats-storage@1.20.3
nym-gateway-storage@1.20.3
nym-go-ffi@1.20.3
nym-group-contract-common@1.20.3
nym-http-api-client@1.20.3
nym-http-api-client-macro@1.20.3
nym-http-api-common@1.20.3
nym-id@1.20.3
nym-inclusion-probability@1.20.3
nym-ip-packet-client@1.20.3
nym-ip-packet-requests@1.20.3
nym-metrics@1.20.3
nym-mixnet-client@1.20.3
nym-mixnet-contract-common@1.20.3
nym-mixnode-common@1.20.3
nym-multisig-contract-common@1.20.3
nym-network-defaults@1.20.3
nym-node-metrics@1.20.3
nym-node-requests@1.20.3
nym-node-tester-utils@1.20.3
nym-noise@1.20.3
nym-noise-keys@1.20.3
nym-nonexhaustive-delayqueue@1.20.3
nym-ordered-buffer@1.20.3
nym-outfox@1.20.3
nym-pemstore@1.20.3
nym-performance-contract-common@1.20.3
nym-pool-contract-common@1.20.3
nym-registration-common@1.20.3
nym-sdk@1.20.3
nym-serde-helpers@1.20.3
nym-service-provider-requests-common@1.20.3
nym-service-providers-common@1.20.3
nym-socks5-client-core@1.20.3
nym-socks5-proxy-helpers@1.20.3
nym-socks5-requests@1.20.3
nym-sphinx@1.20.3
nym-sphinx-acknowledgements@1.20.3
nym-sphinx-addressing@1.20.3
nym-sphinx-anonymous-replies@1.20.3
nym-sphinx-chunking@1.20.3
nym-sphinx-cover@1.20.3
nym-sphinx-forwarding@1.20.3
nym-sphinx-framing@1.20.3
nym-sphinx-params@1.20.3
nym-sphinx-routing@1.20.3
nym-sphinx-types@1.20.3
nym-sqlx-pool-guard@1.20.3
nym-statistics-common@1.20.3
nym-store-cipher@1.20.3
nym-task@1.20.3
nym-test-utils@1.20.3
nym-ticketbooks-merkle@1.20.3
nym-topology@1.20.3
nym-tun@1.20.3
nym-types@1.20.3
nym-upgrade-mode-check@1.20.3
nym-validator-client@1.20.3
nym-verloc@1.20.3
nym-vesting-contract-common@1.20.3
nym-wasm-client-core@1.20.3
nym-wasm-storage@1.20.3
nym-wasm-utils@1.20.3
nym-wireguard@1.20.3
nym-wireguard-private-metadata-client@1.20.3
nym-wireguard-private-metadata-server@1.20.3
nym-wireguard-private-metadata-shared@1.20.3
nym-wireguard-private-metadata-tests@1.20.3
nym-wireguard-types@1.20.3
nyxd-scraper-shared@1.20.3

Generated by cargo-workspaces

* remove --allow-branch ; no commit, doesn't need branch restrictions

* remove another clashing flag

* again

* exclude build.rs from crate for crates.io

* various in process scripts to pick up deployment where it left off

* rename workflows

* Version bump fix from borked publish run

* add publishing doc + updated publish-resume ci

* move example from service-providers to sdk examples/ to remove circular dev dependency for cargo publication

* remove wildcard version import

* Workflows and documentation for publication

* add contracts/ patch + imports

* Reintroduce missing kkt dep from rebase

* fix borked rebase cargo lock

---------

Co-authored-by: Nym bot <nym-bot@users.noreply.github.com>
2026-02-03 11:32:38 +00:00
Simon Wicky b6d22abc01 configurable LP timeouts (#6409) 2026-02-03 11:50:18 +01:00
Simon Wicky bd755385ed LP-fix : add LP x25519 key to the description (#6408)
* add x25519 key in LP description

* gateway probe adapt
2026-02-03 10:25:43 +01:00
Andrej Mihajlov 940fb09ae4 Merge pull request #6401 from nymtech/am/update-reqwest-v0.13
Update reqwest to v0.13.1
2026-02-02 16:48:35 +01:00
jmwample 47af0b24f0 fmt 2026-02-02 08:13:43 -07:00
jmwample 52edfdcc2f move reqwest dep to dev only 2026-02-02 08:11:59 -07:00
Simon Wicky af04afbe5e use rng that is Send (#6404) 2026-02-02 11:45:56 +01:00
Andrej Mihajlov 63f158cccb nym-api: add query feat 2026-02-02 11:45:53 +01:00
Andrej Mihajlov b4aee7a1d9 zulip-client: add form feat 2026-02-02 11:29:15 +01:00
Andrej Mihajlov c55b215b65 Update reqwest to v0.13.1, switch to using rustls (default); ring is no longer available 2026-02-02 11:20:36 +01:00
Simon Wicky 7e8faf0ec6 use local kem key instead of local x25519 (#6402) 2026-02-02 11:14:04 +01:00
benedettadavico 0082b9fc50 Merge remote-tracking branch 'origin/release/2026.3-parmigiano' into release/2026.3-parmigiano 2026-02-02 10:14:36 +01:00
benedettadavico e16a337354 bump versions 2026-02-02 10:14:15 +01:00
Simon Wicky cd0881462b [LP Gateway Probe] CLI and behavior improvements (#6400)
* attempt to de-spaghettificationize the gateway probe

* applying suggestions
2026-01-30 16:55:29 +01:00
Jędrzej Stuczyński 8916b021a9 lp: attempt to negotiate (and use) protocol version (#6399) 2026-01-30 12:38:32 +00:00
Jędrzej Stuczyński dccdde108c Lp/bugfix/share ip allocation (#6395)
* feat: use shared PeerManager between Authenticator and LpHandlerState

* feat: share IpPool

* clippy and test fixes

* PR suggestions
2026-01-30 11:38:17 +00:00
Jędrzej Stuczyński 9d661e7a7b bugfix: use correct reserved bytes when parsing LpHeader (#6398) 2026-01-30 09:39:47 +00:00
Jędrzej Stuczyński 76ce1bc0f9 feat: use hex-encoding for lp key digests (#6394)
* feat: use hex-encoding for lp key digests

* removed needless borrow in test code

* gateway probe fixes
2026-01-30 08:44:29 +00:00
dynco-nym d3648f13c5 NS API socks5 support (#6361)
* Add conversion from gw_probe crate type

* Move code around
- split 1000+ LoC files into smaller ones

* Add socks5 field
- code improvements in gw_probe crate

* Fix docker build
- install go
- required as build dependency of gw probe

* Add logs to agent

* NS API: configure DB via env

* rebase fix

* socks5 score calc

* Cargo fmt

* use existing div_ceil

* Code improvements

* Bump NS API version

* Rename variables

* Bump API & agent version

* Try to fix CI

* Build only on linux
2026-01-29 20:54:21 +01:00
dynco-nym 9a931b9251 Add socks5 test to gateway-probe (#6393)
* Socks5 in GW probe

Bump NS agent version

Fix bugs
- force route construction
- use same entry = exit

Fix NS API version check workflow

PR feedback

More robust test attempts

CLI arg validation

Fix clippy

PR feedback

* Test provided endpoints in config at startup

Require one valid endpoint

* Bump agent to 1.1.0
2026-01-29 18:20:51 +01:00
Jack Wampler f4ba8ac2b3 add extra configured nym api url to env (#6382) 2026-01-29 07:09:02 -07:00
Andrej Mihajlov c274cc588d Merge pull request #6390 from nymtech/am/reduce-http-error-size
Reduce the size of `HttpClientError`
2026-01-29 14:59:38 +01:00
Jędrzej Stuczyński 7dd1dd1a6c Lp/two step dvpn reg (#6386)
* squashing  Lp/two step dvpn reg #6386

fixed integration tests by extending the mocks

remove dead code

compiling client-side code

gateway side handling of updated lp-wg reg

wip: countless changes on the gateway handler side

splitting up NestedLpSession

* fixed lp-messages tests

* gateway probe fixes

* unused variable

* resolved nits
2026-01-29 13:38:21 +00:00
import this 982786b678 [NTM]: NIP-7 port update & [DOCs/operators]: Release notes for v2026.2 oscypek (#6384)
* add operators notes

* add dev notes

* bump up version

* open NIP-7 ports

* bump up stats

* fix incorrect dash
2026-01-29 13:21:28 +00:00
Simon Wicky 561182ce6b shuffling files around in the probe, before improving it (#6391) 2026-01-29 10:34:57 +01:00
Andrej Mihajlov f4b59158df Box reqwest::Url to keep HttpClientError below 128 byte size which triggers clippy 2026-01-29 07:41:43 +01:00
benedetta davico 944b4f5aad Merge pull request #6380 from nymtech/release/2026.2-oscypek
Merge oscypek to master
2026-01-27 16:45:05 +01:00
Mark Sinclair 6e62e34ac8 bump version to 1.0.1 2025-12-04 16:07:17 +00:00
Mark Sinclair 18e72c90df run the migrations from the data observatory and not the base chain scraper 2025-12-04 16:06:56 +00:00
Mark Sinclair fd051540aa remove nuke db cli args - only makes sense for sqlite and not pgsql 2025-12-03 13:54:23 +00:00
382 changed files with 22179 additions and 19969 deletions
+10 -1
View File
@@ -10,6 +10,7 @@ on:
- 'nym-api/**'
- 'nym-authenticator-client/**'
- 'nym-credential-proxy/**'
- 'nym-gateway-probe/**'
- 'nym-ip-packet-client/**'
- 'nym-network-monitor/**'
- 'nym-node/**'
@@ -89,7 +90,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
- name: Clippy (non-macos)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
@@ -104,6 +105,14 @@ jobs:
with:
command: build
# only build on linux because of wg FFI bindings of its dependency (network probe)
- name: Build nym-node-status-api (linux only)
if: runner.os == 'Linux'
uses: actions-rs/cargo@v1
with:
command: build
args: -p nym-node-status-api
- name: Build all examples
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
on:
pull_request:
paths:
- "nym-node-status-api/**"
- "nym-node-status-api/nym-node-status-api/**"
env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -0,0 +1,79 @@
name: Publish to crates.io (dry run)
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.21.0)"
required: true
type: string
env:
CI_BOT_AUTHOR: "Nym bot"
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
jobs:
publish-dry-run:
runs-on: arc-linux-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Validate version format
run: |
if ! npx semver "${{ inputs.version }}"; then
echo "Error: '${{ inputs.version }}' is not valid semver"
exit 1
fi
- name: Get current version
id: current_version
run: |
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update workspace dependencies
run: |
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
- name: Bump versions (local only)
run: |
cargo workspaces version custom ${{ inputs.version }} \
--allow-branch ${{ github.ref_name }} \
--no-git-commit \
# Dry run may show cascading dependency errors because packages aren't
# actually uploaded - these are expected and ignored. We check for real
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
- name: Publish (dry run)
run: |
output=$(cargo workspaces publish --dry-run --allow-dirty 2>&1) || true
echo "$output"
# Check for real errors (not cascading dependency errors)
# Cascading errors mention "crates.io index", real errors mention "Cargo.toml"
echo "$output" | grep -i "Cargo.toml" && exit 1 || true
# Show the list of packages published
- name: Show package versions
run: cargo workspaces list --long
@@ -0,0 +1,59 @@
# This is in case, for whatever reason, a publication run fails, and we need to restart halfway down the list, of unbumped/unpublished crates.
name: Resume crates.io publish
on:
workflow_dispatch:
inputs:
resume_after:
description: "Last successfully published crate (will start from the next one)"
required: true
type: string
publish_interval:
description: "Seconds to wait between publishes"
required: false
default: "600"
type: string
jobs:
publish:
runs-on: arc-linux-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
# Get crates in publish order, skip up to and including resume_after
- name: Publish remaining crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
CRATES=$(cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2)
if [ -z "$CRATES" ]; then
echo "Error: No crates found after '${{ inputs.resume_after }}'"
echo "Check the crate name matches exactly from 'cargo workspaces plan'"
exit 1
fi
echo "Will publish the following crates:"
echo "$CRATES"
echo ""
echo "$CRATES" | while read crate; do
echo "Publishing $crate..."
cargo publish -p "$crate" --allow-dirty
echo "Waiting ${{ inputs.publish_interval }}s before next publish..."
sleep ${{ inputs.publish_interval }}
done
- name: Show package versions
run: cargo workspaces list --long
+86
View File
@@ -0,0 +1,86 @@
name: Publish crates to crates.io
on:
workflow_dispatch:
inputs:
publish_interval:
description: "Seconds to wait between publishes (600 for first publish, 60 after)"
required: false
default: "600"
type: string
backup_author:
description: "Second team member added as owner of the crate"
required: false
default: "jstuczyn"
type: string
jobs:
publish:
runs-on: arc-linux-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
cargo workspaces publish \
--publish-as-is \
--publish-interval ${{ inputs.publish_interval }}
- name: Show package versions
run: cargo workspaces list --long
- name: Add team as crate owners
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
TEAM="github:nymtech:core"
echo "Checking and adding $TEAM as owner to workspace crates..."
cargo workspaces list | while read crate; do
echo "Checking $crate..."
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM"; then
echo " $TEAM already owns $crate, skipping"
else
echo " Adding $TEAM as owner of $crate..."
cargo owner --add "$TEAM" "$crate"
sleep 2
fi
done
echo "Done!"
- name: Add secondary member as crate owner
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
TEAM_MEMBER="${{ inputs.backup_author }}"
echo "Checking and adding $TEAM_MEMBER as owner to workspace crates..."
cargo workspaces list | while read crate; do
echo "Checking $crate..."
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM_MEMBER"; then
echo " $TEAM_MEMBER already owns $crate, skipping"
else
echo " Adding $TEAM_MEMBER as owner of $crate..."
cargo owner --add "$TEAM_MEMBER" "$crate"
sleep 2
fi
done
echo "Done!"
@@ -0,0 +1,74 @@
name: Bump crate versions
on:
workflow_dispatch:
inputs:
version:
description: "Version to set (e.g. 1.21.0)"
required: true
type: string
env:
CI_BOT_AUTHOR: "Nym bot"
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
jobs:
version-bump:
runs-on: arc-linux-latest
permissions:
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Validate version format
run: |
if ! npx semver "${{ inputs.version }}"; then
echo "Error: '${{ inputs.version }}' is not valid semver"
exit 1
fi
- name: Get current version
id: current_version
run: |
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update workspace dependencies
run: |
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
- name: Bump versions
run: |
cargo workspaces version custom ${{ inputs.version }} \
--no-git-commit \
--yes
- name: Commit and push version bump
run: |
git add -A
git commit -m "crates release: bump version to ${{ inputs.version }}"
git push
- name: Show package versions
run: cargo workspaces list --long
+21
View File
@@ -0,0 +1,21 @@
name: ci-docs-linkcheck
on:
workflow_dispatch:
push:
paths:
- "documentation/docs/**"
- ".github/workflows/ci-docs-linkcheck.yml"
- "lychee.toml"
jobs:
linkcheck:
runs-on: arc-linux-latest
steps:
- uses: actions/checkout@v6
- name: Check links
uses: lycheeverse/lychee-action@v2
with:
args: ${{ github.workspace }}/documentation/docs/ --config ${{ github.workspace }}/lychee.toml --root-dir ${{ github.workspace }}/documentation/docs/pages/
fail: true
@@ -1,43 +0,0 @@
name: Publish to crates.io (dry run)
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.21.0)"
required: true
type: string
jobs:
publish-dry-run:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Bump versions (local only)
run: |
cargo workspaces version ${{ inputs.version }} \
--no-git-commit \
--no-git-tag \
--no-git-push \
--yes
# Note: Dry run may show cascading dependency errors because packages
# aren't actually uploaded. Check if the missing dependency has an
# "aborting upload due to dry run" message earlier in the output - if so,
# it would succeed in a real publish since cargo-workspaces publishes in
# dependency order. cargo-workspaces doesn't fail on err, so there isn't
# a good way to check this at the moment.
- name: Publish (dry run)
run: cargo workspaces publish --from-git --dry-run --allow-dirty
-47
View File
@@ -1,47 +0,0 @@
name: Publish to crates.io
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.21.0)"
required: true
type: string
jobs:
publish:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
# - name: Configure git
# run: |
# git config user.name "github-actions[bot]"
# git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump versions
run: |
cargo workspaces version ${{ inputs.version }} \
--no-git-push \
--no-git-tag \
--yes
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo workspaces publish --from-git --no-git-commit
# - name: Push version commit
# run: |
# git push origin HEAD
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -8,7 +8,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: ubuntu-latest
steps:
- name: Login to Harbor
uses: docker/login-action@v3
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1
uses: mikefarah/yq@v4.52.2
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -0,0 +1,41 @@
name: Resume publish to crates.io
on:
workflow_dispatch:
inputs:
resume_after:
description: "Last successfully published crate (will start from the next one)"
required: true
type: string
jobs:
publish:
runs-on: arc-linux-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Publish remaining crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
# Get crates in publish order, skip up to and including resume_after
cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2 | while read crate; do
echo "Publishing $crate..."
cargo publish -p "$crate" --allow-dirty
echo "Waiting 600s before next publish..."
sleep 600
done
- name: Show package versions
run: cargo workspaces list --long
-1
View File
@@ -67,7 +67,6 @@ nym-api/redocly/formatted-openapi.json
*.profraw
.beads
CLAUDE.md
docs
.claude
.superego
+156
View File
@@ -4,6 +4,162 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.4-quark] (2026-02-24)
- Enhance CI workflow with feature inputs ([#6462])
- Chore/revert 6433 ([#6445])
- Lp/stateless handshake ([#6437])
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/client/internal-dev ([#6435])
- build(deps-dev): bump webpack from 5.102.1 to 5.104.1 ([#6432])
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/mix-fetch/internal-dev ([#6431])
- build(deps-dev): bump webpack from 5.94.0 to 5.104.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6430])
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/zknym-lib/internal-dev ([#6429])
- build(deps-dev): bump webpack from 5.76.0 to 5.105.0 in /clients/native/examples/js-examples/websocket ([#6428])
- HTTP & DNS Improvements ([#6423])
- Endpoint for exit GW IPs ([#6418])
- build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts ([#6416])
- build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([#6415])
- build(deps): bump bytes from 1.11.0 to 1.11.1 ([#6414])
- build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 ([#6407])
- build(deps-dev): bump eslint from 8.57.1 to 9.26.0 ([#6405])
- Update reqwest to v0.13.1 ([#6401])
- build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs ([#6387])
- build(deps): bump next from 15.4.10 to 16.1.5 in /nym-node-status-api/nym-node-status-ui ([#6385])
- build(deps): bump lodash from 4.17.21 to 4.17.23 ([#6369])
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 ([#6360])
- build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/codegen/contract-clients ([#6359])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/packages/nodejs-client ([#6354])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs ([#6353])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /clients/native/examples/js-examples/websocket ([#6351])
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 in /documentation/docs ([#6350])
- build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs ([#6345])
- Max/crates publishing tweaks ([#6343])
- build(deps): bump h3 from 1.15.4 to 1.15.5 ([#6339])
- build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs ([#6332])
- build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs ([#6325])
- build(deps): bump rsa from 0.9.8 to 0.9.10 ([#6311])
- build(deps): bump qs and express in /wasm/mix-fetch/internal-dev ([#6308])
- build(deps): bump qs and express in /clients/native/examples/js-examples/websocket ([#6307])
- feat: introduce on-disk cache persistance for major nym-api caches ([#6302])
- Fix migrations in the Data Observatory ([#6271])
[#6462]: https://github.com/nymtech/nym/pull/6462
[#6445]: https://github.com/nymtech/nym/pull/6445
[#6437]: https://github.com/nymtech/nym/pull/6437
[#6435]: https://github.com/nymtech/nym/pull/6435
[#6432]: https://github.com/nymtech/nym/pull/6432
[#6431]: https://github.com/nymtech/nym/pull/6431
[#6430]: https://github.com/nymtech/nym/pull/6430
[#6429]: https://github.com/nymtech/nym/pull/6429
[#6428]: https://github.com/nymtech/nym/pull/6428
[#6423]: https://github.com/nymtech/nym/pull/6423
[#6418]: https://github.com/nymtech/nym/pull/6418
[#6416]: https://github.com/nymtech/nym/pull/6416
[#6415]: https://github.com/nymtech/nym/pull/6415
[#6414]: https://github.com/nymtech/nym/pull/6414
[#6407]: https://github.com/nymtech/nym/pull/6407
[#6405]: https://github.com/nymtech/nym/pull/6405
[#6401]: https://github.com/nymtech/nym/pull/6401
[#6387]: https://github.com/nymtech/nym/pull/6387
[#6385]: https://github.com/nymtech/nym/pull/6385
[#6369]: https://github.com/nymtech/nym/pull/6369
[#6360]: https://github.com/nymtech/nym/pull/6360
[#6359]: https://github.com/nymtech/nym/pull/6359
[#6354]: https://github.com/nymtech/nym/pull/6354
[#6353]: https://github.com/nymtech/nym/pull/6353
[#6351]: https://github.com/nymtech/nym/pull/6351
[#6350]: https://github.com/nymtech/nym/pull/6350
[#6345]: https://github.com/nymtech/nym/pull/6345
[#6343]: https://github.com/nymtech/nym/pull/6343
[#6339]: https://github.com/nymtech/nym/pull/6339
[#6332]: https://github.com/nymtech/nym/pull/6332
[#6325]: https://github.com/nymtech/nym/pull/6325
[#6311]: https://github.com/nymtech/nym/pull/6311
[#6308]: https://github.com/nymtech/nym/pull/6308
[#6307]: https://github.com/nymtech/nym/pull/6307
[#6302]: https://github.com/nymtech/nym/pull/6302
[#6271]: https://github.com/nymtech/nym/pull/6271
## [2026.3-parmigiano] (2026-02-10)
- chore: disable LP on parmigiano branch ([#6422])
- revert mixnet-based client fautly changes from LP ([#6420])
- [LP fix] Registration client with fallback ([#6419])
- Lp/ip pool fixes ([#6412])
- [LP-fix] expose wg psk for the vpn-client ([#6411])
- LP-fix : configurable LP timeouts ([#6409])
- LP-fix : add LP x25519 key to the description ([#6408])
- use rng that is Send ([#6404])
- use local kem key instead of local x25519 ([#6402])
- [LP Gateway Probe] CLI and behavior improvements ([#6400])
- lp: attempt to negotiate (and use) protocol version ([#6399])
- bugfix: use correct reserved bytes when parsing LpHeader ([#6398])
- Lp/bugfix/share ip allocation ([#6395])
- feat: use hex-encoding for lp key digests ([#6394])
- Add socks5 test to gateway-probe ([#6393])
- [LP Gateway probe] Improve file structure ([#6391])
- Reduce the size of `HttpClientError` ([#6390])
- Lp/two step dvpn reg ([#6386])
- Add extra configured nym api url to env ([#6382])
- Lp/dvpn psk injection ([#6378])
- LP: include signing key digests to LP responses ([#6373])
- Lp/use noise x25519 ([#6372])
- Topology fallback ([#6363])
- NS API socks5 support ([#6361])
- LP: modified LPRemotePeer to dynamically choose required KEM key hash ([#6358])
- Fix KKT Integration into LP ([#6357])
- LP: mixnet reg fixes ([#6356])
- LP: announced KEM key hashes ([#6349])
- revert faulty drop changes ([#6346])
- small qol changes ([#6340])
- Apply configured api urls via env ([#6337])
- lp chore: make sure to take reserved bytes straight from the header ([#6336])
- LP: x25519/ed22519 cleanup round ([#6335])
- Lp/encrypted kkt ([#6331])
- ensure packets with incompatible versions are rejected ([#6326])
- standarise lp serialisation: ([#6324])
- Upgrade to def_guard_wireguard v0.8.0 ([#6315])
- Max/crates io prep v2 ([#6270])
[#6422]: https://github.com/nymtech/nym/pull/6422
[#6420]: https://github.com/nymtech/nym/pull/6420
[#6419]: https://github.com/nymtech/nym/pull/6419
[#6412]: https://github.com/nymtech/nym/pull/6412
[#6411]: https://github.com/nymtech/nym/pull/6411
[#6409]: https://github.com/nymtech/nym/pull/6409
[#6408]: https://github.com/nymtech/nym/pull/6408
[#6404]: https://github.com/nymtech/nym/pull/6404
[#6402]: https://github.com/nymtech/nym/pull/6402
[#6400]: https://github.com/nymtech/nym/pull/6400
[#6399]: https://github.com/nymtech/nym/pull/6399
[#6398]: https://github.com/nymtech/nym/pull/6398
[#6395]: https://github.com/nymtech/nym/pull/6395
[#6394]: https://github.com/nymtech/nym/pull/6394
[#6393]: https://github.com/nymtech/nym/pull/6393
[#6391]: https://github.com/nymtech/nym/pull/6391
[#6390]: https://github.com/nymtech/nym/pull/6390
[#6386]: https://github.com/nymtech/nym/pull/6386
[#6382]: https://github.com/nymtech/nym/pull/6382
[#6378]: https://github.com/nymtech/nym/pull/6378
[#6373]: https://github.com/nymtech/nym/pull/6373
[#6372]: https://github.com/nymtech/nym/pull/6372
[#6363]: https://github.com/nymtech/nym/pull/6363
[#6361]: https://github.com/nymtech/nym/pull/6361
[#6358]: https://github.com/nymtech/nym/pull/6358
[#6357]: https://github.com/nymtech/nym/pull/6357
[#6356]: https://github.com/nymtech/nym/pull/6356
[#6349]: https://github.com/nymtech/nym/pull/6349
[#6346]: https://github.com/nymtech/nym/pull/6346
[#6340]: https://github.com/nymtech/nym/pull/6340
[#6337]: https://github.com/nymtech/nym/pull/6337
[#6336]: https://github.com/nymtech/nym/pull/6336
[#6335]: https://github.com/nymtech/nym/pull/6335
[#6331]: https://github.com/nymtech/nym/pull/6331
[#6326]: https://github.com/nymtech/nym/pull/6326
[#6324]: https://github.com/nymtech/nym/pull/6324
[#6315]: https://github.com/nymtech/nym/pull/6315
[#6270]: https://github.com/nymtech/nym/pull/6270
## [2026.2-oscypek] (2026-01-27)
- bugfix: downgrade gateway protocol to clients proposed version ([#6377])
Generated
+875 -688
View File
File diff suppressed because it is too large Load Diff
+105 -119
View File
@@ -173,9 +173,8 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
# "nym-gateway-probe",
"nym-gateway-probe",
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
"common/nym-lp-sandbox"
]
default-members = [
@@ -186,7 +185,6 @@ default-members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -207,7 +205,7 @@ edition = "2024"
license = "Apache-2.0"
rust-version = "1.85"
readme = "README.md"
version = "1.20.1"
version = "1.20.4"
[workspace.dependencies]
addr = "0.15.6"
@@ -234,7 +232,7 @@ blake3 = "1.7.0"
bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.10.1"
bytes = "1.11.1"
cargo_metadata = "0.19.2"
celes = "2.6.0"
cfg-if = "1.0.0"
@@ -305,6 +303,7 @@ ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
mime = "0.3.17"
mock_instant = "0.6.0"
moka = { version = "0.12", features = ["future"] }
nix = "0.30.1"
notify = "5.1.0"
@@ -321,13 +320,13 @@ publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand09 = { package = "rand", version = "0.9.2" }
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.12.15", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
rs_merkle = "1.5.0"
schemars = "0.8.22"
semver = "1.0.26"
@@ -383,7 +382,7 @@ url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "8.1"
utoipauto = "0.2"
uuid = "*"
uuid = "1.19.0"
vergen = { version = "=8.3.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
walkdir = "2"
@@ -392,119 +391,107 @@ zeroize = "1.7.0"
prometheus = { version = "0.14.0" }
# libcrux
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux" }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.1", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.1", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.1", path = "common/config" }
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.1", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.1", path = "common/nym-id" }
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.4", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.4", path = "common/config" }
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.4", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
nym-registration-common = { version = "1.20.1", path = "common/registration" }
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
nym-task = { version = "1.20.1", path = "common/task" }
nym-tun = { version = "1.20.1", path = "common/tun" }
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.1", path = "common/topology" }
nym-types = { version = "1.20.1", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.20.1", path = "common/verloc" }
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
nym-registration-common = { version = "1.20.4", path = "common/registration" }
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
nym-task = { version = "1.20.4", path = "common/task" }
nym-tun = { version = "1.20.4", path = "common/tun" }
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.4", path = "common/topology" }
nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.20.1", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
@@ -554,7 +541,6 @@ wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1"
web-sys = "0.3.76"
# for local development:
#[patch.crates-io]
#sphinx-packet = { path = "../sphinx" }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.69"
version = "1.1.71"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@
"license": "Apache-2.0",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"webpack": "^5.76.0",
"webpack": "^5.105.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.69"
version = "1.1.71"
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"
+1
View File
@@ -18,6 +18,7 @@ mod util;
mod version;
pub use error::Error;
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
pub use v6 as latest;
pub use version::AuthenticatorVersion;
@@ -7,6 +7,7 @@ use crate::traits::{
TopUpBandwidthResponse, UpgradeModeStatus,
};
use crate::{v2, v3, v4, v5, v6};
use nym_sphinx::addressing::Recipient;
#[derive(Debug)]
pub enum AuthenticatorResponse {
@@ -17,6 +18,17 @@ pub enum AuthenticatorResponse {
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
}
pub struct SerialisedResponse {
pub bytes: Vec<u8>,
pub reply_to: Option<Recipient>,
}
impl SerialisedResponse {
pub fn new(bytes: Vec<u8>, reply_to: Option<Recipient>) -> Self {
Self { bytes, reply_to }
}
}
impl UpgradeModeStatus for AuthenticatorResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
match self {
+32
View File
@@ -1,6 +1,38 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use std::net::{Ipv4Addr, Ipv6Addr};
pub fn authenticator_ipv6_to_ipv4(addr: Ipv6Addr) -> Ipv4Addr {
let before_last_byte = addr.octets()[14];
let last_byte = addr.octets()[15];
Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
)
}
pub fn authenticator_ipv4_to_ipv6(addr: Ipv4Addr) -> Ipv6Addr {
let before_last_byte = addr.octets()[2];
let last_byte = addr.octets()[3];
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
)
}
#[cfg(test)]
pub(crate) mod tests {
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData;
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -56,27 +56,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData;
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -54,27 +54,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
@@ -3,13 +3,12 @@
use crate::error::Error;
use crate::models::BandwidthClaim;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
@@ -20,13 +19,11 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
@@ -54,27 +51,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
+5 -1
View File
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
impl BandwidthTicketProvider for MockBandwidthController {
async fn get_ecash_ticket(
&self,
_ticket_type: TicketType,
ticket_type: TicketType,
_gateway_id: PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
@@ -100,6 +100,10 @@ impl BandwidthTicketProvider for MockBandwidthController {
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
.expect("Failed to deserialize test credential - this is a bug in the test harness");
// change the ticket type to the requested ticket
// note that verification outside mocks is going to fail
credential.payment.t_type = ticket_type.to_repr() as u8;
// Update spend_date to today to pass validation
credential.spend_date = OffsetDateTime::now_utc().date();
+19
View File
@@ -57,3 +57,22 @@ where
Ok(Some(token))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box<T> {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
(**self)
.get_ecash_ticket(ticket_type, gateway_id, tickets_to_spend)
.await
}
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ pub use opentelemetry;
pub use opentelemetry_jaeger;
#[cfg(feature = "tracing")]
pub use tracing_opentelemetry;
#[cfg(feature = "tracing")]
#[cfg(feature = "basic_tracing")]
pub use tracing_subscriber;
#[cfg(feature = "tracing")]
pub use tracing_tree;
@@ -26,7 +26,7 @@ use crate::{
error::ClientCoreError,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_client_core_gateways_storage as gateways_storage;
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
@@ -76,7 +76,7 @@ features = ["json"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
workspace = true
features = ["json", "rustls-tls"]
features = ["json", "rustls"]
[dev-dependencies]
anyhow = { workspace = true }
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
};
use nym_api_requests::models::{
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
};
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, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
NymNodeDescriptionV1, 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,
],
&params,
)
.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,
// ],
// &params,
// )
// .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 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)
}
// async fn (&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
// let mut page = 0;
// let mut descriptions = Vec::new();
//
// loop {
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
//
// descriptions.append(&mut res.data);
// if descriptions.len() < res.pagination.total {
// page += 1
// } else {
// break;
// }
// }
//
// Ok(descriptions)
// }
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nym_nodes(
@@ -14,7 +14,7 @@ pub struct Args {
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.get_all_cached_described_nodes_v2().await {
match client.get_all_cached_described_nodes().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_v2().await {
match client.get_all_cached_described_nodes().await {
Ok(res) => match args.identity_key {
Some(identity_key) => {
let node = res.iter().find(|node| {
+1 -1
View File
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
futures = { workspace = true }
humantime = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls"] }
reqwest = { workspace = true, features = ["rustls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true, features = ["derive"] }
@@ -22,6 +22,8 @@ pub mod ecash;
pub mod error;
pub mod upgrade_mode;
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
// Histogram buckets for ecash verification duration (in seconds)
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0];
@@ -111,6 +113,13 @@ impl CredentialVerifier {
}
pub async fn verify(&mut self) -> Result<i64> {
if self.ecash_verifier.is_mock() {
// if we're in the mock mode (local testing), skip cryptographic verification
// and just return a dummy bandwidth value since we don't have blockchain access
// Return a reasonable test bandwidth value (e.g., 1GB in bytes)
return Ok(MOCK_BANDWIDTH);
}
let start = Instant::now();
nym_metrics::inc!("ecash_verification_attempts");
@@ -291,3 +291,40 @@ struct UpgradeModeStateInner {
// (and dealing with the async consequences of that)
status: UpgradeModeStatus,
}
pub mod testing {
use crate::UpgradeModeState;
use crate::upgrade_mode::{
CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
};
use futures::channel::mpsc::UnboundedReceiver;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
pub fn mock_dummy_upgrade_mode_details() -> (UpgradeModeDetails, UnboundedReceiver<CheckRequest>)
{
let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded();
const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
];
pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey {
let private_key =
ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap();
private_key.public_key()
}
let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key());
let upgrade_mode_details = UpgradeModeDetails::new(
UpgradeModeCheckConfig {
// essentially we never want to trigger this in our tests
min_staleness_recheck: Duration::from_nanos(1),
},
UpgradeModeCheckRequestSender::new(um_recheck_tx),
upgrade_mode_state.clone(),
);
(upgrade_mode_details, um_recheck_rx)
}
}
+2 -3
View File
@@ -25,7 +25,6 @@ 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 }
serde_bytes = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
sha2 = { workspace = true, optional = true }
@@ -34,7 +33,7 @@ thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
# internal
nym-sphinx-types = { workspace = true }
nym-sphinx-types = { workspace = true, optional = true }
nym-pemstore = { workspace = true }
[dev-dependencies]
@@ -52,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"]
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
[lints]
workspace = true
-149
View File
@@ -109,152 +109,3 @@ impl DerivationMaterial {
}
}
}
pub mod blake3 {
//! Key Derivation Functions using Blake3.
use blake3::Hasher;
use rand09::{RngCore, rng};
use zeroize::Zeroize;
pub fn derive_key_blake3_multi_input(
info: &str,
input_key_material: &[&[u8]],
salt: &[u8],
) -> [u8; 32] {
let mut hasher = Hasher::new_derive_key(info);
for input_key in input_key_material {
hasher.update(input_key);
}
hasher.update(salt);
hasher.finalize().as_bytes().to_owned()
}
/// Derives a 32-byte key using Blake3's key derivation mode.
///
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
/// * `salt` - Additional salt for freshness (nonce)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
/// ```
pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
derive_key_blake3_multi_input(info, &[input_key_material], salt)
}
pub fn derive_fresh_key_blake3_multi_input(
info: &str,
input_key_material: &[&[u8]],
) -> [u8; 32] {
let mut salt = [0u8; 32];
rng().fill_bytes(&mut salt);
let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt);
// Zeroize salt
salt.zeroize();
derived_key
}
/// Derives a fresh 32-byte key using Blake3's key derivation mode.
/// The function calls a random number generator to generate a fresh salt.
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes());
/// ```
pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] {
derive_fresh_key_blake3_multi_input(info, &[input_key_material])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic_derivation() {
let context = "test-context";
let key_material = b"shared_secret_12345";
let salt = b"salt_67890";
let key1 = derive_key_blake3(context, key_material, salt);
let key2 = derive_key_blake3(context, key_material, salt);
assert_eq!(key1, key2, "Same inputs should produce same output");
}
#[test]
fn test_different_contexts_produce_different_keys() {
let key_material = b"shared_secret";
let salt = b"salt";
let key1 = derive_key_blake3("context1", key_material, salt);
let key2 = derive_key_blake3("context2", key_material, salt);
assert_ne!(
key1, key2,
"Different contexts should produce different keys"
);
}
#[test]
fn test_different_salts_produce_different_keys() {
let context = "test-context";
let key_material = b"shared_secret";
let key1 = derive_key_blake3(context, key_material, b"salt1");
let key2 = derive_key_blake3(context, key_material, b"salt2");
assert_ne!(key1, key2, "Different salts should produce different keys");
}
#[test]
fn test_different_key_material_produces_different_keys() {
let context = "test-context";
let salt = b"salt";
let key1 = derive_key_blake3(context, b"secret1", salt);
let key2 = derive_key_blake3(context, b"secret2", salt);
assert_ne!(
key1, key2,
"Different key material should produce different keys"
);
}
#[test]
fn test_output_length() {
let key = derive_key_blake3("test", b"key", b"salt");
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
}
#[test]
fn test_empty_inputs() {
// Should not panic with empty inputs
let key = derive_key_blake3("test", b"", b"");
assert_eq!(key.len(), 32);
}
}
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Key Derivation Functions using Blake3.
/// Derives a 32-byte key using Blake3's key derivation mode.
///
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
/// * `salt` - Additional salt for freshness (timestamp + nonce)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
/// ```
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
// Concatenate key_material and salt as input
let input = [key_material, salt].concat();
// Use Blake3's derive_key with context for domain separation
// blake3::derive_key returns [u8; 32] directly
blake3::derive_key(context, &input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic_derivation() {
let context = "test-context";
let key_material = b"shared_secret_12345";
let salt = b"salt_67890";
let key1 = derive_key_blake3(context, key_material, salt);
let key2 = derive_key_blake3(context, key_material, salt);
assert_eq!(key1, key2, "Same inputs should produce same output");
}
#[test]
fn test_different_contexts_produce_different_keys() {
let key_material = b"shared_secret";
let salt = b"salt";
let key1 = derive_key_blake3("context1", key_material, salt);
let key2 = derive_key_blake3("context2", key_material, salt);
assert_ne!(
key1, key2,
"Different contexts should produce different keys"
);
}
#[test]
fn test_different_salts_produce_different_keys() {
let context = "test-context";
let key_material = b"shared_secret";
let key1 = derive_key_blake3(context, key_material, b"salt1");
let key2 = derive_key_blake3(context, key_material, b"salt2");
assert_ne!(key1, key2, "Different salts should produce different keys");
}
#[test]
fn test_different_key_material_produces_different_keys() {
let context = "test-context";
let salt = b"salt";
let key1 = derive_key_blake3(context, b"secret1", salt);
let key2 = derive_key_blake3(context, b"secret2", salt);
assert_ne!(
key1, key2,
"Different key material should produce different keys"
);
}
#[test]
fn test_output_length() {
let key = derive_key_blake3("test", b"key", b"salt");
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
}
#[test]
fn test_empty_inputs() {
// Should not panic with empty inputs
let key = derive_key_blake3("test", b"", b"");
assert_eq!(key.len(), 32);
}
}
+2
View File
@@ -10,6 +10,8 @@ pub mod crypto_hash;
pub mod hkdf;
#[cfg(feature = "hashing")]
pub mod hmac;
#[cfg(feature = "hashing")]
pub mod kdf;
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
pub mod shared_key;
pub mod symmetric;
+1
View File
@@ -15,6 +15,7 @@ description = "Functions to interact with zknym signers, checking their status a
futures = { workspace = true }
thiserror = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
url = { workspace = true }
+8 -6
View File
@@ -3,14 +3,9 @@
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::QueryHttpRpcNyxdClient;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use std::collections::HashMap;
use url::Url;
pub use error::SignerCheckError;
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
@@ -18,6 +13,12 @@ use nym_validator_client::models::{
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, Epoch,
};
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use url::Url;
pub use error::SignerCheckError;
mod client_check;
pub mod error;
@@ -31,6 +32,7 @@ pub type TypedSignerResult = SignerResult<
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
#[derive(Serialize, Deserialize)]
pub struct SignersTestResult {
pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>,
+1 -1
View File
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
async-trait = { workspace = true }
bincode = { workspace = true }
cfg-if = { workspace = true}
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls"] }
http.workspace = true
url = { workspace = true }
once_cell = { workspace = true }
+215 -83
View File
@@ -46,7 +46,10 @@ use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
str::FromStr,
sync::{Arc, LazyLock},
sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering::Relaxed},
},
time::Duration,
};
@@ -70,14 +73,23 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
impl ClientBuilder {
/// Override the DNS resolver implementation used by the underlying http client.
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
self = self.non_shared();
// because of the call to non-shared this conditional should always run.
if let Some(rb) = self.reqwest_client_builder {
self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
}
self.use_secure_dns = false;
self
}
/// Override the DNS resolver implementation used by the underlying http client.
/// Override the DNS resolver implementation used by the underlying http client. If
/// [`Self::dns_resolver`] is called directly that will take priority over this, there is no
/// need to call both.
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
pub fn no_hickory_dns(mut self) -> Self {
self = self.non_shared();
self.use_secure_dns = false;
self
}
@@ -129,7 +141,8 @@ pub struct HickoryDnsResolver {
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
use_system: Arc<AtomicBool>,
system_resolver: Arc<OnceCell<TokioResolver>>,
static_base: Option<Arc<OnceCell<StaticResolver>>>,
use_shared: bool,
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
@@ -141,7 +154,8 @@ impl Default for HickoryDnsResolver {
fn default() -> Self {
Self {
state: Default::default(),
fallback: Default::default(),
use_system: Arc::new(AtomicBool::new(false)),
system_resolver: Default::default(),
static_base: Some(Default::default()),
use_shared: true,
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
@@ -151,16 +165,28 @@ impl Default for HickoryDnsResolver {
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let maybe_fallback = self.fallback.clone();
let maybe_static = self.static_base.clone();
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let use_shared = self.use_shared;
let resolver = if use_system {
match self
.system_resolver
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
{
Ok(r) => r.clone(),
Err(e) => return Box::pin(return_err(e)),
}
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
.clone()
};
let maybe_static = self.static_base.clone();
let overall_dns_timeout = self.overall_dns_timeout;
Box::pin(async move {
resolve(
name,
resolver,
maybe_fallback,
maybe_static,
use_shared,
overall_dns_timeout,
@@ -171,16 +197,17 @@ impl Resolve for HickoryDnsResolver {
}
}
async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
async fn resolve(
name: Name,
resolver: Arc<OnceCell<TokioResolver>>,
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
resolver: TokioResolver,
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
independent: bool,
overall_dns_timeout: Duration,
) -> Result<Addrs, ResolveError> {
let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent));
// try checking the static table to see if any of the addresses in the table have been
// looked up previously within the timeout to where we are not yet ready to try the
// default resolver yet again.
@@ -214,22 +241,6 @@ async fn resolve(
}
};
// If the primary resolver encountered an error, attempt a lookup using the fallback
// resolver if one is configured.
if let Some(ref fallback) = maybe_fallback {
let resolver =
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
let resolve_fut =
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
if let Ok(Ok(lookup)) = resolve_fut.await {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
return Ok(addrs);
}
}
// If no record has been found and a static map of fallback addresses is configured
// check the table for our entry
if let Some(ref static_resolver) = maybe_static {
@@ -258,6 +269,11 @@ impl Iterator for SocketAddrs {
}
impl HickoryDnsResolver {
/// Returns an instance of the shared resolver.
pub fn shared() -> Self {
SHARED_RESOLVER.clone()
}
/// Attempt to resolve a domain name to a set of ['IpAddr']s
pub async fn resolve_str(
&self,
@@ -265,10 +281,20 @@ impl HickoryDnsResolver {
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
let n =
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let resolver = if use_system {
self.system_resolver
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
.clone()
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
.clone()
};
resolve(
n,
self.state.clone(),
self.fallback.clone(),
resolver,
self.static_base.clone(),
self.use_shared,
self.overall_dns_timeout,
@@ -298,13 +324,11 @@ impl HickoryDnsResolver {
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
if !use_shared {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.unwrap()
.system_resolver
.get_or_try_init(new_resolver_system)?
.clone())
}
@@ -320,45 +344,80 @@ impl HickoryDnsResolver {
}
}
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?;
/// Swap the primary internal resolver to the system resolver rather than the
/// configured custom resolver.
pub fn use_system_resolver(&self) {
self.use_system.store(true, Relaxed);
// IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
// if self.use_shared {
// SHARED_RESOLVER.enable_system_fallback()?;
// }
Ok(())
if self.use_shared {
SHARED_RESOLVER.use_system_resolver();
}
}
/// Disable fallback resolution. If the primary resolver fails the error is
/// returned immediately
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
/// Swap the primary internal resolver to the configured custom resolver rather than the
/// system resolver.
pub fn use_configured_resolver(&self) {
self.use_system.store(false, Relaxed);
// // IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
// if self.use_shared {
// SHARED_RESOLVER.fallback = None;
// }
if self.use_shared {
SHARED_RESOLVER.use_configured_resolver();
}
}
/// Get the current map of hostname to address in use by the fallback static lookup if one
/// Clear entries from the static table that would return entries during the pre-resolve stage.
/// This means that all lookups will attempt to use the network resolver again before the static
/// table is consulted.
///
/// Entries elevated to pre-resolve from fallback (added from default or using
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
pub fn clear_preresolve(&self) {
debug!("clearing pre-resolve table");
if let Some(cell) = &self.static_base
&& let Some(static_base) = cell.get()
{
static_base.clear_preresolve()
}
}
/// Get the current map of hostnames to addresses used in the fallback static lookup stage if one
/// exists.
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
Some(self.static_base.as_ref()?.get()?.get_addrs())
Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
}
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
let cell = OnceCell::new();
cell.set(StaticResolver::new(addrs))
.expect("infallible assign");
self.static_base = Some(Arc::new(cell));
pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
debug!("setting fallback entries for {:?}", addrs.keys());
if self.static_base.is_none() {
let cell = OnceCell::new();
self.static_base = Some(Arc::new(cell));
}
self.static_base
.as_ref()
.unwrap()
.get_or_init(|| Self::new_static_fallback(self.use_shared))
.set_fallback(addrs);
}
/// Get the current map of hostnames to addresses used in the preresolve static lookup stage
/// if one exists.
pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
}
/// Set (or overwrite) the map of addresses used in the preresolve static hostname lookup
pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
debug!("setting pre-resolve entries for {:?}", addrs.keys());
if self.static_base.is_none() {
let cell = OnceCell::new();
self.static_base = Some(Arc::new(cell));
}
self.static_base
.as_ref()
.unwrap()
.get_or_init(|| Self::new_static_fallback(self.use_shared))
.set_preresolve(addrs);
}
/// Successfully resolved addresses are cached for a minimum of 30 minutes
@@ -495,7 +554,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
}
fn new_default_static_fallback() -> StaticResolver {
StaticResolver::new(constants::default_static_addrs())
StaticResolver::new().with_fallback(constants::default_static_addrs())
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -532,10 +591,7 @@ mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::Instant,
};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -552,7 +608,7 @@ mod test {
let var_name = HickoryDnsResolver::default();
let resolver = var_name;
let client = reqwest::ClientBuilder::new()
.dns_resolver(resolver.into())
.dns_resolver(resolver)
.build()
.unwrap();
@@ -597,7 +653,7 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
resolver.set_static_fallbacks(addr_map);
resolver.set_fallback_addrs(addr_map);
let mut addrs = resolver.resolve_str(example_domain).await?;
assert!(addrs.contains(&example_ip4));
@@ -738,18 +794,19 @@ mod test {
}
#[tokio::test]
#[ignore]
// this test is dependent of external network setup -- i.e. blocking all traffic to the default
// resolvers. Otherwise the default resolvers will succeed without using the static fallback,
// making the test pointless
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// This test impacts the state of the shared resolver and as such is disabled to avoid
// interference with other tests.
//
// this test is dependent of external network setup -- i.e. blocking all traffic to the
// default resolvers. Otherwise the default resolvers will succeed without using the static
// fallback, making the test pointless
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
let time_start = Instant::now();
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
let resolver1 = HickoryDnsResolver::shared();
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver::default();
let time_start = std::time::Instant::now();
// create a new resolver that uses the shared resolver
let resolver = HickoryDnsResolver::shared();
// successful lookup using fallback to static resolver
let domain = "rpc.nymtech.net";
@@ -758,9 +815,27 @@ mod test {
.await
.expect("failed to resolve address in static lookup");
println!(
"{}ms resolved {domain}",
(Instant::now() - time_start).as_millis()
let lookup_dur = Instant::now() - time_start;
assert!(
lookup_dur > resolver.overall_dns_timeout,
"expected lookup timeout - took {}ms",
(lookup_dur).as_millis()
);
let time_start = std::time::Instant::now();
// successful lookup using pre-resolve entry promoted from fallback
let domain = "rpc.nymtech.net";
let _ = resolver1
.resolve_str(domain)
.await
.expect("domain expected to be in pre-resolve");
// this lookup should basically be instant as we are using pre-resolve
let lookup_dur = std::time::Instant::now() - time_start;
assert!(
lookup_dur < Duration::from_millis(10),
"expected instant - took {}ms",
(lookup_dur).as_millis()
);
// unsuccessful lookup - primary times out, and not in static table
@@ -771,5 +846,62 @@ mod test {
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
Ok(())
}
#[tokio::test]
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// This test impacts the state of the shared resolver and as such is disabled to avoid
// interference with other tests.
async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
let resolver1 = HickoryDnsResolver::shared();
// create a new resolver that uses the shared resolver
let mut resolver = HickoryDnsResolver::shared();
let example_domains = [
String::from("static1.nymvpn.com"),
String::from("static2.nymvpn.com"),
];
let mut addr_map1 = HashMap::new();
addr_map1.insert(
example_domains[0].clone(),
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
);
addr_map1.insert(
example_domains[1].clone(),
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
);
resolver.set_static_preresolve(addr_map1);
let time_start = std::time::Instant::now();
// successful lookup using pre-resolve entry promoted from fallback
let _ = resolver1
.resolve_str(&example_domains[0])
.await
.expect("domain expected to be in pre-resolve");
// this lookup should basically be instant as we are using pre-resolve
let lookup_dur = std::time::Instant::now() - time_start;
assert!(
lookup_dur < Duration::from_millis(10),
"expected instant - took {}ms",
(lookup_dur).as_millis()
);
// After clearing the pre-resolve in one instance of the shared resolver ...
resolver.clear_preresolve();
// ... other instances have their pre-resolve entries cleared.
let prereslve_lookup = resolver1
.static_base
.as_ref()
.unwrap()
.get()
.unwrap()
.pre_resolve(&example_domains[0]);
assert!(prereslve_lookup.is_none());
Ok(())
}
}
}
@@ -51,6 +51,9 @@ pub const VERCEL_COM_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
];
pub const NYM_API_CDN: &str = "cdn1.media-platform.net";
pub const NYM_API_CDN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(172, 104, 178, 252))];
pub const NYM_COM_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
@@ -69,6 +72,12 @@ pub const NYM_RPC_IPS: &[IpAddr] = &[
)),
];
#[allow(unused)]
pub fn empty_static_addrs() -> HashMap<String, Vec<IpAddr>> {
HashMap::new()
}
#[allow(unused)]
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
let mut m = HashMap::new();
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
@@ -88,6 +97,7 @@ pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
m.insert(NYM_API_CDN.to_string(), NYM_API_CDN_IPS.to_vec());
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
+279 -52
View File
@@ -14,42 +14,78 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
#[derive(Debug, Default, Clone)]
pub struct StaticResolver {
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
pre_resolve_timeout: Option<Duration>,
}
#[derive(Debug, Clone, Default)]
enum PreResolveStatus {
#[default]
Valid,
ValidUntil(Instant),
}
#[derive(Debug, Clone, Default)]
struct Entry {
valid_for_pre_resolve_until: Option<Instant>,
status: PreResolveStatus,
addrs: Vec<IpAddr>,
}
impl Entry {
fn new(addrs: Vec<IpAddr>) -> Self {
Self {
valid_for_pre_resolve_until: None,
status: PreResolveStatus::Valid,
addrs,
}
}
fn new_timeout(addrs: Vec<IpAddr>, timeout: Duration) -> Self {
Self {
status: PreResolveStatus::ValidUntil(Instant::now() + timeout),
addrs,
}
}
fn is_valid(&self) -> bool {
match self.status {
PreResolveStatus::Valid => true,
PreResolveStatus::ValidUntil(t) => t > Instant::now(),
}
}
}
impl StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
debug!("building static resolver");
let static_entries = static_entries
.into_iter()
.map(|(name, ips)| (name, Entry::new(ips)))
.collect();
pub fn new() -> StaticResolver {
Self {
static_addr_map: Arc::new(Mutex::new(static_entries)),
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
}
}
/// Return the full set of domain names and associated addresses stored in this static lookup table
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
/// Initialize the contents of the pre-resolve table for this instance of the static resolver
#[allow(unused)]
pub fn with_preresolve(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
let entries = entries
.into_iter()
.map(|(name, ips)| (name, Entry::new(ips)))
.collect();
self.preresolve_addr_map = Arc::new(Mutex::new(entries));
self
}
/// Initialize the contenes of the fallback table for this instance of the static resolver
pub fn with_fallback(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
self.fallback_addr_map = Arc::new(Mutex::new(entries));
self
}
/// Return the set of domain names and associated addresses stored in the pre-resolve static
/// lookup table
pub fn get_preresolve_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
let mut out = HashMap::new();
self.static_addr_map
self.preresolve_addr_map
.lock()
.unwrap()
.iter()
@@ -59,6 +95,38 @@ impl StaticResolver {
out
}
/// Return the set of domain names and associated addresses stored in the fallback static lookup
/// table
pub fn get_fallback_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
self.fallback_addr_map.lock().unwrap().clone()
}
/// Set (or overwrite) the map of static addresses to be returned only after attempting a lookup
/// over the network resolver.
pub fn set_fallback(&self, addrs: HashMap<String, Vec<IpAddr>>) {
self.fallback_addr_map.lock().unwrap().extend(addrs);
}
/// Clear entries from the static table that would return entries during the pre-resolve stage.
/// This means that all lookups will attempt to use the network resolver again before the static
/// table is consulted.
///
/// Entries elevated to pre-resolve from fallback (added from default or using
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
/// pre-resolve (using [`Self::preresolve_to_addrs`]) will be removed.
pub fn clear_preresolve(&self) {
*self.preresolve_addr_map.lock().unwrap() = HashMap::new();
}
/// Set (or overwrite) the map of static addresses and mark these domains to be returned
/// WITHOUT attempting a lookup over the network resolver.
pub fn set_preresolve(&self, addrs: HashMap<String, Vec<IpAddr>>) {
let mut current_map = self.preresolve_addr_map.lock().unwrap();
for (domain, ips) in addrs.into_iter() {
_ = current_map.insert(domain, Entry::new(ips))
}
}
/// Change the timeout for which domains can be pre-resolved after they are looked up in the
/// static lookup table.
#[allow(unused)]
@@ -71,44 +139,58 @@ impl StaticResolver {
/// recently (within the configured timeout) looked it up previously in this static table using
/// a regular resolve.
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
debug!("found {name:?} in pre-resolve static table resolver");
self.pre_resolve_timeout?;
self.static_addr_map
self.preresolve_addr_map
.lock()
.unwrap()
.get(name)
.filter(|e| {
e.valid_for_pre_resolve_until
.is_some_and(|t| t > Instant::now())
.filter(|entry| entry.is_valid())
.map(|entry| {
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
entry.addrs.clone()
})
.map(|e| e.addrs.clone())
}
#[allow(unused)]
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
Self::resolve_inner(
self.static_addr_map.lock().unwrap(),
self.fallback_addr_map.lock().unwrap(),
self.preresolve_addr_map.lock().unwrap(),
name,
self.pre_resolve_timeout,
)
.map(|e| e.addrs)
}
fn resolve_inner(
mut table: MutexGuard<'_, HashMap<String, Entry>>,
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
name: &str,
timeout: Option<Duration>,
) -> Option<Entry> {
let resolved = table.get_mut(name)?;
pre_resolve_cache_timeout: Option<Duration>,
) -> Option<Vec<IpAddr>> {
let resolved = fallback_table.get(name)?;
debug!("found {name:?} in static table resolver");
debug!("lookup hit for \"{name:?}\" in static table resolver");
if let Some(pre_resolve_timeout) = timeout {
// We had to look this entry up and a pre-resolve duration is defined, so it will
// trigger in pre-resolve lookups for the next _timeout_ window.
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout);
// We had to look this entry up and a pre-resolve duration is defined, so it will
// trigger in pre-resolve lookups for the next _timeout_ window if it wasn't already
// triggering.
if let Some(pre_resolve_timeout) = pre_resolve_cache_timeout {
match preresolve_table.get_mut(name) {
None => {
_ = preresolve_table.insert(
name.to_string(),
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
);
}
// Not sure how we would get cases where this is Some( ) -- it requires having a
// Valid entry in the preresolve table and still doing a lookup against fallback.
Some(entry) if matches!(entry.status, PreResolveStatus::ValidUntil(_)) => {
_ = preresolve_table.insert(
name.to_string(),
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
);
}
_ => {}
}
}
Some(resolved.clone())
}
@@ -117,13 +199,23 @@ impl StaticResolver {
impl Resolve for StaticResolver {
fn resolve(&self, name: Name) -> Resolving {
debug!("looking up {name:?} in static resolver");
let addr_map = self.static_addr_map.clone();
// these should clone arcs, not the actual tables
let fallback_addr_map = self.fallback_addr_map.clone();
let presesolve_addr_map = self.preresolve_addr_map.clone();
let timeout = self.pre_resolve_timeout;
// Also the returned future doesn't try to take the lock on the tables until the
// future is awaited, so no blocking issues.
Box::pin(async move {
let addr_map = addr_map.lock().unwrap();
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
let fallback_addr_map = fallback_addr_map.lock().unwrap();
let presesolve_addr_map = presesolve_addr_map.lock().unwrap();
let lookup = match Self::resolve_inner(
fallback_addr_map,
presesolve_addr_map,
name.as_str(),
timeout,
) {
None => return Err(ResolveError::StaticLookupMiss.into()),
Some(entry) => entry.addrs,
Some(addrs) => addrs,
};
let addrs: Addrs = Box::new(
lookup
@@ -142,6 +234,7 @@ mod test {
use super::*;
use std::error::Error as StdError;
use std::net::Ipv4Addr;
use std::str::FromStr;
#[tokio::test]
@@ -149,7 +242,7 @@ mod test {
let example_domain = String::from("static.nymvpn.com");
// lookup for domain for which there is no entry
let resolver = StaticResolver::new(HashMap::new());
let resolver = StaticResolver::new();
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let result = resolver.resolve(url).await;
@@ -166,7 +259,7 @@ mod test {
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let resolver = StaticResolver::new(addr_map);
let resolver = StaticResolver::new().with_fallback(addr_map);
let mut addrs = resolver.resolve(url).await?;
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
@@ -175,7 +268,7 @@ mod test {
}
#[test]
fn static_lookup_pre_resolve() {
fn elevate_fallback_to_pre_resolve() {
let example_duration = Duration::from_secs(3);
let example_domain = String::from("static.nymvpn.com");
let mut addr_map = HashMap::new();
@@ -183,24 +276,23 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration);
let resolver = StaticResolver::new()
.with_fallback(addr_map)
.with_pre_resolve_timeout(example_duration);
// ensure that attempting to pre-resolve without first resolving returns none
let result = resolver.pre_resolve(&example_domain);
assert!(result.is_none());
// resolving should now update the pre-resolve validity timeout for the entry
let entry = StaticResolver::resolve_inner(
resolver.static_addr_map.lock().unwrap(),
&example_domain,
Some(example_duration),
)
.expect("missing entry???!!!!");
assert!(
entry
.valid_for_pre_resolve_until
.is_some_and(|t| t < Instant::now() + example_duration)
);
let _addrs = resolver
.resolve_str(&example_domain)
.expect("entry should exist");
assert!(matches!(
resolver.preresolve_status(&example_domain),
Some(PreResolveStatus::ValidUntil(t))
if t < Instant::now() + example_duration
));
// check that pre-resolve now returns the expected record
let addrs = resolver
@@ -214,4 +306,139 @@ mod test {
let result = resolver.pre_resolve(&example_domain);
assert!(result.is_none());
}
#[test]
fn set_and_use_preresolve() {
let example_duration = Duration::from_secs(3);
let example_domains = [
String::from("static1.nymvpn.com"),
String::from("static2.nymvpn.com"),
String::from("preresolve.nymvpn.com"),
];
let mut addr_map1 = HashMap::new();
addr_map1.insert(
example_domains[0].clone(),
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
);
addr_map1.insert(
example_domains[1].clone(),
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
);
let mut addr_map2 = HashMap::new();
addr_map2.insert(
example_domains[1].clone(),
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
);
addr_map2.insert(
example_domains[2].clone(),
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
);
let resolver = StaticResolver::new()
.with_fallback(addr_map1)
.with_pre_resolve_timeout(example_duration);
// Attempting to pre-resolve without setting the table returns none
let result = resolver.pre_resolve(&example_domains[0]);
assert!(result.is_none());
resolver.set_preresolve(addr_map2);
// After setting the pre-resolve, addresses in the the table are returned
let result = resolver.pre_resolve(&example_domains[1]);
assert!(result.is_some());
// If the domain wasn't in the pre-resolve table it returns none.
let result = resolver.pre_resolve(&example_domains[0]);
assert!(result.is_none());
resolver.clear_preresolve();
}
#[test]
fn preresolve_with_fallback() {
let example_duration = Duration::from_secs(3);
let example_domains = [
String::from("static1.nymvpn.com"),
String::from("static2.nymvpn.com"),
String::from("preresolve.nymvpn.com"),
];
let mut addr_map1 = HashMap::new();
addr_map1.insert(
example_domains[0].clone(),
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
);
addr_map1.insert(
example_domains[1].clone(),
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
);
let mut addr_map2 = HashMap::new();
addr_map2.insert(
example_domains[1].clone(),
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
);
addr_map2.insert(
example_domains[2].clone(),
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
);
let resolver = StaticResolver::new()
.with_fallback(addr_map1)
.with_preresolve(addr_map2)
.with_pre_resolve_timeout(example_duration);
// when using both pre-resolve and fallback elevating entries from fallback to pre-resolve
// leaves the entries as `Valid`.
assert!(matches!(
resolver.preresolve_status(&example_domains[1]),
Some(PreResolveStatus::Valid)
));
let _addrs = resolver
.resolve_str(&example_domains[1])
.expect("entry should exist");
assert!(matches!(
resolver.preresolve_status(&example_domains[1]),
Some(PreResolveStatus::Valid)
));
// entries not already in pre-resolve get elevated with a timeout.
assert!(!resolver.preresolve_contains(&example_domains[0]));
let _addrs = resolver
.resolve_str(&example_domains[0])
.expect("entry should exist");
assert!(resolver.preresolve_contains(&example_domains[0]));
assert!(matches!(
resolver.preresolve_status(&example_domains[0]),
Some(PreResolveStatus::ValidUntil(_))
));
// clearing the pre-resolve table doesn't impact the fallback table.
resolver.clear_preresolve();
assert!(!resolver.preresolve_contains(&example_domains[0]));
assert!(!resolver.preresolve_contains(&example_domains[1]));
assert!(!resolver.preresolve_contains(&example_domains[2]));
assert!(!resolver.fallback_contains(&example_domains[0]));
assert!(!resolver.fallback_contains(&example_domains[1]));
}
/// convenience functions for testing
impl StaticResolver {
fn preresolve_status(&self, name: &str) -> Option<PreResolveStatus> {
self.preresolve_addr_map
.lock()
.unwrap()
.get(name)
.map(|e| e.status.clone())
}
fn preresolve_contains(&self, name: &str) -> bool {
self.preresolve_addr_map.lock().unwrap().contains_key(name)
}
fn fallback_contains(&self, name: &str) -> bool {
self.preresolve_addr_map.lock().unwrap().contains_key(name)
}
}
}
+217 -16
View File
@@ -3,15 +3,21 @@
//! Utilities for and implementation of request tunneling
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{
Arc, LazyLock, RwLock,
atomic::{AtomicBool, Ordering},
};
use tracing::warn;
use crate::ClientBuilder;
use crate::{Client, ClientBuilder};
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
// #[cfg(feature = "tunneling")]
#[derive(Debug)]
pub(crate) struct Front {
pub(crate) policy: FrontPolicy,
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
enabled: AtomicBool,
}
@@ -19,7 +25,7 @@ impl Clone for Front {
fn clone(&self) -> Self {
Self {
policy: self.policy.clone(),
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
enabled: AtomicBool::new(false),
}
}
}
@@ -27,13 +33,30 @@ impl Clone for Front {
impl Front {
pub(crate) fn new(policy: FrontPolicy) -> Self {
Self {
enabled: AtomicBool::new(policy == FrontPolicy::Always),
enabled: AtomicBool::new(false),
policy: Arc::new(RwLock::new(policy)),
}
}
pub(crate) fn off() -> Self {
Self::new(FrontPolicy::Off)
}
pub(crate) fn shared() -> Self {
let policy = SHARED_FRONTING_POLICY.clone();
Self {
enabled: AtomicBool::new(false),
policy,
}
}
pub(crate) fn set_policy(&self, policy: FrontPolicy) {
*self.policy.write().unwrap() = policy;
self.enabled.store(false, Ordering::Relaxed);
}
pub(crate) fn is_enabled(&self) -> bool {
match self.policy {
match *self.policy.read().unwrap() {
FrontPolicy::Off => false,
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
FrontPolicy::Always => true,
@@ -46,14 +69,13 @@ impl Front {
if self.is_enabled() {
return;
}
if matches!(self.policy, FrontPolicy::OnRetry) {
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
self.enabled.store(true, Ordering::Relaxed);
}
}
}
#[derive(Debug, Default, PartialEq, Clone)]
#[cfg(feature = "tunneling")]
/// Policy for when to use domain fronting for HTTP requests.
pub enum FrontPolicy {
/// Always use domain fronting for all requests.
@@ -66,29 +88,208 @@ pub enum FrontPolicy {
}
impl ClientBuilder {
/// Enable and configure request tunneling for API requests.
#[cfg(feature = "tunneling")]
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
let front = Front::new(policy);
/// Enable and configure request tunneling for API requests. If no front policy is
/// provided the shared fronting policy will be used.
pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
let front = if let Some(p) = policy {
Front::new(p)
} else {
Front::shared()
};
// Check if any of the supplied urls even support fronting
if !self.urls.iter().any(|url| url.has_front()) {
warn!(
"fronting is enabled, but none of the supplied urls have configured fronting domains"
"fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
self.urls
);
}
self.front = Some(front);
self.front = front;
self
}
}
impl Client {
/// Set the policy for enabling fronting. If fronting was previously unset this will set it, and
/// make it possible to enable (i.e [`FrontPolicy::Off`] will not enable it).
///
/// Calling this function sets a custom policy for this client, disconnecting it from the shared
/// fronting policy -- i.e. changes applied through [`Client::set_shared_front_policy`] will not
/// be impact this client.
pub fn set_front_policy(&mut self, policy: FrontPolicy) {
self.front.set_policy(policy)
}
/// Set the fronting policy for this client to follow the shared policy.
pub fn use_shared_front_policy(&mut self) {
self.front = Front::shared();
}
/// Set the fronting policy for all clients using the shared policy.
//
// NOTE: this does not reset the per-instance enabled flag like it will when using
// [`Front::set_front_policy`]. So if a client is using shared policy with the `OnRetry` policy
// and this function is used to swap that policy away from and then back to `OnRetry` the
// fronting will still be enabled. Noting this here just in case this triggers any corner cases
// down the road.
pub fn set_shared_front_policy(policy: FrontPolicy) {
*SHARED_FRONTING_POLICY.write().unwrap() = policy;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ApiClientCore, NO_PARAMS, Url};
impl Front {
pub(crate) fn policy(&self) -> FrontPolicy {
self.policy.read().unwrap().clone()
}
}
/// Policy can be set for an independent client and the update is applied properly
#[test]
fn set_policy_independent_client() {
let url1 = Url::new(
"https://validator.global.ssl.fastly.net",
Some(vec!["https://yelp.global.ssl.fastly.net"]),
)
.unwrap();
let mut client1 = ClientBuilder::new(url1.clone())
.unwrap()
.with_fronting(Some(FrontPolicy::Off))
.build()
.unwrap();
assert!(client1.front.policy() == FrontPolicy::Off);
let client2 = ClientBuilder::new(url1.clone())
.unwrap()
.with_fronting(Some(FrontPolicy::OnRetry))
.build()
.unwrap();
// Ensure that setting the policy for a client it gets properly applied.
client1.set_front_policy(FrontPolicy::Always);
assert!(client1.front.policy() == FrontPolicy::Always);
// ensure that setting the policy in a client NOT using the shared policy does NOT update
// the policy used by another client.
assert!(client2.front.policy() == FrontPolicy::OnRetry);
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
// requests
let req = client1
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
.unwrap()
.build()
.unwrap();
let expected_host = url1.host_str().unwrap();
assert!(
req.headers()
.get(reqwest::header::HOST)
.is_some_and(|h| h.to_str().unwrap() == expected_host),
"{:?} != {:?}",
expected_host,
req,
);
let expected_front = url1.front_str().unwrap();
assert!(
req.url()
.host()
.is_some_and(|url| url.to_string() == expected_front),
"{:?} != {:?}",
expected_front,
req,
);
}
/// Policy can be set for the shared client and the update is applied properly
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH THE SHARED POLICY AND AS SUCH CAN HAVE
// AN IMPACT ON OTHER TESTS
#[test]
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
fn set_policy_shared_client() {
let url1 = Url::new(
"https://validator.global.ssl.fastly.net",
Some(vec!["https://yelp.global.ssl.fastly.net"]),
)
.unwrap();
Client::set_shared_front_policy(FrontPolicy::Off);
assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
let client1 = ClientBuilder::new(url1.clone())
.unwrap()
.with_fronting(None)
.build()
.unwrap();
assert!(client1.front.policy() == FrontPolicy::Off);
let mut client2 = ClientBuilder::new(url1.clone())
.unwrap()
.with_fronting(Some(FrontPolicy::Off))
.build()
.unwrap();
// Ensure that setting the shared policy gets properly applied
Client::set_shared_front_policy(FrontPolicy::Always);
assert!(client1.front.policy() == FrontPolicy::Always);
// Setting the shared policy should NOT update clients NOT using the shared policy.
assert!(client2.front.policy() == FrontPolicy::Off);
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
// requests
let req = client1
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
.unwrap()
.build()
.unwrap();
let expected_host = url1.host_str().unwrap();
assert!(
req.headers()
.get(reqwest::header::HOST)
.is_some_and(|h| h.to_str().unwrap() == expected_host),
"{:?} != {:?}",
expected_host,
req,
);
let expected_front = url1.front_str().unwrap();
assert!(
req.url()
.host()
.is_some_and(|url| url.to_string() == expected_front),
"{:?} != {:?}",
expected_front,
req,
);
// ensure that setting to the shared policy works
client2.use_shared_front_policy();
assert!(client2.front.policy() == FrontPolicy::Always);
// ensure that if the policy is OnRetry then the `enabled` fields are still independent,
// despite the policy being shared.
Client::set_shared_front_policy(FrontPolicy::OnRetry);
assert!(client1.front.policy() == FrontPolicy::OnRetry);
assert!(client2.front.policy() == FrontPolicy::OnRetry);
assert!(!client1.front.is_enabled());
assert!(!client2.front.is_enabled());
client1.front.retry_enable();
assert!(client1.front.is_enabled());
assert!(!client2.front.is_enabled());
}
#[tokio::test]
async fn nym_api_works() {
let url1 = Url::new(
@@ -104,7 +305,7 @@ mod tests {
let client = ClientBuilder::new(url1)
.expect("bad url")
.with_fronting(FrontPolicy::Always)
.with_fronting(Some(FrontPolicy::Always))
.build()
.expect("failed to build client");
@@ -140,7 +341,7 @@ mod tests {
let client = ClientBuilder::new_with_urls(vec![url1, url2])
.expect("bad url")
.with_fronting(FrontPolicy::Always)
.with_fronting(Some(FrontPolicy::Always))
.build()
.expect("failed to build client");
+191 -102
View File
@@ -136,6 +136,7 @@
//! ```
#![warn(missing_docs)]
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
@@ -147,6 +148,7 @@ pub mod registry;
use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use itertools::Itertools;
@@ -161,9 +163,7 @@ use std::time::Duration;
use thiserror::Error;
use tracing::{debug, instrument, warn};
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
#[cfg(feature = "tunneling")]
mod fronted;
@@ -195,6 +195,8 @@ use nym_http_api_client_macro::client_defaults;
/// high and chatty protocols take a while to complete.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
#[cfg(not(target_arch = "wasm32"))]
client_defaults!(
priority = -100;
@@ -206,6 +208,24 @@ client_defaults!(
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
);
static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
tracing::info!("Initializing shared HTTP client");
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
reqwest::ClientBuilder::new().build()
.expect("failed to initialize shared http client")
} else {
let mut builder = default_builder();
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
builder
.build()
.expect("failed to initialize shared http client")
}
}
});
/// Collection of URL Path Segments
pub type PathSegments<'a> = &'a [&'a str];
/// Collection of HTTP Request Parameters
@@ -327,16 +347,22 @@ pub enum HttpClientError {
source: reqwest::Error,
},
#[error("failed to parse header value: {source}")]
InvalidHeaderValue {
#[source]
source: http::Error,
},
#[error("failed to send request for {url}: {source}")]
RequestSendFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
#[source]
source: ReqwestErrorWrapper,
},
#[error("failed to read response body from {url}: {source}")]
ResponseReadFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
headers: Box<HeaderMap>,
status: StatusCode,
#[source]
@@ -353,7 +379,7 @@ pub enum HttpClientError {
},
#[error("the requested resource could not be found at {url}")]
NotFound { url: reqwest::Url },
NotFound { url: Box<reqwest::Url> },
#[error("attempted to use domain fronting and clone a request containing stream data")]
AttemptedToCloneStreamRequest,
@@ -365,7 +391,7 @@ pub enum HttpClientError {
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
)]
RequestFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -374,7 +400,7 @@ pub enum HttpClientError {
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
)]
EmptyResponse {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -383,7 +409,7 @@ pub enum HttpClientError {
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
)]
EndpointFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
error: String,
@@ -453,7 +479,7 @@ impl HttpClientError {
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
HttpClientError::RequestSendFailure {
url,
url: Box::new(url),
source: ReqwestErrorWrapper(source),
}
}
@@ -554,6 +580,19 @@ pub trait ApiClientCore {
let req = self.create_request(method, path, params, json_body)?;
self.send(req).await
}
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
///
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
/// multiple times.
fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
/// If the fronting policy for the client is set to `OnRetry` this function will enable the
/// fronting if not already enabled.
#[cfg(feature = "tunneling")]
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
}
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
@@ -562,16 +601,18 @@ pub struct ClientBuilder {
urls: Vec<Url>,
timeout: Option<Duration>,
custom_user_agent: bool,
reqwest_client_builder: reqwest::ClientBuilder,
custom_user_agent: Option<HeaderValue>,
reqwest_client_builder: Option<reqwest::ClientBuilder>,
#[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool,
#[cfg(feature = "tunneling")]
front: Option<fronted::Front>,
front: fronted::Front,
retry_limit: usize,
serialization: SerializationFormat,
error: Option<HttpClientError>,
}
impl ClientBuilder {
@@ -642,10 +683,10 @@ impl ClientBuilder {
let mut builder = Self::new_with_urls(urls)?;
// Enable domain fronting by default (on retry)
// Enable domain fronting using the shared fronting policy
#[cfg(feature = "tunneling")]
{
builder = builder.with_fronting(FrontPolicy::OnRetry);
builder = builder.with_fronting(None);
}
Ok(builder)
@@ -659,26 +700,31 @@ impl ClientBuilder {
let urls = Self::check_urls(urls);
#[cfg(target_arch = "wasm32")]
let reqwest_client_builder = reqwest::ClientBuilder::new();
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = default_builder();
Ok(ClientBuilder {
urls,
timeout: None,
custom_user_agent: false,
reqwest_client_builder,
custom_user_agent: None,
reqwest_client_builder: None,
use_secure_dns: true,
#[cfg(feature = "tunneling")]
front: None,
front: fronted::Front::off(),
retry_limit: 0,
serialization: SerializationFormat::Json,
error: None,
})
}
/// Configure use of an independent HTTP request executor. This prevents use of beneficial
/// features like connection pooling under the hood.
#[cfg(not(target_arch = "wasm32"))]
pub fn non_shared(mut self) -> Self {
if self.reqwest_client_builder.is_none() {
self.reqwest_client_builder = Some(default_builder());
}
self
}
/// Add an additional URL to the set usable by this constructed `Client`
pub fn add_url(mut self, url: Url) -> Self {
self.urls.push(url);
@@ -723,7 +769,7 @@ impl ClientBuilder {
/// Provide a pre-configured [`reqwest::ClientBuilder`]
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
self.reqwest_client_builder = reqwest_builder;
self.reqwest_client_builder = Some(reqwest_builder);
self
}
@@ -733,18 +779,12 @@ impl ClientBuilder {
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
self.custom_user_agent = true;
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
self
}
/// Override DNS resolution for specific domains to particular IP addresses.
///
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
match value.try_into() {
Ok(v) => self.custom_user_agent = Some(v),
Err(err) => {
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
}
}
self
}
@@ -761,30 +801,33 @@ impl ClientBuilder {
/// Returns a Client that uses this ClientBuilder configuration.
pub fn build(self) -> Result<Client, HttpClientError> {
if let Some(err) = self.error {
return Err(err);
}
#[cfg(target_arch = "wasm32")]
let reqwest_client = self.reqwest_client_builder.build()?;
let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
// TODO: we should probably be propagating the error rather than panicking,
// but that'd break bunch of things due to type changes
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client = {
let mut builder = self.reqwest_client_builder;
let reqwest_client = self
.reqwest_client_builder
.map(|mut builder| {
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
builder
.build()
.map_err(HttpClientError::reqwest_client_build_error)?
};
builder
.build()
.map_err(HttpClientError::reqwest_client_build_error)
})
.transpose()?;
let client = Client {
base_urls: self.urls,
current_idx: Arc::new(AtomicUsize::new(0)),
reqwest_client,
using_secure_dns: self.use_secure_dns,
custom_user_agent: self.custom_user_agent,
#[cfg(feature = "tunneling")]
front: self.front,
@@ -804,11 +847,11 @@ impl ClientBuilder {
pub struct Client {
base_urls: Vec<Url>,
current_idx: Arc<AtomicUsize>,
reqwest_client: reqwest::Client,
using_secure_dns: bool,
reqwest_client: Option<reqwest::Client>,
custom_user_agent: Option<HeaderValue>,
#[cfg(feature = "tunneling")]
front: Option<fronted::Front>,
front: fronted::Front,
#[cfg(target_arch = "wasm32")]
request_timeout: Duration,
@@ -862,8 +905,8 @@ impl Client {
Client {
base_urls: vec![new_url],
current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(),
using_secure_dns: self.using_secure_dns,
reqwest_client: None,
custom_user_agent: None,
#[cfg(feature = "tunneling")]
front: self.front.clone(),
@@ -897,9 +940,7 @@ impl Client {
#[cfg(feature = "tunneling")]
fn matches_current_host(&self, url: &Url) -> bool {
if let Some(ref front) = self.front
&& front.is_enabled()
{
if self.front.is_enabled() {
url.host_str() == self.current_url().front_str()
} else {
url.host_str() == self.current_url().host_str()
@@ -926,9 +967,7 @@ impl Client {
}
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front
&& front.is_enabled()
{
if self.front.is_enabled() {
// if we are using fronting, try updating to the next front
let url = self.current_url();
@@ -948,9 +987,7 @@ impl Client {
// if fronting is enabled we want to update to a host that has fronts configured
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front
&& front.is_enabled()
{
if self.front.is_enabled() {
while next != orig {
if self.base_urls[next].has_front() {
// we have a front for the next host, so we can use it
@@ -981,14 +1018,12 @@ impl Client {
/// this method. For example, if the client is configured to rotate hosts after each error, this
/// method should be called after the host has been updated -- i.e. as part of the subsequent
/// send.
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
let url = self.current_url();
r.url_mut().set_host(url.host_str()).unwrap();
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front
&& front.is_enabled()
{
if self.front.is_enabled() {
if let Some(front_host) = url.front_str() {
if let Some(actual_host) = url.host_str() {
tracing::debug!(
@@ -1008,6 +1043,13 @@ impl Client {
.headers_mut()
.insert(reqwest::header::HOST, actual_host_header);
// Set a custom header to capture the outer host (used in the SNI) of the request
let front_host_header: HeaderValue =
front_host.parse().unwrap_or(HeaderValue::from_static(""));
_ = r
.headers_mut()
.insert(NYM_OUTER_SNI_HEADER, front_host_header);
return (url.as_str(), url.front_str());
} else {
tracing::debug!(
@@ -1048,12 +1090,21 @@ impl ApiClientCore for Client {
self.apply_hosts_to_req(&mut req);
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
let client = if let Some(client) = &self.reqwest_client {
client.clone()
} else {
SHARED_CLIENT.clone()
};
let mut rb = RequestBuilder::from_parts(client, req);
rb = rb
.header(ACCEPT, self.serialization.content_type())
.header(CONTENT_TYPE, self.serialization.content_type());
if let Some(user_agent) = &self.custom_user_agent {
rb = rb.header(USER_AGENT, user_agent.clone());
}
if let Some(body) = body {
match self.serialization {
SerializationFormat::Json => {
@@ -1096,16 +1147,19 @@ impl ApiClientCore for Client {
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = {
Ok(wasmtimer::tokio::timeout(
self.request_timeout,
self.reqwest_client.execute(req),
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
Ok(
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
)
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
};
#[cfg(not(target_arch = "wasm32"))]
let response = self.reqwest_client.execute(req).await;
let response = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
client.execute(req).await
};
match response {
Ok(resp) => return Ok(resp),
@@ -1121,20 +1175,10 @@ impl ApiClientCore for Client {
if is_network_err {
// if we have multiple urls, update to the next
self.update_host(Some(url.clone()));
self.maybe_rotate_hosts(Some(url.clone()));
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
// If fronting is set to be enabled on error, enable domain fronting as we
// have encountered an error.
let was_enabled = front.is_enabled();
front.retry_enable();
if !was_enabled && front.is_enabled() {
tracing::info!(
"Domain fronting activated after connection failure: {err}",
);
}
}
self.maybe_enable_fronting(("network", url.as_str(), &err));
}
if attempts < self.retry_limit {
@@ -1158,6 +1202,21 @@ impl ApiClientCore for Client {
}
}
}
fn maybe_rotate_hosts(&self, offending: Option<Url>) {
self.update_host(offending);
}
#[cfg(feature = "tunneling")]
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
// If fronting is set to be OnRetry, enable domain fronting as we
// have encountered an error.
let was_enabled = self.front.is_enabled();
self.front.retry_enable();
if !was_enabled && self.front.is_enabled() {
tracing::debug!("Domain fronting activated after failure: {context:?}",);
}
}
}
/// Common usage functionality for the http client.
@@ -1310,6 +1369,35 @@ pub trait ApiClient: ApiClientCore {
self.get_response(path, params).await
}
/// Attempt to parse a response object from an HTTP response
async fn parse_response<T>(
&self,
res: Response,
allow_empty: bool,
) -> Result<T, HttpClientError>
where
T: DeserializeOwned,
{
let url = Url::from(res.url());
parse_response(res, allow_empty).await.inspect_err(|e| {
if matches!(
// if we encounter a read error while we attempt to parse it could be caused by censorship and we should
// rotate hosts / enable fronting.
e,
HttpClientError::ResponseReadFailure {
url: _,
headers: _,
status: _,
source: _,
}
) {
self.maybe_rotate_hosts(Some(url.clone()));
#[cfg(feature = "tunneling")]
self.maybe_enable_fronting(("parse/read", url.as_str(), e));
}
})
}
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T` based on the content type header
@@ -1327,7 +1415,8 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::GET, path, params, None::<&()>)
.await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
@@ -1349,7 +1438,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::POST, path, params, Some(json_body))
.await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
@@ -1369,7 +1458,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
.await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
@@ -1391,7 +1480,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
.await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1403,7 +1492,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
let res = self.send(req).await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1420,7 +1509,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
let res = self.send(req).await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// `delete` json data from the provided absolute endpoint, e.g.
@@ -1432,7 +1521,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
let res = self.send(req).await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1450,7 +1539,7 @@ pub trait ApiClient: ApiClientCore {
let req =
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
let res = self.send(req).await?;
parse_response(res, false).await
self.parse_response(res, false).await
}
}
@@ -1517,7 +1606,7 @@ where
if !allow_empty && let Some(0) = res.content_length() {
return Err(HttpClientError::EmptyResponse {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
});
@@ -1530,25 +1619,25 @@ where
.bytes()
.await
.map_err(|source| HttpClientError::ResponseReadFailure {
url,
url: Box::new(url),
headers: Box::new(headers.clone()),
status,
source: ReqwestErrorWrapper(source),
})?;
decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url })
Err(HttpClientError::NotFound { url: Box::new(url) })
} else {
let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
});
};
Err(HttpClientError::EndpointFailure {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
error: plaintext,
+3 -2
View File
@@ -89,7 +89,8 @@ fn sanitizing_urls() {
// - on error without retries is where we have multiple urls, is the url updated?
#[tokio::test]
#[ignore] // test relies on external services being available and behaving in a specific way.
#[cfg(any())] // #[ignore] we run ignore assuming it just means slow in Ci/CD -_-
// test relies on external services being available and behaving in a specific way.
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
@@ -199,7 +200,7 @@ fn fronted_host_updating() {
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let mut client = ClientBuilder::new(url)
.unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always)
.with_fronting(Some(crate::fronted::FrontPolicy::Always))
.build()
.unwrap();
+10
View File
@@ -123,6 +123,16 @@ impl From<reqwest::Url> for Url {
}
}
impl From<&reqwest::Url> for Url {
fn from(url: &url::Url) -> Self {
Self {
url: url.clone(),
fronts: None,
current_front: Arc::new(AtomicUsize::new(0)),
}
}
}
impl AsRef<url::Url> for Url {
fn as_ref(&self) -> &url::Url {
&self.url
+3
View File
@@ -6,6 +6,9 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
# Exclude build.rs from published crate - it's only used for dev-time sync
# of env files and requires workspace context
exclude = ["build.rs"]
[dependencies]
dotenvy = { workspace = true, optional = true }
+4
View File
@@ -51,6 +51,10 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
ApiUrlConst {
url: "https://cdn1.media-platform.net/api/",
front_hosts: None,
},
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
+1
View File
@@ -6,6 +6,7 @@ edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
publish = false
[lib]
name = "nym_kcp"
+2 -1
View File
@@ -9,6 +9,7 @@ license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
thiserror = { workspace = true }
@@ -17,7 +18,7 @@ strum = { workspace = true }
strum_macros = { workspace = true }
blake3 = { workspace = true, optional = true }
libcrux-sha3 = { workspace = true, optional = true }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
[features]
digests = ["blake3", "libcrux-sha3"]
+6 -28
View File
@@ -226,17 +226,6 @@ impl KEM {
}
}
// 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",
// }),
// }
// }
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Ciphersuite {
hash_function: HashFunction,
@@ -284,16 +273,16 @@ impl Ciphersuite {
self.verification_key_length
}
pub fn hash_function(&self) -> &HashFunction {
&self.hash_function
pub fn hash_function(&self) -> HashFunction {
self.hash_function
}
pub fn kem(&self) -> &KEM {
&self.kem
pub fn kem(&self) -> KEM {
self.kem
}
pub fn signature_scheme(&self) -> &SignatureScheme {
&self.signature_scheme
pub fn signature_scheme(&self) -> SignatureScheme {
self.signature_scheme
}
pub fn hash_len(&self) -> usize {
@@ -362,17 +351,6 @@ impl Display for Ciphersuite {
}
}
impl Default for Ciphersuite {
fn default() -> Self {
Self::new(
KEM::MlKem768,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
+8 -6
View File
@@ -7,23 +7,25 @@ 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 = ["hashing"] }
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
libcrux-kem = { workspace = true }
libcrux-ecdh = { workspace = true, features = ["codec"] }
libcrux-chacha20poly1305 = { workspace = true }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
# 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"
+106 -39
View File
@@ -7,20 +7,29 @@
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, generate_keypair_mlkem,
hash_encapsulation_key,
},
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
session::{
initiator_ingest_response, initiator_process, responder_ingest_message, responder_process,
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(|| {
@@ -32,12 +41,15 @@ pub fn gen_mlkem768_keypair(c: &mut Criterion) {
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,
@@ -57,8 +69,8 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => (
EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
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),
@@ -95,14 +107,12 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|b| {
b.iter(|| {
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap()
});
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
},
);
let (mut i_context, i_frame) =
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap();
let (i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
c.bench_function(
&format!(
@@ -127,12 +137,14 @@ pub fn kkt_benchmark(c: &mut Criterion) {
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
),
|b| {
b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap());
b.iter(|| {
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
});
},
);
let (mut r_context, _) =
responder_ingest_message(&r_context, None, &i_frame_r).unwrap();
let (r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
c.bench_function(
&format!(
@@ -141,8 +153,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
@@ -150,8 +163,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
@@ -170,9 +184,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
@@ -181,9 +196,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
@@ -192,14 +208,27 @@ pub fn kkt_benchmark(c: &mut Criterion) {
}
// Initiator, OneWay
{
let (mut i_context, i_frame) =
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap();
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, None).unwrap()
initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap()
});
},
);
@@ -221,12 +250,25 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|b| {
b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap());
b.iter(|| {
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) =
responder_ingest_message(&r_context, 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());
@@ -237,8 +279,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
@@ -247,8 +290,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
@@ -267,9 +311,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
@@ -278,9 +323,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
@@ -298,6 +344,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap()
@@ -305,10 +352,11 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
@@ -335,14 +383,24 @@ pub fn kkt_benchmark(c: &mut Criterion) {
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|b| {
b.iter(|| {
responder_ingest_message(&r_context, Some(&i_dir_hash), &i_frame_r)
.unwrap()
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) =
responder_ingest_message(&r_context, Some(&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);
@@ -353,8 +411,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
@@ -363,8 +422,9 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
@@ -385,9 +445,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
@@ -396,9 +457,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
@@ -409,5 +471,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
}
}
criterion_group!(benches, gen_mlkem768_keypair, kkt_benchmark);
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
kkt_benchmark
);
criterion_main!(benches);
-189
View File
@@ -1,189 +0,0 @@
use libcrux_chacha20poly1305::TAG_LEN;
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_crypto::hkdf::blake3::derive_key_blake3;
use rand09::{CryptoRng, RngCore};
use zeroize::{Zeroize, ZeroizeOnDrop};
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_RX";
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_TX";
#[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],
) -> 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),
ephemeral_keypair.pk,
))
}
pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8]) -> Self {
let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context);
let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context);
Self::init(tx_key, rx_key)
}
pub fn from_secret(mut secret: [u8; 32], context: &[u8]) -> Self {
let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret.as_ref(), context);
let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret.as_ref(), context);
secret.zeroize();
Self::init(tx_key, rx_key)
}
pub(crate) fn flip_keys(self) -> Self {
Self {
tx_key: self.rx_key,
rx_key: self.tx_key,
tx_counter: self.rx_counter,
rx_counter: self.tx_counter,
}
}
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_keypair_x25519};
use rand09::RngCore;
#[test]
fn test_e2e() {
let mut rng = rand09::rng();
// generate responder x25519 keys
let r_x25519 = generate_keypair_x25519(&mut rng);
let mut context: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut context);
let ephemeral_keypair = generate_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);
let mut r_carrier =
Carrier::from_secret_slice(r_shared_secret.as_ref(), &context).flip_keys();
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);
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::KKTError;
use libcrux_kem::Algorithm;
pub use nym_kkt_ciphersuite::*;
pub enum EncapsulationKey<'a> {
MlKem768(libcrux_kem::PublicKey),
XWing(libcrux_kem::PublicKey),
X25519(libcrux_kem::PublicKey),
McEliece(classic_mceliece_rust::PublicKey<'a>),
}
pub enum DecapsulationKey<'a> {
MlKem768(libcrux_kem::PrivateKey),
XWing(libcrux_kem::PrivateKey),
X25519(libcrux_kem::PrivateKey),
McEliece(classic_mceliece_rust::SecretKey<'a>),
}
impl<'a> EncapsulationKey<'a> {
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
match kem {
KEM::McEliece => {
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
Err(KKTError::KEMError {
info: "Received McEliece Encapsulation Key with Invalid Length",
})
} else {
let mut public_key_bytes =
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
public_key_bytes.clone_from_slice(bytes);
Ok(EncapsulationKey::McEliece(
classic_mceliece_rust::PublicKey::from(public_key_bytes),
))
}
}
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
EncapsulationKey::XWing(public_key)
| EncapsulationKey::MlKem768(public_key)
| EncapsulationKey::X25519(public_key) => public_key.encode(),
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
}
}
}
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result<Algorithm, KKTError> {
match kem {
KEM::MlKem768 => Ok(Algorithm::MlKem768),
KEM::XWing => Ok(Algorithm::XWingKemDraft06),
KEM::X25519 => Ok(Algorithm::X25519),
KEM::McEliece => Err(KKTError::KEMMapping {
info: "attempted to map McEliece KEM to libcrux_kem",
}),
}
}
+46 -21
View File
@@ -1,9 +1,9 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{KKT_VERSION, error::KKTError};
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;
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
@@ -15,11 +15,11 @@ pub enum KKTStatus {
Ok = 0b0000_0000,
InvalidRequestFormat = 0b0010_0000,
InvalidResponseFormat = 0b0100_0000,
UnsupportedCiphersuite = 0b0110_0000,
UnsupportedKKTVersion = 0b1000_0000,
InvalidKey = 0b1010_0000,
Timeout = 0b1100_0000,
UnverifiedKEMKey = 0b1110_0000,
InvalidSignature = 0b0110_0000,
UnsupportedCiphersuite = 0b1000_0000,
UnsupportedKKTVersion = 0b1010_0000,
InvalidKey = 0b1100_0000,
Timeout = 0b1110_0000,
}
impl Display for KKTStatus {
@@ -28,10 +28,10 @@ impl Display for KKTStatus {
KKTStatus::Ok => "Ok",
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
KKTStatus::InvalidSignature => "Invalid Signature",
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
KKTStatus::InvalidKey => "Invalid Key",
KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key",
KKTStatus::Timeout => "Timeout",
})
}
@@ -43,6 +43,7 @@ impl Display for KKTStatus {
pub enum KKTRole {
Initiator = 0b0000_0000,
Responder = 0b0000_0001,
AnonymousInitiator = 0b0000_0010,
}
// bitmask used: 0b0001_1100
@@ -63,15 +64,20 @@ pub struct KKTContext {
ciphersuite: Ciphersuite,
}
impl KKTContext {
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: &Ciphersuite) -> Self {
Self {
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
return Err(KKTError::IncompatibilityError {
info: "Anonymous Initiator can only use OneWay mode",
});
}
Ok(Self {
version: KKT_VERSION,
message_sequence: 0,
status: KKTStatus::Ok,
mode,
role,
ciphersuite: *ciphersuite,
}
ciphersuite,
})
}
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
@@ -101,8 +107,8 @@ impl KKTContext {
pub fn status(&self) -> KKTStatus {
self.status
}
pub fn ciphersuite(&self) -> &Ciphersuite {
&self.ciphersuite
pub fn ciphersuite(&self) -> Ciphersuite {
self.ciphersuite
}
pub fn role(&self) -> KKTRole {
self.role
@@ -112,10 +118,9 @@ impl KKTContext {
}
pub fn body_len(&self) -> usize {
if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey)
||
// no payload
(self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator)
if self.status != KKTStatus::Ok
|| (self.mode == KKTMode::OneWay
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
{
0
} else {
@@ -123,12 +128,31 @@ 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.header_len()
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
}
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> {
@@ -204,8 +228,9 @@ mod tests {
let valid_context = KKTContext::new(
KKTRole::Initiator,
KKTMode::Mutual,
&Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
);
Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
)
.unwrap();
let encoded = valid_context.encode().unwrap();
let decoded = KKTContext::try_decode(encoded).unwrap();
+253
View File
@@ -0,0 +1,253 @@
// 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(())
}
}
+4 -21
View File
@@ -8,12 +8,13 @@ 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 },
@@ -47,28 +48,10 @@ 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,
}
#[derive(Error, Debug)]
pub enum MaskedByteError {
#[error(
"Invalid Masked Byte Length: Expected({}), Actual({}).",
expected,
actual
)]
InvalidLength { expected: usize, actual: usize },
#[error("Failed to Unmask Byte.")]
Failure,
}
impl From<libcrux_kem::Error> for KKTError {
fn from(err: libcrux_kem::Error) -> Self {
match err {
+70 -101
View File
@@ -7,120 +7,42 @@
// [2..=5] => Ciphersuite
// [6] => Reserved
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_kkt_ciphersuite::x25519::PUBLIC_KEY_LENGTH;
use rand09::{CryptoRng, RngCore};
use crate::{
carrier::Carrier,
context::{KKT_CONTEXT_LEN, KKTContext},
error::KKTError,
masked_byte::{MASKED_BYTE_LEN, MaskedByte},
};
const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF";
pub const KKT_SESSION_ID_LEN: usize = 16;
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
#[derive(Debug, PartialEq, Clone)]
pub struct KKTFrame {
context: [u8; KKT_CONTEXT_LEN],
session_id: KKTSessionId,
body: Vec<u8>,
signature: Vec<u8>,
}
// if oneway and message coming from initiator => body is empty.
// if mutual and message coming from initiator => body has the initiator's kem public key.
// if coming from responder => body has the responder's kem public key.
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
// if message coming from anonymous initiator => body is empty, there is no signature.
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
impl KKTFrame {
pub fn new(context: &KKTContext, body: &[u8]) -> Result<Self, KKTError> {
let context_bytes = context.encode()?;
Ok(Self {
context: context_bytes,
pub fn new(
context: [u8; KKT_CONTEXT_LEN],
body: &[u8],
session_id: [u8; KKT_SESSION_ID_LEN],
signature: &[u8],
) -> Self {
Self {
context,
body: Vec::from(body),
})
session_id,
signature: Vec::from(signature),
}
}
pub fn encrypt_initiator_frame<R>(
&self,
rng: &mut R,
responder_public_key: &DHPublicKey,
version_byte: u8,
) -> Result<(Carrier, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let ephemeral_keypair = DHKeyPair::new(rng);
let shared_secret = ephemeral_keypair
.sk()
.diffie_hellman(responder_public_key)
.map_err(|_| KKTError::X25519Error {
info: "Key Derivation Error",
})?;
let mut mask = Vec::from(ephemeral_keypair.pk.as_ref());
mask.extend_from_slice(responder_public_key.as_ref());
let masked_byte = MaskedByte::new(version_byte, &mask);
let mut context = Vec::from(masked_byte.as_slice());
context.extend_from_slice(KKT_CARRIER_CONTEXT);
context.extend_from_slice(ephemeral_keypair.pk.as_ref());
context.extend_from_slice(responder_public_key.as_ref());
let mut carrier = Carrier::from_secret_slice(shared_secret.as_ref(), &context);
let mut full_kkt_message = Vec::from(ephemeral_keypair.pk.as_ref());
full_kkt_message.extend_from_slice(masked_byte.as_slice());
let encrypted_kkt_frame = carrier.encrypt(&self.to_bytes())?;
full_kkt_message.extend_from_slice(&encrypted_kkt_frame);
Ok((carrier, full_kkt_message))
}
pub fn decrypt_initiator_frame(
responder_keypair: &DHKeyPair,
message: &[u8],
supported_versions: &[u8],
) -> Result<(Carrier, KKTFrame, KKTContext), KKTError> {
let mut initiator_public_key_bytes: [u8; PUBLIC_KEY_LENGTH] = [0; PUBLIC_KEY_LENGTH];
initiator_public_key_bytes.clone_from_slice(&message[0..PUBLIC_KEY_LENGTH]);
// check mask
let masked_byte =
MaskedByte::try_from(&message[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN])?;
let mut mask = Vec::from(&initiator_public_key_bytes);
mask.extend_from_slice(responder_keypair.pk.as_ref());
// 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 =
masked_byte.unmask_check_version(&mask, supported_versions)?;
// now that the version is ok, we can try dh
let initiator_public_key = DHPublicKey::from_bytes(&initiator_public_key_bytes);
let shared_secret = responder_keypair
.sk()
.diffie_hellman(&initiator_public_key)
.map_err(|_| KKTError::X25519Error {
info: "Key Derivation Error",
})?;
let mut context = Vec::from(masked_byte.as_slice());
context.extend_from_slice(KKT_CARRIER_CONTEXT);
context.extend_from_slice(initiator_public_key.as_ref());
context.extend_from_slice(responder_keypair.pk.as_ref());
let mut carrier = Carrier::from_secret_slice(shared_secret.as_ref(), &context).flip_keys();
let decrypted_message = carrier.decrypt(&message[PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN..])?;
let (frame, context) = KKTFrame::from_bytes(&decrypted_message)?;
Ok((carrier, frame, context))
}
pub fn context_ref(&self) -> &[u8] {
&self.context
}
@@ -129,22 +51,42 @@ impl KKTFrame {
KKTContext::try_decode(self.context)
}
pub fn signature_ref(&self) -> &[u8] {
&self.signature
}
pub fn body_ref(&self) -> &[u8] {
&self.body
}
pub fn session_id_ref(&self) -> &[u8] {
&self.session_id
}
pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] {
self.session_id
}
pub fn signature_mut(&mut self) -> &mut [u8] {
&mut self.signature
}
pub fn body_mut(&mut self) -> &mut [u8] {
&mut self.body
}
pub fn session_id_mut(&mut self) -> &mut [u8] {
&mut self.session_id
}
pub fn frame_length(&self) -> usize {
self.context.len() + self.body.len()
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.frame_length());
bytes.extend_from_slice(&self.context);
bytes.extend_from_slice(&self.body);
bytes.extend_from_slice(&self.session_id);
bytes.extend_from_slice(&self.signature);
bytes
}
@@ -173,6 +115,7 @@ impl KKTFrame {
}
let mut body = Vec::new();
let mut signature = Vec::new();
// decode body
if context.body_len() > 0 {
@@ -180,7 +123,33 @@ impl KKTFrame {
body.extend_from_slice(body_bytes);
}
let frame = KKTFrame::new(&context, &body)?;
Ok((frame, context))
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();
// // 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,
))
}
}
-167
View File
@@ -1,167 +0,0 @@
use libcrux_psq::handshake::types::DHPublicKey;
use nym_kkt_ciphersuite::Ciphersuite;
use rand09::{CryptoRng, RngCore};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::{
carrier::Carrier,
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::KKTFrame,
key_utils::validate_encapsulation_key,
};
pub struct KKTInitiator<'a> {
carrier: Carrier,
context: KKTContext,
expected_hash: &'a [u8],
}
impl<'a> Zeroize for KKTInitiator<'a> {
fn zeroize(&mut self) {
self.carrier.zeroize();
}
}
impl<'a> ZeroizeOnDrop for KKTInitiator<'a> {}
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,
) -> Result<(Self, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
Self::generate_encrypted_request(
rng,
KKTMode::OneWay,
ciphersuite,
None,
responder_dh_public_key,
expected_hash,
outer_protocol_version,
)
}
// 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,
) -> Result<(Self, Vec<u8>), 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,
)
}
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,
) -> Result<(Self, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let (context, frame) = initiator_process(mode, ciphersuite, local_encapsulation_key)?;
let (carrier, message_bytes) =
frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?;
Ok((
Self {
carrier,
context,
expected_hash,
},
message_bytes,
))
}
// bool would be true if the initiator was using mutual mode
// and the responder was able to verify the initiator's kem key
pub fn process_response(&mut self, response_bytes: &[u8]) -> Result<(Vec<u8>, bool), KKTError> {
let decrypted_response_bytes = self.carrier.decrypt(response_bytes)?;
let (response_frame, remote_context) = KKTFrame::from_bytes(&decrypted_response_bytes)?;
initiator_ingest_response(
&mut self.context,
&response_frame,
&remote_context,
self.expected_hash,
)
}
}
pub fn initiator_process<'a>(
mode: KKTMode,
ciphersuite: &Ciphersuite,
own_encapsulation_key: Option<&'a [u8]>,
) -> Result<(KKTContext, 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",
});
}
},
};
let frame = KKTFrame::new(&context, body)?;
Ok((context, frame))
}
pub fn initiator_ingest_response(
own_context: &mut KKTContext,
remote_frame: &KKTFrame,
remote_context: &KKTContext,
expected_hash: &[u8],
) -> Result<(Vec<u8>, bool), KKTError> {
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 => Ok((
remote_frame.body_ref().to_vec(),
remote_context.status() != KKTStatus::UnverifiedKEMKey,
)),
// 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(KKTError::ResponderFlaggedError {
status: remote_context.status(),
}),
}
}
+48 -16
View File
@@ -1,38 +1,70 @@
use crate::ciphersuite::HashFunction;
use std::collections::HashMap;
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests};
use classic_mceliece_rust::keypair_boxed;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
use rand09::{CryptoRng, RngCore};
pub fn generate_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
pub fn generate_keypair_ed25519<R>(
rng: &mut R,
index: Option<u32>,
) -> nym_crypto::asymmetric::ed25519::KeyPair
where
R: RngCore + CryptoRng,
{
DHKeyPair::new(rng)
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
}
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> (MlKem768PrivateKey, MlKem768PublicKey)
pub fn generate_keypair_x25519<R>(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair
where
R: RngCore + CryptoRng,
{
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng).into_parts()
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()
}
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_mceliece<R>(
pub fn generate_keypair_libcrux<R>(
rng: &mut R,
) -> (
libcrux_psq::classic_mceliece::SecretKey,
libcrux_psq::classic_mceliece::PublicKey,
)
kem: crate::ciphersuite::KEM,
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError>
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let kp = libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng);
(kp.sk, kp.pk)
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)
}
pub fn hash_key_bytes(
+449
View File
@@ -0,0 +1,449 @@
// 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());
}
}
+451 -163
View File
@@ -1,210 +1,498 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod carrier;
pub mod ciphersuite;
pub mod context;
pub mod encryption;
pub mod error;
pub mod frame;
pub mod initiator;
pub mod key_utils;
pub mod masked_byte;
pub mod rekey;
pub mod responder;
// pub mod kkt;
pub mod session;
// This must be less than 4 bits
pub const KKT_VERSION: u8 = 1;
const _: () = assert!(KKT_VERSION < 1 << 4);
pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME";
#[cfg(test)]
mod test {
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
use crate::{
initiator::KKTInitiator,
key_utils::{
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
hash_encapsulation_key,
KKT_RESPONSE_AAD,
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
encryption::{
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
encrypt_kkt_frame,
},
frame::KKTFrame,
key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
generate_keypair_x25519, hash_encapsulation_key,
},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
},
responder::KKTResponder,
};
#[test]
fn test_kkt_psq_e2e_encrypted_carrier() {
fn test_kkt_psq_e2e_clear() {
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));
// generate responder x25519 keys
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
for hash_function in [
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::Shake128,
HashFunction::Shake256,
] {
// generate kem public keys
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let r_dir_hash_mlkem = hash_encapsulation_key(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mlkem_keypair.1.as_slice().as_slice(),
);
let r_dir_hash_mceliece = hash_encapsulation_key(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mceliece_keypair.1.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(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
initiator_mlkem_keypair.1.as_slice().as_slice(),
);
let _i_dir_hash_mceliece = hash_encapsulation_key(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
initiator_mceliece_keypair.1.as_ref(),
);
let responder = KKTResponder::new(
&responder_x25519_keypair,
Some(&responder_mlkem_keypair.1),
Some(&responder_mceliece_keypair.1),
&[
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::Shake128,
HashFunction::Shake256,
],
&[1],
&[SignatureScheme::Ed25519],
)
.unwrap();
// OneWay - MlKem
{
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::MlKem768,
kem,
hash_function,
SignatureScheme::Ed25519,
crate::ciphersuite::SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
1u8,
)
.unwrap();
let (response_bytes, _) = responder.process_request(&request_bytes).unwrap();
// generate kem public keys
let (i_obtained_key, _) = initiator.process_response(&response_bytes).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!(
i_obtained_key,
responder_mlkem_keypair.1.as_slice().as_slice(),
)
}
// Mutual - MlKem
{
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::MlKem768,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
1u8,
)
.unwrap();
let i_kem_key_bytes = initiator_kem_public_key.encode();
let (response_bytes, r_obtained_key) =
responder.process_request(&request_bytes).unwrap();
let r_kem_key_bytes = responder_kem_public_key.encode();
// if we keep unverified keys, this should change
assert!(r_obtained_key.is_none());
let i_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&i_kem_key_bytes,
);
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
let r_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&r_kem_key_bytes,
);
assert_eq!(
i_obtained_key,
responder_mlkem_keypair.1.as_slice().as_slice(),
)
}
// Anonymous Initiator, OneWay
{
let (i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// OneWay - McEliece
{
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::McEliece,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mceliece,
1u8,
)
.unwrap();
// encryption - initiator frame
let (response_bytes, _) = responder.process_request(&request_bytes).unwrap();
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
&mut rng,
responder_x25519_keypair.public_key(),
&i_frame,
)
.unwrap();
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
// decryption - initiator frame
assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),)
}
// Mutual - MlKem
{
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::McEliece,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mceliece,
1u8,
)
.unwrap();
let (r_session_secret, i_frame_r, i_context_r) =
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (response_bytes, r_obtained_key) =
responder.process_request(&request_bytes).unwrap();
let (r_context, _) =
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
// if we keep unverified keys, this should change
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 (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),)
// 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, OneWay
{
let (i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
&mut rng,
responder_x25519_keypair.public_key(),
&i_frame,
)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, r_context) =
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (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)
}
}
}
}
-190
View File
@@ -1,190 +0,0 @@
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
use crate::error::{
MaskedByteError,
MaskedByteError::{Failure, InvalidLength},
};
pub const MASKED_BYTE_LEN: usize = 16;
pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1";
const U8_RANGE: [u8; 256] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192,
193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
250, 251, 252, 253, 254, 255,
];
#[derive(Clone, Copy)]
pub struct MaskedByte([u8; MASKED_BYTE_LEN]);
impl MaskedByte {
/// Mask a byte by hashing it with some mask.
/// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte)
pub fn new(byte: u8, mask: &[u8]) -> Self {
let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
let mut hasher = blake3::Hasher::new();
hasher.update(MASKED_BYTE_CONTEXT_STR);
hasher.update(mask);
// avoid zero update
hasher.update(&[0xFF, byte]);
hasher.finalize_xof_into(&mut output);
Self(output)
}
/// Unmasks a byte by trial hashing.
/// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF).
/// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max).
/// If we find an `i` which yields back the hash input, then we found the masked byte.
/// Otherwise, the function returns an error.
pub fn unmask(&self, mask: &[u8]) -> Result<u8, MaskedByteError> {
self.unmask_check_version(mask, &U8_RANGE)
}
// This could be more efficient than unmask,
// because we just could check against a smaller list of supported versions.
pub fn unmask_check_version(
&self,
mask: &[u8],
supported_versions: &[u8],
) -> Result<u8, MaskedByteError> {
let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
let mut hasher = blake3::Hasher::new();
hasher.update(MASKED_BYTE_CONTEXT_STR);
hasher.update(mask);
// avoid zero update
hasher.update(&[0xFF]);
for i in supported_versions {
let mut t_hasher = hasher.clone();
t_hasher.update(&[*i]);
t_hasher.finalize_xof_into(&mut buf);
if buf == self.0 {
return Ok(*i);
}
}
Err(Failure)
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn to_bytes(self) -> [u8; 16] {
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.to_owned())
}
}
impl TryFrom<&[u8]> for MaskedByte {
type Error = MaskedByteError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() != MASKED_BYTE_LEN {
Err(InvalidLength {
expected: MASKED_BYTE_LEN,
actual: value.len(),
})
} else {
Ok(Self::from(value.as_chunks::<MASKED_BYTE_LEN>().0[0]))
}
}
}
#[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!(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!(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!(wire_bytes_messy.len() == MASKED_BYTE_LEN - 1);
// should fail
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
}
}
-232
View File
@@ -1,232 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Post-Quantum Re-Key Protocol
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 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 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 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",
});
}
// We don't support X25519 because it's not post-quantum secure.
KEM::X25519 => {
return Err(KKTError::UnsupportedAlgorithm {
info: "X25519 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::rekey::{RekeyInitiator, responder_process};
use nym_kkt_ciphersuite::KEM;
#[test]
fn rekey_test() {
let mut rng = rand09::rng();
for kem in [KEM::MlKem768, KEM::XWing] {
let (rekey_state, request_message) =
RekeyInitiator::generate_request(&mut rng, kem).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);
}
}
}
-214
View File
@@ -1,214 +0,0 @@
use std::collections::HashSet;
use crate::key_utils::validate_encapsulation_key;
use crate::{
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::KKTFrame,
};
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme};
pub struct KKTResponder<'a> {
x25519_keypair: &'a DHKeyPair,
mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>,
mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>,
supported_hash_functions: HashSet<HashFunction>,
supported_signature_schemes: HashSet<SignatureScheme>,
supported_outer_protocol_versions: HashSet<u8>,
}
impl<'a> KKTResponder<'a> {
pub fn new(
x25519_keypair: &'a DHKeyPair,
mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>,
mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>,
supported_hash_functions: &[HashFunction],
supported_outer_protocol_versions: &[u8],
supported_signature_schemes: &[SignatureScheme],
) -> Result<Self, KKTError> {
let hash_functions: HashSet<HashFunction> =
supported_hash_functions.iter().copied().collect();
if hash_functions.is_empty() {
Err(KKTError::FunctionInputError {
info: "Did not provide a supported HashFunction when instaciating a KKTResponder",
})
} else {
let signature_schemes: HashSet<SignatureScheme> =
supported_signature_schemes.iter().copied().collect();
if signature_schemes.is_empty() {
Err(KKTError::FunctionInputError {
info: "Did not provide a supported SignatureScheme when instaciating a KKTResponder",
})
} else {
let outer_protocol_versions: HashSet<u8> =
supported_outer_protocol_versions.iter().copied().collect();
if outer_protocol_versions.is_empty() {
Err(KKTError::FunctionInputError {
info: "Did not provide a supported outer protocol version when instaciating a KKTResponder",
})
} else {
if mlkem_encapsulation_key.is_none() && mceliece_encapsulation_key.is_none() {
return Err(KKTError::FunctionInputError {
info: "Did not provide an encapsulation key when instanciating a KKTResponder.",
});
} else {
Ok(Self {
x25519_keypair,
mlkem_encapsulation_key,
mceliece_encapsulation_key,
supported_hash_functions: hash_functions,
supported_signature_schemes: signature_schemes,
supported_outer_protocol_versions: outer_protocol_versions,
})
}
}
}
}
}
fn supported_protocol_versions(&self) -> Vec<u8> {
self.supported_outer_protocol_versions
.iter()
.copied()
.collect()
}
fn check_ciphersuite_compatiblity(
&self,
remote_ciphersuite: &Ciphersuite,
) -> Result<(), KKTError> {
if !self
.supported_hash_functions
.contains(remote_ciphersuite.hash_function())
{
Err(KKTError::IncompatibilityError {
info: "Unsupported HashFunction",
})
} else {
if !self
.supported_signature_schemes
.contains(remote_ciphersuite.signature_scheme())
{
Err(KKTError::IncompatibilityError {
info: "Unsupported SignatureScheme",
})
} else {
if match remote_ciphersuite.kem() {
KEM::MlKem768 => self.mlkem_encapsulation_key.is_some(),
KEM::McEliece => self.mceliece_encapsulation_key.is_some(),
_ => false,
} {
Ok(())
} else {
Err(KKTError::IncompatibilityError {
info: "Unsupported KEM",
})
}
}
}
}
// When this function fails, we do that silently (i.e. we dont generate a response to the initiator).
pub fn process_request(
&self,
request_bytes: &[u8],
) -> Result<(Vec<u8>, Option<Vec<u8>>), KKTError> {
let (mut carrier, remote_frame, remote_context) = KKTFrame::decrypt_initiator_frame(
self.x25519_keypair,
request_bytes,
&self.supported_protocol_versions(),
)?;
self.check_ciphersuite_compatiblity(remote_context.ciphersuite())?;
let (local_context, remote_encapsulation_key) = match remote_context.mode() {
KKTMode::OneWay => responder_ingest_message(&remote_context, 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(&remote_context, None, &remote_frame)?
}
};
let frame = if local_context.ciphersuite().kem() == &KEM::MlKem768 {
KKTFrame::new(
&local_context,
// SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place
#[allow(clippy::unwrap_used)]
&self.mlkem_encapsulation_key.unwrap().as_slice().as_slice(),
)?
} else {
KKTFrame::new(
&local_context,
// SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place
#[allow(clippy::unwrap_used)]
&self.mceliece_encapsulation_key.unwrap().as_ref(),
)?
};
// encryption - responder frame
let response_bytes = carrier.encrypt(&frame.to_bytes())?;
Ok((response_bytes, remote_encapsulation_key))
}
}
pub fn responder_ingest_message(
remote_context: &KKTContext,
expected_hash: Option<&[u8]>,
remote_frame: &KKTFrame,
) -> Result<(KKTContext, Option<Vec<u8>>), KKTError> {
let mut own_context = remote_context.derive_responder_header()?;
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 => {
match expected_hash {
Some(expected_hash) => {
if validate_encapsulation_key(
own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
remote_frame.body_ref(),
expected_hash,
) {
Ok((own_context, Some(remote_frame.body_ref().to_vec())))
}
// 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 => {
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)))
//
Ok((own_context, None))
}
}
}
}
}
KKTRole::Responder => Err(KKTError::IncompatibilityError {
info: "Responder received a request from another responder.",
}),
}
}
+230
View File
@@ -0,0 +1,230 @@
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(())
}
-42
View File
@@ -1,42 +0,0 @@
[package]
name = "nym-lp-sandbox"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
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 }
rand09 = { workspace = true }
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
nym-kkt = { path = "../nym-kkt" }
nym-lp-common = { path = "../nym-lp-common" }
nym-kkt-ciphersuite = { path ="../nym-kkt-ciphersuite" }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
libcrux-psq = { workspace = true, features = ["test-utils"] }
libcrux-kem = { workspace = true }
tls_codec = { workspace = true }
num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand_chacha = "0.3"
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { workspace = true }
-262
View File
@@ -1,262 +0,0 @@
#[cfg(test)]
mod tests {
use libcrux_psq::{
Channel, IntoSession,
handshake::{
builders::{CiphersuiteBuilder, PrincipalBuilder},
ciphersuites::CiphersuiteName,
types::{Authenticator, PQEncapsulationKey},
},
session::{Session, SessionBinding},
};
use nym_kkt::{
initiator::KKTInitiator,
key_utils::{
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
hash_encapsulation_key,
},
responder::KKTResponder,
};
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
#[test]
fn test_e2e_client_node() {
let mut rng = rand09::rng();
// we should add these as consts
let aad_initiator_outer = b"Test Data I Outer";
let aad_initiator_inner = b"Test Data I Inner";
let aad_responder = b"Test Data R";
let ctx = b"Test Context";
// generate responder x25519 keys
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
let hash_function = HashFunction::Blake3;
// generate kem public keys
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let r_dir_hash_mlkem = hash_encapsulation_key(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mlkem_keypair.1.as_slice().as_slice(),
);
let _r_dir_hash_mceliece = hash_encapsulation_key(
// &ciphersuite.hash_function(),
&hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mceliece_keypair.1.as_ref(),
);
let kkt_responder = KKTResponder::new(
&responder_x25519_keypair,
Some(&responder_mlkem_keypair.1),
Some(&responder_mceliece_keypair.1),
&[
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::Shake128,
HashFunction::Shake256,
],
&[1],
&[SignatureScheme::Ed25519],
)
.unwrap();
// OneWay - MlKem
let psq_ciphersuite = CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256;
let responder_ciphersuite = CiphersuiteBuilder::new(psq_ciphersuite)
.longterm_x25519_keys(&responder_x25519_keypair)
.longterm_mlkem_encapsulation_key(&responder_mlkem_keypair.1)
.longterm_mlkem_decapsulation_key(&responder_mlkem_keypair.0)
.build_responder_ciphersuite()
.unwrap();
let mut responder = PrincipalBuilder::new(rand09::rng())
.context(ctx)
.outer_aad(aad_responder)
.recent_keys_upper_bound(30)
.build_responder(responder_ciphersuite)
.unwrap();
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::MlKem768,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
1u8,
)
.unwrap();
let (response_bytes, _) = kkt_responder.process_request(&request_bytes).unwrap();
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
assert_eq!(
i_obtained_key,
responder_mlkem_keypair.1.as_slice().as_slice(),
);
let mlkem_key =
libcrux_kem::MlKem768PublicKey::try_from(i_obtained_key.as_slice()).unwrap();
let initiator_psq_keys = generate_keypair_x25519(&mut rng);
let initiator_cbuilder = CiphersuiteBuilder::new(psq_ciphersuite)
.longterm_x25519_keys(&initiator_psq_keys)
.peer_longterm_x25519_pk(&responder_x25519_keypair.pk)
.peer_longterm_mlkem_pk(&mlkem_key);
let initiator_ciphersuite = initiator_cbuilder.build_initiator_ciphersuite().unwrap();
let mut msg_channel = vec![0u8; 8192];
let mut payload_buf_responder = vec![0u8; 4096];
let mut payload_buf_initiator = vec![0u8; 4096];
let mut initiator = PrincipalBuilder::new(rand09::rng())
.outer_aad(aad_initiator_outer)
.inner_aad(aad_initiator_inner)
.context(ctx)
.build_registration_initiator(initiator_ciphersuite)
.unwrap();
// Send first message
let registration_payload_initiator = b"Registration_init";
let len_i = initiator
.write_message(registration_payload_initiator, &mut msg_channel)
.unwrap();
// Read first message
let (len_r_deserialized, len_r_payload) = responder
.read_message(&msg_channel, &mut payload_buf_responder)
.unwrap();
// We read the same amount of data.
assert_eq!(len_r_deserialized, len_i);
assert_eq!(len_r_payload, registration_payload_initiator.len());
assert_eq!(
&payload_buf_responder[0..len_r_payload],
registration_payload_initiator
);
// 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")
};
// Respond
let registration_payload_responder = b"Registration_respond";
let len_r = responder
.write_message(registration_payload_responder, &mut msg_channel)
.unwrap();
// Finalize on registration initiator
let (len_i_deserialized, len_i_payload) = initiator
.read_message(&msg_channel, &mut payload_buf_initiator)
.unwrap();
// We read the same amount of data.
assert_eq!(len_r, len_i_deserialized);
assert_eq!(registration_payload_responder.len(), len_i_payload);
assert_eq!(
&payload_buf_initiator[0..len_i_payload],
registration_payload_responder
);
// 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(initiator_psq_keys.pk),
responder_ecdh_pk: &responder_x25519_keypair.pk,
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
},
)
.unwrap();
let mut i_transport = Session::deserialize(
&session_storage,
SessionBinding {
initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk),
responder_ecdh_pk: &responder_x25519_keypair.pk,
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
},
)
.unwrap();
r_transport
.serialize(
&mut session_storage,
SessionBinding {
initiator_authenticator: &initiator_authenticator,
responder_ecdh_pk: &responder_x25519_keypair.pk,
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_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(PQEncapsulationKey::MlKem(&mlkem_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 len_i = channel_i
.write_message(app_data_i, &mut msg_channel)
.unwrap();
let (len_r_deserialized, len_r_payload) = channel_r
.read_message(&msg_channel, &mut payload_buf_responder)
.unwrap();
// We read the same amount of data.
assert_eq!(len_r_deserialized, len_i);
assert_eq!(len_r_payload, app_data_i.len());
assert_eq!(&payload_buf_responder[0..len_r_payload], app_data_i);
let len_r = channel_r
.write_message(app_data_r, &mut msg_channel)
.unwrap();
let (len_i_deserialized, len_i_payload) = channel_i
.read_message(&msg_channel, &mut payload_buf_initiator)
.unwrap();
assert_eq!(len_r, len_i_deserialized);
assert_eq!(app_data_r.len(), len_i_payload);
assert_eq!(&payload_buf_initiator[0..len_i_payload], app_data_r);
}
}
+2 -1
View File
@@ -12,8 +12,9 @@ readme.workspace = true
publish = false
[dependencies]
tokio = { workspace = true, features = ["net"] }
tokio = { workspace = true, features = ["net", "io-util"] }
nym-test-utils = { path = "../test-utils", optional = true }
tracing = { workspace = true }
[features]
io-mocks = ["nym-test-utils"]
+103 -2
View File
@@ -4,15 +4,100 @@
#[cfg(feature = "io-mocks")]
use nym_test_utils::mocks::async_read_write::MockIOStream;
use std::net::SocketAddr;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::debug;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
pub trait LpTransport: Sized {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
///
/// # Arguments
/// * `packet_data` - The serialised LP packet to send
///
/// # Errors
/// Returns an error on network transmission fails.
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
/// Receives an LP packet from a TCP stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
///
/// # Errors
/// Returns an error on network transmission fails.
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
}
async fn send_serialised_packet_async_write<W>(
writer: &mut W,
packet_data: &[u8],
) -> std::io::Result<()>
where
W: AsyncWrite + Unpin,
{
// Send 4-byte length prefix (u32 big-endian)
let len = packet_data.len() as u32;
writer
.write_all(&len.to_be_bytes())
.await
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
// Send the actual packet data
writer
.write_all(packet_data)
.await
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
// Flush to ensure data is sent immediately
writer
.flush()
.await
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
tracing::trace!(
"Sent LP packet ({} bytes + 4 byte header)",
packet_data.len()
);
Ok(())
}
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
where
R: AsyncRead + Unpin,
{
// Read 4-byte length prefix (u32 big-endian)
let mut len_buf = [0u8; 4];
reader
.read_exact(&mut len_buf)
.await
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
let packet_len = u32::from_be_bytes(len_buf) as usize;
// Sanity check to prevent huge allocations
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
if packet_len > MAX_PACKET_SIZE {
return Err(std::io::Error::other(format!(
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
)));
}
// Read the actual packet data
let mut packet_buf = vec![0u8; packet_len];
reader
.read_exact(&mut packet_buf)
.await
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
Ok(packet_buf)
}
impl LpTransport for TcpStream {
@@ -24,6 +109,14 @@ impl LpTransport for TcpStream {
// Set TCP_NODELAY for low latency
self.set_nodelay(nodelay)
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
send_serialised_packet_async_write(self, packet_data).await
}
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
receive_raw_packet_async_read(self).await
}
}
#[cfg(feature = "io-mocks")]
@@ -35,4 +128,12 @@ impl LpTransport for MockIOStream {
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
Ok(())
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
send_serialised_packet_async_write(self, packet_data).await
}
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
receive_raw_packet_async_read(self).await
}
}
+17 -4
View File
@@ -16,27 +16,40 @@ 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 }
ouroboros = "0.18.5"
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
nym-kkt = { path = "../nym-kkt" }
nym-lp-common = { path = "../nym-lp-common" }
nym-lp-transport = { path = "../nym-lp-transport" }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
libcrux-psq = { workspace = true, features = ["test-utils"] }
libcrux-kem = { workspace = true }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
"test-utils",
] }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
tls_codec = { workspace = true }
num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
# needed for the 'mock 'feature
nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand_chacha = "0.3"
#rand_chacha = "0.3"
mock_instant = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
[features]
mock = ["nym-test-utils", "nym-crypto/rand"]
[[bench]]
name = "replay_protection"
+3 -3
View File
@@ -1,8 +1,8 @@
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
use nym_lp::replay::ReceivingKeyCounterValidator;
use nym_test_utils::helpers::u64_seeded_rng;
use parking_lot::Mutex;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rand::Rng;
use std::sync::Arc;
fn bench_sequential_counters(c: &mut Criterion) {
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
let validator = ReceivingKeyCounterValidator::default();
// Create random counters within a valid window
let mut rng = ChaCha8Rng::seed_from_u64(42);
let mut rng = u64_seeded_rng(42);
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
b.iter(|| {
+90 -19
View File
@@ -14,7 +14,6 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
/// Size of outer header (receiver_idx + counter) - always cleartext
pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes
// georgio: maybe remove this?
/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode
const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
@@ -39,7 +38,6 @@ const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions.
/// Each connection performs fresh KKT+PSQ handshake.
///
// noiserm
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct OuterAeadKey {
key: [u8; 32],
@@ -54,7 +52,7 @@ impl OuterAeadKey {
/// Uses Blake3 KDF with domain separation to avoid key reuse
/// between the outer AEAD layer and the inner Noise layer.
pub fn from_psk(psk: &[u8; 32]) -> Self {
let key = nym_crypto::hkdf::blake3::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
Self { key }
}
@@ -72,7 +70,6 @@ impl std::fmt::Debug for OuterAeadKey {
}
}
// noiserm
/// Build 12-byte nonce from 8-byte counter (zero-padded).
///
/// Format: counter (8 bytes LE) || 0x00000000 (4 bytes)
@@ -180,7 +177,6 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
trailer,
})
}
// noiserm (potentially)
Some(key) => {
// AEAD decryption mode
// AAD is the outer header (12 bytes)
@@ -228,7 +224,6 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
}
}
// georgio: start with no outer_key
/// Serializes an LpPacket into the provided BytesMut buffer.
///
/// ## Unified Packet Format
@@ -323,7 +318,7 @@ mod tests {
use super::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
// Keep necessary imports
use crate::LpError;
use crate::message::{EncryptedDataPayload, LpMessage, MessageType, PSQRequestData};
use crate::message::{EncryptedDataPayload, HandshakeData, LpMessage, MessageType};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
use nym_crypto::asymmetric::{ed25519, x25519};
@@ -378,7 +373,7 @@ mod tests {
receiver_idx: 42,
counter: 123,
},
message: LpMessage::PSQRequest(PSQRequestData(payload.clone())),
message: LpMessage::Handshake(HandshakeData(payload.clone())),
trailer: [0; TRAILER_LEN],
};
@@ -395,8 +390,8 @@ mod tests {
// Verify message type and data
match decoded.message {
LpMessage::PSQRequest(decoded_payload) => {
assert_eq!(decoded_payload, PSQRequestData(payload));
LpMessage::Handshake(decoded_payload) => {
assert_eq!(decoded_payload, HandshakeData(payload));
}
_ => panic!("Expected Handshake message"),
}
@@ -560,7 +555,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
// Need payload and trailer to meet min_size requirement
let payload_size = 10; // Arbitrary
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
@@ -570,7 +565,7 @@ mod tests {
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::InvalidMessageType(255)) => {} // Expected error
Err(LpError::InvalidMessageType(231)) => {} // Expected error
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
@@ -633,7 +628,7 @@ mod tests {
receiver_idx: 42,
counter: 123,
},
message: LpMessage::ClientHello(hello_data.clone()),
message: LpMessage::ClientHello(hello_data),
trailer: [0; TRAILER_LEN],
};
@@ -686,7 +681,7 @@ mod tests {
receiver_idx: 100,
counter: 200,
},
message: LpMessage::ClientHello(hello_data.clone()),
message: LpMessage::ClientHello(hello_data),
trailer: [55; TRAILER_LEN],
};
@@ -762,12 +757,12 @@ mod tests {
}
#[test]
fn test_forward_packet_encode_decode_roundtrip() {
fn test_forward_packet_encode_decode_roundtrip_v4() {
let mut dst = BytesMut::new();
let forward_data = crate::message::ForwardPacketData {
target_gateway_identity: [77u8; 32],
target_lp_address: "1.2.3.4:41264".to_string(),
target_lp_address: "1.2.3.4:41264".parse().unwrap(),
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
};
@@ -794,7 +789,50 @@ mod tests {
if let LpMessage::ForwardPacket(data) = decoded.message {
assert_eq!(data.target_gateway_identity, [77u8; 32]);
assert_eq!(data.target_lp_address, "1.2.3.4:41264");
assert_eq!(data.target_lp_address, "1.2.3.4:41264".parse().unwrap());
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
} else {
panic!("Expected ForwardPacket message");
}
}
#[test]
fn test_forward_packet_encode_decode_roundtrip_v6() {
let mut dst = BytesMut::new();
let forward_data = crate::message::ForwardPacketData {
target_gateway_identity: [77u8; 32],
target_lp_address: "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap(),
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 999,
counter: 555,
},
message: LpMessage::ForwardPacket(forward_data),
trailer: [0xff; TRAILER_LEN],
};
// Serialize
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse back
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify LP protocol handling works correctly
assert_eq!(decoded.header.receiver_idx, 999);
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
if let LpMessage::ForwardPacket(data) = decoded.message {
assert_eq!(data.target_gateway_identity, [77u8; 32]);
assert_eq!(
data.target_lp_address,
"[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap()
);
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
} else {
panic!("Expected ForwardPacket message");
@@ -1055,7 +1093,7 @@ mod tests {
receiver_idx: 99999,
counter: 2,
},
message: LpMessage::PSQRequest(PSQRequestData(handshake_data.clone())),
message: LpMessage::Handshake(HandshakeData(handshake_data.clone())),
trailer: [0; TRAILER_LEN],
};
@@ -1065,7 +1103,7 @@ mod tests {
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::PSQResponse(data) => {
LpMessage::Handshake(data) => {
assert_eq!(data.0, handshake_data);
}
_ => panic!("Expected Handshake message"),
@@ -1251,4 +1289,37 @@ mod tests {
_ => panic!("Expected SubsessionKK1 message"),
}
}
#[test]
fn test_serialize_parse_error() {
use crate::message::ErrorPacketData;
let mut dst = BytesMut::new();
let error_data = ErrorPacketData {
message: "this is an error".to_string(),
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 200,
},
message: LpMessage::Error(error_data.clone()),
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 42);
match decoded.message {
LpMessage::Error(data) => {
assert_eq!(data.message, "this is an error");
}
_ => panic!("Expected Error message"),
}
}
}
+6 -6
View File
@@ -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.
/// Supported KEMs: MlKem768, McEliece
/// X25519 = classical (testing), MlKem768 = PQ, XWing = hybrid.
#[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::MlKem768,
kem_algorithm: KEM::X25519,
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))),
}
}
+23 -16
View File
@@ -1,13 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::message::MessageType;
use crate::{noise_protocol::NoiseError, replay::ReplayError};
use libcrux_psq::handshake::builders::BuilderError;
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_kkt::{
ciphersuite::{HashFunction, KEM},
error::KKTError,
};
use nym_kkt::ciphersuite::{HashFunction, KEM};
use nym_kkt::error::KKTError;
use thiserror::Error;
#[derive(Error, Debug)]
@@ -15,21 +13,15 @@ pub enum LpError {
#[error("IO Error: {0}")]
IoError(#[from] std::io::Error),
// noiserm
#[error("Snow Error: {0}")]
SnowKeyError(#[from] snow::Error),
// noiserm
#[error("Snow Pattern Error: {0}")]
SnowPatternError(String),
// noiserm
#[error("Noise Protocol Error: {0}")]
NoiseError(#[from] NoiseError),
#[error("PSQ Error: {0}")]
PSQError(String),
#[error("Replay detected: {0}")]
Replay(#[from] ReplayError),
@@ -102,8 +94,8 @@ pub enum LpError {
#[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 for {kem} ")]
ResponderWithMissingKEMKey { kem: KEM },
#[error("attempted to create an LP responder without providing a valid KEM key")]
ResponderWithMissingKEMKey,
#[error(
"there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function"
@@ -112,10 +104,25 @@ pub enum LpError {
kem: KEM,
hash_function: HashFunction,
},
#[error("failed to complete KKT/PSQ handshake: {0}")]
KKTPSQHandshake(String),
#[error("failed to complete the KKT exchange: {source}")]
KKTFailure {
#[from]
source: KKTError,
},
}
impl From<KKTError> for LpError {
fn from(err: KKTError) -> Self {
LpError::KKTError(format!("KKT Error: {:?}", err))
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:?}"
))
}
}
+295 -218
View File
@@ -2,25 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
pub mod codec;
// georgio: config does not seem to be used anywhere
// pub mod config;
// pub use config::LpConfig;
pub mod config;
pub mod error;
// georgio: no use for this
// pub mod kkt_orchestrator;
pub mod message;
pub mod noise_protocol;
pub mod packet;
pub mod peer;
pub mod psk;
pub mod psq;
mod psq;
pub mod replay;
pub mod session;
mod session_integration;
pub mod session_manager;
pub mod state_machine;
pub use config::LpConfig;
pub use error::LpError;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
@@ -29,30 +26,135 @@ pub use session::LpSession;
pub use session_manager::SessionManager;
pub use state_machine::LpStateMachine;
// noiserm
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
pub const NOISE_PSK_INDEX: u8 = 3;
#[cfg(test)]
pub fn kem_list() -> Vec<nym_kkt::ciphersuite::KEM> {
use nym_kkt::ciphersuite::KEM;
vec![KEM::MlKem768, KEM::McEliece, KEM::X25519]
#[cfg(any(feature = "mock", test))]
pub struct SessionsMock {
pub initiator: LpSession,
pub responder: LpSession,
}
#[cfg(test)]
pub fn sessions_for_tests<'a>(kem: nym_kkt::ciphersuite::KEM) -> (LpSession<'a>, LpSession<'a>) {
let (init, resp) = crate::peer::mock_peers(kem);
// Use a fixed receiver_index for deterministic tests
let receiver_index: u32 = 12345;
#[cfg(any(feature = "mock", test))]
impl SessionsMock {
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
use crate::peer::mock_peers;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
let initiator_session =
LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
.expect("Test session creation failed");
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 responder_session = LpSession::new(receiver_index, false, resp, init.as_remote(), &salt)
.expect("Test session creation failed");
// skip KKT by just deriving the kem key locally
let kem_keys = resp.kem_psq.as_ref().unwrap();
(initiator_session, responder_session)
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.private_key().as_bytes(),
)
.unwrap();
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
let libcrux_public_key = libcrux_kem::PublicKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.public_key().as_bytes(),
)
.unwrap();
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
// INIT -> RESP: PSQ MSG1
let psq_initiator = crate::psk::psq_initiator_create_message(
init.x25519.private_key(),
&resp_remote.x25519_public,
&encapsulation_key,
init.ed25519.private_key(),
init.ed25519.public_key(),
&salt,
&session_id_bytes,
)
.unwrap();
let psk = psq_initiator.psk;
let psq_payload = psq_initiator.payload;
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
.local_private_key(init.x25519().private_key().as_bytes())
.remote_public_key(resp_remote.x25519_public.as_bytes())
.psk(crate::NOISE_PSK_INDEX, &psk)
.build_initiator()
.unwrap();
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
let psq_responder = crate::psk::psq_responder_process_message(
resp.x25519.private_key(),
&init_remote.x25519_public,
(&decapsulation_key, &encapsulation_key),
&init_remote.ed25519_public,
&psq_payload,
&salt,
&session_id_bytes,
)
.unwrap();
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
.local_private_key(resp.x25519().private_key().as_bytes())
.remote_public_key(init_remote.x25519_public.as_bytes())
.psk(crate::NOISE_PSK_INDEX, &psk)
.build_responder()
.unwrap();
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
noise_protocol_resp.read_message(&noise_msg1).unwrap();
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
noise_protocol_init.read_message(&noise_msg2).unwrap();
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
assert!(noise_protocol_init.is_handshake_finished());
noise_protocol_resp.read_message(&noise_msg3).unwrap();
assert!(noise_protocol_resp.is_handshake_finished());
SessionsMock {
initiator: LpSession::new(
session_id,
1,
outer_aead_key.clone(),
init,
resp_remote,
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
noise_protocol_init,
),
responder: LpSession::new(
session_id,
1,
outer_aead_key,
resp,
init_remote,
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
noise_protocol_resp,
),
}
}
// we just need a dummy 'valid' session for simpler tests
pub fn mock_initiator() -> LpSession {
Self::mock_post_handshake(1234).initiator
}
}
#[cfg(any(feature = "mock", test))]
pub fn sessions_for_tests() -> (LpSession, LpSession) {
let sessions = SessionsMock::mock_post_handshake(69);
(sessions.initiator, sessions.responder)
}
#[cfg(any(feature = "mock", test))]
pub fn mock_session_for_test() -> LpSession {
SessionsMock::mock_initiator()
}
#[cfg(test)]
@@ -60,243 +162,218 @@ mod tests {
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::session_manager::SessionManager;
use crate::{LpError, kem_list, sessions_for_tests};
use crate::{LpError, SessionsMock, mock_session_for_test};
use bytes::BytesMut;
use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, SignatureScheme};
// Import the new standalone functions
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::peer::mock_peers;
#[test]
fn test_replay_protection_integration() {
for kem in kem_list() {
// Create session
let session = sessions_for_tests(kem).0;
// 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],
};
// === 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();
// 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();
// 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");
// 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");
// 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],
};
// === 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();
// 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();
// 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),
// 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));
}
// 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)
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();
for kem in kem_list() {
// Generate Ed25519 keypairs for PSQ authentication
let (init, resp) = mock_peers(kem);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
let mut ciphersuite = Ciphersuite::resolve_ciphersuite(
kem,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
);
let sessions = SessionsMock::mock_post_handshake(receiver_index);
let local_session = sessions.initiator;
let remote_session = sessions.responder;
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
// Create a session via manager
let _ = local_manager.create_session_state_machine(local_session);
let _ = remote_manager.create_session_state_machine(remote_session);
// Test salt
let salt = [46u8; 32];
// === 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],
};
// Create a session via manager
let _ = local_manager
.create_session_state_machine(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
)
.unwrap();
// Serialize
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
let _ = remote_manager
.create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt)
.unwrap();
// === 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],
};
// Parse
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Serialize
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut 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.
// Parse
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// 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");
// 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.
// === 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],
};
// 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");
// Serialize
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// === 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],
};
// Parse
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Serialize
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut 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");
// Parse
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// === 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],
};
// 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");
// Serialize
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// === 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],
};
// Parse
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// 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),
// 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));
}
// Do not mark received
e => panic!("Expected replay error for packet 3, got {:?}", e),
}
// Do not mark received
}
}
+220 -76
View File
@@ -1,17 +1,19 @@
// 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};
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, write};
use std::fmt::{self, Display};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
@@ -20,16 +22,25 @@ pub struct ClientHelloData {
pub client_lp_public_key: x25519::PublicKey,
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
pub client_ed25519_public_key: ed25519::PublicKey,
// noiserm
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
pub salt: [u8; 32],
}
impl ClientHelloData {
// noiserm (remove 32 bytes for salt)
// 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
}
@@ -43,7 +54,6 @@ impl ClientHelloData {
}
}
// noiserm
/// Generates a new ClientHelloData with fresh salt.
///
/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce
@@ -72,7 +82,7 @@ impl ClientHelloData {
salt,
}
}
// noiserm
/// Extracts the timestamp from the salt.
///
/// # Returns
@@ -87,7 +97,6 @@ impl ClientHelloData {
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());
// noiserm
dst.put_slice(&self.salt);
}
@@ -111,7 +120,6 @@ impl ClientHelloData {
client_ed25519_public_key: ed25519::PublicKey::from_byte_array(
client_ed25519_public_key_bytes,
)?,
// noiserm
salt: b[68..].try_into().unwrap(),
})
}
@@ -138,8 +146,6 @@ pub enum MessageType {
Ack = 0x0008,
/// Subsession request - client initiates subsession creation
SubsessionRequest = 0x0009,
// georgio: this should be the psq msg
/// Subsession KK1 - first message of Noise KK handshake
SubsessionKK1 = 0x000A,
/// Subsession KK2 - second message of Noise KK handshake
@@ -148,6 +154,8 @@ pub enum MessageType {
SubsessionReady = 0x000C,
/// Subsession abort - race winner tells loser to become responder
SubsessionAbort = 0x000D,
/// General error
Error = 0x00FF,
}
impl MessageType {
@@ -164,6 +172,9 @@ impl MessageType {
pub struct HandshakeData(pub Vec<u8>);
impl HandshakeData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -181,6 +192,11 @@ impl HandshakeData {
pub struct EncryptedDataPayload(pub Vec<u8>);
impl EncryptedDataPayload {
#[allow(dead_code)]
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -199,6 +215,10 @@ impl EncryptedDataPayload {
pub struct KKTRequestData(pub Vec<u8>);
impl KKTRequestData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -217,6 +237,10 @@ impl KKTRequestData {
pub struct KKTResponseData(pub Vec<u8>);
impl KKTResponseData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -230,39 +254,49 @@ impl KKTResponseData {
}
}
/// PSQ request frame data (serialized bytes)
/// General human-readable error message
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PSQRequestData(pub Vec<u8>);
impl PSQRequestData {
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(PSQRequestData(bytes.to_vec()))
}
pub struct ErrorPacketData {
pub message: String,
}
/// PSQ response frame data (serialized bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PSQResponseData(pub Vec<u8>);
impl ErrorPacketData {
pub(crate) fn new(message: impl Into<String>) -> Self {
ErrorPacketData {
message: message.into(),
}
}
impl PSQResponseData {
fn len(&self) -> usize {
self.0.len()
// length-encoding + message
4 + self.message.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
dst.put_u32_le(self.message.len() as u32);
dst.put_slice(self.message.as_bytes());
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(PSQResponseData(bytes.to_vec()))
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 })
}
}
@@ -272,9 +306,8 @@ pub struct ForwardPacketData {
/// Target gateway's Ed25519 identity (32 bytes)
pub target_gateway_identity: [u8; 32],
// TODO: replace it with `SocketAddr`
/// Target gateway's LP address (IP:port string)
pub target_lp_address: String,
pub target_lp_address: SocketAddr,
/// Complete inner LP packet bytes (serialized LpPacket)
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
@@ -282,23 +315,46 @@ pub struct ForwardPacketData {
}
impl ForwardPacketData {
pub fn new(
target_gateway_identity: ed25519::PublicKey,
target_lp_address: SocketAddr,
inner_packet_bytes: Vec<u8>,
) -> Self {
ForwardPacketData {
target_gateway_identity: target_gateway_identity.to_bytes(),
target_lp_address,
inner_packet_bytes,
}
}
fn len(&self) -> usize {
// 32 bytes target gateway identity
// +
// 4 bytes length of target lp address
// 1 byte length of target lp address type
// +
// target_lp_address.len()
// {4,16} target_lp_address IPv{4,6}
// +
// 2 bytes target_lp_address port
// +
// 4 bytes of length of inner packet bytes
// +
// inner_packet_bytes.len()
32 + 4 + self.target_lp_address.len() + 4 + self.inner_packet_bytes.len()
match self.target_lp_address {
SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(),
SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(),
}
}
fn encode(&self, dst: &mut BytesMut) {
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
};
dst.put_slice(&self.target_gateway_identity);
dst.put_u16_le(self.target_lp_address.len() as u16);
dst.put_slice(self.target_lp_address.as_bytes());
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
dst.put_slice(&ip_bytes); // IP bytes
dst.put_u16_le(self.target_lp_address.port()); // Port
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
dst.put_slice(&self.inner_packet_bytes);
}
@@ -310,42 +366,56 @@ impl ForwardPacketData {
}
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
// smallest possible packet with empty address and empty data
if bytes.len() < 38 {
// smallest possible packet with ipv4 and empty data
if bytes.len() < 43 {
// 32 + 1 + 4 + 2 + 4 + 0
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ForwardPacketData[1]. got {}",
"Too few bytes to deserialise ForwardPacketData. got {}",
bytes.len()
)));
}
// SAFETY: we ensured we have sufficient data
#[allow(clippy::unwrap_used)]
let target_gateway_identity = bytes[0..32].try_into().unwrap();
let target_lp_address_len = u16::from_le_bytes([bytes[32], bytes[33]]);
let target_lp_address_is_ipv6 = bytes[32] != 0;
// smallest possible packet with empty data
if bytes[34..].len() < 4 + target_lp_address_len as usize {
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ForwardPacketData[2]. got {}",
bytes.len()
)));
}
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
// IPv6, first check we have actually enough bytes
// smallest possible packet with ipv6 and empty data
if bytes.len() < 55 {
// 32 + 1 + 16 + 2 + 4 + 0
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
bytes.len()
)));
}
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
#[allow(clippy::unwrap_used)]
let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap()));
let port = u16::from_le_bytes([bytes[49], bytes[50]]);
(SocketAddr::new(ipv6, port), 51)
} else {
// IPv4. Length check done at the start
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
#[allow(clippy::unwrap_used)]
let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap()));
let port = u16::from_le_bytes([bytes[37], bytes[38]]);
(SocketAddr::new(ipv4, port), 39)
};
let target_lp_address =
String::from_utf8_lossy(&bytes[34..34 + target_lp_address_len as usize]).to_string();
let inner_packet_bytes_len = u32::from_le_bytes([
bytes[34 + target_lp_address_len as usize],
bytes[34 + target_lp_address_len as usize + 1],
bytes[34 + target_lp_address_len as usize + 2],
bytes[34 + target_lp_address_len as usize + 3],
bytes[next_index],
bytes[next_index + 1],
bytes[next_index + 2],
bytes[next_index + 3],
]);
if bytes[34 + target_lp_address_len as usize + 4..].len() != inner_packet_bytes_len as usize
{
if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize {
return Err(LpError::DeserializationError(format!(
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
bytes[34 + target_lp_address_len as usize + 4..].len()
bytes[next_index + 4..].len()
)));
}
let inner_packet_bytes = bytes[34 + target_lp_address_len as usize + 4..].to_vec();
let inner_packet_bytes = bytes[next_index + 4..].to_vec();
Ok(ForwardPacketData {
target_gateway_identity,
@@ -355,7 +425,6 @@ impl ForwardPacketData {
}
}
// georgio: swap with psq
/// Subsession KK1 message - first message of Noise KK handshake
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubsessionKK1Data {
@@ -436,8 +505,7 @@ impl SubsessionReadyData {
#[derive(Debug, Clone)]
pub enum LpMessage {
Busy,
PSQRequest(PSQRequestData),
PSQResponse(PSQResponseData),
Handshake(HandshakeData),
EncryptedData(EncryptedDataPayload),
ClientHello(ClientHelloData),
KKTRequest(KKTRequestData),
@@ -447,7 +515,6 @@ pub enum LpMessage {
Collision,
/// Acknowledgment - gateway confirms receipt of message
Ack,
// georgio: this should become psq stuff
/// Subsession request - client initiates subsession creation (empty, signal only)
SubsessionRequest,
/// Subsession KK1 - first message of Noise KK handshake
@@ -458,12 +525,69 @@ pub enum LpMessage {
SubsessionReady(SubsessionReadyData),
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
SubsessionAbort,
/// An error has occurred
Error(ErrorPacketData),
}
impl From<HandshakeData> for LpMessage {
fn from(value: HandshakeData) -> Self {
LpMessage::Handshake(value)
}
}
impl From<EncryptedDataPayload> for LpMessage {
fn from(value: EncryptedDataPayload) -> Self {
LpMessage::EncryptedData(value)
}
}
impl From<ClientHelloData> for LpMessage {
fn from(value: ClientHelloData) -> Self {
LpMessage::ClientHello(value)
}
}
impl From<KKTRequestData> for LpMessage {
fn from(value: KKTRequestData) -> Self {
LpMessage::KKTRequest(value)
}
}
impl From<KKTResponseData> for LpMessage {
fn from(value: KKTResponseData) -> Self {
LpMessage::KKTResponse(value)
}
}
impl From<ForwardPacketData> for LpMessage {
fn from(value: ForwardPacketData) -> Self {
LpMessage::ForwardPacket(value)
}
}
impl From<SubsessionKK1Data> for LpMessage {
fn from(value: SubsessionKK1Data) -> Self {
LpMessage::SubsessionKK1(value)
}
}
impl From<SubsessionKK2Data> for LpMessage {
fn from(value: SubsessionKK2Data) -> Self {
LpMessage::SubsessionKK2(value)
}
}
impl From<SubsessionReadyData> for LpMessage {
fn from(value: SubsessionReadyData) -> Self {
LpMessage::SubsessionReady(value)
}
}
impl Display for LpMessage {
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"),
@@ -476,16 +600,36 @@ impl Display for LpMessage {
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
LpMessage::PSQRequest(_) => write!(f, "PSQRequest"),
LpMessage::PSQResponse(_) => write!(f, "PSQResponse"),
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(),
@@ -498,16 +642,14 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => false, // Always has payload
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
LpMessage::SubsessionAbort => true, // Empty signal
LpMessage::PSQRequest(payload) => true, // Always had data (?)
LpMessage::PSQResponse(payload) => true, // Always had data (?)
LpMessage::Error(_) => false,
}
}
pub fn len(&self) -> usize {
match self {
LpMessage::Busy => 0,
LpMessage::PSQRequest(payload) => payload.len(),
LpMessage::PSQResponse(payload) => payload.len(),
LpMessage::Handshake(payload) => payload.len(),
LpMessage::EncryptedData(payload) => payload.len(),
LpMessage::ClientHello(payload) => payload.len(),
LpMessage::KKTRequest(payload) => payload.len(),
@@ -520,14 +662,14 @@ impl LpMessage {
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::PSQRequest(_) => todo!(),
LpMessage::PSQResponse(_) => todo!(),
LpMessage::Handshake(_) => MessageType::Handshake,
LpMessage::EncryptedData(_) => MessageType::EncryptedData,
LpMessage::ClientHello(_) => MessageType::ClientHello,
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
@@ -540,14 +682,14 @@ impl LpMessage {
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::PSQRequest(payload) => payload.encode(dst),
LpMessage::PSQResponse(payload) => payload.encode(dst),
LpMessage::Handshake(payload) => payload.encode(dst),
LpMessage::EncryptedData(payload) => payload.encode(dst),
LpMessage::ClientHello(data) => data.encode(dst),
LpMessage::KKTRequest(payload) => payload.encode(dst),
@@ -560,6 +702,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(data) => data.encode(dst),
LpMessage::SubsessionReady(data) => data.encode(dst),
LpMessage::SubsessionAbort => { /* No content - signal only */ }
LpMessage::Error(data) => data.encode(dst),
}
}
@@ -573,7 +716,7 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::Busy)
}
MessageType::Handshake => todo!(),
MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData::decode(content)?)),
MessageType::EncryptedData => Ok(LpMessage::EncryptedData(
EncryptedDataPayload::decode(content)?,
)),
@@ -612,6 +755,7 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::SubsessionAbort)
}
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
}
}
}
+47 -40
View File
@@ -82,6 +82,13 @@ pub enum ReadResult {
// --- Implementation ---
impl NoiseProtocol {
pub fn params() -> NoiseParams {
// SAFETY: the hardcoded pattern must be valid
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
#[allow(clippy::unwrap_used)]
crate::NOISE_PATTERN.parse().unwrap()
}
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
///
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
@@ -91,6 +98,46 @@ impl NoiseProtocol {
}
}
fn prepare_handshake_state<'a>(
local_private_key: &'a [u8],
remote_public_key: &'a [u8],
psk: &'a [u8],
) -> snow::Builder<'a> {
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params = NoiseProtocol::params();
snow::Builder::new(noise_params)
.local_private_key(local_private_key)
.remote_public_key(remote_public_key)
.psk(psk_index, psk)
}
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
/// remote public key and psk
pub fn build_new_initiator(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_initiator()?;
Ok(Self::new(handshake_state))
}
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
/// remote public key and psk
pub fn build_new_responder(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_responder()?;
Ok(Self::new(handshake_state))
}
/// Processes a single, complete incoming Noise message frame.
///
/// Assumes the caller handles buffering and framing to provide one full message.
@@ -288,43 +335,3 @@ impl NoiseProtocol {
}
}
}
pub fn create_noise_state(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<NoiseProtocol, NoiseError> {
let pattern_name = crate::NOISE_PATTERN;
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params: NoiseParams = pattern_name.parse().unwrap();
let builder = snow::Builder::new(noise_params.clone());
// Using dummy remote key as it's not needed for state creation itself
// In a real scenario, the key would depend on initiator/responder role
let handshake_state = builder
.local_private_key(local_private_key)
.remote_public_key(remote_public_key) // Use own public as dummy remote
.psk(psk_index, psk)
.build_initiator()?;
Ok(NoiseProtocol::new(handshake_state))
}
pub fn create_noise_state_responder(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<NoiseProtocol, NoiseError> {
let pattern_name = crate::NOISE_PATTERN;
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params: NoiseParams = pattern_name.parse().unwrap();
let builder = snow::Builder::new(noise_params.clone());
// Using dummy remote key as it's not needed for state creation itself
// In a real scenario, the key would depend on initiator/responder role
let handshake_state = builder
.local_private_key(local_private_key)
.remote_public_key(remote_public_key) // Use own public as dummy remote
.psk(psk_index, psk)
.build_responder()?;
Ok(NoiseProtocol::new(handshake_state))
}
+8 -4
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::LpError;
use crate::message::LpMessage;
use crate::message::{LpMessage, MessageType};
use crate::replay::ReceivingKeyCounterValidator;
use bytes::{BufMut, BytesMut};
use nym_lp_common::format_debug_bytes;
@@ -53,6 +53,10 @@ impl LpPacket {
}
}
pub fn typ(&self) -> MessageType {
self.message.typ()
}
/// Compute a hash of the message payload
///
/// This can be used for message integrity verification or deduplication
@@ -203,9 +207,9 @@ impl LpHeader {
}
impl LpHeader {
pub fn new(receiver_idx: u32, counter: u64) -> Self {
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
Self {
protocol_version: version::CURRENT,
protocol_version,
reserved: [0u8; 3],
receiver_idx,
counter,
@@ -265,7 +269,7 @@ impl LpHeader {
Ok(LpHeader {
protocol_version,
reserved: [0u8; 3],
reserved,
receiver_idx,
counter,
})
+95 -115
View File
@@ -1,59 +1,46 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use libcrux_psq::handshake::Responder;
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
use crate::{ClientHelloData, LpError};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{
Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KEMKeyDigests, KemKeyPair,
SignatureScheme, SigningKeyDigests,
};
use rand::rngs::ThreadRng;
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use crate::psq::build_responder;
/// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct LpLocalPeer {
pub(crate) ciphersuite: Ciphersuite,
/// Local Ed25519 keys for PSQ authentication
pub(crate) ed25519: Arc<ed25519::KeyPair>,
/// Local x25519 keys (Noise static key)
pub(crate) x25519: Arc<DHKeyPair>,
pub(crate) x25519: Arc<x25519::KeyPair>,
/// Local KEM keys used for PSQ
pub(crate) kem_keypairs: HashMap<KEM, Arc<KemKeyPair>>,
/// Local KEM key used for PSQ
pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>,
}
impl LpLocalPeer {
pub fn new(
ciphersuite: Ciphersuite,
ed25519: Arc<ed25519::KeyPair>,
x25519: Arc<x25519::KeyPair>,
) -> Self {
// TODO: make nicer conversion (without cloning) + error handling
let initiator_libcrux_x25519_private_key =
DHPrivateKey::from_bytes(x25519.private_key().as_bytes()).unwrap();
let initiator_x25519_keypair = DHKeyPair::from(initiator_libcrux_x25519_private_key);
pub fn new(ed25519: Arc<ed25519::KeyPair>, x25519: Arc<x25519::KeyPair>) -> Self {
LpLocalPeer {
ciphersuite,
ed25519,
x25519: Arc::new(initiator_x25519_keypair),
kem_keypairs: Default::default(),
x25519,
kem_psq: None,
}
}
pub fn with_kem_keypair(mut self, keypair: Arc<KemKeyPair>) -> Self {
let kem = keypair.kem();
self.kem_keypairs.insert(kem, keypair);
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
ClientHelloData::new_with_fresh_salt(
*self.x25519().public_key(),
*self.ed25519().public_key(),
timestamp,
)
}
#[must_use]
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
self.kem_psq = Some(key);
self
}
@@ -61,48 +48,55 @@ impl LpLocalPeer {
&self.ed25519
}
pub fn x25519(&self) -> &Arc<DHKeyPair> {
pub fn x25519(&self) -> &Arc<x25519::KeyPair> {
&self.x25519
}
pub fn kem_key(&self, kem: KEM) -> Option<&Arc<KemKeyPair>> {
self.kem_keypairs.get(&kem)
/// 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 mut expected_kem_key_digests = HashMap::new();
for (kem, kem_key) in &self.kem_keypairs {
expected_kem_key_digests.insert(
*kem,
nym_kkt::key_utils::produce_key_digests(&kem_key.encoded_encapsulation_key()),
);
}
LpRemotePeer {
ed25519_public: *self.ed25519.public_key(),
x25519_public: self.x25519.pk,
x25519_public: *self.x25519.public_key(),
expected_kem_key_digests,
expected_signing_key_digests,
}
}
}
impl Debug for LpLocalPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LpLocalPeer")
.field("ciphersuite", &self.ciphersuite)
.field("ed25519", &self.ed25519)
.field("x25519", &self.x25519.pk)
.field("kem_keypairs", &self.kem_keypairs)
.finish()
// this is only exposed in tests as ideally we should be storing the proper types to begin with
#[cfg(test)]
pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey<'_>> {
let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes();
let libcrux_pk =
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?;
Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk))
}
}
@@ -114,7 +108,7 @@ pub struct LpRemotePeer {
pub(crate) ed25519_public: ed25519::PublicKey,
/// Remote X25519 public key (Noise static key)
pub(crate) x25519_public: DHPublicKey,
pub(crate) x25519_public: x25519::PublicKey,
/// Expected digests of the remote's KEM key
pub(crate) expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
@@ -125,11 +119,9 @@ pub struct LpRemotePeer {
impl LpRemotePeer {
pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self {
// TODO: make nicer conversion (without cloning) + error handling
let responder_x25519_public_key = DHPublicKey::from_bytes(x25519_public.as_bytes());
LpRemotePeer {
ed25519_public,
x25519_public: responder_x25519_public_key,
x25519_public,
expected_kem_key_digests: Default::default(),
expected_signing_key_digests: Default::default(),
}
@@ -139,8 +131,8 @@ impl LpRemotePeer {
self.ed25519_public
}
pub fn x25519(&self) -> &DHPublicKey {
&self.x25519_public
pub fn x25519(&self) -> x25519::PublicKey {
self.x25519_public
}
#[must_use]
@@ -153,65 +145,53 @@ impl LpRemotePeer {
self.expected_signing_key_digests = expected_signing_key_digests;
self
}
}
#[cfg(test)]
pub fn mock_peer(kem: KEM) -> LpLocalPeer {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
/// Attempt to retrieve expected KEM key hash of the remote
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
pub(crate) fn expected_kem_key_hash(
&self,
ciphersuite: Ciphersuite,
) -> Result<Vec<u8>, LpError> {
let kem = ciphersuite.kem();
let hash_function = ciphersuite.hash_function();
let ciphersuite = Ciphersuite::new(
kem,
nym_kkt::ciphersuite::HashFunction::Blake3,
SignatureScheme::Ed25519,
nym_kkt::ciphersuite::HashLength::Default,
);
let digests = self
.expected_kem_key_digests
.get(&kem)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
random_peer(&mut rng, ciphersuite)
}
#[cfg(test)]
pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>(
rng: &mut R,
ciphersuite: Ciphersuite,
) -> LpLocalPeer {
use nym_kkt::key_utils::{generate_keypair_mceliece, generate_keypair_mlkem};
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let mut sk = [0u8; 32];
rng.fill_bytes(&mut sk);
// clamp
sk[0] &= 248u8;
sk[31] &= 127u8;
sk[31] |= 64u8;
let x25519 = Arc::new(DHKeyPair::from(DHPrivateKey::from_bytes(&sk).unwrap()));
let default_peer = LpLocalPeer {
ciphersuite: Arc::new(ciphersuite),
ed25519,
x25519,
mlkem: None,
mceliece: None,
};
match ciphersuite.kem() {
KEM::MlKem768 => {
let mlkem_keypair = generate_keypair_mlkem(&mut rand09::rng());
default_peer.with_mlkem_keypair(&mlkem_keypair.0, &mlkem_keypair.1)
}
KEM::McEliece => {
let mceliece_keypair = generate_keypair_mceliece(&mut rand09::rng());
default_peer.with_mceliece_keypair(mceliece_keypair.0, mceliece_keypair.1)
}
_ => unreachable!(),
digests
.get(&hash_function)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
.cloned()
}
}
#[cfg(test)]
pub fn mock_peers(kem: KEM) -> (LpLocalPeer, LpLocalPeer) {
println!("KEM: {:?}", kem);
(mock_peer(kem), mock_peer(kem))
#[cfg(any(feature = "mock", test))]
pub fn mock_peer() -> LpLocalPeer {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
random_peer(&mut rng)
}
#[cfg(any(feature = "mock", test))]
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let x25519 = Arc::new(ed25519.to_x25519());
let kem_psq = Some(x25519.clone());
LpLocalPeer {
ed25519,
x25519,
kem_psq,
}
}
#[cfg(any(feature = "mock", test))]
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
(random_peer(&mut rng), random_peer(&mut rng))
}
+80 -63
View File
@@ -47,12 +47,11 @@
//! - **No cleanup needed**: No state was mutated
use crate::LpError;
use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey};
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;
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};
@@ -137,15 +136,13 @@ pub struct PsqResponderResult {
/// // Send ciphertext to gateway
/// ```
pub fn derive_psk_with_psq_initiator(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
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);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
// Step 2: PSQ encapsulation for post-quantum security
// KEM algorithm migration path:
@@ -174,7 +171,7 @@ pub fn derive_psk_with_psq_initiator(
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
combined.extend_from_slice(salt);
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
// Serialize ciphertext using TLS encoding for transport
let ct_bytes = ciphertext
@@ -222,16 +219,14 @@ pub fn derive_psk_with_psq_initiator(
/// )?;
/// ```
pub fn derive_psk_with_psq_responder(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
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);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
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) {
@@ -257,7 +252,7 @@ pub fn derive_psk_with_psq_responder(
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
combined.extend_from_slice(salt);
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
Ok(final_psk)
}
@@ -284,8 +279,8 @@ pub fn derive_psk_with_psq_responder(
/// # Returns
/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret
pub fn psq_initiator_create_message(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
local_x25519_private: &x25519::PrivateKey,
remote_x25519_public: &x25519::PublicKey,
remote_kem_public: &EncapsulationKey,
client_ed25519_sk: &ed25519::PrivateKey,
client_ed25519_pk: &ed25519::PublicKey,
@@ -293,9 +288,7 @@ pub fn psq_initiator_create_message(
session_context: &[u8],
) -> Result<PsqInitiatorResult, LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
// Step 2: PSQ v1 with Ed25519 authentication
// Extract X25519 KEM key from EncapsulationKey
@@ -345,7 +338,7 @@ pub fn psq_initiator_create_message(
combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32]
combined.extend_from_slice(salt);
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
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
@@ -381,8 +374,8 @@ pub fn psq_initiator_create_message(
/// # Returns
/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret
pub fn psq_responder_process_message(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
local_x25519_private: &x25519::PrivateKey,
remote_x25519_public: &x25519::PublicKey,
local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
initiator_ed25519_pk: &ed25519::PublicKey,
psq_payload: &[u8],
@@ -390,8 +383,7 @@ pub fn psq_responder_process_message(
session_context: &[u8],
) -> Result<PsqResponderResult, LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
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) {
@@ -455,7 +447,7 @@ pub fn psq_responder_process_message(
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
combined.extend_from_slice(salt);
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
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;
@@ -490,7 +482,7 @@ pub fn psq_responder_process_message(
/// # 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::hkdf::blake3::derive_key_blake3(
nym_crypto::kdf::derive_key_blake3(
SUBSESSION_PSK_CONTEXT,
pq_shared_secret,
&subsession_index.to_le_bytes(),
@@ -500,10 +492,10 @@ pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64)
#[cfg(test)]
mod tests {
use super::*;
use libcrux_psq::handshake::types::DHKeyPair;
use rand::thread_rng;
fn generate_x25519_keypair() -> DHKeyPair {
DHKeyPair::new(&mut rand09::rng())
fn generate_x25519_keypair() -> x25519::KeyPair {
x25519::KeyPair::new(&mut thread_rng())
}
#[test]
@@ -518,13 +510,18 @@ mod tests {
let dec_key = DecapsulationKey::X25519(_kem_sk);
// Client derives PSK
let (client_psk, ciphertext) =
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
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.sk(),
&keypair_1.pk,
keypair_2.private_key(),
keypair_1.public_key(),
(&dec_key, &enc_key),
&ciphertext,
&salt,
@@ -548,10 +545,20 @@ mod tests {
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.sk(), &keypair_2.pk, &enc_key, &salt1).unwrap();
let psk2 =
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt2).unwrap();
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");
}
@@ -567,10 +574,20 @@ mod tests {
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.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
let psk2 =
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_3.pk, &enc_key, &salt).unwrap();
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,
@@ -599,16 +616,16 @@ mod tests {
// Derive PSK twice with same inputs (initiator side)
let (_psk1, ct1) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt,
)
.unwrap();
let (_psk2, _ct2) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt,
)
@@ -617,8 +634,8 @@ mod tests {
// 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.sk(),
&client_keypair.pk,
gateway_keypair.private_key(),
client_keypair.public_key(),
(&dec_key, &enc_key),
&ct1,
&salt,
@@ -626,8 +643,8 @@ mod tests {
.unwrap();
let psk_responder2 = derive_psk_with_psq_responder(
gateway_keypair.sk(),
&client_keypair.pk,
gateway_keypair.private_key(),
client_keypair.public_key(),
(&dec_key, &enc_key),
&ct1, // Same ciphertext
&salt,
@@ -657,8 +674,8 @@ mod tests {
// Client derives PSK (initiator)
let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt,
)
@@ -666,8 +683,8 @@ mod tests {
// Gateway derives PSK from ciphertext (responder)
let gateway_psk = derive_psk_with_psq_responder(
gateway_keypair.sk(),
&client_keypair.pk,
gateway_keypair.private_key(),
client_keypair.public_key(),
(&dec_key, &enc_key),
&ciphertext,
&salt,
@@ -697,16 +714,16 @@ mod tests {
let salt = [3u8; 32];
let (psk1, _) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key1,
&salt,
)
.unwrap();
let (psk2, _) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key2,
&salt,
)
@@ -731,8 +748,8 @@ mod tests {
let salt = [4u8; 32];
let (psk, _) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt,
)
@@ -755,16 +772,16 @@ mod tests {
let salt2 = [2u8; 32];
let (psk1, _) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt1,
)
.unwrap();
let (psk2, _) = derive_psk_with_psq_initiator(
client_keypair.sk(),
&gateway_keypair.pk,
client_keypair.private_key(),
gateway_keypair.public_key(),
&enc_key,
&salt2,
)
-134
View File
@@ -1,134 +0,0 @@
use libcrux_psq::{
Channel,
handshake::{
RegistrationInitiator, Responder,
builders::{CiphersuiteBuilder, PrincipalBuilder},
ciphersuites::CiphersuiteName,
types::{DHKeyPair, DHPublicKey},
},
};
use nym_kkt::ciphersuite::{Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KemKeyPair};
use rand09::rngs::ThreadRng;
use std::fmt::Debug;
const AAD_INITIATOR_OUTER: &[u8] = b"Test Data I Outer";
const AAD_INITIATOR_INNER: &[u8] = b"Test Data I Inner";
const AAD_RESPONDER: &[u8] = b"Test Data R";
const SESSION_CONTEXT: &[u8] = b"Test Context";
pub enum PSQState<'a> {
Initiator(RegistrationInitiator<'a, ThreadRng>),
Responder(Responder<'a, ThreadRng>),
}
impl Debug for PSQState<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Initiator(_) => f.debug_tuple("PSQ Initiator").finish(),
Self::Responder(_) => f.debug_tuple("PSQ Responder").finish(),
}
}
}
pub fn initiator_process(initiator: &mut RegistrationInitiator<ThreadRng>) -> Vec<u8> {
let mut buffer = vec![0u8; 4096];
let msg_len = initiator.write_message(b"", &mut buffer).unwrap();
buffer.resize(msg_len, 0);
buffer
}
pub fn build_initiator<'a>(
ciphersuite: &'a Ciphersuite,
session_context: &'a [u8],
local_x25519_keys: &'a DHKeyPair,
remote_x25519_public: &'a DHPublicKey,
remote_kem_public: &'a EncapsulationKey,
) -> RegistrationInitiator<'a, rand09::rngs::ThreadRng> {
//georgio: handle errors
let initiator_cbuilder = match ciphersuite.kem() {
nym_kkt::ciphersuite::KEM::MlKem768 => match remote_kem_public {
EncapsulationKey::MlKem768(ml_kem_public_key) => CiphersuiteBuilder::new(
CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256,
)
.peer_longterm_mlkem_pk(ml_kem_public_key),
_ => panic!(
"wrong key type passed (remote_kem_public should be EncapsulationKey::MlKem768)"
),
},
nym_kkt::ciphersuite::KEM::McEliece => match remote_kem_public {
EncapsulationKey::McEliece(mceliece_public_key) => CiphersuiteBuilder::new(
CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256,
)
.peer_longterm_cmc_pk(mceliece_public_key),
_ => panic!(
"wrong key type passed (remote_kem_public should be EncapsulationKey::McEliece)"
),
},
_ => panic!("undefined"),
};
let initiator_ciphersuite = initiator_cbuilder
.longterm_x25519_keys(local_x25519_keys)
.peer_longterm_x25519_pk(remote_x25519_public)
.build_initiator_ciphersuite()
.unwrap();
PrincipalBuilder::new(rand09::rng())
.outer_aad(AAD_INITIATOR_OUTER)
.inner_aad(AAD_INITIATOR_INNER)
.context(session_context)
.build_registration_initiator(initiator_ciphersuite)
.unwrap()
}
// JS: I have removed the `ciphersuite` argument as it was only matching on the key types,
// which we already obtained matching on the ciphersuite kem type in `LpSession::new`
pub fn build_responder<'a>(
local_x25519_keys: &'a DHKeyPair,
local_kem_keys: &'a KemKeyPair,
) -> Responder<'a, rand09::rngs::ThreadRng> {
let responder_ciphersuite = match local_kem_keys {
KemKeyPair::MlKem768 {
encapsulation_key,
decapsulation_key,
} => CiphersuiteBuilder::new(
CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256,
)
.longterm_mlkem_encapsulation_key(encapsulation_key)
.longterm_mlkem_decapsulation_key(decapsulation_key),
KemKeyPair::McEliece {
encapsulation_key,
decapsulation_key,
} => CiphersuiteBuilder::new(
CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256,
)
.longterm_cmc_encapsulation_key(encapsulation_key)
.longterm_cmc_decapsulation_key(decapsulation_key),
KemKeyPair::XWing { .. } => panic!("unsupported"),
KemKeyPair::X25519 { .. } => panic!("unsupported"),
}
.longterm_x25519_keys(local_x25519_keys)
.build_responder_ciphersuite()
.unwrap();
PrincipalBuilder::new(rand09::rng())
.outer_aad(AAD_RESPONDER)
.context(SESSION_CONTEXT)
.build_responder(responder_ciphersuite)
.unwrap()
}
pub fn psq_responder_process<'a>(
responder: &'a mut Responder<ThreadRng>,
initiator_message: &[u8],
) -> Vec<u8> {
let mut payload = vec![0u8; 4096];
responder
.read_message(initiator_message, &mut payload)
.unwrap();
let mut buffer = vec![0u8; 4096];
let msg_len = responder.write_message(b"", &mut buffer).unwrap();
buffer.resize(msg_len, 0);
buffer
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
use crate::{LpError, LpPacket};
use bytes::BytesMut;
use nym_lp_transport::traits::LpTransport;
#[cfg(test)]
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
#[cfg(not(test))]
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
.map(|d| d.as_secs())
}
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransportHandshakeExt: LpTransport {
// the outer key is temporary until the algorithm is changed with psqv2
async fn receive_packet(
&mut self,
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError>
where
Self: Unpin,
{
let raw = self.receive_raw_packet().await?;
parse_lp_packet(&raw, outer_key)
}
async fn send_packet(
&mut self,
packet: LpPacket,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError>
where
Self: Unpin,
{
let mut packet_buf = BytesMut::new();
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
self.send_serialised_packet(&packet_buf).await?;
Ok(())
}
}
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
+391
View File
@@ -0,0 +1,391 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::{HandshakeData, KKTRequestData, MessageType};
use crate::noise_protocol::NoiseProtocol;
use crate::peer::LpRemotePeer;
use crate::psk::psq_initiator_create_message;
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::EncapsulationKey;
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
use nym_lp_transport::traits::LpTransport;
use rand09::rng;
use tracing::debug;
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
/// Generate and send client hello to the responder
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
let protocol = self.protocol_version()?;
// 1. Generate and send ClientHelloData with fresh salt and both public keys
let timestamp = current_timestamp()?;
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
self.connection
.send_packet(client_hello_data.into_lp_packet(protocol), None)
.await?;
Ok(client_hello_data)
}
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
/// whether the request has been successful or whether there has been a collision in receiver
/// index requiring a retry
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
match self.receive_non_error(None).await?.message {
LpMessage::Ack => Ok(true),
LpMessage::Collision => Ok(false),
other => {
// TODO: retry on collision
Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Ack,
))
}
}
}
/// Attempt to send KKT request to begin the handshake
pub(crate) async fn send_kkt_request(
&mut self,
session_id: u32,
remote_peer: &LpRemotePeer,
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
let protocol = self.protocol_version()?;
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
let (session_secret, encrypted_frame) =
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
let lp_message = KKTRequestData::new(encrypted_frame).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok((kkt_context, session_secret))
}
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
/// the received encapsulation key
pub(crate) async fn receive_kkt_response(
&mut self,
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
remote_peer: &LpRemotePeer,
) -> Result<EncapsulationKey<'static>, LpError> {
let kkt_response = match self.receive_non_error(None).await?.message {
LpMessage::KKTResponse(response) => response,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::KKTResponse,
));
}
};
debug!("received KKT response");
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
let (response_frame, remote_context) =
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
let encapsulation_key = initiator_ingest_response(
&kkt_context,
&response_frame,
&remote_context,
&remote_peer.ed25519_public,
&expected_kem_key_digest,
)?;
Ok(encapsulation_key)
}
/// Attempt to prepare and send initial PSQ msg1
pub(crate) async fn send_psq_initiator_message(
&mut self,
remote_peer: &LpRemotePeer,
encapsulation_key: &EncapsulationKey<'_>,
salt: &[u8; 32],
session_id_bytes: &[u8; 4],
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
let protocol = self.protocol_version()?;
let session_id = u32::from_le_bytes(*session_id_bytes);
let psq_initiator = psq_initiator_create_message(
self.local_peer.x25519.private_key(),
&remote_peer.x25519_public,
encapsulation_key,
self.local_peer.ed25519.private_key(),
self.local_peer.ed25519.public_key(),
salt,
session_id_bytes,
)?;
let psk = psq_initiator.psk;
let psq_payload = psq_initiator.payload;
// TEMP \/
let outer_aead_key = OuterAeadKey::from_psk(&psk);
// TEMP /\
// prepare noise state and msg1
let mut noise_protocol = NoiseProtocol::build_new_initiator(
self.local_peer.x25519().private_key().as_bytes(),
remote_peer.x25519_public.as_bytes(),
&psk,
)?;
// prepare noise msg1
let noise_msg1 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
let psq_len = psq_payload.len() as u16;
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
combined.extend_from_slice(&psq_len.to_le_bytes());
combined.extend_from_slice(&psq_payload);
combined.extend_from_slice(&noise_msg1);
let lp_message = HandshakeData::new(combined).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok((
outer_aead_key,
noise_protocol,
PqSharedSecret::new(psq_initiator.pq_shared_secret),
))
}
/// Attempt to receive and validate received PSQ msg2
pub(crate) async fn receive_psq_responder_message(
&mut self,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let psq_msg2 = match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
if psq_msg2.len() < 2 {
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
}
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
if psq_msg2.len() < 2 + handle_len {
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
}
// Extract and "store" the PSK handle
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
let noise_payload = &psq_msg2[2 + handle_len..];
// *sigh* ignore the message
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
Ok(())
}
/// Attempt to prepare and send final PSQ msg3
pub(crate) async fn send_final_psq_message(
&mut self,
session_id: u32,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let noise_msg3 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
let lp_message = HandshakeData::new(noise_msg3).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection
.send_packet(lp_packet, Some(outer_aead_key))
.await?;
if !noise_protocol.is_handshake_finished() {
return Err(LpError::kkt_psq_handshake(
"noise handshake not finished after msg3",
));
}
Ok(())
}
/// Receive final ACK that indicates finalisation of the handshake
pub(crate) async fn receive_final_ack(
&mut self,
outer_aead_key: &OuterAeadKey,
) -> Result<(), LpError> {
match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Ack => Ok(()),
other => Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Ack,
)),
}
}
async fn complete_as_initiator_inner(
&mut self,
) -> Result<LpSession, IntermediateHandshakeFailure>
where
S: LpTransport + Unpin,
{
// 0. retrieve the expected kem key hash. if we don't know it,
// there's no point in even trying to start the handshake
let Some(remote_peer) = self.remote_peer.take() else {
return Err(IntermediateHandshakeFailure::plain(
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
));
};
// 1. Generate and send ClientHelloData with fresh salt and both public keys
// and keep retrying until we manage to establish a receiver index without collisions
let mut attempt = 0;
let client_hello_data = loop {
attempt += 1;
debug!("sending client hello");
let client_hello = self
.send_client_hello()
.await
.map_err(IntermediateHandshakeFailure::plain)?;
if self
.receive_client_hello_ack()
.await
.map_err(IntermediateHandshakeFailure::plain)?
{
debug!("received client hello ACK");
break client_hello;
}
debug!("received client hello collision");
// TODO: make it configurable
if attempt > 3 {
return Err(IntermediateHandshakeFailure::plain(
LpError::kkt_psq_handshake(
"failed to establish receiver index without collision",
),
));
}
};
let session_id = client_hello_data.receiver_index;
let session_id_bytes = session_id.to_le_bytes();
let salt = client_hello_data.salt;
// 3. prepare and send KKT request
debug!("sending KKT request");
let kkt_data = self
.send_kkt_request(session_id, &remote_peer)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 4. receive and process KKT response
let encapsulation_key = self
.receive_kkt_response(kkt_data, &remote_peer)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received KKT response");
// 5. prepare and send PSQ msg1
debug!("sending PSQ msg1");
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 6. receive and process PSQ msg2
debug!("received PSQ msg2");
if let Err(source) = self
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 7. prepare and send PSQ msg3
debug!("sending PSQ msg3");
if let Err(source) = self
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 8. receive final ACK and finalise
debug!("received final ACK");
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
#[allow(clippy::expect_used)]
Ok(LpSession::new(
session_id,
self.protocol_version()
.expect("protocol version is known at this point"),
outer_aead_key,
self.local_peer.clone(),
remote_peer,
pq_shared_secret,
noise_protocol,
))
}
// TODO: missing: receive counter check
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
{
match self.complete_as_initiator_inner().await {
Ok(res) => Ok(res),
Err(err) => Err(self.try_send_error_packet(err).await),
}
}
}
+340
View File
@@ -0,0 +1,340 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::ErrorPacketData;
use crate::packet::LpHeader;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::psq::helpers::LpTransportHandshakeExt;
use crate::{LpError, LpMessage, LpPacket};
use nym_kkt::ciphersuite::Ciphersuite;
use nym_lp_transport::traits::LpTransport;
use tracing::debug;
mod helpers;
mod initiator;
mod responder;
pub(crate) struct IntermediateHandshakeFailure {
/// Session id established during exchange if we managed to derive it
session_id: Option<u32>,
/// Protocol version established during the exchange
protocol_version: Option<u8>,
/// Outer aead key established during exchange if we managed to derive it
outer_aead_key: Option<OuterAeadKey>,
/// The error source
source: LpError,
}
impl IntermediateHandshakeFailure {
fn plain(source: LpError) -> IntermediateHandshakeFailure {
IntermediateHandshakeFailure {
session_id: None,
protocol_version: None,
outer_aead_key: None,
source,
}
}
}
pub struct PSQHandshakeState<'a, S> {
/// The underlying connection established for the handshake
connection: &'a mut S,
/// Protocol version used for the exchange.
/// either known implicitly through the directory (initiator)
/// or established through client hello (responder)
protocol_version: Option<u8>,
/// Ciphersuite selected for the KKT/PSQ exchange
ciphersuite: Ciphersuite,
/// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys.
local_peer: LpLocalPeer,
/// Representation of a remote Lewes Protocol peer
/// encapsulating all the known information and keys.
remote_peer: Option<LpRemotePeer>,
/// Counter for outgoing packets
sending_counter: u64,
}
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
PSQHandshakeState {
connection,
protocol_version: None,
ciphersuite,
local_peer,
remote_peer: None,
sending_counter: 0,
}
}
#[must_use]
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
self.protocol_version = Some(protocol_version);
self
}
#[must_use]
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
self.remote_peer = Some(remote_peer);
self
}
fn protocol_version(&self) -> Result<u8, LpError> {
self.protocol_version
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
}
/// Generates the next counter value for outgoing packets.
pub fn next_counter(&mut self) -> u64 {
let counter = self.sending_counter;
self.sending_counter += 1;
counter
}
pub fn next_packet(
&mut self,
session_id: u32,
protocol_version: u8,
message: LpMessage,
) -> LpPacket {
let counter = self.next_counter();
let header = LpHeader::new(session_id, counter, protocol_version);
LpPacket::new(header, message)
}
pub(crate) async fn try_send_error_packet(
&mut self,
err: IntermediateHandshakeFailure,
) -> LpError {
// if session_id is not known, we can't send the packet back (with the current design)
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
return err.source;
};
if let Err(err) = self
.send_error_packet(
session_id,
protocol,
err.source.to_string(),
err.outer_aead_key.as_ref(),
)
.await
{
debug!("failed to send back error response: {err}")
}
err.source
}
/// Attempt to send an error packet
pub(crate) async fn send_error_packet(
&mut self,
session_id: u32,
protocol_version: u8,
msg: impl Into<String>,
outer_aead_key: Option<&OuterAeadKey>,
) -> Result<(), LpError> {
let packet = self.next_packet(
session_id,
protocol_version,
LpMessage::Error(ErrorPacketData::new(msg)),
);
self.connection.send_packet(packet, outer_aead_key).await?;
Ok(())
}
/// Attempt to receive a packet from connection, explicitly checking for an error response
/// and returning corresponding message if received
pub(crate) async fn receive_non_error(
&mut self,
outer_aead_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError> {
let packet = self.connection.receive_packet(outer_aead_key).await?;
match &packet.message {
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
"remote error: {}",
error_packet.message
))),
_ => Ok(packet),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer::mock_peers;
use crate::psq::helpers::LpTransportHandshakeExt;
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
use mock_instant::thread_local::MockClock;
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
use nym_test_utils::mocks::async_read_write::MockIOStream;
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
use std::time::Duration;
use tokio::join;
#[allow(dead_code)]
async fn extract_error(conn: &mut MockIOStream) -> String {
let packet = conn.receive_packet(None).await.unwrap();
match packet.message {
LpMessage::Error(error) => error.message,
_ => panic!("non error packet"),
}
}
#[tokio::test]
async fn e2e_psq_handshake() -> anyhow::Result<()> {
let conn_init = MockIOStream::default();
let conn_resp = conn_init.try_get_remote_handle();
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
// so they'd get 'static lifetime
let conn_init = conn_init.leak();
let conn_resp = conn_resp.leak();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (init, resp) = mock_peers();
let resp_remote = resp.as_remote();
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
.with_protocol_version(1)
.with_remote_peer(resp_remote);
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
let (session_init, session_resp) = join!(init_fut, resp_fut);
let session_init = session_init???;
let session_resp = session_resp???;
assert_eq!(session_init.id(), session_resp.id());
assert_eq!(
session_init.outer_aead_key().as_bytes(),
session_resp.outer_aead_key().as_bytes()
);
assert_eq!(
session_init.pq_shared_secret().as_bytes(),
session_resp.pq_shared_secret().as_bytes()
);
Ok(())
}
#[tokio::test]
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (init, resp) = mock_peers();
let resp_remote = resp.as_remote();
// as initiator
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
.with_protocol_version(1)
.with_remote_peer(resp_remote);
// you can generate and send (valid) client hello as initiator
let client_hello = handshake_init.send_client_hello().await?;
let LpMessage::ClientHello(received_client_hello) =
conn_resp.receive_packet(None).await?.message
else {
panic!("wrong message type");
};
assert_eq!(client_hello, received_client_hello);
Ok(())
}
// essentially make sure you can't accidentally trigger the handshake as the responder
#[tokio::test]
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
let conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (_, resp) = mock_peers();
// as initiator
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
// you can generate and send (valid) client hello as initiator
let sending_res = handshake_resp.send_client_hello().await;
assert!(sending_res.is_err());
Ok(())
}
#[tokio::test]
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
let current_time = Duration::from_secs(10000);
MockClock::set_system_time(current_time);
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
// TOO OLD
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let (init, resp) = mock_peers();
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
conn_init
.send_packet(client_hello_too_old.into_lp_packet(1), None)
.await?;
let err = handshake_resp.receive_client_hello().await.unwrap_err();
assert!(err.to_string().contains("too old"));
// TOO RECENT
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let (init, resp) = mock_peers();
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
conn_init
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
.await?;
let err = handshake_resp.receive_client_hello().await.unwrap_err();
assert!(err.to_string().contains("too future"));
Ok(())
}
}
+461
View File
@@ -0,0 +1,461 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::{HandshakeData, KKTResponseData, MessageType};
use crate::noise_protocol::NoiseProtocol;
use crate::peer::LpRemotePeer;
use crate::psk::psq_responder_process_message;
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
use nym_kkt::frame::KKTSessionId;
use nym_kkt::session::{responder_ingest_message, responder_process};
use nym_lp_transport::traits::LpTransport;
use rand09::rng;
use std::time::Duration;
use tracing::debug;
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
fn validate_client_hello_timestamp(
client_timestamp: u64,
tolerance: Duration,
) -> Result<(), LpError> {
let now = current_timestamp()?;
let age = now.abs_diff(client_timestamp);
if age > tolerance.as_secs() {
let direction = if now >= client_timestamp {
"old"
} else {
"future"
};
return Err(LpError::kkt_psq_handshake(format!(
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
tolerance.as_secs()
)));
}
Ok(())
}
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
pub(crate) fn encapsulated_kem_keys(
&self,
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
let kem_keys = self
.local_peer
.kem_psq
.as_ref()
.ok_or(LpError::ResponderWithMissingKEMKey)?;
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.private_key().as_bytes(),
)
.map_err(|e| {
LpError::KKTError(format!(
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
))
})?;
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
let libcrux_public_key = libcrux_kem::PublicKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.public_key().as_bytes(),
)
.map_err(|e| {
LpError::KKTError(format!(
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
))
})?;
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
Ok((dec_key, enc_key))
}
/// Attempt to receive and validate ClientHello
pub(crate) async fn receive_client_hello(
&mut self,
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
let client_hello_packet = self.receive_non_error(None).await?;
let client_hello = match client_hello_packet.message {
LpMessage::ClientHello(client_hello) => client_hello,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::ClientHello,
));
}
};
validate_client_hello_timestamp(
client_hello.extract_timestamp(),
DEFAULT_TIMESTAMP_TOLERANCE,
)?;
// TODO: somehow check for collision
// set version and remote peer information
self.protocol_version = Some(client_hello_packet.header.protocol_version);
let remote_peer = LpRemotePeer::new(
client_hello.client_ed25519_public_key,
client_hello.client_lp_public_key,
);
Ok((client_hello, remote_peer))
}
/// Send client hello ACK
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
self.connection.send_packet(ack, None).await?;
Ok(())
}
/// Attempt to receive and process a KKT request
pub(crate) async fn receive_kkt_request(
&mut self,
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
let kkt_request = match self.receive_non_error(None).await?.message {
LpMessage::KKTRequest(request) => request.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::KKTRequest,
));
}
};
let (session_secret, request_frame, remote_context) =
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
Ok((context, session_secret, request_frame.session_id()))
}
/// Attempt to send KKT response to the previously received request
pub(crate) async fn send_kkt_response(
&mut self,
session_id: u32,
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
encapsulation_key: &EncapsulationKey<'_>,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let response_frame = responder_process(
&kkt_context,
kkt_session_id,
self.local_peer.ed25519().private_key(),
encapsulation_key,
)?;
let encrypted_frame = encrypt_kkt_frame(
&mut rng(),
&session_secret,
&response_frame,
KKT_RESPONSE_AAD,
)?;
let lp_message = KKTResponseData::new(encrypted_frame).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok(())
}
/// Attempt to receive and process a PSQ msg1 request
pub(crate) async fn receive_psq_initiator_message(
&mut self,
remote_peer: &LpRemotePeer,
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
salt: &[u8; 32],
session_id_bytes: &[u8; 4],
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
let psq_msg1 = match self.receive_non_error(None).await?.message {
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
if psq_msg1.len() < 2 {
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
}
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
if psq_msg1.len() < 2 + handle_len {
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
}
let psq_payload = &psq_msg1[2..2 + handle_len];
let noise_payload = &psq_msg1[2 + handle_len..];
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
let psq_responder = psq_responder_process_message(
self.local_peer.x25519.private_key(),
&remote_peer.x25519_public,
local_kem_keypair,
&remote_peer.ed25519_public,
psq_payload,
salt,
session_id_bytes,
)?;
let psk = psq_responder.psk;
let psk_handle = psq_responder.psk_handle;
// TEMP \/
let outer_aead_key = OuterAeadKey::from_psk(&psk);
// TEMP /\
let mut noise_protocol = NoiseProtocol::build_new_responder(
self.local_peer.x25519().private_key().as_bytes(),
remote_peer.x25519_public.as_bytes(),
&psk,
)?;
noise_protocol.read_message(noise_payload)?;
Ok((
outer_aead_key,
noise_protocol,
PqSharedSecret::new(psq_responder.pq_shared_secret),
psk_handle,
))
}
/// Attempt to prepare and generate a responder PSQ msg2
pub(crate) async fn send_psq_responder_message(
&mut self,
session_id: u32,
psk_handle: &[u8],
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let msg2 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
let handle_len = psk_handle.len() as u16;
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
combined.extend_from_slice(&handle_len.to_le_bytes());
combined.extend_from_slice(psk_handle);
combined.extend_from_slice(&msg2);
let lp_message = HandshakeData::new(combined).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection
.send_packet(lp_packet, Some(outer_aead_key))
.await?;
Ok(())
}
/// Attempt to receive and process final PSQ msg3
pub(crate) async fn receive_final_psq_message(
&mut self,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let psq_msg3 = match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
noise_protocol.read_message(&psq_msg3)?;
if !noise_protocol.is_handshake_finished() {
return Err(LpError::kkt_psq_handshake(
"noise handshake not finished after msg3",
));
}
Ok(())
}
/// Send final ACK to indicate finalisation of the handshake
pub(crate) async fn send_final_ack(
&mut self,
session_id: u32,
outer_aead_key: &OuterAeadKey,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
self.connection
.send_packet(ack, Some(outer_aead_key))
.await?;
Ok(())
}
async fn complete_as_responder_inner(
&mut self,
) -> Result<LpSession, IntermediateHandshakeFailure>
where
S: LpTransport + Unpin,
{
// 1. receive and validate ClientHello
let (client_hello_data, remote_peer) =
self.receive_client_hello()
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: None,
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received client hello");
let session_id = client_hello_data.receiver_index;
let session_id_bytes = session_id.to_le_bytes();
let salt = client_hello_data.salt;
// 2. send ack
debug!("sending client hello ACK");
self.send_client_hello_ack(session_id)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 3. receive and process KKT request
let kkt_data =
self.receive_kkt_request()
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received KKT request");
// TEMP: 'derive' KEM keys
let (dec_key, enc_key) =
self.encapsulated_kem_keys()
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 4. prepare and send KKT response
debug!("sending KKT response");
self.send_kkt_response(session_id, kkt_data, &enc_key)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 5. receive and process PSQ msg1
debug!("received PSQ msg1");
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
.receive_psq_initiator_message(
&remote_peer,
(&dec_key, &enc_key),
&salt,
&session_id_bytes,
)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 6. prepare and send PSQ msg2
debug!("sending PSQ msg2");
if let Err(source) = self
.send_psq_responder_message(
session_id,
&psk_handle,
&outer_aead_key,
&mut noise_protocol,
)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 7. receive and process PSQ msg3
debug!("received PSQ msg3");
if let Err(source) = self
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 8. [optionally] send ACK to finalise
debug!("sending final ACK");
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
#[allow(clippy::expect_used)]
Ok(LpSession::new(
session_id,
self.protocol_version()
.expect("protocol version is known at this point"),
outer_aead_key,
self.local_peer.clone(),
remote_peer,
pq_shared_secret,
noise_protocol,
))
}
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
{
match self.complete_as_responder_inner().await {
Ok(res) => Ok(res),
Err(err) => Err(self.try_send_error_packet(err).await),
}
}
}

Some files were not shown because too many files have changed in this diff Show More