Compare commits

..

149 Commits

Author SHA1 Message Date
benedettadavico d1ba5acd72 mebbe 2026-03-06 07:53:44 +01:00
benedettadavico b7b238584d add file 2026-03-06 07:53:44 +01:00
benedettadavico 7ed5b5477b ? 2026-03-06 07:53:44 +01:00
benedettadavico 09506683ce test.. 2026-03-06 07:53:44 +01:00
benedettadavico 98f1480ae8 attempt at fix 2026-03-06 07:53:44 +01:00
Jędrzej Stuczyński 05b6f5e282 removed redundant LP states (#6509) 2026-03-03 13:58:47 +00:00
benedettadavico 5093450004 bump versions 2026-03-02 10:44:54 +01:00
Jędrzej Stuczyński f6bd511599 feat: Lewes Protocol with PSQv2 (#6491)
* merging georgio/lp-psqv2-integration

* use authenicator on the responder's side

* nym-lp crate compiling

* moved the e2e test to nym-lp

* move key generation to peer

* moved principal generation

* update KKTResponder

* encapsulation key parsing

* Adding concrete types within KKT exchange

* initiator side of the full handshake

* responder side of the handshake and full e2e test

* fixed unit-tests within nym-kkt

* LpSession cleanup

* helpers for Transport

* revamp of the transport traits and initial work on client-side transport

* compiling nym-crypto

* 'working' client-entry dvpn reg

* Fix key conversion

* Slightly reduce use of rand08

* reverted back to libcrux repo refs

* intial telescoping reg

* removing dead code

* wip

* moved data encryption into the state machine

* restoring nym-lp tests

* update lp api model

* Add receiver index derivation

* Add receiver index derivation

* use derived receiver index

* feat: add kem key generation to nodes

* generate fresh x25519, mlkem768 and mceliece keys on config migration

* add lp peer config

* nym-node startup cleanup

* removed dependency on pre-rand09 from nym-lp

* re-expose LP information on the http API

* fixed tests compilation

* add peer config happy path tests

* formatting

* add more tests and fix bug

* better docs

* clippy and formatting issues

* return error on mceliece within NestedSession

* wasm fixes

* removed legacy nym-vpn-lib-wasm

* fixing wasm for real this time

* additional fixes

* add payload to kkt

* make clippy happy

* moved LP to nym-node crate

* cargo fmt

* integrate lpconfig payload

* fix response size trait impl

* Migrate receiver index

* Change receiver index to u32 and regorganize crates

* clippy

* hopefully final wasm fixes

* simple conversion method from semver to ciphersuite

* updated nym-node config template

* chore: remove duplicated code

---------

Co-authored-by: Georgio Nicolas <me@georgio.xyz>
2026-02-27 13:49:08 +00:00
benedetta davico e5c3f39a57 Merge pull request #6498 from nymtech/master
Merge pull request #6481 from nymtech/release/2026.4-quark
2026-02-27 11:13:58 +01:00
Merve 76f999fc88 {DOCs/operators]: Platform release docs and changelog + docs cleanup (#6482)
* changelog-updates

* Update changelog.mdx

* Update changelog.mdx

* Edits per reviewer request

* fixes

* fixes

* typo fixed

* removed outdated info

* Update docs based on reviewer feedback

* Update changelog.mdx

---------

Co-authored-by: merve <e@E-MacBook-Air.local>
2026-02-27 10:10:16 +00:00
dependabot[bot] 2fce8c7ca3 build(deps): bump qs and express in /wasm/client/internal-dev (#6461)
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.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.2)

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.2
  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-26 14:20:59 +00:00
Jędrzej Stuczyński 468bd8b5d1 chore: removed all matrix notifications from github actions (#6495) 2026-02-26 13:48:10 +00:00
dependabot[bot] 45022b1671 build(deps): bump ajv from 6.12.6 to 6.14.0 in /documentation/docs (#6477)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.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-26 13:44:29 +00:00
dependabot[bot] 3b3c5beae4 build(deps-dev): bump webpack in /wasm/node-tester/internal-dev (#6451)
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-26 13:35:57 +00:00
dependabot[bot] 650917e216 build(deps): bump mikefarah/yq from 4.52.2 to 4.52.4 (#6465)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.52.2 to 4.52.4.
- [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.52.2...v4.52.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 13:34:24 +00:00
dependabot[bot] c02adaa019 build(deps-dev): bump qs (#6466)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.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-26 13:34:16 +00:00
dependabot[bot] d01c34263a build(deps): bump keccak from 0.1.5 to 0.1.6 (#6472)
Bumps [keccak](https://github.com/RustCrypto/sponges) from 0.1.5 to 0.1.6.
- [Commits](https://github.com/RustCrypto/sponges/compare/keccak-v0.1.5...keccak-v0.1.6)

---
updated-dependencies:
- dependency-name: keccak
  dependency-version: 0.1.6
  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-26 13:33:23 +00:00
dependabot[bot] f247e028f2 build(deps): bump hono from 4.11.9 to 4.12.0 (#6475)
Bumps [hono](https://github.com/honojs/hono) from 4.11.9 to 4.12.0.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.11.9...v4.12.0)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.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-26 13:32:55 +00:00
dependabot[bot] 20fe8dd028 build(deps): bump minimatch and glob (#6476)
Bumps [minimatch](https://github.com/isaacs/minimatch) to 10.2.2 and updates ancestor dependency [glob](https://github.com/isaacs/node-glob). These dependencies need to be updated together.


Updates `minimatch` from 9.0.5 to 10.2.2
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v10.2.2)

Updates `glob` from 10.5.0 to 13.0.6
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.5.0...v13.0.6)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.2
  dependency-type: indirect
- dependency-name: glob
  dependency-version: 13.0.6
  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-26 13:32:28 +00:00
dependabot[bot] 89edabf796 build(deps): bump ajv in /clients/native/examples/js-examples/websocket (#6478)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 8.17.1 to 8.18.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 8.18.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-26 13:32:01 +00:00
dependabot[bot] bf5352906f build(deps): bump bn.js from 4.12.2 to 4.12.3 (#6483)
Bumps [bn.js](https://github.com/indutny/bn.js) from 4.12.2 to 4.12.3.
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v4.12.2...v4.12.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 4.12.3
  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-26 13:31:44 +00:00
dependabot[bot] 8eb9999876 build(deps): bump bn.js from 4.12.2 to 4.12.3 in /documentation/docs (#6484)
Bumps [bn.js](https://github.com/indutny/bn.js) from 4.12.2 to 4.12.3.
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v4.12.2...v4.12.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 4.12.3
  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-26 13:31:26 +00:00
dependabot[bot] c0f582b336 build(deps): bump minimatch from 3.1.2 to 3.1.4 in /documentation/docs (#6486)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.4.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.4)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.4
  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-26 13:31:09 +00:00
mfahampshire 133a855e01 Max/ci seo tweaks (#6488)
* Tweak README ordering

* Linting

* Add sitemap generation + NEXT env var to CI

* Fix lockfile

* Regenerate with newer pnpm
2026-02-25 11:07:35 +00:00
mfahampshire 98149dde87 Max/docs theme tweaks (#6480)
* Simplified landing page card layout, centered text, switched to raw
layout on index page for theming flexibility.

* Tweak theme
2026-02-25 10:05:20 +00:00
bnemeroff 5e733a5ebf SEO: Add frontmatter, structured data, and sitemap config (#6453)
* SEO: Add frontmatter, structured data, and sitemap config

* Fix: restore deleted prebuild output file

---------

Co-authored-by: Benjamin Nemeroff <ben@Benjamins-MacBook-Air.local>
Co-authored-by: mfahampshire <maxhampshire@pm.me>
2026-02-25 09:48:15 +00:00
benedetta davico 5647ae6a41 Merge pull request #6469 from nymtech/release/2026.4-quark
quark to develop
2026-02-25 08:53:48 +01:00
benedetta davico 4ed9d8fb7a Merge pull request #6481 from nymtech/release/2026.4-quark
Quark to master
2026-02-25 08:53:45 +01:00
benedettadavico a2081af603 . 2026-02-24 12:02:35 +01:00
benedettadavico 5b62fd76ba update changelog 2026-02-24 11:29:04 +01:00
mfahampshire 77a34fe3bf Update MixFetch docs playground + components (#6479) 2026-02-24 09:29:15 +00:00
mfahampshire 630c4922ac Max/mixfetch concurrent test (#6417)
* * Experiment with changing address mapping from canonical -> full URL as
  string.
* Up MaxConns config.

* Bump webpack-cli version

* Modify internal-dev tester for concurrent testing

* Add logging + POST request to internal-dev/ 

* push lockfiles

* Remove RequestURL from RequestOptions struct for interface

* Bump versions + update lockfiles
2026-02-23 15:30:49 +00:00
Jędrzej Stuczyński 6edbece3ad bugfix: restore 'latest_measurement' field for nym-node /verloc endpoint (#6452) 2026-02-21 19:10:15 +00:00
import this 8529a3c351 [DOCs/operators]: Cleanup (#6474) 2026-02-20 14:43:05 +00:00
import this 453e1cbe70 [DOCs/operators]: Documentation for SOCKS5 probe score (#6473)
* bump up stats and run prebuild

* fix typos

* add socks5 probe calculation

* fix conflicts

* fix wording
2026-02-20 14:19:25 +00:00
import this 94a3599b4d [DOCs]: Fix missing diagnostic tool in developers menu (#6470)
* bump up stats and run prebuild

* fix typos
2026-02-19 15:08:04 +00:00
import this a6bc54461a [DOCs]: Diagnostic tool (#6467)
* create diagnostic-tool page

* add to menu

* add to list of tools

* syntax fix

* syntax fix

* syntax fix

* syntax fix

* rm old
2026-02-18 16:57:55 +00:00
Tommy Verrall 4f0c40dab7 Merge pull request #6464 from nymtech/otel-minimal-v2
Otel minimal v2
2026-02-18 14:23:35 +01:00
Tommy Verrall 3eff6e5e3b fix testthroughput 2026-02-18 11:06:42 +01:00
Tommy Verrall a519f4ccb8 pr feedback
- Moved OTel CLI options into a separate OtelArgs
- Otel is built behind the feature flag otel
- Store timing is in microseconds
- Restore comments to existing files
2026-02-18 10:48:54 +01:00
Tommy Verrall a3ba3bfc5a remove non OTEL work here 2026-02-17 10:17:22 +01:00
Tommy Verrall 988df7cff7 sampling to avoid costs
- add otel timeouts
2026-02-17 09:10:52 +01:00
Tommy Verrall 260f8e9714 revert docker/localnet to develop; localnet work to follow in separate PR 2026-02-17 08:37:49 +01:00
Tommy Verrall d28d0ac39e fix replay batch drop, harden error handling and scripts 2026-02-16 19:42:24 +01:00
Tommy Verrall dce4d6b34b otel: refactor key selection, add environment label, fix clippy 2026-02-16 19:13:11 +01:00
Tommy Verrall bc47e9a1b2 otel: explicit TLS config for https endpoints 2026-02-16 18:11:28 +01:00
Tommy Verrall 3b693741b2 Merge branch 'develop' of https://github.com/nymtech/nym into otel-minimal-v2 2026-02-16 16:41:16 +01:00
Tommy Verrall 5d7f3402c7 Merge pull request #6462 from nymtech/update-features
Enhance CI workflow with feature inputs
2026-02-16 16:33:55 +01:00
Tommy Verrall 2d73ea5c82 Update Rust toolchain to use master branch
This is correct unless we want to pin the stable version
2026-02-16 16:27:21 +01:00
Tommy Verrall b8d8ee6109 Update ci-build-upload-binaries.yml
Fix bash errors
2026-02-16 16:25:43 +01:00
Tommy Verrall a779b7a266 Update Rust toolchain version to stable 2026-02-16 16:21:42 +01:00
Tommy Verrall cb277fe487 otel: support signoz cloud ingestion key and TLS 2026-02-16 16:11:31 +01:00
Tommy Verrall b2d7b54f34 Enhance CI workflow with feature inputs
Allow features in the CI workflow. Updated handling of cargo features and RUSTFLAGS based on inputs.
2026-02-16 16:10:55 +01:00
Tommy Verrall 8bb29f4d07 localnet: add loadtest script and signoz docs 2026-02-16 15:44:55 +01:00
Tommy Verrall e753f24ed1 localnet: fix runtime and gateway flags 2026-02-16 15:21:45 +01:00
Tommy Verrall c7cd962627 localnet: multi-stage dockerfile 2026-02-16 14:45:05 +01:00
Tommy Verrall 00467e4440 fix upstream build: update lockfile and stabilise nym-lp 2026-02-16 14:11:40 +01:00
Tommy Verrall f3d1000472 Add gitignore 2026-02-16 13:57:04 +01:00
Tommy Verrall 597aae1a20 localnet: wire otel 2026-02-16 13:54:15 +01:00
Tommy Verrall 40a3cd28b7 otel: add tracing 2026-02-16 13:46:17 +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
522 changed files with 32587 additions and 34040 deletions
+1
View File
@@ -3,4 +3,5 @@
.gitignore .gitignore
**/node_modules **/node_modules
**/target **/target
target-otel
dist dist
+4
View File
@@ -6,6 +6,8 @@ on:
jobs: jobs:
build: build:
runs-on: arc-ubuntu-22.04 runs-on: arc-ubuntu-22.04
env:
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
defaults: defaults:
run: run:
working-directory: documentation/docs working-directory: documentation/docs
@@ -41,6 +43,8 @@ jobs:
run: pnpm i run: pnpm i
- name: Build project - name: Build project
run: pnpm run build run: pnpm run build
- name: Generate sitemap
run: npx next-sitemap
- name: Move files to /dist/ - name: Move files to /dist/
run: ../scripts/move-to-dist.sh run: ../scripts/move-to-dist.sh
+69 -32
View File
@@ -3,13 +3,28 @@ name: ci-build-upload-binaries
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
feature_profile:
description: "Select a predefined cargo feature profile"
required: false
default: "none"
type: choice
options:
- none
- tokio-console
- otel
- otel,tokio-console
extra_features:
description: "Additional comma-separated cargo features (e.g. feat1,feat2)"
required: false
default: ""
type: string
add_tokio_unstable: add_tokio_unstable:
description: 'True to add RUSTFLAGS="--cfg tokio_unstable"' description: 'Force RUSTFLAGS="--cfg tokio_unstable" (auto-set when tokio-console is selected)'
required: true required: false
default: false default: false
type: boolean type: boolean
enable_deb: enable_deb:
description: "True to enable cargo-deb installation and .deb package building" description: "Enable cargo-deb installation and .deb package building"
required: false required: false
default: false default: false
type: boolean type: boolean
@@ -21,7 +36,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [ arc-linux-latest ] platform: [arc-linux-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
env: env:
@@ -36,38 +51,62 @@ jobs:
OUTPUT_DIR: ci-builds/${{ github.ref_name }} OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: | run: |
rm -rf ci-builds || true rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR mkdir -p "$OUTPUT_DIR"
echo $OUTPUT_DIR echo "$OUTPUT_DIR"
- name: Install Dependencies (Linux) - name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Sets env vars for tokio if set in manual dispatch inputs - name: Resolve cargo features and RUSTFLAGS
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true if: github.event_name == 'workflow_dispatch'
shell: bash
run: | run: |
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV FEATURES=""
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV PROFILE="${{ inputs.feature_profile }}"
EXTRA="${{ inputs.extra_features }}"
if [[ "$PROFILE" != "none" && -n "$PROFILE" ]]; then
FEATURES="$PROFILE"
fi
if [[ -n "$EXTRA" ]]; then
if [[ -n "$FEATURES" ]]; then
FEATURES="${FEATURES},${EXTRA}"
else
FEATURES="$EXTRA"
fi
fi
if [[ -n "$FEATURES" ]]; then
echo "CARGO_FEATURES=--features ${FEATURES}" >> "$GITHUB_ENV"
echo "::notice::Selected cargo features: $FEATURES"
else
echo "::notice::No additional cargo features selected"
fi
if [[ "$FEATURES" == *"tokio-console"* ]] || [[ "${{ inputs.add_tokio_unstable }}" == "true" ]]; then
echo "RUSTFLAGS=--cfg tokio_unstable" >> "$GITHUB_ENV"
echo "::notice::Enabled RUSTFLAGS --cfg tokio_unstable"
fi
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }} toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Build all binaries - name: Build all binaries
uses: actions-rs/cargo@v1 shell: bash
with: run: cargo build --workspace --release ${{ env.CARGO_FEATURES }}
command: build
args: --workspace --release ${{ env.CARGO_FEATURES }}
- name: Install cargo-deb - name: Install cargo-deb
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-deb
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
shell: bash
run: cargo install cargo-deb
- name: Build deb packages - name: Build deb packages
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
shell: bash shell: bash
run: make deb run: make deb
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
- name: Upload Artifact - name: Upload Artifact
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
@@ -84,24 +123,22 @@ jobs:
target/release/nym-node target/release/nym-node
retention-days: 30 retention-days: 30
# If this was a pull_request or nightly, upload to build server
- name: Prepare build output - name: Prepare build output
# if: github.event_name == 'schedule' || github.event_name == 'pull_request'
shell: bash shell: bash
env: env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }} OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: | run: |
cp target/release/nym-client $OUTPUT_DIR cp target/release/nym-client "$OUTPUT_DIR"
cp target/release/nym-socks5-client $OUTPUT_DIR cp target/release/nym-socks5-client "$OUTPUT_DIR"
cp target/release/nym-api $OUTPUT_DIR cp target/release/nym-api "$OUTPUT_DIR"
cp target/release/nym-network-requester $OUTPUT_DIR cp target/release/nym-network-requester "$OUTPUT_DIR"
cp target/release/nymvisor $OUTPUT_DIR cp target/release/nymvisor "$OUTPUT_DIR"
cp target/release/nym-node $OUTPUT_DIR cp target/release/nym-node "$OUTPUT_DIR"
cp target/release/nym-cli $OUTPUT_DIR cp target/release/nym-cli "$OUTPUT_DIR"
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.enable_deb }}" == "true" ]]; then
cp target/debian/*.deb $OUTPUT_DIR cp target/debian/*.deb "$OUTPUT_DIR"
fi fi
- name: Deploy branch to CI www - name: Deploy branch to CI www
continue-on-error: true continue-on-error: true
uses: easingthemes/ssh-deploy@main uses: easingthemes/ssh-deploy@main
@@ -1,42 +0,0 @@
name: ci-build-vpn-api-wasm
on:
pull_request:
paths:
- 'common/**'
- 'nym-credential-proxy/**'
- '.github/workflows/ci-build-vpn-api-wasm.yml'
jobs:
wasm:
runs-on: arc-linux-latest
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
target: wasm32-unknown-unknown
override: true
components: rustfmt, clippy
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: '116'
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli
- name: "Build"
run: make
working-directory: nym-credential-proxy/vpn-api-lib-wasm
+10 -1
View File
@@ -10,6 +10,7 @@ on:
- 'nym-api/**' - 'nym-api/**'
- 'nym-authenticator-client/**' - 'nym-authenticator-client/**'
- 'nym-credential-proxy/**' - 'nym-credential-proxy/**'
- 'nym-gateway-probe/**'
- 'nym-ip-packet-client/**' - 'nym-ip-packet-client/**'
- 'nym-network-monitor/**' - 'nym-network-monitor/**'
- 'nym-node/**' - 'nym-node/**'
@@ -89,7 +90,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: clippy 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) - name: Clippy (non-macos)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows') if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
@@ -104,6 +105,14 @@ jobs:
with: with:
command: build 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 - name: Build all examples
if: contains(matrix.os, 'linux') if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
on: on:
pull_request: pull_request:
paths: paths:
- "nym-node-status-api/**" - "nym-node-status-api/nym-node-status-api/**"
env: env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api" WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -0,0 +1,122 @@
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
timeout-minutes: 35
env:
RUSTUP_PERMIT_COPY_RENAME: 1
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 }} \
--no-git-commit \
--yes
- name: Preflight publish checks
run: |
python3 tools/internal/check_publish_preflight.py
# Dry run may show cascading dependency errors because packages aren't
# actually uploaded - these are expected and ignored. We check for real
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
- name: Publish (dry run)
run: |
set +e
publish_status=1
max_attempts=2
attempt=1
rm -f /tmp/publish-dry-run.log
while [ "$attempt" -le "$max_attempts" ]; do
echo "Dry-run publish attempt ${attempt}/${max_attempts}"
cargo workspaces publish --dry-run --allow-dirty 2>&1 | tee /tmp/publish-dry-run.log
publish_status=${PIPESTATUS[0]}
if [ "$publish_status" -eq 0 ]; then
break
fi
# Retry once for interruption/runner issues.
if [ "$attempt" -lt "$max_attempts" ] && \
{ [ "$publish_status" -eq 130 ] || [ "$publish_status" -eq 137 ]; }; then
echo "Publish dry-run interrupted (exit ${publish_status}), retrying in 10s..."
sleep 10
attempt=$((attempt + 1))
continue
fi
break
done
set -e
if grep -Eiq \
"failed to verify manifest|failed to parse manifest|invalid Cargo.toml|error: package .* has no (description|license|repository)" \
/tmp/publish-dry-run.log; then
echo "Detected real packaging/manifest errors"
exit 1
fi
# In dry-run mode, non-zero publish status is expected due to
# dependency-cascade failures against crates.io index.
if [ "$publish_status" -ne 0 ]; then
echo "Dry-run publish returned non-zero (${publish_status}) but no real manifest blockers were detected."
fi
echo "Only expected dry-run dependency cascade errors detected (if any)."
# Show the list of packages published
- name: Show package versions
run: cargo workspaces list --long
@@ -0,0 +1,61 @@
# 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
env:
RUSTUP_PERMIT_COPY_RENAME: 1
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
+88
View File
@@ -0,0 +1,88 @@
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
env:
RUSTUP_PERMIT_COPY_RENAME: 1
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,76 @@
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
env:
RUSTUP_PERMIT_COPY_RENAME: 1
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
@@ -51,25 +51,3 @@ jobs:
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }} TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/" EXCLUDE: "/dist/, /node_modules/"
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-wallet
NYM_PROJECT_NAME: "nym-wallet"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ job.status == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+2 -37
View File
@@ -10,8 +10,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
rust: [stable, beta] rust: [ stable, beta ]
os: [ubuntu-22.04, windows-latest, macos-latest] os: [ ubuntu-22.04, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -93,38 +93,3 @@ jobs:
with: with:
command: clippy command: clippy
args: --workspace --all-targets -- -D warnings args: --workspace --all-targets -- -D warnings
notification:
needs: build
runs-on: custom-linux
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v3
- name: Check out repository code
uses: actions/checkout@v6
- name: install npm
uses: actions/setup-node@v4
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 20
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym nightly build"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+1 -36
View File
@@ -10,7 +10,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-22.04, macos-latest, windows-latest] os: [ ubuntu-22.04, macos-latest, windows-latest ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -55,38 +55,3 @@ jobs:
with: with:
command: clippy command: clippy
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
notification:
needs: build
runs-on: custom-linux
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v3
- name: Check out repository code
uses: actions/checkout@v6
- name: install npm
uses: actions/setup-node@v4
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 20
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "nym-wallet-nightly-build"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -24,34 +24,3 @@ jobs:
with: with:
name: report name: report
path: .github/workflows/support-files/notifications/deny.message path: .github/workflows/support-files/notifications/deny.message
notification:
needs: cargo-deny
runs-on: custom-linux
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: Download report from previous job
uses: actions/download-artifact@v7
with:
name: report
path: .github/workflows/support-files/notifications
- name: install npm
uses: actions/setup-node@v4
with:
node-version: 20
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: security
NYM_PROJECT_NAME: "Daily security report"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_AUDIT }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -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" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml 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" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml 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" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml 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" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml 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" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -8,7 +8,7 @@ env:
jobs: jobs:
build-container: build-container:
runs-on: arc-ubuntu-22.04-dind runs-on: ubuntu-latest
steps: steps:
- name: Login to Harbor - name: Login to Harbor
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.50.1 uses: mikefarah/yq@v4.52.4
id: get_version id: get_version
with: with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -0,0 +1,45 @@
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: Preflight publish checks
run: |
python3 tools/internal/check_publish_preflight.py
- 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
+7 -35
View File
@@ -4,51 +4,23 @@ This is a collection of scripts and files to support GitHub Actions.
## Sending Notifications ## Sending Notifications
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub Actions. These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub
Actions.
### Adding notifications to a GitHub Action
```
jobs:
build:
...
- name: Notifications - Node Install
run: npm install
working-directory: .github/workflows/support-files/notifications
- name: Notifications - Send
env:
NYM_NOTIFICATION_KIND: "my-component"
GIT_BRANCH: "${GITHUB_REF##*/}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
IS_SUCCESS: "${{ job.status == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
```
Notifications are run by adding the snippet above to a GitHub Action, and:
1. Installing node packages needed at run time
2. Set the env vars as required:
- `NYM_NOTIFICATION_KIND` matches the directory in `.github/workflows/support-files/${NYM_NOTIFICATION_KIND}` to provide the templates and extra scripting in `index.js`
- Matrix credentials, room and other env vars for the status of the build and repo
3. Replacing the default entry point shell script on the `keybaseio/client:stable-node` docker image to run `.github/workflows/support-files/notifications/entry_point.sh`
### Running locally ### Running locally
You will need: You will need:
- Node 16 LTS - Node 16 LTS
- npm - npm
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix credentials. Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix
credentials.
Then run `npm install` to get dependencies. Then run `npm install` to get dependencies.
Start development mode for the notification type you want either by passing the value as an env var called `NYM_NOTIFICATION_KIND` or set the `.env` file values correctly. Start development mode for the notification type you want either by passing the value as an env var called
`NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
```bash ```bash
cd .github/workflows/support-files cd .github/workflows/support-files
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
# pass exit codes out to GitHub Actions
set -euxo pipefail
# change to the directory that contains this script
cd "${0%/*}"
# run the node script
node send_message.js
@@ -1,126 +0,0 @@
require('dotenv').config();
const { sendMatrixMessage } = require('./send_message_to_matrix');
let context = {
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security','ci-docs','cd-docs','ci-dev','cd-dev'],
};
/**
* Validate that all required env and context vars are available
*/
function validateContext() {
if (!context.env.NYM_NOTIFICATION_KIND) {
throw new Error(
'Please set env var NYM_NOTIFICATION_KIND with the project kind that matches a directory in ".github/workflows/support-files"',
);
}
if (!context.kinds.includes(context.env.NYM_NOTIFICATION_KIND)) {
throw new Error(`Env var NYM_NOTIFICATION_KIND is not in ${context.kinds}`);
}
if (!context.env.NYM_PROJECT_NAME) {
throw new Error(
'Please set env var NYM_PROJECT_NAME with the project name for displaying in notification messages',
);
}
if (context.env.MATRIX_ROOM) {
if (!context.env.MATRIX_SERVER) {
throw new Error(
'Matrix server is not defined. Please set env var MATRIX_SERVER',
);
}
if (!context.env.MATRIX_USER_ID) {
throw new Error(
'Matrix user id is not defined. Please set env var MATRIX_USER_ID',
);
}
if (!context.env.MATRIX_TOKEN) {
throw new Error(
'Matrix token is not defined. Please set env var MATRIX_TOKEN',
);
}
if (!context.env.MATRIX_DEVICE_ID) {
throw new Error(
'Matrix device id is not defined. Please set env var MATRIX_DEVICE_ID',
);
}
}
}
/**
* Creates a context that will be available in the templates for rendering notifications
*/
function createTemplateContext() {
const options = { dateStyle: 'full', timeStyle: 'long' };
context.timestamp = new Date().toLocaleString(undefined, options);
// add environment to template context and validate
context.env = process.env;
try {
validateContext();
} catch (e) {
if(process.env.SHOW_DEBUG) {
// recursively print the context for easy debugging and rethrow the error
console.dir({ context }, { depth: null });
}
throw e;
}
context.kind = context.env.NYM_NOTIFICATION_KIND;
if (!context.env.GIT_BRANCH_NAME) {
context.env.GIT_BRANCH_NAME = context.env.GITHUB_REF.split('/')
.slice(2)
.join('/');
}
context.status = process.env.IS_SUCCESS === 'true' ? 'success' : 'failure';
}
/**
* Uses the `kind` set in the context to process the context and generate a notification message
* @returns {Promise<string>} A string notification message body
*/
async function processKindScript() {
const script = require(`../${context.kind}`);
if (!script.addToContextAndValidate) {
throw new Error(
`"./${context.kind}/index.js" does not export a method called "async addToContextAndValidate(context)"`,
);
}
if (!script.getMessageBody) {
throw new Error(
`"./${context.kind}/index.js" does not export a method called "async getMessageBody(context)"`,
);
}
// call the script to modify and validate the context
await script.addToContextAndValidate(context);
// let the script create a message body and return the result as a string for sending
return await script.getMessageBody(context);
}
/**
* The main function, as async so that await syntax is available
*/
async function main() {
createTemplateContext();
console.log(`Sending notification for kind "${context.kind}"...`);
const messageBody = await processKindScript();
if(process.env.SHOW_DEBUG) {
console.log('-----------------------------------------');
console.log(messageBody);
console.log('-----------------------------------------');
}
if(context.env.MATRIX_ROOM) {
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM)
}
if(context.env.MATRIX_ROOM_OF_SHAME && context.env.IS_SUCCESS !== 'true') {
// when a job fails
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM_OF_SHAME)
}
}
// call main function and let NodeJS handle the promise
main();
@@ -1,67 +0,0 @@
const sdk = require('matrix-js-sdk');
global.Olm = require('olm');
const { LocalStorage } = require('node-localstorage');
const localStorage = new LocalStorage('./scratch');
const {
LocalStorageCryptoStore,
} = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
var showdown = require('showdown');
// hide all matrix client output
console.error = (error) => console.log('❌ error: ', error);
process.stderr.write = () => {};
process.stdout.write = () => {};
function createClient(context, room, message) {
const server = context.env.MATRIX_SERVER;
const token = context.env.MATRIX_TOKEN;
const deviceId = context.env.MATRIX_DEVICE_ID;
const userId = context.env.MATRIX_USER_ID;
const client = sdk.createClient({
baseUrl: server,
accessToken: token,
userId,
deviceId,
sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage),
});
client.on('sync', async function(state, prevState, res) {
if (state !== 'PREPARED') return;
client.setGlobalErrorOnUnknownDevices(false);
try {
await client.joinRoom(room);
await client.sendEvent(
room,
'm.room.message',
{
msgtype: 'm.text',
format: 'org.matrix.custom.html',
body: message,
formatted_body: message,
},
'',
);
} catch (error) {
console.error('Job failed: ' + error.message);
}
client.stopClient();
process.exit(0);
});
return client;
}
async function sendMatrixMessage(contextArg, messageAsMarkdown, roomId) {
const converter = new showdown.Converter();
const messageAsHtml = converter.makeHtml(messageAsMarkdown);
const client = createClient(contextArg, roomId, messageAsHtml);
await client.initCrypto();
await client.startClient({ initialSyncLimit: 1 });
}
module.exports = {
sendMatrixMessage,
};
+1 -1
View File
@@ -67,7 +67,6 @@ nym-api/redocly/formatted-openapi.json
*.profraw *.profraw
.beads .beads
CLAUDE.md CLAUDE.md
docs
.claude .claude
.superego .superego
@@ -77,3 +76,4 @@ docs
.claude/settings.json .claude/settings.json
/notes /notes
/target-otel
+156
View File
@@ -4,6 +4,162 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased] ## [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) ## [2026.2-oscypek] (2026-01-27)
- bugfix: downgrade gateway protocol to clients proposed version ([#6377]) - bugfix: downgrade gateway protocol to clients proposed version ([#6377])
Generated
+2010 -1846
View File
File diff suppressed because it is too large Load Diff
+127 -112
View File
@@ -74,7 +74,6 @@ members = [
"common/nym-id", "common/nym-id",
"common/nym-kcp", "common/nym-kcp",
"common/nym-lp", "common/nym-lp",
"common/nym-lp-common",
"common/nym-kkt", "common/nym-kkt",
"common/nym-metrics", "common/nym-metrics",
"common/nym_offline_compact_ecash", "common/nym_offline_compact_ecash",
@@ -129,7 +128,6 @@ members = [
"nym-browser-extension/storage", "nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests", "nym-credential-proxy/nym-credential-proxy-requests",
"nym-credential-proxy/vpn-api-lib-wasm",
"nym-data-observatory", "nym-data-observatory",
"nym-ip-packet-client", "nym-ip-packet-client",
"nym-network-monitor", "nym-network-monitor",
@@ -173,8 +171,9 @@ members = [
"wasm/mix-fetch", "wasm/mix-fetch",
"wasm/node-tester", "wasm/node-tester",
"wasm/zknym-lib", "wasm/zknym-lib",
"nym-gateway-probe", # "nym-gateway-probe",
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite", "integration-tests",
"common/nym-kkt-ciphersuite", "common/nym-kkt-context",
] ]
default-members = [ default-members = [
@@ -185,7 +184,6 @@ default-members = [
"nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy",
"nym-node", "nym-node",
"nym-node-status-api/nym-node-status-agent", "nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api", "nym-statistics-api",
"nym-validator-rewarder", "nym-validator-rewarder",
"nyx-chain-watcher", "nyx-chain-watcher",
@@ -206,7 +204,7 @@ edition = "2024"
license = "Apache-2.0" license = "Apache-2.0"
rust-version = "1.85" rust-version = "1.85"
readme = "README.md" readme = "README.md"
version = "1.20.1" version = "1.20.4"
[workspace.dependencies] [workspace.dependencies]
addr = "0.15.6" addr = "0.15.6"
@@ -233,7 +231,7 @@ blake3 = "1.7.0"
bloomfilter = "3.0.1" bloomfilter = "3.0.1"
bs58 = "0.5.1" bs58 = "0.5.1"
bytecodec = "0.4.15" bytecodec = "0.4.15"
bytes = "1.10.1" bytes = "1.11.1"
cargo_metadata = "0.19.2" cargo_metadata = "0.19.2"
celes = "2.6.0" celes = "2.6.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
@@ -275,6 +273,7 @@ futures = "0.3.31"
futures-util = "0.3" futures-util = "0.3"
generic-array = "0.14.7" generic-array = "0.14.7"
getrandom = "0.2.10" getrandom = "0.2.10"
getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3" glob = "0.3"
handlebars = "3.5.5" handlebars = "3.5.5"
hex = "0.4.3" hex = "0.4.3"
@@ -304,13 +303,16 @@ ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0" ledger-transport-hid = "0.10.0"
log = "0.4" log = "0.4"
mime = "0.3.17" mime = "0.3.17"
mock_instant = "0.6.0"
moka = { version = "0.12", features = ["future"] } moka = { version = "0.12", features = ["future"] }
nix = "0.30.1" nix = "0.30.1"
notify = "5.1.0" notify = "5.1.0"
num_enum = "0.7.5" num_enum = "0.7.5"
once_cell = "1.21.3" once_cell = "1.21.3"
opentelemetry = "0.19.0" opentelemetry = "0.31.0"
opentelemetry-jaeger = "0.18.0" opentelemetry_sdk = "0.31.0"
opentelemetry-otlp = "0.31.0"
tonic = "0.14.4"
parking_lot = "0.12.3" parking_lot = "0.12.3"
pem = "0.8" pem = "0.8"
petgraph = "0.6.5" petgraph = "0.6.5"
@@ -320,12 +322,14 @@ publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3" proc_pidinfo = "0.1.3"
quote = "1" quote = "1"
rand = "0.8.5" rand = "0.8.5"
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3" rand_chacha = "0.3"
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
rand_core = "0.6.3" rand_core = "0.6.3"
rand_distr = "0.4" rand_distr = "0.4"
rayon = "1.5.1" rayon = "1.5.1"
regex = "1.10.6" 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" rs_merkle = "1.5.0"
schemars = "0.8.22" schemars = "0.8.22"
semver = "1.0.26" semver = "1.0.26"
@@ -367,9 +371,8 @@ tower = "0.5.2"
tower-http = "0.6.6" tower-http = "0.6.6"
tracing = "0.1.41" tracing = "0.1.41"
tracing-log = "0.2" tracing-log = "0.2"
tracing-opentelemetry = "0.19.0" tracing-opentelemetry = "0.32.1"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"
tracing-tree = "0.2.2"
tracing-indicatif = "0.3.9" tracing-indicatif = "0.3.9"
tracing-test = "0.2.5" tracing-test = "0.2.5"
ts-rs = "10.1.0" ts-rs = "10.1.0"
@@ -381,7 +384,7 @@ url = "2.5"
utoipa = "5.2" utoipa = "5.2"
utoipa-swagger-ui = "8.1" utoipa-swagger-ui = "8.1"
utoipauto = "0.2" utoipauto = "0.2"
uuid = "*" uuid = "1.19.0"
vergen = { version = "=8.3.1", default-features = false } vergen = { version = "=8.3.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false } vergen-gitcl = { version = "1.0.8", default-features = false }
walkdir = "2" walkdir = "2"
@@ -390,107 +393,119 @@ zeroize = "1.7.0"
prometheus = { version = "0.14.0" } prometheus = { version = "0.14.0" }
# libcrux
libcrux-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-curve25519 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests # 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-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" } nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" } nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" } nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" } nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.1", path = "common/bin-common" } nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
nym-cache = { version = "1.20.1", path = "common/nym-cache" } nym-cache = { version = "1.20.4", path = "common/nym-cache" }
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false } nym-client-core = { version = "1.20.4", 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-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" } nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" } nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" } nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.1", path = "common/nym-common" } nym-common = { version = "1.20.4", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" } nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.1", path = "common/config" } nym-config = { version = "1.20.4", path = "common/config" }
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" } nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" } nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" } nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" } nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" } nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false } nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" } nym-credentials-interface = { version = "1.20.4", 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-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" } nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false } nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.1", path = "common/dkg" } nym-dkg = { version = "1.20.4", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" } nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" } nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" } nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" } nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" } nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" } nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false } nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" } nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" } nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" } nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" } nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" } nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" } nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false } nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.1", path = "common/nym-id" } nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" } nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" } nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" } nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" } nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" } nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" } nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" } nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" } nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" } nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
nym-noise = { version = "1.20.1", path = "common/nymnoise" } nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" } nym-noise = { version = "1.20.4", path = "common/nymnoise" }
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" } nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false } nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" } nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" } nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
nym-outfox = { version = "1.20.1", path = "nym-outfox" } nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
nym-registration-common = { version = "1.20.1", path = "common/registration" } nym-outfox = { version = "1.20.4", path = "nym-outfox" }
nym-pemstore = { version = "1.20.1", path = "common/pemstore" } nym-registration-common = { version = "1.20.4", path = "common/registration" }
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" } nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" } nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" } nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" } nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" } nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" } nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" } nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" } nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" } nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" } nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" } nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" } nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" } nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" } nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" } nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" } nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" } nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" } nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" } nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
nym-statistics-common = { version = "1.20.1", path = "common/statistics" } nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" } nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
nym-task = { version = "1.20.1", path = "common/task" } nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
nym-tun = { version = "1.20.1", path = "common/tun" } nym-task = { version = "1.20.4", path = "common/task" }
nym-test-utils = { version = "1.20.1", path = "common/test-utils" } nym-tun = { version = "1.20.4", path = "common/tun" }
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" } nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
nym-topology = { version = "1.20.1", path = "common/topology" } nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
nym-types = { version = "1.20.1", path = "common/types" } nym-topology = { version = "1.20.4", path = "common/topology" }
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" } nym-types = { version = "1.20.4", path = "common/types" }
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false } nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" } nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-verloc = { version = "1.20.1", path = "common/verloc" } nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-wireguard = { version = "1.20.1", path = "common/wireguard" } nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" } nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" } nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" } nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" } 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-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-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" } nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false } nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" } nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
# coconut/DKG related # coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork # unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
+5 -4
View File
@@ -104,11 +104,11 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
sdk-wasm-build: sdk-wasm-build:
$(MAKE) -C nym-browser-extension/storage wasm-pack # $(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client $(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester $(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch $(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/zknym-lib # $(MAKE) -C wasm/zknym-lib
# $(MAKE) -C wasm/full-nym-wasm # $(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo # run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
@@ -119,13 +119,14 @@ sdk-typescript-build:
yarn --cwd sdk/typescript/codegen/contract-clients build yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown) # NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib # WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
sdk-wasm-test: sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings #cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
sdk-wasm-lint: sdk-wasm-lint:
cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
$(MAKE) -C wasm/mix-fetch check-fmt $(MAKE) -C wasm/mix-fetch check-fmt
# Add to top-level targets # Add to top-level targets
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-client" name = "nym-client"
version = "1.1.69" version = "1.1.72"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"] authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client" description = "Implementation of the Nym Client"
edition = "2021" edition = "2021"
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"webpack": "^5.76.0", "webpack": "^5.105.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4" "webpack-dev-server": "^4.7.4"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-socks5-client" name = "nym-socks5-client"
version = "1.1.69" version = "1.1.72"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"] 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" description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021" edition = "2021"
+1
View File
@@ -18,6 +18,7 @@ mod util;
mod version; mod version;
pub use error::Error; pub use error::Error;
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
pub use v6 as latest; pub use v6 as latest;
pub use version::AuthenticatorVersion; pub use version::AuthenticatorVersion;
@@ -7,6 +7,7 @@ use crate::traits::{
TopUpBandwidthResponse, UpgradeModeStatus, TopUpBandwidthResponse, UpgradeModeStatus,
}; };
use crate::{v2, v3, v4, v5, v6}; use crate::{v2, v3, v4, v5, v6};
use nym_sphinx::addressing::Recipient;
#[derive(Debug)] #[derive(Debug)]
pub enum AuthenticatorResponse { pub enum AuthenticatorResponse {
@@ -17,6 +18,17 @@ pub enum AuthenticatorResponse {
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>), 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 { impl UpgradeModeStatus for AuthenticatorResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
match self { match self {
+32
View File
@@ -1,6 +1,38 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // 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)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::error::Error; use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData; 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 nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -56,27 +56,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair { impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self { fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value { let (ipv4, ipv6) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), 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) IpPair::new(ipv4, ipv6)
} }
} }
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::error::Error; use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData; 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 nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -54,27 +54,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair { impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self { fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value { let (ipv4, ipv6) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), 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) IpPair::new(ipv4, ipv6)
} }
} }
@@ -3,13 +3,12 @@
use crate::error::Error; use crate::error::Error;
use crate::models::BandwidthClaim; use crate::models::BandwidthClaim;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose}; 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 nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr}; use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
@@ -20,13 +19,11 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
use sha2::Sha256; use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>; pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>; pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64; pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair { pub struct IpPair {
@@ -54,27 +51,11 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair { impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self { fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value { let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), 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) IpPair::new(ipv4, ipv6)
} }
} }
+5 -1
View File
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
impl BandwidthTicketProvider for MockBandwidthController { impl BandwidthTicketProvider for MockBandwidthController {
async fn get_ecash_ticket( async fn get_ecash_ticket(
&self, &self,
_ticket_type: TicketType, ticket_type: TicketType,
_gateway_id: PublicKey, _gateway_id: PublicKey,
tickets_to_spend: u32, tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> { ) -> Result<PreparedCredential, BandwidthControllerError> {
@@ -100,6 +100,10 @@ impl BandwidthTicketProvider for MockBandwidthController {
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES) let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
.expect("Failed to deserialize test credential - this is a bug in the test harness"); .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 // Update spend_date to today to pass validation
credential.spend_date = OffsetDateTime::now_utc().date(); credential.spend_date = OffsetDateTime::now_utc().date();
+19
View File
@@ -57,3 +57,22 @@ where
Ok(Some(token)) 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
}
}
+13 -9
View File
@@ -19,12 +19,15 @@ serde_json = { workspace = true, optional = true }
## tracing ## tracing
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
tracing-tree = { workspace = true, optional = true }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
tracing-opentelemetry = { workspace = true, optional = true } tracing-opentelemetry = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true } utoipa = { workspace = true, optional = true }
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true } opentelemetry = { workspace = true, features = ["trace"], optional = true }
## otel-otlp (modern OTLP export to SigNoz/any OTLP collector)
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "trace", "tls-roots"], optional = true }
tonic = { workspace = true, optional = true }
[build-dependencies] [build-dependencies]
@@ -35,13 +38,14 @@ default = []
openapi = ["utoipa"] openapi = ["utoipa"]
output_format = ["serde_json", "dep:clap"] output_format = ["serde_json", "dep:clap"]
bin_info_schema = ["schemars"] bin_info_schema = ["schemars"]
basic_tracing = ["dep:tracing", "tracing-subscriber"] basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
tracing = [ otel-otlp = [
"basic_tracing", "basic_tracing",
"tracing-tree", "dep:opentelemetry",
"opentelemetry-jaeger", "dep:opentelemetry_sdk",
"tracing-opentelemetry", "dep:opentelemetry-otlp",
"opentelemetry", "dep:tracing-opentelemetry",
"dep:tonic",
] ]
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"] clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
models = [] models = []
+98 -39
View File
@@ -4,16 +4,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::IsTerminal; use std::io::IsTerminal;
#[cfg(feature = "tracing")] // Re-export tracing_subscriber for consumers that need to compose layers
pub use opentelemetry; #[cfg(feature = "basic_tracing")]
#[cfg(feature = "tracing")]
pub use opentelemetry_jaeger;
#[cfg(feature = "tracing")]
pub use tracing_opentelemetry;
#[cfg(feature = "tracing")]
pub use tracing_subscriber; pub use tracing_subscriber;
#[cfg(feature = "tracing")]
pub use tracing_tree;
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] #[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -69,40 +62,106 @@ pub fn setup_tracing_logger() {
build_tracing_logger().init() build_tracing_logger().init()
} }
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason /// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
#[cfg(feature = "tracing")] ///
#[macro_export] /// This produces a layer compatible with `tracing_subscriber::registry()` that
macro_rules! setup_tracing { /// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
($service_name: expr) => { ///
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt; /// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt; /// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
///
/// # Arguments
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
/// or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
/// sent as the `signoz-ingestion-key` gRPC metadata header on every export.
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
/// Attached as the `deployment.environment` OTel resource attribute.
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
/// Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
/// unbounded blocking if the collector is slow or unreachable.
#[cfg(feature = "otel-otlp")]
pub fn init_otel_layer<S>(
service_name: &str,
endpoint: &str,
ingestion_key: Option<&str>,
environment: &str,
sample_ratio: f64,
export_timeout_secs: u64,
) -> Result<
(
tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
opentelemetry_sdk::trace::SdkTracerProvider,
),
Box<dyn std::error::Error + Send + Sync>,
>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_otlp::WithTonicConfig;
use opentelemetry_sdk::trace::Sampler;
use std::time::Duration;
let registry = nym_bin_common::logging::tracing_subscriber::Registry::default() // Validate endpoint URI early to fail with a clear message
.with(nym_bin_common::logging::tracing_subscriber::EnvFilter::from_default_env()) if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
.with( return Err(format!(
nym_bin_common::logging::tracing_tree::HierarchicalLayer::new(4) "invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
.with_targets(true) )
.with_bracketed_fields(true), .into());
); }
let tracer = nym_bin_common::logging::opentelemetry_jaeger::new_collector_pipeline() let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
.with_endpoint("http://44.199.230.10:14268/api/traces")
.with_service_name($service_name)
.with_isahc()
.with_trace_config(
nym_bin_common::logging::opentelemetry::sdk::trace::config().with_sampler(
nym_bin_common::logging::opentelemetry::sdk::trace::Sampler::TraceIdRatioBased(
0.1,
),
),
)
.install_batch(nym_bin_common::logging::opentelemetry::runtime::Tokio)
.expect("Could not init tracer");
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer); let mut builder = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.with_timeout(Duration::from_secs(export_timeout_secs));
registry.with(telemetry).init(); // Explicitly configure TLS when the endpoint uses HTTPS
}; if endpoint.starts_with("https://") {
builder =
builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
}
if let Some(key) = ingestion_key {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"signoz-ingestion-key",
key.parse()
.map_err(|_| "invalid ingestion key format (value redacted)")?,
);
builder = builder.with_metadata(metadata);
}
let exporter = builder
.build()
.map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
.with_batch_exporter(exporter)
.with_resource(
opentelemetry_sdk::Resource::builder()
.with_service_name(service_name.to_owned())
.with_attribute(opentelemetry::KeyValue::new(
"deployment.environment",
environment.to_owned(),
))
.build(),
)
.build();
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
let tracer = tracer_provider.tracer(service_name.to_owned());
Ok((
tracing_opentelemetry::layer().with_tracer(tracer),
tracer_provider,
))
} }
pub fn banner(crate_name: &str, crate_version: &str) -> String { pub fn banner(crate_name: &str, crate_version: &str) -> String {
+4
View File
@@ -121,6 +121,10 @@ features = ["wasm-bindgen"]
workspace = true workspace = true
features = ["full"] features = ["full"]
[target."cfg(target_arch = \"wasm32\")".dependencies.getrandom03]
workspace = true
features = ["wasm_js"]
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
@@ -26,7 +26,7 @@ use crate::{
error::ClientCoreError, error::ClientCoreError,
}; };
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))] #[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 as gateways_storage;
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails}; pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
@@ -15,3 +15,13 @@ pub(crate) fn get_time_now() -> Instant {
pub(crate) fn new_interval_stream(polling_rate: Duration) -> IntervalStream { pub(crate) fn new_interval_stream(polling_rate: Duration) -> IntervalStream {
gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32) gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32)
} }
#[unsafe(no_mangle)]
unsafe extern "Rust" fn __getrandom_v03_custom(
dest: *mut u8,
len: usize,
) -> Result<(), getrandom03::Error> {
let _ = dest;
let _ = len;
Err(getrandom03::Error::UNSUPPORTED)
}
+80 -26
View File
@@ -128,54 +128,95 @@ impl ManagedConnection {
async fn run(self) { async fn run(self) {
let address = self.address; let address = self.address;
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
let connect_start = tokio::time::Instant::now();
let connection_fut = TcpStream::connect(address); let connection_fut = TcpStream::connect(address);
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await { let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
Ok(stream_res) => match stream_res { Ok(stream_res) => match stream_res {
Ok(stream) => { Ok(stream) => {
debug!("Managed to establish connection to {}", self.address); let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
let noise_start = tokio::time::Instant::now();
let noise_stream = let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await { match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream, Ok(noise_stream) => noise_stream,
Err(err) => { Err(err) => {
error!("Failed to perform Noise handshake with {address} - {err}"); let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
// we failed to finish the noise handshake - increase reconnection attempt warn!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst); self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return; return;
} }
}; };
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been) let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release); self.current_reconnection.store(0, Ordering::Release);
debug!("Noise initiator handshake completed for {:?}", address); debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
Framed::new(noise_stream, NymCodec) Framed::new(noise_stream, NymCodec)
} }
Err(err) => { Err(err) => {
debug!("failed to establish connection to {address} (err: {err})",); let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return; return;
} }
}, },
Err(_) => { Err(_) => {
debug!( let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
connect_ms,
reconnection_attempt,
exit_reason = "timeout",
"failed to connect to {address} within {:?}", "failed to connect to {address} within {:?}",
self.connection_timeout self.connection_timeout
); );
// we failed to connect - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst); self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return; return;
} }
}; };
// Take whatever the receiver channel produces and put it on the connection.
// We could have as well used conn.send_all(receiver.map(Ok)), but considering we don't care
// about neither receiver nor the connection, it doesn't matter which one gets consumed
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await { if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
warn!("Failed to forward packets to {address}: {err}"); warn!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packets to {address}: {err}"
);
} }
debug!( debug!(
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped", peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
); );
} }
} }
@@ -272,16 +313,18 @@ impl SendWithoutResponse for Client {
trace!("Sending packet to {address}"); trace!("Sending packet to {address}");
// TODO: optimisation for the future: rather than constantly using legacy encoding, // TODO: optimisation for the future: rather than constantly using legacy encoding,
// once we're addressing by node_id (and thus have full node info here), // use the mix packet type / flags to pick encoding per packet
// we could simply infer supported encoding based on their version
let framed_packet = let framed_packet =
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding); FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else { let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with // there was never a connection to begin with
debug!("establishing initial connection to {address}"); debug!(
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet event = "mixclient.try_send",
// for sending for as soon as the connection is created peer = %address,
result = "not_connected",
"establishing initial connection to {address}"
);
self.make_connection(address, framed_packet); self.make_connection(address, framed_packet);
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::NotConnected, io::ErrorKind::NotConnected,
@@ -289,15 +332,24 @@ impl SendWithoutResponse for Client {
)); ));
}; };
let channel_capacity = sender.channel.max_capacity();
let channel_available = sender.channel.capacity();
let channel_used = channel_capacity - channel_available;
let sending_res = sender.channel.try_send(framed_packet); let sending_res = sender.channel.try_send(framed_packet);
drop(sender); drop(sender);
sending_res.map_err(|err| { sending_res.map_err(|err| {
match err { match err {
TrySendError::Full(_) => { TrySendError::Full(_) => {
debug!("Connection to {address} seems to not be able to handle all the traffic - dropping the current packet"); warn!(
// it's not a 'big' error, but we did not manage to send the packet event = "mixclient.try_send",
// if the queue is full, we can't really do anything but to drop the packet peer = %address,
result = "full_dropped",
channel_capacity,
channel_used,
"dropping packet: connection buffer to {address} is full ({channel_used}/{channel_capacity})"
);
io::Error::new( io::Error::new(
io::ErrorKind::WouldBlock, io::ErrorKind::WouldBlock,
"connection queue is full", "connection queue is full",
@@ -305,11 +357,13 @@ impl SendWithoutResponse for Client {
} }
TrySendError::Closed(dropped) => { TrySendError::Closed(dropped) => {
debug!( debug!(
"Connection to {address} seems to be dead. attempting to re-establish it...", event = "mixclient.try_send",
peer = %address,
result = "closed_reconnecting",
channel_capacity,
channel_used,
"connection to {address} dead, attempting re-establishment"
); );
// it's not a 'big' error, but we did not manage to send the packet, but queue
// it up to send it as soon as the connection is re-established
self.make_connection(address, dropped); self.make_connection(address, dropped);
io::Error::new( io::Error::new(
io::ErrorKind::ConnectionAborted, io::ErrorKind::ConnectionAborted,
@@ -76,7 +76,7 @@ features = ["json"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
workspace = true workspace = true
features = ["json", "rustls-tls"] features = ["json", "rustls"]
[dev-dependencies] [dev-dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
+1 -1
View File
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
humantime = { workspace = true } humantime = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls"] } reqwest = { workspace = true, features = ["rustls"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
strum = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] }
@@ -22,6 +22,8 @@ pub mod ecash;
pub mod error; pub mod error;
pub mod upgrade_mode; pub mod upgrade_mode;
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
// Histogram buckets for ecash verification duration (in seconds) // Histogram buckets for ecash verification duration (in seconds)
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] = const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]; &[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> { 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(); let start = Instant::now();
nym_metrics::inc!("ecash_verification_attempts"); nym_metrics::inc!("ecash_verification_attempts");
@@ -291,3 +291,40 @@ struct UpgradeModeStateInner {
// (and dealing with the async consequences of that) // (and dealing with the async consequences of that)
status: UpgradeModeStatus, 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)
}
}
+8 -4
View File
@@ -21,10 +21,13 @@ generic-array = { workspace = true, optional = true }
hkdf = { workspace = true, optional = true } hkdf = { workspace = true, optional = true }
hmac = { workspace = true, optional = true } hmac = { workspace = true, optional = true }
jwt-simple = { workspace = true, optional = true } jwt-simple = { workspace = true, optional = true }
libcrux-psq = { workspace = true, optional = true }
libcrux-curve25519 = { workspace = true, optional = true }
cipher = { workspace = true, optional = true } cipher = { workspace = true, optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
rand = { workspace = true, optional = true } rand = { workspace = true, optional = true }
rand09 = { workspace = true, optional = true }
serde_bytes = { workspace = true, optional = true } serde_bytes = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, features = ["derive"], optional = true }
sha2 = { workspace = true, optional = true } sha2 = { workspace = true, optional = true }
@@ -33,25 +36,26 @@ thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] } zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
# internal # internal
nym-sphinx-types = { workspace = true } nym-sphinx-types = { workspace = true, optional = true }
nym-pemstore = { workspace = true } nym-pemstore = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
rand_chacha = { workspace = true } rand_chacha = { workspace = true }
serde_json = { workspace = true }
nym-test-utils = { workspace = true } nym-test-utils = { workspace = true }
serde_json = { workspace = true }
[features] [features]
default = [] default = []
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
naive_jwt = ["asymmetric", "jwt-simple"] naive_jwt = ["asymmetric", "jwt-simple"]
libcrux_x25519 = ["libcrux-psq", "libcrux-curve25519"]
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"] asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize", "rand09"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"] stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"] sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
[lints] [lints]
workspace = true workspace = true
+103
View File
@@ -17,6 +17,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
pub mod serde_helpers; pub mod serde_helpers;
#[cfg(feature = "libcrux_x25519")]
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
/// Size of a X25519 private key /// Size of a X25519 private key
pub const PRIVATE_KEY_SIZE: usize = 32; pub const PRIVATE_KEY_SIZE: usize = 32;
@@ -45,6 +48,9 @@ pub enum KeyRecoveryError {
#[source] #[source]
source: bs58::decode::Error, source: bs58::decode::Error,
}, },
#[error("the x25519 private key could not be converted to its PSQ representation")]
IncompatiblePSQPrivateKey,
} }
#[derive(Zeroize, ZeroizeOnDrop)] #[derive(Zeroize, ZeroizeOnDrop)]
@@ -413,6 +419,88 @@ impl AsRef<[u8]> for PrivateKey {
} }
} }
// libcrux-psq conversion
#[cfg(feature = "libcrux_x25519")]
impl TryFrom<PrivateKey> for libcrux_psq::handshake::types::DHPrivateKey {
type Error = KeyRecoveryError;
fn try_from(
key: PrivateKey,
) -> Result<libcrux_psq::handshake::types::DHPrivateKey, Self::Error> {
Self::try_from(&key)
}
}
#[cfg(feature = "libcrux_x25519")]
impl From<libcrux_psq::handshake::types::DHPrivateKey> for PrivateKey {
fn from(key: libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey {
// SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length
#[allow(clippy::unwrap_used)]
PrivateKey::from_bytes(key.as_ref()).unwrap()
}
}
#[cfg(feature = "libcrux_x25519")]
impl TryFrom<&PrivateKey> for libcrux_psq::handshake::types::DHPrivateKey {
type Error = KeyRecoveryError;
fn try_from(
key: &PrivateKey,
) -> Result<libcrux_psq::handshake::types::DHPrivateKey, Self::Error> {
let mut private_key_bytes = zeroize::Zeroizing::new(key.to_bytes());
libcrux_curve25519::clamp(&mut private_key_bytes);
match libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&private_key_bytes) {
Ok(key) => Ok(key),
Err(_) => Err(KeyRecoveryError::IncompatiblePSQPrivateKey),
}
}
}
#[cfg(feature = "libcrux_x25519")]
impl From<&libcrux_psq::handshake::types::DHPrivateKey> for PrivateKey {
fn from(key: &libcrux_psq::handshake::types::DHPrivateKey) -> PrivateKey {
// SAFETY: the DHPrivateKey is guaranteed to be 32 bytes in length
#[allow(clippy::unwrap_used)]
PrivateKey::from_bytes(key.as_ref()).unwrap()
}
}
#[cfg(feature = "libcrux_x25519")]
impl From<PublicKey> for libcrux_psq::handshake::types::DHPublicKey {
fn from(key: PublicKey) -> libcrux_psq::handshake::types::DHPublicKey {
libcrux_psq::handshake::types::DHPublicKey::from_bytes(key.as_bytes())
}
}
#[cfg(feature = "libcrux_x25519")]
impl From<libcrux_psq::handshake::types::DHPublicKey> for PublicKey {
fn from(key: libcrux_psq::handshake::types::DHPublicKey) -> PublicKey {
// SAFETY: the DHPublicKey is guaranteed to be 32 bytes in length
#[allow(clippy::unwrap_used)]
PublicKey::from_bytes(key.as_ref()).unwrap()
}
}
#[cfg(feature = "libcrux_x25519")]
impl TryFrom<KeyPair> for libcrux_psq::handshake::types::DHKeyPair {
type Error = KeyRecoveryError;
fn try_from(
key: KeyPair,
) -> Result<libcrux_psq::handshake::types::DHKeyPair, KeyRecoveryError> {
Ok(libcrux_psq::handshake::types::DHKeyPair::from(
libcrux_psq::handshake::types::DHPrivateKey::try_from(&key.private_key)?,
))
}
}
#[cfg(feature = "libcrux_x25519")]
impl From<libcrux_psq::handshake::types::DHKeyPair> for KeyPair {
fn from(key: libcrux_psq::handshake::types::DHKeyPair) -> KeyPair {
KeyPair::from(PrivateKey::from(key.sk()))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -421,6 +509,21 @@ mod tests {
fn assert_zeroize<T: Zeroize>() {} fn assert_zeroize<T: Zeroize>() {}
#[test]
fn test_key_conversion() {
let dalek_kp = super::KeyPair::new(&mut rand::thread_rng());
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
let libcrux_private_key =
libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes)
.unwrap();
let libcrux_public_key = libcrux_private_key.to_public();
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
}
#[test] #[test]
fn private_key_is_zeroized() { fn private_key_is_zeroized() {
assert_zeroize::<PrivateKey>(); assert_zeroize::<PrivateKey>();
@@ -44,3 +44,25 @@ pub mod option_bs58_x25519_pubkey {
} }
} }
} }
#[cfg(feature = "libcrux_x25519")]
pub mod bs58_dh_public_key {
use crate::asymmetric::x25519;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(
key: &libcrux_psq::handshake::types::DHPublicKey,
serializer: S,
) -> Result<S::Ok, S::Error> {
let x25519: x25519::PublicKey = (*key).into();
serializer.serialize_str(&x25519.to_base58_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<libcrux_psq::handshake::types::DHPublicKey, D::Error> {
let s = String::deserialize(deserializer)?;
let x25519 = x25519::PublicKey::from_base58_string(s).map_err(serde::de::Error::custom)?;
Ok(x25519.into())
}
}
+149
View File
@@ -109,3 +109,152 @@ impl DerivationMaterial {
} }
} }
} }
pub mod blake3 {
//! Key Derivation Functions using Blake3.
use blake3::Hasher;
use rand09::{RngCore, rng};
use zeroize::Zeroize;
pub fn derive_key_blake3_multi_input(
info: &str,
input_key_material: &[&[u8]],
salt: &[u8],
) -> [u8; 32] {
let mut hasher = Hasher::new_derive_key(info);
for input_key in input_key_material {
hasher.update(input_key);
}
hasher.update(salt);
hasher.finalize().as_bytes().to_owned()
}
/// Derives a 32-byte key using Blake3's key derivation mode.
///
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
/// * `salt` - Additional salt for freshness (nonce)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
/// ```
pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
derive_key_blake3_multi_input(info, &[input_key_material], salt)
}
pub fn derive_fresh_key_blake3_multi_input(
info: &str,
input_key_material: &[&[u8]],
) -> [u8; 32] {
let mut salt = [0u8; 32];
rng().fill_bytes(&mut salt);
let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt);
// Zeroize salt
salt.zeroize();
derived_key
}
/// Derives a fresh 32-byte key using Blake3's key derivation mode.
/// The function calls a random number generator to generate a fresh salt.
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes());
/// ```
pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] {
derive_fresh_key_blake3_multi_input(info, &[input_key_material])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic_derivation() {
let context = "test-context";
let key_material = b"shared_secret_12345";
let salt = b"salt_67890";
let key1 = derive_key_blake3(context, key_material, salt);
let key2 = derive_key_blake3(context, key_material, salt);
assert_eq!(key1, key2, "Same inputs should produce same output");
}
#[test]
fn test_different_contexts_produce_different_keys() {
let key_material = b"shared_secret";
let salt = b"salt";
let key1 = derive_key_blake3("context1", key_material, salt);
let key2 = derive_key_blake3("context2", key_material, salt);
assert_ne!(
key1, key2,
"Different contexts should produce different keys"
);
}
#[test]
fn test_different_salts_produce_different_keys() {
let context = "test-context";
let key_material = b"shared_secret";
let key1 = derive_key_blake3(context, key_material, b"salt1");
let key2 = derive_key_blake3(context, key_material, b"salt2");
assert_ne!(key1, key2, "Different salts should produce different keys");
}
#[test]
fn test_different_key_material_produces_different_keys() {
let context = "test-context";
let salt = b"salt";
let key1 = derive_key_blake3(context, b"secret1", salt);
let key2 = derive_key_blake3(context, b"secret2", salt);
assert_ne!(
key1, key2,
"Different key material should produce different keys"
);
}
#[test]
fn test_output_length() {
let key = derive_key_blake3("test", b"key", b"salt");
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
}
#[test]
fn test_empty_inputs() {
// Should not panic with empty inputs
let key = derive_key_blake3("test", b"", b"");
assert_eq!(key.len(), 32);
}
}
}
-98
View File
@@ -1,98 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Key Derivation Functions using Blake3.
/// Derives a 32-byte key using Blake3's key derivation mode.
///
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
///
/// # Arguments
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
/// * `salt` - Additional salt for freshness (timestamp + nonce)
///
/// # Returns
/// 32-byte derived key suitable for use as PSK
///
/// # Example
/// ```ignore
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
/// ```
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
// Concatenate key_material and salt as input
let input = [key_material, salt].concat();
// Use Blake3's derive_key with context for domain separation
// blake3::derive_key returns [u8; 32] directly
blake3::derive_key(context, &input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic_derivation() {
let context = "test-context";
let key_material = b"shared_secret_12345";
let salt = b"salt_67890";
let key1 = derive_key_blake3(context, key_material, salt);
let key2 = derive_key_blake3(context, key_material, salt);
assert_eq!(key1, key2, "Same inputs should produce same output");
}
#[test]
fn test_different_contexts_produce_different_keys() {
let key_material = b"shared_secret";
let salt = b"salt";
let key1 = derive_key_blake3("context1", key_material, salt);
let key2 = derive_key_blake3("context2", key_material, salt);
assert_ne!(
key1, key2,
"Different contexts should produce different keys"
);
}
#[test]
fn test_different_salts_produce_different_keys() {
let context = "test-context";
let key_material = b"shared_secret";
let key1 = derive_key_blake3(context, key_material, b"salt1");
let key2 = derive_key_blake3(context, key_material, b"salt2");
assert_ne!(key1, key2, "Different salts should produce different keys");
}
#[test]
fn test_different_key_material_produces_different_keys() {
let context = "test-context";
let salt = b"salt";
let key1 = derive_key_blake3(context, b"secret1", salt);
let key2 = derive_key_blake3(context, b"secret2", salt);
assert_ne!(
key1, key2,
"Different key material should produce different keys"
);
}
#[test]
fn test_output_length() {
let key = derive_key_blake3("test", b"key", b"salt");
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
}
#[test]
fn test_empty_inputs() {
// Should not panic with empty inputs
let key = derive_key_blake3("test", b"", b"");
assert_eq!(key.len(), 32);
}
}
-2
View File
@@ -10,8 +10,6 @@ pub mod crypto_hash;
pub mod hkdf; pub mod hkdf;
#[cfg(feature = "hashing")] #[cfg(feature = "hashing")]
pub mod hmac; pub mod hmac;
#[cfg(feature = "hashing")]
pub mod kdf;
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))] #[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
pub mod shared_key; pub mod shared_key;
pub mod symmetric; pub mod symmetric;
+1
View File
@@ -15,6 +15,7 @@ description = "Functions to interact with zknym signers, checking their status a
futures = { workspace = true } futures = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["time"] } tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true } tracing = { workspace = true }
url = { workspace = true } url = { workspace = true }
+8 -6
View File
@@ -3,14 +3,9 @@
use crate::client_check::check_client; use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt}; use futures::stream::{FuturesUnordered, StreamExt};
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_network_defaults::NymNetworkDetails; use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::QueryHttpRpcNyxdClient; 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::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{ use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse, ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
@@ -18,6 +13,12 @@ use nym_validator_client::models::{
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, Epoch, 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; mod client_check;
pub mod error; pub mod error;
@@ -31,6 +32,7 @@ pub type TypedSignerResult = SignerResult<
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>; pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>; pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
#[derive(Serialize, Deserialize)]
pub struct SignersTestResult { pub struct SignersTestResult {
pub threshold: Option<u64>, pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>, 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 } async-trait = { workspace = true }
bincode = { workspace = true } bincode = { workspace = true }
cfg-if = { 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 http.workspace = true
url = { workspace = true } url = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
+215 -83
View File
@@ -46,7 +46,10 @@ use std::{
collections::HashMap, collections::HashMap,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
str::FromStr, str::FromStr,
sync::{Arc, LazyLock}, sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering::Relaxed},
},
time::Duration, time::Duration,
}; };
@@ -70,14 +73,23 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
impl ClientBuilder { impl ClientBuilder {
/// Override the DNS resolver implementation used by the underlying http client. /// 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 { 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.use_secure_dns = false;
self 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 { pub fn no_hickory_dns(mut self) -> Self {
self = self.non_shared();
self.use_secure_dns = false; self.use_secure_dns = false;
self self
} }
@@ -129,7 +141,8 @@ pub struct HickoryDnsResolver {
// Tokio Runtime in initialization, so we must delay the actual // Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver. // construction of the resolver.
state: Arc<OnceCell<TokioResolver>>, state: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>, use_system: Arc<AtomicBool>,
system_resolver: Arc<OnceCell<TokioResolver>>,
static_base: Option<Arc<OnceCell<StaticResolver>>>, static_base: Option<Arc<OnceCell<StaticResolver>>>,
use_shared: bool, use_shared: bool,
/// Overall timeout for dns lookup associated with any individual host resolution. For example, /// Overall timeout for dns lookup associated with any individual host resolution. For example,
@@ -141,7 +154,8 @@ impl Default for HickoryDnsResolver {
fn default() -> Self { fn default() -> Self {
Self { Self {
state: Default::default(), state: Default::default(),
fallback: Default::default(), use_system: Arc::new(AtomicBool::new(false)),
system_resolver: Default::default(),
static_base: Some(Default::default()), static_base: Some(Default::default()),
use_shared: true, use_shared: true,
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT, overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
@@ -151,16 +165,28 @@ impl Default for HickoryDnsResolver {
impl Resolve for HickoryDnsResolver { impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving { fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone(); let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let maybe_fallback = self.fallback.clone();
let maybe_static = self.static_base.clone();
let use_shared = self.use_shared; 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; let overall_dns_timeout = self.overall_dns_timeout;
Box::pin(async move { Box::pin(async move {
resolve( resolve(
name, name,
resolver, resolver,
maybe_fallback,
maybe_static, maybe_static,
use_shared, use_shared,
overall_dns_timeout, 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( async fn resolve(
name: Name, name: Name,
resolver: Arc<OnceCell<TokioResolver>>, resolver: TokioResolver,
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
maybe_static: Option<Arc<OnceCell<StaticResolver>>>, maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
independent: bool, independent: bool,
overall_dns_timeout: Duration, overall_dns_timeout: Duration,
) -> Result<Addrs, ResolveError> { ) -> 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 // 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 // looked up previously within the timeout to where we are not yet ready to try the
// default resolver yet again. // 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 // If no record has been found and a static map of fallback addresses is configured
// check the table for our entry // check the table for our entry
if let Some(ref static_resolver) = maybe_static { if let Some(ref static_resolver) = maybe_static {
@@ -258,6 +269,11 @@ impl Iterator for SocketAddrs {
} }
impl HickoryDnsResolver { 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 /// Attempt to resolve a domain name to a set of ['IpAddr']s
pub async fn resolve_str( pub async fn resolve_str(
&self, &self,
@@ -265,10 +281,20 @@ impl HickoryDnsResolver {
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> { ) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
let n = let n =
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?; 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( resolve(
n, n,
self.state.clone(), resolver,
self.fallback.clone(),
self.static_base.clone(), self.static_base.clone(),
self.use_shared, self.use_shared,
self.overall_dns_timeout, self.overall_dns_timeout,
@@ -298,13 +324,11 @@ impl HickoryDnsResolver {
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> { fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the // 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 // 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() new_resolver_system()
} else { } else {
Ok(SHARED_RESOLVER Ok(SHARED_RESOLVER
.fallback .system_resolver
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)? .get_or_try_init(new_resolver_system)?
.clone()) .clone())
} }
@@ -320,45 +344,80 @@ impl HickoryDnsResolver {
} }
} }
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails /// Swap the primary internal resolver to the system resolver rather than the
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> { /// configured custom resolver.
self.fallback = Some(Default::default()); pub fn use_system_resolver(&self) {
let _ = self self.use_system.store(true, Relaxed);
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?;
// 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 {
// if self.use_shared { SHARED_RESOLVER.use_system_resolver();
// SHARED_RESOLVER.enable_system_fallback()?; }
// }
Ok(())
} }
/// Disable fallback resolution. If the primary resolver fails the error is /// Swap the primary internal resolver to the configured custom resolver rather than the
/// returned immediately /// system resolver.
pub fn disable_system_fallback(&mut self) { pub fn use_configured_resolver(&self) {
self.fallback = None; 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 {
// if self.use_shared { SHARED_RESOLVER.use_configured_resolver();
// SHARED_RESOLVER.fallback = None; }
// }
} }
/// 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. /// exists.
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> { 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 /// 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>>) { pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
let cell = OnceCell::new(); debug!("setting fallback entries for {:?}", addrs.keys());
cell.set(StaticResolver::new(addrs)) if self.static_base.is_none() {
.expect("infallible assign"); let cell = OnceCell::new();
self.static_base = Some(Arc::new(cell)); 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 /// 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 { 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 /// Do a trial resolution using each nameserver individually to test which are working and which
@@ -532,10 +591,7 @@ mod test {
use super::*; use super::*;
use itertools::Itertools; use itertools::Itertools;
use std::collections::HashMap; use std::collections::HashMap;
use std::{ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::Instant,
};
/// IP addresses guaranteed to fail attempts to resolve /// IP addresses guaranteed to fail attempts to resolve
/// ///
@@ -552,7 +608,7 @@ mod test {
let var_name = HickoryDnsResolver::default(); let var_name = HickoryDnsResolver::default();
let resolver = var_name; let resolver = var_name;
let client = reqwest::ClientBuilder::new() let client = reqwest::ClientBuilder::new()
.dns_resolver(resolver.into()) .dns_resolver(resolver)
.build() .build()
.unwrap(); .unwrap();
@@ -597,7 +653,7 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap(); let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]); 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?; let mut addrs = resolver.resolve_str(example_domain).await?;
assert!(addrs.contains(&example_ip4)); assert!(addrs.contains(&example_ip4));
@@ -738,18 +794,19 @@ mod test {
} }
#[tokio::test] #[tokio::test]
#[ignore] #[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// this test is dependent of external network setup -- i.e. blocking all traffic to the default // This test impacts the state of the shared resolver and as such is disabled to avoid
// resolvers. Otherwise the default resolvers will succeed without using the static fallback, // interference with other tests.
// making the test pointless //
// 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> { async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
let time_start = Instant::now(); let resolver1 = HickoryDnsResolver::shared();
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
// create a new resolver that won't mess with the shared resolver used by other tests let time_start = std::time::Instant::now();
let resolver = HickoryDnsResolver::default(); // create a new resolver that uses the shared resolver
let resolver = HickoryDnsResolver::shared();
// successful lookup using fallback to static resolver // successful lookup using fallback to static resolver
let domain = "rpc.nymtech.net"; let domain = "rpc.nymtech.net";
@@ -758,9 +815,27 @@ mod test {
.await .await
.expect("failed to resolve address in static lookup"); .expect("failed to resolve address in static lookup");
println!( let lookup_dur = Instant::now() - time_start;
"{}ms resolved {domain}", assert!(
(Instant::now() - time_start).as_millis() 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 // 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()))); // assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
Ok(()) 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)), 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_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))]; 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>> { pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec()); 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(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_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_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_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_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_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)] #[derive(Debug, Default, Clone)]
pub struct StaticResolver { 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>, pre_resolve_timeout: Option<Duration>,
} }
#[derive(Debug, Clone, Default)]
enum PreResolveStatus {
#[default]
Valid,
ValidUntil(Instant),
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct Entry { struct Entry {
valid_for_pre_resolve_until: Option<Instant>, status: PreResolveStatus,
addrs: Vec<IpAddr>, addrs: Vec<IpAddr>,
} }
impl Entry { impl Entry {
fn new(addrs: Vec<IpAddr>) -> Self { fn new(addrs: Vec<IpAddr>) -> Self {
Self { Self {
valid_for_pre_resolve_until: None, status: PreResolveStatus::Valid,
addrs, 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 { impl StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver { pub fn new() -> StaticResolver {
debug!("building static resolver");
let static_entries = static_entries
.into_iter()
.map(|(name, ips)| (name, Entry::new(ips)))
.collect();
Self { 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), pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
} }
} }
/// Return the full set of domain names and associated addresses stored in this static lookup table /// Initialize the contents of the pre-resolve table for this instance of the static resolver
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> { #[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(); let mut out = HashMap::new();
self.static_addr_map self.preresolve_addr_map
.lock() .lock()
.unwrap() .unwrap()
.iter() .iter()
@@ -59,6 +95,38 @@ impl StaticResolver {
out 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 /// Change the timeout for which domains can be pre-resolved after they are looked up in the
/// static lookup table. /// static lookup table.
#[allow(unused)] #[allow(unused)]
@@ -71,44 +139,58 @@ impl StaticResolver {
/// recently (within the configured timeout) looked it up previously in this static table using /// recently (within the configured timeout) looked it up previously in this static table using
/// a regular resolve. /// a regular resolve.
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> { pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
debug!("found {name:?} in pre-resolve static table resolver"); self.preresolve_addr_map
self.pre_resolve_timeout?;
self.static_addr_map
.lock() .lock()
.unwrap() .unwrap()
.get(name) .get(name)
.filter(|e| { .filter(|entry| entry.is_valid())
e.valid_for_pre_resolve_until .map(|entry| {
.is_some_and(|t| t > Instant::now()) debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
entry.addrs.clone()
}) })
.map(|e| e.addrs.clone())
} }
#[allow(unused)] #[allow(unused)]
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> { pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
Self::resolve_inner( Self::resolve_inner(
self.static_addr_map.lock().unwrap(), self.fallback_addr_map.lock().unwrap(),
self.preresolve_addr_map.lock().unwrap(),
name, name,
self.pre_resolve_timeout, self.pre_resolve_timeout,
) )
.map(|e| e.addrs)
} }
fn resolve_inner( 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, name: &str,
timeout: Option<Duration>, pre_resolve_cache_timeout: Option<Duration>,
) -> Option<Entry> { ) -> Option<Vec<IpAddr>> {
let resolved = table.get_mut(name)?; 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
// 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
// trigger in pre-resolve lookups for the next _timeout_ window. // triggering.
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout); 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()) Some(resolved.clone())
} }
@@ -117,13 +199,23 @@ impl StaticResolver {
impl Resolve for StaticResolver { impl Resolve for StaticResolver {
fn resolve(&self, name: Name) -> Resolving { fn resolve(&self, name: Name) -> Resolving {
debug!("looking up {name:?} in static resolver"); 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; 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 { Box::pin(async move {
let addr_map = addr_map.lock().unwrap(); let fallback_addr_map = fallback_addr_map.lock().unwrap();
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) { 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()), None => return Err(ResolveError::StaticLookupMiss.into()),
Some(entry) => entry.addrs, Some(addrs) => addrs,
}; };
let addrs: Addrs = Box::new( let addrs: Addrs = Box::new(
lookup lookup
@@ -142,6 +234,7 @@ mod test {
use super::*; use super::*;
use std::error::Error as StdError; use std::error::Error as StdError;
use std::net::Ipv4Addr;
use std::str::FromStr; use std::str::FromStr;
#[tokio::test] #[tokio::test]
@@ -149,7 +242,7 @@ mod test {
let example_domain = String::from("static.nymvpn.com"); let example_domain = String::from("static.nymvpn.com");
// lookup for domain for which there is no entry // 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 url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let result = resolver.resolve(url).await; let result = resolver.resolve(url).await;
@@ -166,7 +259,7 @@ mod test {
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]); addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let url = reqwest::dns::Name::from_str(&example_domain).unwrap(); 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?; let mut addrs = resolver.resolve(url).await?;
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0))); assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0))); assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
@@ -175,7 +268,7 @@ mod test {
} }
#[test] #[test]
fn static_lookup_pre_resolve() { fn elevate_fallback_to_pre_resolve() {
let example_duration = Duration::from_secs(3); let example_duration = Duration::from_secs(3);
let example_domain = String::from("static.nymvpn.com"); let example_domain = String::from("static.nymvpn.com");
let mut addr_map = HashMap::new(); let mut addr_map = HashMap::new();
@@ -183,24 +276,23 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap(); let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]); 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 // ensure that attempting to pre-resolve without first resolving returns none
let result = resolver.pre_resolve(&example_domain); let result = resolver.pre_resolve(&example_domain);
assert!(result.is_none()); assert!(result.is_none());
// resolving should now update the pre-resolve validity timeout for the entry // resolving should now update the pre-resolve validity timeout for the entry
let entry = StaticResolver::resolve_inner( let _addrs = resolver
resolver.static_addr_map.lock().unwrap(), .resolve_str(&example_domain)
&example_domain, .expect("entry should exist");
Some(example_duration), assert!(matches!(
) resolver.preresolve_status(&example_domain),
.expect("missing entry???!!!!"); Some(PreResolveStatus::ValidUntil(t))
assert!( if t < Instant::now() + example_duration
entry ));
.valid_for_pre_resolve_until
.is_some_and(|t| t < Instant::now() + example_duration)
);
// check that pre-resolve now returns the expected record // check that pre-resolve now returns the expected record
let addrs = resolver let addrs = resolver
@@ -214,4 +306,139 @@ mod test {
let result = resolver.pre_resolve(&example_domain); let result = resolver.pre_resolve(&example_domain);
assert!(result.is_none()); 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 //! 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 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")] // #[cfg(feature = "tunneling")]
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Front { pub(crate) struct Front {
pub(crate) policy: FrontPolicy, pub(crate) policy: Arc<RwLock<FrontPolicy>>,
enabled: AtomicBool, enabled: AtomicBool,
} }
@@ -19,7 +25,7 @@ impl Clone for Front {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
policy: self.policy.clone(), 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 { impl Front {
pub(crate) fn new(policy: FrontPolicy) -> Self { pub(crate) fn new(policy: FrontPolicy) -> Self {
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, 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 { pub(crate) fn is_enabled(&self) -> bool {
match self.policy { match *self.policy.read().unwrap() {
FrontPolicy::Off => false, FrontPolicy::Off => false,
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed), FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
FrontPolicy::Always => true, FrontPolicy::Always => true,
@@ -46,14 +69,13 @@ impl Front {
if self.is_enabled() { if self.is_enabled() {
return; return;
} }
if matches!(self.policy, FrontPolicy::OnRetry) { if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
self.enabled.store(true, Ordering::Relaxed); self.enabled.store(true, Ordering::Relaxed);
} }
} }
} }
#[derive(Debug, Default, PartialEq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
#[cfg(feature = "tunneling")]
/// Policy for when to use domain fronting for HTTP requests. /// Policy for when to use domain fronting for HTTP requests.
pub enum FrontPolicy { pub enum FrontPolicy {
/// Always use domain fronting for all requests. /// Always use domain fronting for all requests.
@@ -66,29 +88,208 @@ pub enum FrontPolicy {
} }
impl ClientBuilder { impl ClientBuilder {
/// Enable and configure request tunneling for API requests. /// Enable and configure request tunneling for API requests. If no front policy is
#[cfg(feature = "tunneling")] /// provided the shared fronting policy will be used.
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self { pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
let front = Front::new(policy); let front = if let Some(p) = policy {
Front::new(p)
} else {
Front::shared()
};
// Check if any of the supplied urls even support fronting // Check if any of the supplied urls even support fronting
if !self.urls.iter().any(|url| url.has_front()) { if !self.urls.iter().any(|url| url.has_front()) {
warn!( 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 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ApiClientCore, NO_PARAMS, Url}; 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] #[tokio::test]
async fn nym_api_works() { async fn nym_api_works() {
let url1 = Url::new( let url1 = Url::new(
@@ -104,7 +305,7 @@ mod tests {
let client = ClientBuilder::new(url1) let client = ClientBuilder::new(url1)
.expect("bad url") .expect("bad url")
.with_fronting(FrontPolicy::Always) .with_fronting(Some(FrontPolicy::Always))
.build() .build()
.expect("failed to build client"); .expect("failed to build client");
@@ -140,7 +341,7 @@ mod tests {
let client = ClientBuilder::new_with_urls(vec![url1, url2]) let client = ClientBuilder::new_with_urls(vec![url1, url2])
.expect("bad url") .expect("bad url")
.with_fronting(FrontPolicy::Always) .with_fronting(Some(FrontPolicy::Always))
.build() .build()
.expect("failed to build client"); .expect("failed to build client");
+191 -102
View File
@@ -136,6 +136,7 @@
//! ``` //! ```
#![warn(missing_docs)] #![warn(missing_docs)]
use http::header::USER_AGENT;
pub use inventory; pub use inventory;
pub use reqwest; pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder; pub use reqwest::ClientBuilder as ReqwestClientBuilder;
@@ -147,6 +148,7 @@ pub mod registry;
use crate::path::RequestPath; use crate::path::RequestPath;
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap; use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE}; use http::header::{ACCEPT, CONTENT_TYPE};
use itertools::Itertools; use itertools::Itertools;
@@ -161,9 +163,7 @@ use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
#[cfg(not(target_arch = "wasm32"))] use std::sync::{Arc, LazyLock};
use std::net::SocketAddr;
use std::sync::Arc;
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
mod fronted; mod fronted;
@@ -195,6 +195,8 @@ use nym_http_api_client_macro::client_defaults;
/// high and chatty protocols take a while to complete. /// high and chatty protocols take a while to complete.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
client_defaults!( client_defaults!(
priority = -100; priority = -100;
@@ -206,6 +208,24 @@ client_defaults!(
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")) 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 /// Collection of URL Path Segments
pub type PathSegments<'a> = &'a [&'a str]; pub type PathSegments<'a> = &'a [&'a str];
/// Collection of HTTP Request Parameters /// Collection of HTTP Request Parameters
@@ -327,16 +347,22 @@ pub enum HttpClientError {
source: reqwest::Error, source: reqwest::Error,
}, },
#[error("failed to parse header value: {source}")]
InvalidHeaderValue {
#[source]
source: http::Error,
},
#[error("failed to send request for {url}: {source}")] #[error("failed to send request for {url}: {source}")]
RequestSendFailure { RequestSendFailure {
url: reqwest::Url, url: Box<reqwest::Url>,
#[source] #[source]
source: ReqwestErrorWrapper, source: ReqwestErrorWrapper,
}, },
#[error("failed to read response body from {url}: {source}")] #[error("failed to read response body from {url}: {source}")]
ResponseReadFailure { ResponseReadFailure {
url: reqwest::Url, url: Box<reqwest::Url>,
headers: Box<HeaderMap>, headers: Box<HeaderMap>,
status: StatusCode, status: StatusCode,
#[source] #[source]
@@ -353,7 +379,7 @@ pub enum HttpClientError {
}, },
#[error("the requested resource could not be found at {url}")] #[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")] #[error("attempted to use domain fronting and clone a request containing stream data")]
AttemptedToCloneStreamRequest, AttemptedToCloneStreamRequest,
@@ -365,7 +391,7 @@ pub enum HttpClientError {
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}" "the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
)] )]
RequestFailure { RequestFailure {
url: reqwest::Url, url: Box<reqwest::Url>,
status: StatusCode, status: StatusCode,
headers: Box<HeaderMap>, headers: Box<HeaderMap>,
}, },
@@ -374,7 +400,7 @@ pub enum HttpClientError {
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}" "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
)] )]
EmptyResponse { EmptyResponse {
url: reqwest::Url, url: Box<reqwest::Url>,
status: StatusCode, status: StatusCode,
headers: Box<HeaderMap>, 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}" "failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
)] )]
EndpointFailure { EndpointFailure {
url: reqwest::Url, url: Box<reqwest::Url>,
status: StatusCode, status: StatusCode,
headers: Box<HeaderMap>, headers: Box<HeaderMap>,
error: String, error: String,
@@ -453,7 +479,7 @@ impl HttpClientError {
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self { pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
HttpClientError::RequestSendFailure { HttpClientError::RequestSendFailure {
url, url: Box::new(url),
source: ReqwestErrorWrapper(source), source: ReqwestErrorWrapper(source),
} }
} }
@@ -554,6 +580,19 @@ pub trait ApiClientCore {
let req = self.create_request(method, path, params, json_body)?; let req = self.create_request(method, path, params, json_body)?;
self.send(req).await 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 /// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
@@ -562,16 +601,18 @@ pub struct ClientBuilder {
urls: Vec<Url>, urls: Vec<Url>,
timeout: Option<Duration>, timeout: Option<Duration>,
custom_user_agent: bool, custom_user_agent: Option<HeaderValue>,
reqwest_client_builder: reqwest::ClientBuilder, reqwest_client_builder: Option<reqwest::ClientBuilder>,
#[allow(dead_code)] // not dead code, just unused in wasm #[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool, use_secure_dns: bool,
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
front: Option<fronted::Front>, front: fronted::Front,
retry_limit: usize, retry_limit: usize,
serialization: SerializationFormat, serialization: SerializationFormat,
error: Option<HttpClientError>,
} }
impl ClientBuilder { impl ClientBuilder {
@@ -642,10 +683,10 @@ impl ClientBuilder {
let mut builder = Self::new_with_urls(urls)?; 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")] #[cfg(feature = "tunneling")]
{ {
builder = builder.with_fronting(FrontPolicy::OnRetry); builder = builder.with_fronting(None);
} }
Ok(builder) Ok(builder)
@@ -659,26 +700,31 @@ impl ClientBuilder {
let urls = Self::check_urls(urls); 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 { Ok(ClientBuilder {
urls, urls,
timeout: None, timeout: None,
custom_user_agent: false, custom_user_agent: None,
reqwest_client_builder, reqwest_client_builder: None,
use_secure_dns: true, use_secure_dns: true,
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
front: None, front: fronted::Front::off(),
retry_limit: 0, retry_limit: 0,
serialization: SerializationFormat::Json, 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` /// Add an additional URL to the set usable by this constructed `Client`
pub fn add_url(mut self, url: Url) -> Self { pub fn add_url(mut self, url: Url) -> Self {
self.urls.push(url); self.urls.push(url);
@@ -723,7 +769,7 @@ impl ClientBuilder {
/// Provide a pre-configured [`reqwest::ClientBuilder`] /// Provide a pre-configured [`reqwest::ClientBuilder`]
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self { 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 self
} }
@@ -733,18 +779,12 @@ impl ClientBuilder {
V: TryInto<HeaderValue>, V: TryInto<HeaderValue>,
V::Error: Into<http::Error>, V::Error: Into<http::Error>,
{ {
self.custom_user_agent = true; match value.try_into() {
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value); Ok(v) => self.custom_user_agent = Some(v),
self Err(err) => {
} self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
}
/// Override DNS resolution for specific domains to particular IP addresses. }
///
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
self self
} }
@@ -761,30 +801,33 @@ impl ClientBuilder {
/// Returns a Client that uses this ClientBuilder configuration. /// Returns a Client that uses this ClientBuilder configuration.
pub fn build(self) -> Result<Client, HttpClientError> { pub fn build(self) -> Result<Client, HttpClientError> {
if let Some(err) = self.error {
return Err(err);
}
#[cfg(target_arch = "wasm32")] #[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"))] #[cfg(not(target_arch = "wasm32"))]
let reqwest_client = { let reqwest_client = self
let mut builder = self.reqwest_client_builder; .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 builder
if self.use_secure_dns { .build()
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default())); .map_err(HttpClientError::reqwest_client_build_error)
} })
.transpose()?;
builder
.build()
.map_err(HttpClientError::reqwest_client_build_error)?
};
let client = Client { let client = Client {
base_urls: self.urls, base_urls: self.urls,
current_idx: Arc::new(AtomicUsize::new(0)), current_idx: Arc::new(AtomicUsize::new(0)),
reqwest_client, reqwest_client,
using_secure_dns: self.use_secure_dns, custom_user_agent: self.custom_user_agent,
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
front: self.front, front: self.front,
@@ -804,11 +847,11 @@ impl ClientBuilder {
pub struct Client { pub struct Client {
base_urls: Vec<Url>, base_urls: Vec<Url>,
current_idx: Arc<AtomicUsize>, current_idx: Arc<AtomicUsize>,
reqwest_client: reqwest::Client, reqwest_client: Option<reqwest::Client>,
using_secure_dns: bool, custom_user_agent: Option<HeaderValue>,
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
front: Option<fronted::Front>, front: fronted::Front,
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
request_timeout: Duration, request_timeout: Duration,
@@ -862,8 +905,8 @@ impl Client {
Client { Client {
base_urls: vec![new_url], base_urls: vec![new_url],
current_idx: Arc::new(Default::default()), current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(), reqwest_client: None,
using_secure_dns: self.using_secure_dns, custom_user_agent: None,
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
front: self.front.clone(), front: self.front.clone(),
@@ -897,9 +940,7 @@ impl Client {
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
fn matches_current_host(&self, url: &Url) -> bool { fn matches_current_host(&self, url: &Url) -> bool {
if let Some(ref front) = self.front if self.front.is_enabled() {
&& front.is_enabled()
{
url.host_str() == self.current_url().front_str() url.host_str() == self.current_url().front_str()
} else { } else {
url.host_str() == self.current_url().host_str() url.host_str() == self.current_url().host_str()
@@ -926,9 +967,7 @@ impl Client {
} }
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
if let Some(ref front) = self.front if self.front.is_enabled() {
&& front.is_enabled()
{
// if we are using fronting, try updating to the next front // if we are using fronting, try updating to the next front
let url = self.current_url(); 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 // if fronting is enabled we want to update to a host that has fronts configured
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
if let Some(ref front) = self.front if self.front.is_enabled() {
&& front.is_enabled()
{
while next != orig { while next != orig {
if self.base_urls[next].has_front() { if self.base_urls[next].has_front() {
// we have a front for the next host, so we can use it // 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 /// 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 /// method should be called after the host has been updated -- i.e. as part of the subsequent
/// send. /// 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(); let url = self.current_url();
r.url_mut().set_host(url.host_str()).unwrap(); r.url_mut().set_host(url.host_str()).unwrap();
#[cfg(feature = "tunneling")] #[cfg(feature = "tunneling")]
if let Some(ref front) = self.front if self.front.is_enabled() {
&& front.is_enabled()
{
if let Some(front_host) = url.front_str() { if let Some(front_host) = url.front_str() {
if let Some(actual_host) = url.host_str() { if let Some(actual_host) = url.host_str() {
tracing::debug!( tracing::debug!(
@@ -1008,6 +1043,13 @@ impl Client {
.headers_mut() .headers_mut()
.insert(reqwest::header::HOST, actual_host_header); .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()); return (url.as_str(), url.front_str());
} else { } else {
tracing::debug!( tracing::debug!(
@@ -1048,12 +1090,21 @@ impl ApiClientCore for Client {
self.apply_hosts_to_req(&mut req); 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 rb = rb
.header(ACCEPT, self.serialization.content_type()) .header(ACCEPT, self.serialization.content_type())
.header(CONTENT_TYPE, 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 { if let Some(body) = body {
match self.serialization { match self.serialization {
SerializationFormat::Json => { SerializationFormat::Json => {
@@ -1096,16 +1147,19 @@ impl ApiClientCore for Client {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = { let response: Result<Response, HttpClientError> = {
Ok(wasmtimer::tokio::timeout( let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
self.request_timeout, Ok(
self.reqwest_client.execute(req), 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"))] #[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 { match response {
Ok(resp) => return Ok(resp), Ok(resp) => return Ok(resp),
@@ -1121,20 +1175,10 @@ impl ApiClientCore for Client {
if is_network_err { if is_network_err {
// if we have multiple urls, update to the next // 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")] #[cfg(feature = "tunneling")]
if let Some(ref front) = self.front { self.maybe_enable_fronting(("network", url.as_str(), &err));
// If fronting is set to be enabled on error, enable domain fronting as we
// have encountered an error.
let was_enabled = front.is_enabled();
front.retry_enable();
if !was_enabled && front.is_enabled() {
tracing::info!(
"Domain fronting activated after connection failure: {err}",
);
}
}
} }
if attempts < self.retry_limit { 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. /// Common usage functionality for the http client.
@@ -1310,6 +1369,35 @@ pub trait ApiClient: ApiClientCore {
self.get_response(path, params).await 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 /// '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 /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T` based on the content type header /// into the provided type `T` based on the content type header
@@ -1327,7 +1415,8 @@ pub trait ApiClient: ApiClientCore {
let res = self let res = self
.send_request(reqwest::Method::GET, path, params, None::<&()>) .send_request(reqwest::Method::GET, path, params, None::<&()>)
.await?; .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 /// '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 let res = self
.send_request(reqwest::Method::POST, path, params, Some(json_body)) .send_request(reqwest::Method::POST, path, params, Some(json_body))
.await?; .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 /// '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 let res = self
.send_request(reqwest::Method::DELETE, path, params, None::<&()>) .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
.await?; .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 /// '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 let res = self
.send_request(reqwest::Method::PATCH, path, params, Some(json_body)) .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
.await?; .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"`. /// `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 req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
let res = self.send(req).await?; 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"`. /// `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 req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
let res = self.send(req).await?; 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. /// `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 req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
let res = self.send(req).await?; 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"`. /// `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 = let req =
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?; self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
let res = self.send(req).await?; 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() { if !allow_empty && let Some(0) = res.content_length() {
return Err(HttpClientError::EmptyResponse { return Err(HttpClientError::EmptyResponse {
url, url: Box::new(url),
status, status,
headers: Box::new(headers), headers: Box::new(headers),
}); });
@@ -1530,25 +1619,25 @@ where
.bytes() .bytes()
.await .await
.map_err(|source| HttpClientError::ResponseReadFailure { .map_err(|source| HttpClientError::ResponseReadFailure {
url, url: Box::new(url),
headers: Box::new(headers.clone()), headers: Box::new(headers.clone()),
status, status,
source: ReqwestErrorWrapper(source), source: ReqwestErrorWrapper(source),
})?; })?;
decode_raw_response(&headers, full) decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND { } else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url }) Err(HttpClientError::NotFound { url: Box::new(url) })
} else { } else {
let Ok(plaintext) = res.text().await else { let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure { return Err(HttpClientError::RequestFailure {
url, url: Box::new(url),
status, status,
headers: Box::new(headers), headers: Box::new(headers),
}); });
}; };
Err(HttpClientError::EndpointFailure { Err(HttpClientError::EndpointFailure {
url, url: Box::new(url),
status, status,
headers: Box::new(headers), headers: Box::new(headers),
error: plaintext, 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? // - on error without retries is where we have multiple urls, is the url updated?
#[tokio::test] #[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>> { async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![ let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate) "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 url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let mut client = ClientBuilder::new(url) let mut client = ClientBuilder::new(url)
.unwrap() .unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always) .with_fronting(Some(crate::fronted::FrontPolicy::Always))
.build() .build()
.unwrap(); .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 { impl AsRef<url::Url> for Url {
fn as_ref(&self) -> &url::Url { fn as_ref(&self) -> &url::Url {
&self.url &self.url
+3
View File
@@ -6,6 +6,9 @@ edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.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] [dependencies]
dotenvy = { workspace = true, optional = true } 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/", url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]), 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/"; pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
+1
View File
@@ -6,6 +6,7 @@ edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
publish = false
[lib] [lib]
name = "nym_kcp" name = "nym_kcp"
+4 -1
View File
@@ -1,5 +1,6 @@
[package] [package]
name = "nym-kkt-ciphersuite" name = "nym-kkt-ciphersuite"
description = "Nym KKT ciphersuite"
authors.workspace = true authors.workspace = true
repository.workspace = true repository.workspace = true
homepage.workspace = true homepage.workspace = true
@@ -9,15 +10,17 @@ license.workspace = true
rust-version.workspace = true rust-version.workspace = true
readme.workspace = true readme.workspace = true
version.workspace = true version.workspace = true
publish = false
[dependencies] [dependencies]
thiserror = { workspace = true } thiserror = { workspace = true }
num_enum = { workspace = true } num_enum = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
semver = { workspace = true }
blake3 = { workspace = true, optional = true } blake3 = { workspace = true, optional = true }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true } libcrux-sha3 = { workspace = true, optional = true }
[features] [features]
digests = ["blake3", "libcrux-sha3"] digests = ["blake3", "libcrux-sha3"]
+75 -9
View File
@@ -3,10 +3,12 @@
use crate::error::KKTCiphersuiteError; use crate::error::KKTCiphersuiteError;
use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::collections::HashMap; use std::collections::BTreeMap;
use std::fmt::Display; use std::fmt::Display;
use strum_macros::{Display, EnumIter, EnumString}; use strum_macros::{Display, EnumIter, EnumString};
pub use strum::IntoEnumIterator;
pub mod error; pub mod error;
pub const DEFAULT_HASH_LEN: usize = 32; pub const DEFAULT_HASH_LEN: usize = 32;
@@ -45,10 +47,13 @@ pub mod xwing {
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH; pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
} }
pub type KEMKeyDigests = KeyDigests; pub type KEMKeyDigests = BTreeMap<HashFunction, Vec<u8>>;
pub type SigningKeyDigests = KeyDigests;
pub type KeyDigests = HashMap<HashFunction, Vec<u8>>; pub mod node_compatibility {
/// Indicates the initial version where kkt has been introduced
/// 1.27.0 Raclette release
pub const INTRODUCTION: semver::Version = semver::Version::new(1, 27, 0);
}
#[derive( #[derive(
Clone, Clone,
@@ -62,6 +67,8 @@ pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
EnumIter, EnumIter,
EnumString, EnumString,
Display, Display,
Ord,
PartialOrd,
)] )]
#[strum(ascii_case_insensitive)] #[strum(ascii_case_insensitive)]
#[strum(serialize_all = "lowercase")] #[strum(serialize_all = "lowercase")]
@@ -204,23 +211,26 @@ impl SignatureScheme {
EnumIter, EnumIter,
EnumString, EnumString,
Display, Display,
Default,
Ord,
PartialOrd,
)] )]
#[strum(ascii_case_insensitive)] #[strum(ascii_case_insensitive)]
#[strum(serialize_all = "lowercase")] #[strum(serialize_all = "lowercase")]
#[repr(u8)] #[repr(u8)]
pub enum KEM { pub enum KEM {
XWing = 0, // unsupported
// XWing = 0,
#[default]
MlKem768 = 1, MlKem768 = 1,
McEliece = 2, McEliece = 2,
X25519 = 255,
} }
impl KEM { impl KEM {
pub fn encapsulation_key_length(&self) -> usize { pub const fn encapsulation_key_length(&self) -> usize {
match self { match self {
KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH, KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH,
KEM::XWing => xwing::PUBLIC_KEY_LENGTH, // KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
KEM::X25519 => x25519::PUBLIC_KEY_LENGTH,
KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH, KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH,
} }
} }
@@ -238,6 +248,17 @@ pub struct Ciphersuite {
signature_length: usize, signature_length: usize,
} }
impl Default for Ciphersuite {
fn default() -> Self {
Ciphersuite::new(
KEM::MlKem768,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
)
}
}
impl Ciphersuite { impl Ciphersuite {
pub fn new( pub fn new(
kem: KEM, kem: KEM,
@@ -257,6 +278,51 @@ impl Ciphersuite {
} }
} }
/// Determine optimal `Ciphersuite` based on remote's node's version
pub fn from_node_version(semver: semver::Version) -> Option<Self> {
if semver < node_compatibility::INTRODUCTION {
// node can't possibly support any Ciphersuite
return None;
}
// currently there are no other branches known to the client
// once changes to defaults are introduced, follow pattern similar to the one implemented in
// `common/authenticator-requests/src/version.rs`
Some(Ciphersuite::new(
KEM::MlKem768,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
))
}
#[must_use]
pub fn with_kem(mut self, kem: KEM) -> Self {
self.kem = kem;
self.encapsulation_key_length = kem.encapsulation_key_length();
self
}
#[must_use]
pub fn with_signature_scheme(mut self, signature_scheme: SignatureScheme) -> Self {
self.signature_scheme = signature_scheme;
self.signing_key_length = signature_scheme.signing_key_length();
self.verification_key_length = signature_scheme.verification_key_length();
self.signature_length = signature_scheme.signature_length();
self
}
#[must_use]
pub fn with_hash_function(mut self, hash_function: HashFunction) -> Self {
self.hash_function = hash_function;
self
}
#[must_use]
pub fn with_hash_length(mut self, hash_length: HashLength) -> Self {
self.hash_length = hash_length;
self
}
pub fn kem_key_len(&self) -> usize { pub fn kem_key_len(&self) -> usize {
self.encapsulation_key_length self.encapsulation_key_length
} }
@@ -1,6 +1,5 @@
[package] [package]
name = "nym-lp-transport" name = "nym-kkt-context"
version = "0.1.0"
authors.workspace = true authors.workspace = true
repository.workspace = true repository.workspace = true
homepage.workspace = true homepage.workspace = true
@@ -9,14 +8,14 @@ edition.workspace = true
license.workspace = true license.workspace = true
rust-version.workspace = true rust-version.workspace = true
readme.workspace = true readme.workspace = true
version.workspace = true
publish = false publish = false
[dependencies] [dependencies]
tokio = { workspace = true, features = ["net"] } num_enum = { workspace = true }
nym-test-utils = { path = "../test-utils", optional = true } thiserror = { workspace = true }
[features] nym-kkt-ciphersuite = { path = "../nym-kkt-ciphersuite" }
io-mocks = ["nym-test-utils"]
[lints] [lints]
workspace = true workspace = true
@@ -1,13 +1,38 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN;
use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN};
use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_enum::{IntoPrimitive, TryFromPrimitive};
use nym_kkt_ciphersuite::{CIPHERSUITE_ENCODING_LEN, Ciphersuite};
use std::fmt::Display; use std::fmt::Display;
use thiserror::Error;
// This must be less than 4 bits
pub const KKT_VERSION: u8 = 1;
const _: () = assert!(KKT_VERSION < 1 << 4);
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN; pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
#[derive(Debug, Error)]
pub enum KKTContextEncodingError {
#[error("KKT Message Count Limit Reached")]
MessageCountLimitReached,
#[error("{version} is not a valid KKT version")]
InvalidVersion { version: u8 },
#[error("{raw} is not a valid KKTStatus")]
InvalidStatus { raw: u8 },
#[error("{raw} is not a valid KKTRole")]
InvalidRole { raw: u8 },
#[error("{raw} is not a valid KKTMode")]
InvalidMode { raw: u8 },
#[error(transparent)]
InvalidCiphersuite(#[from] nym_kkt_ciphersuite::error::KKTCiphersuiteError),
}
// bitmask used: 0b1110_0000 // bitmask used: 0b1110_0000
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)] #[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)] #[repr(u8)]
@@ -15,11 +40,11 @@ pub enum KKTStatus {
Ok = 0b0000_0000, Ok = 0b0000_0000,
InvalidRequestFormat = 0b0010_0000, InvalidRequestFormat = 0b0010_0000,
InvalidResponseFormat = 0b0100_0000, InvalidResponseFormat = 0b0100_0000,
InvalidSignature = 0b0110_0000, UnsupportedCiphersuite = 0b0110_0000,
UnsupportedCiphersuite = 0b1000_0000, UnsupportedKKTVersion = 0b1000_0000,
UnsupportedKKTVersion = 0b1010_0000, InvalidKey = 0b1010_0000,
InvalidKey = 0b1100_0000, Timeout = 0b1100_0000,
Timeout = 0b1110_0000, UnverifiedKEMKey = 0b1110_0000,
} }
impl Display for KKTStatus { impl Display for KKTStatus {
@@ -28,10 +53,10 @@ impl Display for KKTStatus {
KKTStatus::Ok => "Ok", KKTStatus::Ok => "Ok",
KKTStatus::InvalidRequestFormat => "Invalid Request Format", KKTStatus::InvalidRequestFormat => "Invalid Request Format",
KKTStatus::InvalidResponseFormat => "Invalid Response Format", KKTStatus::InvalidResponseFormat => "Invalid Response Format",
KKTStatus::InvalidSignature => "Invalid Signature",
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite", KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version", KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
KKTStatus::InvalidKey => "Invalid Key", KKTStatus::InvalidKey => "Invalid Key",
KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key",
KKTStatus::Timeout => "Timeout", KKTStatus::Timeout => "Timeout",
}) })
} }
@@ -43,7 +68,16 @@ impl Display for KKTStatus {
pub enum KKTRole { pub enum KKTRole {
Initiator = 0b0000_0000, Initiator = 0b0000_0000,
Responder = 0b0000_0001, Responder = 0b0000_0001,
AnonymousInitiator = 0b0000_0010, }
impl KKTRole {
pub const fn is_initiator(&self) -> bool {
matches!(self, KKTRole::Initiator)
}
pub const fn is_responder(&self) -> bool {
matches!(self, KKTRole::Responder)
}
} }
// bitmask used: 0b0001_1100 // bitmask used: 0b0001_1100
@@ -54,6 +88,16 @@ pub enum KKTMode {
Mutual = 0b0000_0100, Mutual = 0b0000_0100,
} }
impl KKTMode {
pub const fn is_one_way(&self) -> bool {
matches!(self, KKTMode::OneWay)
}
pub const fn is_mutual(&self) -> bool {
matches!(self, KKTMode::Mutual)
}
}
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub struct KKTContext { pub struct KKTContext {
version: u8, version: u8,
@@ -63,24 +107,20 @@ pub struct KKTContext {
role: KKTRole, role: KKTRole,
ciphersuite: Ciphersuite, ciphersuite: Ciphersuite,
} }
impl KKTContext { impl KKTContext {
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> { pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Self {
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay { Self {
return Err(KKTError::IncompatibilityError {
info: "Anonymous Initiator can only use OneWay mode",
});
}
Ok(Self {
version: KKT_VERSION, version: KKT_VERSION,
message_sequence: 0, message_sequence: 0,
status: KKTStatus::Ok, status: KKTStatus::Ok,
mode, mode,
role, role,
ciphersuite, ciphersuite,
}) }
} }
pub fn derive_responder_header(&self) -> Result<Self, KKTError> { pub fn derive_responder_header(&self) -> Result<Self, KKTContextEncodingError> {
let mut responder_header = *self; let mut responder_header = *self;
responder_header.increment_message_sequence_count()?; responder_header.increment_message_sequence_count()?;
@@ -89,12 +129,12 @@ impl KKTContext {
Ok(responder_header) Ok(responder_header)
} }
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> { pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTContextEncodingError> {
if self.message_sequence + 1 < (1 << 4) { if self.message_sequence + 1 < (1 << 4) {
self.message_sequence += 1; self.message_sequence += 1;
Ok(()) Ok(())
} else { } else {
Err(KKTError::MessageCountLimitReached) Err(KKTContextEncodingError::MessageCountLimitReached)
} }
} }
@@ -118,9 +158,10 @@ impl KKTContext {
} }
pub fn body_len(&self) -> usize { pub fn body_len(&self) -> usize {
if self.status != KKTStatus::Ok if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey)
|| (self.mode == KKTMode::OneWay ||
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator)) // no payload
(self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator)
{ {
0 0
} else { } else {
@@ -128,37 +169,18 @@ impl KKTContext {
} }
} }
pub fn signature_len(&self) -> usize {
match self.role {
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
KKTRole::AnonymousInitiator => 0,
}
}
pub const fn header_len(&self) -> usize { pub const fn header_len(&self) -> usize {
KKT_CONTEXT_LEN 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 { pub fn full_message_len(&self) -> usize {
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len() self.body_len() + self.header_len()
} }
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> { pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTContextEncodingError> {
let mut header_bytes = [0u8; KKT_CONTEXT_LEN]; let mut header_bytes = [0u8; KKT_CONTEXT_LEN];
if self.message_sequence >= 1 << 4 { if self.message_sequence >= 1 << 4 {
return Err(KKTError::MessageCountLimitReached); return Err(KKTContextEncodingError::MessageCountLimitReached);
} }
let ciphersuite_bytes = self.ciphersuite.encode(); let ciphersuite_bytes = self.ciphersuite.encode();
@@ -175,15 +197,17 @@ impl KKTContext {
Ok(header_bytes) Ok(header_bytes)
} }
pub fn try_decode(header_bytes: [u8; KKT_CONTEXT_LEN]) -> Result<Self, KKTError> { pub fn try_decode(
header_bytes: [u8; KKT_CONTEXT_LEN],
) -> Result<Self, KKTContextEncodingError> {
let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4; let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4;
let message_sequence_counter = header_bytes[0] & 0b0000_1111; let message_sequence_counter = header_bytes[0] & 0b0000_1111;
// We only check if stuff is valid here, not necessarily if it's compatible // We only check if stuff is valid here, not necessarily if it's compatible
if kkt_version > KKT_VERSION { if kkt_version > KKT_VERSION {
return Err(KKTError::FrameDecodingError { return Err(KKTContextEncodingError::InvalidVersion {
info: format!("Header - Invalid KKT Version: {kkt_version}"), version: kkt_version,
}); });
} }
@@ -191,16 +215,15 @@ impl KKTContext {
let raw_kkt_role = header_bytes[1] & 0b0000_0011; let raw_kkt_role = header_bytes[1] & 0b0000_0011;
let raw_kkt_mode = header_bytes[1] & 0b0001_1100; let raw_kkt_mode = header_bytes[1] & 0b0001_1100;
let status = let status = KKTStatus::try_from(raw_kkt_status).map_err(|_| {
KKTStatus::try_from(raw_kkt_status).map_err(|_| KKTError::FrameDecodingError { KKTContextEncodingError::InvalidStatus {
info: format!("Header - Invalid KKT Status: {raw_kkt_status}"), raw: raw_kkt_status,
})?; }
let role = KKTRole::try_from(raw_kkt_role).map_err(|_| KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Role: {raw_kkt_role}"),
})?;
let mode = KKTMode::try_from(raw_kkt_mode).map_err(|_| KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
})?; })?;
let role = KKTRole::try_from(raw_kkt_role)
.map_err(|_| KKTContextEncodingError::InvalidRole { raw: raw_kkt_role })?;
let mode = KKTMode::try_from(raw_kkt_mode)
.map_err(|_| KKTContextEncodingError::InvalidMode { raw: raw_kkt_mode })?;
// SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes // SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
@@ -228,9 +251,8 @@ mod tests {
let valid_context = KKTContext::new( let valid_context = KKTContext::new(
KKTRole::Initiator, KKTRole::Initiator,
KKTMode::Mutual, KKTMode::Mutual,
Ciphersuite::decode([255, 1, 0, 0]).unwrap(), Ciphersuite::decode([1, 1, 0, 0]).unwrap(),
) );
.unwrap();
let encoded = valid_context.encode().unwrap(); let encoded = valid_context.encode().unwrap();
let decoded = KKTContext::try_decode(encoded).unwrap(); let decoded = KKTContext::try_decode(encoded).unwrap();
+9 -13
View File
@@ -7,34 +7,30 @@ license.workspace = true
publish = false publish = false
[dependencies] [dependencies]
blake3 = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
num_enum = { workspace = true } num_enum = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
# internal # internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] } nym-crypto = { path = "../crypto", features = ["hashing"] }
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] } nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
nym-kkt-context = { path = "../nym-kkt-context" }
nym-pemstore = { workspace = true }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" } libcrux-kem = { workspace = true }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] } libcrux-ecdh = { workspace = true, features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" } libcrux-chacha20poly1305 = { workspace = true }
rand = "0.9.2" rand09 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] } 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] [dev-dependencies]
rand_chacha = "0.9.0" rand_chacha = "0.9.0"
anyhow = { workspace = true } anyhow = { workspace = true }
criterion = { workspace = true }
[[bench]]
name = "benches"
harness = false
[lints] [lints]
workspace = true workspace = true
-480
View File
@@ -1,480 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// fine in benchmarking code
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use criterion::{Criterion, criterion_group, criterion_main};
use nym_crypto::asymmetric::ed25519;
use nym_kkt::{
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme},
context::KKTMode,
frame::KKTFrame,
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
},
};
use rand::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut s);
ed25519::KeyPair::from_secret(s, 0)
});
});
}
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
let mut rng = rand::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::Shake128,
HashFunction::Shake256,
] {
let ciphersuite = Ciphersuite::resolve_ciphersuite(
kem,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// generate kem public keys
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => (
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::X25519 => (
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::McEliece => (
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
),
};
let i_kem_key_bytes = initiator_kem_public_key.encode();
let r_kem_key_bytes = responder_kem_public_key.encode();
let i_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&i_kem_key_bytes,
);
let r_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&r_kem_key_bytes,
);
// Anonymous Initiator, OneWay
{
c.bench_function(
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|b| {
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
},
);
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Encode Frame - Request",
),
|b| b.iter(|| i_frame.to_bytes()),
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Decode Frame - Request",
),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
),
|b| {
b.iter(|| {
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
});
},
);
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Responder Encode Frame",
),
|b| b.iter(|| r_frame.to_bytes()),
);
c.bench_function(
&format!(
"{kem}, {hash_function} | Anonymous Initiator: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
});
},
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",),
|b| {
b.iter(|| {
initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap()
});
},
);
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Encode Frame - Request",),
|b| b.iter(|| i_frame.to_bytes()),
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Decode Frame - Request",),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|b| {
b.iter(|| {
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
c.bench_function(
&format!(
"{kem}, {hash_function} | Initiator OneWay: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Encode Frame",),
|b| {
b.iter(|| r_frame.to_bytes());
},
);
c.bench_function(
&format!(
"{kem}, {hash_function} | Initiator OneWay: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
});
},
);
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
c.bench_function(
&format!("{kem}, {hash_function} | Initiator Mutual: Generate Request",),
|b| {
b.iter(|| {
initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap()
});
},
);
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator Mutual: Encode Frame - Request",),
|b| {
b.iter(|| i_frame.to_bytes());
},
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator Mutual: Decode Frame - Request",),
|b| {
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
},
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|b| {
b.iter(|| {
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
c.bench_function(
&format!(
"{kem}, {hash_function} | Initiator Mutual: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Encode Frame",),
|b| {
b.iter(|| {
r_frame.to_bytes();
});
},
);
c.bench_function(
&format!(
"{kem}, {hash_function} | Initiator Mutual: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap()
});
},
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
}
}
}
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
kkt_benchmark
);
criterion_main!(benches);
+188
View File
@@ -0,0 +1,188 @@
use libcrux_chacha20poly1305::TAG_LEN;
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_crypto::hkdf::blake3::derive_key_blake3;
use rand09::{CryptoRng, RngCore};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use crate::error::KKTError;
// This is arbitrary
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Carrier {
tx_key: [u8; 32],
rx_key: [u8; 32],
tx_counter: u64,
rx_counter: u64,
}
pub enum CarrierRole {
Initiator,
Responder,
}
fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> {
match nonce.checked_add(1) {
Some(incremented_nonce) => {
*nonce = incremented_nonce;
Ok(())
}
None => Err(KKTError::AEADError {
info: "Nonce maxed out.",
}),
}
}
fn as_nonce_bytes(nonce: u64) -> [u8; 12] {
let mut bytes = [0u8; 12];
let nonce_bytes = nonce.to_le_bytes();
bytes[4..].clone_from_slice(&nonce_bytes);
bytes
}
impl Carrier {
fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self {
Self {
tx_key,
rx_key,
tx_counter: 1,
rx_counter: 1,
}
}
pub fn new<R>(
rng: &mut R,
remote_public_key: &DHPublicKey,
context: &[u8],
is_initiator: bool,
) -> Result<(Self, DHPublicKey), KKTError>
where
R: RngCore + CryptoRng,
{
let ephemeral_keypair = DHKeyPair::new(rng);
let shared_secret = ephemeral_keypair
.sk()
.diffie_hellman(remote_public_key)
.map_err(|_| KKTError::X25519Error {
info: "Key Derivation Error",
})?;
Ok((
Self::from_secret_slice(shared_secret.as_ref(), context, is_initiator),
ephemeral_keypair.pk,
))
}
pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8], is_initiator: bool) -> Self {
let (tx_key, rx_key) = if is_initiator {
(
derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context),
derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context),
)
} else {
(
derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context),
derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context),
)
};
Self::init(tx_key, rx_key)
}
pub fn from_secret(secret: [u8; 32], context: &[u8], is_initiator: bool) -> Self {
Self::from_secret_slice(Zeroizing::new(secret).as_slice(), context, is_initiator)
}
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, KKTError> {
if plaintext.len() > MAX_PAYLOAD_LEN {
return Err(KKTError::AEADError {
info: "Plaintext too large",
});
}
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
libcrux_chacha20poly1305::encrypt(
&self.tx_key,
plaintext,
&mut output_buffer,
b"kkt-carrier-v1",
&as_nonce_bytes(self.tx_counter),
)?;
increment_nonce(&mut self.tx_counter)?;
Ok(output_buffer)
}
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, KKTError> {
if ciphertext.len() > MAX_PAYLOAD_LEN + TAG_LEN {
return Err(KKTError::AEADError {
info: "Ciphertext too large",
});
}
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
libcrux_chacha20poly1305::decrypt(
&self.rx_key,
&mut output_buffer,
ciphertext,
b"kkt-carrier-v1",
&as_nonce_bytes(self.rx_counter),
)?;
increment_nonce(&mut self.rx_counter)?;
Ok(output_buffer)
}
}
#[cfg(test)]
mod tests {
use crate::{carrier::Carrier, key_utils::generate_lp_keypair_x25519};
use rand09::RngCore;
#[test]
fn test_e2e() {
let mut rng = rand09::rng();
// generate responder x25519 keys
let r_x25519 = generate_lp_keypair_x25519(&mut rng);
let mut context: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut context);
let ephemeral_keypair = generate_lp_keypair_x25519(&mut rng);
let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap();
let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap();
let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context, true);
let mut r_carrier = Carrier::from_secret_slice(r_shared_secret.as_ref(), &context, false);
let test1 = b"test1: i>r #1";
let ct1 = i_carrier.encrypt(test1).unwrap();
let pt1 = r_carrier.decrypt(&ct1).unwrap();
assert_eq!(pt1, test1);
let test2 = b"test2: r>i #1";
let ct2 = i_carrier.encrypt(test2).unwrap();
let pt2 = r_carrier.decrypt(&ct2).unwrap();
assert_eq!(pt2, test2);
let test3 = b"test3: i>r #2";
let ct3 = i_carrier.encrypt(test3).unwrap();
let pt3 = r_carrier.decrypt(&ct3).unwrap();
assert_eq!(pt3, test3);
let test4 = b"test4: i>r #3";
let ct4 = i_carrier.encrypt(test4).unwrap();
let pt4 = r_carrier.decrypt(&ct4).unwrap();
assert_eq!(pt4, test4);
let test5 = b"test5: r>i #2";
let ct5 = i_carrier.encrypt(test5).unwrap();
let pt5 = r_carrier.decrypt(&ct5).unwrap();
assert_eq!(pt5, test5);
}
}
-74
View File
@@ -1,74 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::KKTError;
use libcrux_kem::Algorithm;
pub use nym_kkt_ciphersuite::*;
pub enum EncapsulationKey<'a> {
MlKem768(libcrux_kem::PublicKey),
XWing(libcrux_kem::PublicKey),
X25519(libcrux_kem::PublicKey),
McEliece(classic_mceliece_rust::PublicKey<'a>),
}
pub enum DecapsulationKey<'a> {
MlKem768(libcrux_kem::PrivateKey),
XWing(libcrux_kem::PrivateKey),
X25519(libcrux_kem::PrivateKey),
McEliece(classic_mceliece_rust::SecretKey<'a>),
}
impl<'a> EncapsulationKey<'a> {
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
match kem {
KEM::McEliece => {
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
Err(KKTError::KEMError {
info: "Received McEliece Encapsulation Key with Invalid Length",
})
} else {
let mut public_key_bytes =
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
public_key_bytes.clone_from_slice(bytes);
Ok(EncapsulationKey::McEliece(
classic_mceliece_rust::PublicKey::from(public_key_bytes),
))
}
}
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem)?,
bytes,
)?)),
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
EncapsulationKey::XWing(public_key)
| EncapsulationKey::MlKem768(public_key)
| EncapsulationKey::X25519(public_key) => public_key.encode(),
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
}
}
}
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result<Algorithm, KKTError> {
match kem {
KEM::MlKem768 => Ok(Algorithm::MlKem768),
KEM::XWing => Ok(Algorithm::XWingKemDraft06),
KEM::X25519 => Ok(Algorithm::X25519),
KEM::McEliece => Err(KKTError::KEMMapping {
info: "attempted to map McEliece KEM to libcrux_kem",
}),
}
}
-254
View File
@@ -1,254 +0,0 @@
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame};
use blake3::Hasher;
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
use nym_crypto::asymmetric::x25519;
use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
#[derive(Clone, Copy, Zeroize)]
pub struct KKTSessionSecret([u8; 32]);
impl KKTSessionSecret {
pub fn new<R>(rng: &mut R, remote_public_key: &x25519::PublicKey) -> (Self, x25519::PublicKey)
where
R: RngCore + CryptoRng,
{
let mut private_key_bytes = [0u8; x25519::PRIVATE_KEY_SIZE];
rng.fill_bytes(&mut private_key_bytes);
let ephemeral_private_key = x25519::PrivateKey::from_secret(private_key_bytes);
let ephemeral_public_key = x25519::PublicKey::from(&ephemeral_private_key);
(
Self::derive(&ephemeral_private_key, remote_public_key),
ephemeral_public_key,
)
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
Self(secret)
}
fn try_derive(private_key: &x25519::PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
let mut pub_key: [u8; 32] = [0u8; 32];
pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]);
// Todo: check validity of pk...
let pk = x25519::PublicKey::from(pub_key);
Ok(Self::derive(private_key, &pk))
}
pub fn derive(private_key: &x25519::PrivateKey, public_key: &x25519::PublicKey) -> Self {
let mut shared_secret = private_key.diffie_hellman(public_key);
let mut hasher = Hasher::new();
hasher.update(&shared_secret);
shared_secret.zeroize();
Self(hasher.finalize().as_bytes().to_owned())
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
pub fn encrypt_initial_kkt_frame<R>(
rng: &mut R,
remote_public_key: &x25519::PublicKey,
kkt_frame: &KKTFrame,
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(rng, remote_public_key);
let mut encrypted_frame =
encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?;
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE);
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
output_buffer.append(&mut encrypted_frame);
// [ 32 | 12 | ciphertext | 16];
// [eph_pub_key | nonce | ciphertext | tag];
Ok((session_secret_key, output_buffer))
}
pub fn decrypt_initial_kkt_frame(
responder_private_key: &x25519::PrivateKey,
encrypted_frame_bytes: &[u8],
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN {
Err(KKTError::AEADError {
info: "Encrypted KKT Frame is too short.",
})
} else {
let shared_secret = KKTSessionSecret::try_derive(
responder_private_key,
&encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE],
)?;
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
&shared_secret,
&encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..],
KKT_INITIAL_FRAME_AAD,
)?;
Ok((shared_secret, kkt_frame, kkt_context))
}
}
pub fn encrypt_kkt_frame<R>(
rng: &mut R,
secret_key: &KKTSessionSecret,
kkt_frame: &KKTFrame,
aad: &[u8],
) -> Result<Vec<u8>, KKTError>
where
R: CryptoRng + RngCore,
{
let kkt_frame_bytes = kkt_frame.to_bytes();
// generate nonce
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
rng.fill_bytes(&mut nonce);
let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?;
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
let mut output_buffer: Vec<u8> =
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);
output_buffer.extend_from_slice(&nonce);
output_buffer.append(&mut ciphertext);
Ok(output_buffer)
}
// kkt_frame_bytes should look like this
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
pub fn decrypt_kkt_frame(
secret_key: &KKTSessionSecret,
kkt_frame_bytes: &[u8],
aad: &[u8],
) -> Result<(KKTFrame, KKTContext), KKTError> {
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);
let plaintext = decrypt(
secret_key.as_bytes(),
&kkt_frame_bytes[NONCE_LEN..],
aad,
&nonce,
)?;
KKTFrame::from_bytes(&plaintext)
}
fn encrypt(
secret_key: &[u8; 32],
plaintext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?;
Ok(output_buffer)
}
fn decrypt(
secret_key: &[u8; 32],
ciphertext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?;
Ok(output_buffer)
}
#[cfg(test)]
mod test {
use crate::ciphersuite::Ciphersuite;
use crate::context::{KKTContext, KKTMode, KKTRole};
use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame};
use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame};
use crate::{
ciphersuite::DEFAULT_HASH_LEN,
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};
use rand::{RngCore, SeedableRng, rng};
use rand_chacha::ChaCha20Rng;
#[test]
fn test_keygen() {
let mut rng = rng();
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
let (session_secret_key, ephemeral_public_key) =
KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key());
let shared_secret = KKTSessionSecret::try_derive(
responder_x25519_keypair.private_key(),
ephemeral_public_key.as_bytes().as_slice(),
)
.unwrap();
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
}
#[test]
fn test_encryption() {
let mut rng = rng();
let mut secret_key = [0u8; DEFAULT_HASH_LEN];
rng.fill_bytes(&mut secret_key);
let mut plaintext = vec![0; 100];
rng.fill_bytes(&mut plaintext);
let mut nonce = [0; 12];
rng.fill_bytes(&mut nonce);
let mut aad = vec![0; 124];
rng.fill_bytes(&mut aad);
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
assert_eq!(o_plaintext, plaintext)
}
#[test]
fn kkt_frame_encryption() -> anyhow::Result<()> {
let mut rng = ChaCha20Rng::seed_from_u64(42);
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
let aad = b"my-amazing-aad";
let valid_context = KKTContext::new(
KKTRole::Initiator,
KKTMode::Mutual,
Ciphersuite::decode([255, 1, 0, 0])?,
)?;
let dummy_frame = KKTFrame::new(
valid_context.encode()?,
&[2u8; 32],
[3u8; KKT_SESSION_ID_LEN],
&[4u8; 64],
);
let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?;
let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?;
assert_eq!(dummy_frame, frame);
assert_eq!(context, valid_context);
Ok(())
}
}
+36 -7
View File
@@ -3,18 +3,18 @@
use crate::context::KKTStatus; use crate::context::KKTStatus;
use nym_kkt_ciphersuite::error::KKTCiphersuiteError; use nym_kkt_ciphersuite::error::KKTCiphersuiteError;
use nym_kkt_context::KKTContextEncodingError;
use std::fmt::Debug; use std::fmt::Debug;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum KKTError { pub enum KKTError {
#[error("Signature constructor error")]
SigConstructorError,
#[error("Signature verification error")]
SigVerifError,
#[error(transparent)] #[error(transparent)]
CiphersuiteDecodingError(#[from] KKTCiphersuiteError), CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
#[error(transparent)]
MaskedByteError(#[from] MaskedByteError),
#[error("KEM mapping failure: {}", info)] #[error("KEM mapping failure: {}", info)]
KEMMapping { info: &'static str }, KEMMapping { info: &'static str },
@@ -33,9 +33,6 @@ pub enum KKTError {
#[error("KKT Responder Flagged Error: {}", status)] #[error("KKT Responder Flagged Error: {}", status)]
ResponderFlaggedError { status: KKTStatus }, ResponderFlaggedError { status: KKTStatus },
#[error("KKT Message Count Limit Reached")]
MessageCountLimitReached,
#[error("PSQ KEM Error: {}", info)] #[error("PSQ KEM Error: {}", info)]
KEMError { info: &'static str }, KEMError { info: &'static str },
@@ -48,8 +45,40 @@ pub enum KKTError {
#[error("{}", info)] #[error("{}", info)]
AEADError { info: &'static str }, AEADError { info: &'static str },
#[error("{}", info)]
DecodingError { info: &'static str },
#[error("{}", info)]
UnsupportedAlgorithm { info: &'static str },
#[error("Generic libcrux error")] #[error("Generic libcrux error")]
LibcruxError, LibcruxError,
#[error("failed to derive shared secret: {inner:?}")]
SharedSecretDerivationFailure {
inner: libcrux_psq::handshake::HandshakeError,
},
#[error("the received encapsulation key hash does not match the expected value")]
MismatchedKEMHash,
#[error(transparent)]
MalformedContext(#[from] KKTContextEncodingError),
}
impl KKTError {
pub fn shared_secret_derivation_failure(inner: libcrux_psq::handshake::HandshakeError) -> Self {
KKTError::SharedSecretDerivationFailure { inner }
}
}
#[derive(Error, Debug)]
pub enum MaskedByteError {
#[error("invalid Masked Byte Length: Expected({expected}), Actual({actual})")]
InvalidLength { expected: usize, actual: usize },
#[error("failed to Unmask Byte")]
Failure,
} }
impl From<libcrux_kem::Error> for KKTError { impl From<libcrux_kem::Error> for KKTError {
+114 -71
View File
@@ -7,90 +7,158 @@
// [2..=5] => Ciphersuite // [2..=5] => Ciphersuite
// [6] => Reserved // [6] => Reserved
use crate::context::{KKTMode, KKTRole};
use crate::message::{
DecryptedRequestFrame, KKTRequest, KKTRequestEncryptionResult, KKTRequestPlaintext,
};
use crate::{ use crate::{
context::{KKT_CONTEXT_LEN, KKTContext}, context::{KKT_CONTEXT_LEN, KKTContext},
error::KKTError, error::KKTError,
}; };
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_kkt_ciphersuite::KEM;
use rand09::{CryptoRng, RngCore};
pub const KKT_SESSION_ID_LEN: usize = 16; pub(crate) const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF";
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct KKTFrame { pub struct KKTFrame {
context: [u8; KKT_CONTEXT_LEN], context: KKTContext,
session_id: KKTSessionId,
body: Vec<u8>, body: Vec<u8>,
signature: Vec<u8>, payload: Vec<u8>,
} }
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes). // if oneway and message coming from initiator => body is empty.
// 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.
// 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.
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
impl KKTFrame { impl KKTFrame {
pub fn new( pub fn new(context: KKTContext, body: &[u8], payload: Vec<u8>) -> Self {
context: [u8; KKT_CONTEXT_LEN],
body: &[u8],
session_id: [u8; KKT_SESSION_ID_LEN],
signature: &[u8],
) -> Self {
Self { Self {
context, context,
body: Vec::from(body), body: Vec::from(body),
session_id, payload,
signature: Vec::from(signature),
} }
} }
pub fn context_ref(&self) -> &[u8] {
pub const fn size_excluding_payload(role: KKTRole, mode: KKTMode, kem: KEM) -> usize {
match role {
KKTRole::Initiator => {
match mode {
KKTMode::OneWay => {
// if oneway and message coming from initiator => body is empty.
KKT_CONTEXT_LEN
}
KKTMode::Mutual => {
// if mutual and message coming from initiator => body has the initiator's kem public key.
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
}
}
}
KKTRole::Responder => {
// if coming from responder => body has the responder's kem public key.
KKT_CONTEXT_LEN + kem.encapsulation_key_length()
}
}
}
pub fn size(&self) -> usize {
self.payload.len()
+ Self::size_excluding_payload(
self.context.role(),
self.context.mode(),
self.context.ciphersuite().kem(),
)
}
pub fn context(&self) -> &KKTContext {
&self.context &self.context
} }
pub fn context(&self) -> Result<KKTContext, KKTError> { pub fn payload(&self) -> &[u8] {
KKTContext::try_decode(self.context) self.payload.as_ref()
} }
pub fn signature_ref(&self) -> &[u8] { pub fn encrypt_initiator_frame<R>(
&self.signature self,
rng: &mut R,
responder_public_key: &DHPublicKey,
version_byte: u8,
) -> Result<KKTRequestEncryptionResult, KKTError>
where
R: CryptoRng + RngCore,
{
let ephemeral_keypair = DHKeyPair::new(rng);
let plaintext =
KKTRequestPlaintext::new(ephemeral_keypair.pk, responder_public_key, version_byte);
let mut carrier =
plaintext.derive_initiator_carrier(ephemeral_keypair.sk(), responder_public_key)?;
let full_kkt_message = plaintext.into_request(&mut carrier, self)?;
Ok(KKTRequestEncryptionResult {
carrier,
request: full_kkt_message,
})
}
pub fn decrypt_initiator_frame(
responder_keypair: &DHKeyPair,
message: KKTRequest,
supported_versions: &[u8],
request_payload_len: usize,
) -> Result<DecryptedRequestFrame, KKTError> {
let mask = message.plaintext.version_mask(&responder_keypair.pk);
// check mask
// this could be used later when we have multiple versions
// if this call fails, it does before the server has to run a DH
let outer_protocol_version = message
.plaintext
.masked_version_bytes
.unmask_check_version(&mask, supported_versions)?;
// after verifying the version, we can perform the DH and continue processing the request
let mut carrier = message
.plaintext
.derive_responder_carrier(responder_keypair)?;
let decrypted_message = carrier.decrypt(&message.encrypted_frame)?;
let frame = KKTFrame::from_bytes(&decrypted_message, request_payload_len)?;
Ok(DecryptedRequestFrame {
carrier,
remote_frame: frame,
outer_protocol_version,
})
} }
pub fn body_ref(&self) -> &[u8] { pub fn body_ref(&self) -> &[u8] {
&self.body &self.body
} }
pub fn session_id_ref(&self) -> &[u8] { pub fn body(self) -> Vec<u8> {
&self.session_id self.body
}
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] { pub fn body_mut(&mut self) -> &mut [u8] {
&mut self.body &mut self.body
} }
pub fn session_id_mut(&mut self) -> &mut [u8] {
&mut self.session_id
}
pub fn frame_length(&self) -> usize { pub fn frame_length(&self) -> usize {
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len() KKT_CONTEXT_LEN + self.body.len()
} }
pub fn to_bytes(&self) -> Vec<u8> { pub fn try_to_bytes(&self) -> Result<Vec<u8>, KKTError> {
let mut bytes = Vec::with_capacity(self.frame_length()); let mut bytes = Vec::with_capacity(self.frame_length());
bytes.extend_from_slice(&self.context); bytes.extend_from_slice(&self.context.encode()?);
bytes.extend_from_slice(&self.body); bytes.extend_from_slice(&self.body);
bytes.extend_from_slice(&self.session_id); bytes.extend_from_slice(&self.payload);
bytes.extend_from_slice(&self.signature); Ok(bytes)
bytes
} }
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> { pub fn from_bytes(bytes: &[u8], payload_len: usize) -> Result<Self, KKTError> {
let len = bytes.len(); let len = bytes.len();
if bytes.len() < KKT_CONTEXT_LEN { if bytes.len() < KKT_CONTEXT_LEN {
return Err(KKTError::FrameDecodingError { return Err(KKTError::FrameDecodingError {
@@ -105,7 +173,7 @@ impl KKTFrame {
let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap(); let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap();
let context = KKTContext::try_decode(context_bytes)?; let context = KKTContext::try_decode(context_bytes)?;
if bytes.len() != context.full_message_len() { if bytes.len() != context.full_message_len() + payload_len {
return Err(KKTError::FrameDecodingError { return Err(KKTError::FrameDecodingError {
info: format!( info: format!(
"Frame is shorter than expected: actual {len} != expected {}", "Frame is shorter than expected: actual {len} != expected {}",
@@ -115,7 +183,6 @@ impl KKTFrame {
} }
let mut body = Vec::new(); let mut body = Vec::new();
let mut signature = Vec::new();
// decode body // decode body
if context.body_len() > 0 { if context.body_len() > 0 {
@@ -123,33 +190,9 @@ impl KKTFrame {
body.extend_from_slice(body_bytes); body.extend_from_slice(body_bytes);
} }
let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() // decode payload. this could be empty.
..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN]; let payload: Vec<u8> = Vec::from(&bytes[KKT_CONTEXT_LEN + context.body_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: Ok(KKTFrame::new(context, &body, payload))
// 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,
))
} }
} }
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use libcrux_psq::handshake::types::DHPublicKey;
use nym_kkt_ciphersuite::Ciphersuite;
use rand09::{CryptoRng, RngCore};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::keys::EncapsulationKey;
use crate::message::{KKTRequest, KKTResponse, ProcessedKKTResponse};
use crate::{
carrier::Carrier,
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::KKTFrame,
key_utils::validate_encapsulation_key,
};
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct KKTInitiator<'a> {
carrier: Carrier,
#[zeroize(skip)]
context: KKTContext,
#[zeroize(skip)]
expected_hash: &'a [u8],
}
impl<'a> KKTInitiator<'a> {
// to be used by clients
pub fn generate_one_way_request<R>(
rng: &mut R,
ciphersuite: Ciphersuite,
responder_dh_public_key: &DHPublicKey,
expected_hash: &'a [u8],
outer_protocol_version: u8,
payload: Option<Vec<u8>>,
) -> Result<(Self, KKTRequest), KKTError>
where
R: CryptoRng + RngCore,
{
Self::generate_encrypted_request(
rng,
KKTMode::OneWay,
ciphersuite,
None,
responder_dh_public_key,
expected_hash,
outer_protocol_version,
payload,
)
}
// to be used by nodes
pub fn generate_mutual_request<'b, R>(
rng: &mut R,
ciphersuite: Ciphersuite,
local_encapsulation_key: &'b [u8],
responder_dh_public_key: &DHPublicKey,
expected_hash: &'a [u8],
outer_protocol_version: u8,
payload: Option<Vec<u8>>,
) -> Result<(Self, KKTRequest), KKTError>
where
R: CryptoRng + RngCore,
{
Self::generate_encrypted_request(
rng,
KKTMode::Mutual,
ciphersuite,
Some(local_encapsulation_key),
responder_dh_public_key,
expected_hash,
outer_protocol_version,
payload,
)
}
#[allow(clippy::too_many_arguments)]
fn generate_encrypted_request<'b, R>(
rng: &mut R,
mode: KKTMode,
ciphersuite: Ciphersuite,
local_encapsulation_key: Option<&'b [u8]>,
responder_dh_public_key: &DHPublicKey,
expected_hash: &'a [u8],
outer_protocol_version: u8,
payload: Option<Vec<u8>>,
) -> Result<(Self, KKTRequest), KKTError>
where
R: CryptoRng + RngCore,
{
let frame = initiator_process(mode, ciphersuite, local_encapsulation_key, payload)?;
let context = *frame.context();
let request =
frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?;
Ok((
Self {
carrier: request.carrier,
context,
expected_hash,
},
request.request,
))
}
pub fn process_response(
&mut self,
response: KKTResponse,
response_payload_len: usize,
) -> Result<ProcessedKKTResponse, KKTError> {
let decrypted_response_bytes = self.carrier.decrypt(&response.encrypted_frame)?;
let response_frame = KKTFrame::from_bytes(&decrypted_response_bytes, response_payload_len)?;
initiator_ingest_response(&self.context, &response_frame, self.expected_hash)
}
}
pub fn initiator_process(
mode: KKTMode,
ciphersuite: Ciphersuite,
own_encapsulation_key: Option<&[u8]>,
payload: Option<Vec<u8>>,
) -> Result<KKTFrame, KKTError> {
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite);
let body: &[u8] = match mode {
KKTMode::OneWay => &[],
KKTMode::Mutual => match own_encapsulation_key {
Some(encaps_key) => encaps_key,
// Missing key
None => {
return Err(KKTError::FunctionInputError {
info: "KEM Key Not Provided",
});
}
},
};
Ok(KKTFrame::new(
context,
body,
match payload {
Some(payload_vec) => payload_vec,
None => Vec::with_capacity(0),
},
))
}
pub fn initiator_ingest_response(
own_context: &KKTContext,
remote_frame: &KKTFrame,
expected_hash: &[u8],
) -> Result<ProcessedKKTResponse, KKTError> {
let remote_context = remote_frame.context();
let verified_initiator_kem_key = match remote_context.status() {
KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => {
match validate_encapsulation_key(
own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
remote_frame.body_ref(),
expected_hash,
) {
true => remote_context.status() != KKTStatus::UnverifiedKEMKey,
// The key does not match the hash obtained from the directory
false => return Err(KKTError::MismatchedKEMHash),
}
}
_ => {
return Err(KKTError::ResponderFlaggedError {
status: remote_context.status(),
});
}
};
let kem = own_context.ciphersuite().kem();
let kem_bytes = remote_frame.body_ref();
let encapsulation_key = EncapsulationKey::try_from_bytes(kem_bytes.to_vec(), kem)?;
Ok(ProcessedKKTResponse {
encapsulation_key,
verified_initiator_kem_key,
response_payload: remote_frame.payload().to_vec(),
})
}
+18 -70
View File
@@ -1,75 +1,35 @@
use crate::ciphersuite::HashFunction; // Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
use std::collections::HashMap; // SPDX-License-Identifier: Apache-2.0
use classic_mceliece_rust::keypair_boxed; use libcrux_ml_kem::mlkem768::MlKem768KeyPair;
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests};
use rand09::{CryptoRng, RngCore};
use std::collections::BTreeMap;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests}; pub fn generate_lp_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
use rand::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(
rng: &mut R,
index: Option<u32>,
) -> nym_crypto::asymmetric::ed25519::KeyPair
where where
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
{ {
let mut secret_initiator: [u8; 32] = [0u8; 32]; DHKeyPair::new(rng)
rng.fill_bytes(&mut secret_initiator);
nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
} }
pub fn generate_keypair_x25519<R>(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair pub fn generate_keypair_mlkem<R>(rng: &mut R) -> MlKem768KeyPair
where where
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
{ {
let mut secret_initiator: [u8; 32] = [0u8; 32]; libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng)
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>(rng: &mut R) -> libcrux_psq::classic_mceliece::KeyPair
pub fn generate_keypair_libcrux<R>(
rng: &mut R,
kem: crate::ciphersuite::KEM,
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError>
where where
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
{ {
match kem { libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng)
crate::ciphersuite::KEM::MlKem768 => {
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, rng)?)
}
crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen(
libcrux_kem::Algorithm::XWingKemDraft06,
rng,
)?),
crate::ciphersuite::KEM::X25519 => {
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?)
}
_ => Err(crate::error::KKTError::KEMError {
info: "Key Generation Error: Unsupported Libcrux Algorithm",
}),
}
}
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_mceliece<'a, R>(
rng: &mut R,
) -> (
classic_mceliece_rust::SecretKey<'a>,
classic_mceliece_rust::PublicKey<'a>,
)
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
(decapsulation_key, encapsulation_key)
} }
pub fn hash_key_bytes( pub fn hash_key_bytes(
hash_function: &HashFunction, hash_function: HashFunction,
hash_length: usize, hash_length: usize,
key_bytes: &[u8], key_bytes: &[u8],
) -> Vec<u8> { ) -> Vec<u8> {
@@ -78,9 +38,9 @@ pub fn hash_key_bytes(
/// attempt to produce digests of the provided key using all known [HashFunction] with a default /// attempt to produce digests of the provided key using all known [HashFunction] with a default
/// hash length where variable output is available /// hash length where variable output is available
pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests { pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests {
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
let mut digests = HashMap::new(); let mut digests = BTreeMap::new();
for hash in HashFunction::iter() { for hash in HashFunction::iter() {
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN)); digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
} }
@@ -94,7 +54,7 @@ fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
} }
pub fn validate_encapsulation_key( pub fn validate_encapsulation_key(
hash_function: &HashFunction, hash_function: HashFunction,
hash_length: usize, hash_length: usize,
encapsulation_key: &[u8], encapsulation_key: &[u8],
expected_hash_bytes: &[u8], expected_hash_bytes: &[u8],
@@ -105,20 +65,8 @@ pub fn validate_encapsulation_key(
) )
} }
pub fn validate_key_bytes(
hash_function: &HashFunction,
hash_length: usize,
key_bytes: &[u8],
expected_hash_bytes: &[u8],
) -> bool {
compare_hashes(
&hash_key_bytes(hash_function, hash_length, key_bytes),
expected_hash_bytes,
)
}
pub fn hash_encapsulation_key( pub fn hash_encapsulation_key(
hash_function: &HashFunction, hash_function: HashFunction,
hash_length: usize, hash_length: usize,
encapsulation_key: &[u8], encapsulation_key: &[u8],
) -> Vec<u8> { ) -> Vec<u8> {
+440
View File
@@ -0,0 +1,440 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::KKTError;
use libcrux_psq::handshake::types::PQEncapsulationKey;
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
use std::collections::BTreeMap;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use crate::key_utils::produce_key_digests;
pub use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey};
pub use libcrux_psq::classic_mceliece as mceliece;
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
/// Wrapper around keys used for the KEM exchange
/// with cheap clones thanks to Arc wrappers
#[derive(Clone)]
pub struct KEMKeys {
mc_eliece_pk: Arc<mceliece::PublicKey>,
mc_eliece_sk: Arc<mceliece::SecretKey>,
ml_kem768_pk: Arc<MlKem768PublicKey>,
ml_kem768_sk: Arc<MlKem768PrivateKey>,
}
impl Debug for KEMKeys {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KEMKeys")
.field("mc_eliece", &"<redacted>")
.field("ml_kem768", &"<redacted>")
.finish()
}
}
impl KEMKeys {
pub fn new(mc_eliece: mceliece::KeyPair, ml_kem768: MlKem768KeyPair) -> Self {
let (ml_kem768_sk, ml_kem768_pk) = ml_kem768.into_parts();
Self {
mc_eliece_pk: Arc::new(mc_eliece.pk),
mc_eliece_sk: Arc::new(mc_eliece.sk),
ml_kem768_pk: Arc::new(ml_kem768_pk),
ml_kem768_sk: Arc::new(ml_kem768_sk),
}
}
pub fn encapsulation_keys_digests(&self) -> BTreeMap<KEM, KEMKeyDigests> {
let mut digests = BTreeMap::new();
let mlkem_digests = produce_key_digests(self.ml_kem768_pk.as_slice());
let mceliece_digests = produce_key_digests(self.mc_eliece_pk.as_ref().as_ref());
digests.insert(KEM::MlKem768, mlkem_digests);
digests.insert(KEM::McEliece, mceliece_digests);
digests
}
pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> {
match kem {
KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()),
KEM::MlKem768 => Some(self.ml_kem768_pk.as_slice()),
// _ => None,
}
}
pub fn encapsulation_key(&self, kem: KEM) -> Option<EncapsulationKey> {
match kem {
KEM::McEliece => Some(EncapsulationKey::McEliece(self.mc_eliece_pk.clone())),
KEM::MlKem768 => Some(EncapsulationKey::MlKem768(self.ml_kem768_pk.clone())),
// _ => None,
}
}
pub fn mc_eliece_encapsulation_key(&self) -> &mceliece::PublicKey {
&self.mc_eliece_pk
}
pub fn ml_kem768_encapsulation_key(&self) -> &MlKem768PublicKey {
self.ml_kem768_pk.as_ref()
}
pub fn mc_eliece_decapsulation_key(&self) -> &mceliece::SecretKey {
&self.mc_eliece_sk
}
pub fn ml_kem768_decapsulation_key(&self) -> &MlKem768PrivateKey {
&self.ml_kem768_sk
}
}
#[derive(Clone)]
pub enum EncapsulationKey {
McEliece(Arc<mceliece::PublicKey>),
MlKem768(Arc<MlKem768PublicKey>),
}
impl Debug for EncapsulationKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EncapsulationKey::McEliece(_) => write!(f, "EncapsulationKey::McEliece"),
EncapsulationKey::MlKem768(_) => write!(f, "EncapsulationKey::MlKem768"),
}
}
}
impl EncapsulationKey {
pub fn kem(&self) -> KEM {
match self {
EncapsulationKey::McEliece(_) => KEM::McEliece,
EncapsulationKey::MlKem768(_) => KEM::MlKem768,
}
}
pub fn as_pq_encapsulation_key(&self) -> PQEncapsulationKey<'_> {
match self {
EncapsulationKey::McEliece(pk) => PQEncapsulationKey::CMC(pk),
EncapsulationKey::MlKem768(pk) => PQEncapsulationKey::MlKem(pk),
}
}
pub fn try_from_bytes(bytes: Vec<u8>, kem: KEM) -> Result<EncapsulationKey, KKTError> {
match kem {
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(Arc::new(
MlKem768PublicKey::try_from(bytes.as_slice()).map_err(|_| KKTError::KEMError {
info: "mlkem768 key of invalid length",
})?,
))),
KEM::McEliece => {
let boxed_array: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> =
bytes
.into_boxed_slice()
.try_into()
.map_err(|_| KKTError::KEMError {
info: "mceliece key of invalid length",
})?;
Ok(EncapsulationKey::McEliece(Arc::new(
mceliece::PublicKey::from(boxed_array),
)))
}
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
EncapsulationKey::McEliece(k) => k.as_ref().as_ref(),
EncapsulationKey::MlKem768(k) => k.as_ref().as_ref(),
}
}
}
// storage helpers
pub mod storage_wrappers {
use nym_pemstore::traits::PemStorableKey;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MalformedStoredKeyError {
#[error("{typ} stored key has an invalid length")]
InvalidKeyLength { typ: &'static str },
#[error("{typ} stored key is malformed: {message}")]
MalformedData { typ: &'static str, message: String },
#[error("attempted to take ownership of a stored {typ} key representation")]
IllegalStoredConversion { typ: &'static str },
}
pub trait StorableKey: Sized {
type StorableRepresentation<'a>: PemStorableKey
+ From<&'a Self>
+ TryInto<Self, Error = MalformedStoredKeyError>
+ Sized
where
Self: 'a;
fn to_storable(&self) -> Self::StorableRepresentation<'_> {
self.into()
}
fn from_storable(
repr: Self::StorableRepresentation<'_>,
) -> Result<Self, MalformedStoredKeyError> {
repr.try_into()
}
}
macro_rules! declare_key_wrappers {
($pub_key_type:ty, $private_key_type:ty) => {
pub enum StorablePublicKey<'a> {
Owned(Box<$pub_key_type>),
Borrowed(&'a $pub_key_type),
}
impl AsRef<$pub_key_type> for StorablePublicKey<'_> {
fn as_ref(&self) -> &$pub_key_type {
match self {
StorablePublicKey::Owned(k) => k,
StorablePublicKey::Borrowed(k) => k,
}
}
}
pub enum StorablePrivateKey<'a> {
Owned(Box<$private_key_type>),
Borrowed(&'a $private_key_type),
}
impl AsRef<$private_key_type> for StorablePrivateKey<'_> {
fn as_ref(&self) -> &$private_key_type {
match self {
StorablePrivateKey::Owned(k) => k,
StorablePrivateKey::Borrowed(k) => k,
}
}
}
impl<'a> From<&'a $pub_key_type> for StorablePublicKey<'a> {
fn from(value: &'a $pub_key_type) -> Self {
StorablePublicKey::Borrowed(value)
}
}
impl<'a> TryFrom<StorablePublicKey<'a>> for $pub_key_type {
type Error = MalformedStoredKeyError;
fn try_from(value: StorablePublicKey<'a>) -> Result<Self, Self::Error> {
match value {
StorablePublicKey::Owned(value) => Ok(*value),
StorablePublicKey::Borrowed(_) => {
Err(MalformedStoredKeyError::IllegalStoredConversion {
typ: <StorablePublicKey as PemStorableKey>::pem_type(),
})
}
}
}
}
impl<'a> From<&'a $private_key_type> for StorablePrivateKey<'a> {
fn from(value: &'a $private_key_type) -> Self {
StorablePrivateKey::Borrowed(value)
}
}
impl<'a> TryFrom<StorablePrivateKey<'a>> for $private_key_type {
type Error = MalformedStoredKeyError;
fn try_from(value: StorablePrivateKey<'a>) -> Result<Self, Self::Error> {
match value {
StorablePrivateKey::Owned(value) => Ok(*value),
StorablePrivateKey::Borrowed(_) => {
Err(MalformedStoredKeyError::IllegalStoredConversion {
typ: <StorablePrivateKey as PemStorableKey>::pem_type(),
})
}
}
}
}
impl $crate::keys::storage_wrappers::StorableKey for $pub_key_type {
type StorableRepresentation<'a> = StorablePublicKey<'a>;
}
impl $crate::keys::storage_wrappers::StorableKey for $private_key_type {
type StorableRepresentation<'a> = StorablePrivateKey<'a>;
}
};
}
pub mod mceliece {
use crate::keys::storage_wrappers::MalformedStoredKeyError;
use libcrux_psq::classic_mceliece;
use nym_pemstore::traits::PemStorableKey;
declare_key_wrappers!(classic_mceliece::PublicKey, classic_mceliece::SecretKey);
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"MCELIECE PRIVATE KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_ref().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::SECRET_KEY_LENGTH]> =
bytes.to_vec().into_boxed_slice().try_into().map_err(|_| {
MalformedStoredKeyError::InvalidKeyLength {
typ: Self::pem_type(),
}
})?;
Ok(StorablePrivateKey::Owned(Box::new(
classic_mceliece::SecretKey::from(bytes),
)))
}
}
impl<'a> PemStorableKey for StorablePublicKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"MCELIECE PUBLIC KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_ref().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let bytes: Box<[u8; nym_kkt_ciphersuite::mceliece::PUBLIC_KEY_LENGTH]> =
bytes.to_vec().into_boxed_slice().try_into().map_err(|_| {
MalformedStoredKeyError::InvalidKeyLength {
typ: Self::pem_type(),
}
})?;
Ok(StorablePublicKey::Owned(Box::new(
classic_mceliece::PublicKey::from(bytes),
)))
}
}
}
pub mod mlkem768 {
use crate::keys::storage_wrappers::MalformedStoredKeyError;
use libcrux_ml_kem::mlkem768::{MlKem768PrivateKey, MlKem768PublicKey};
use nym_pemstore::traits::PemStorableKey;
declare_key_wrappers!(MlKem768PublicKey, MlKem768PrivateKey);
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"MLKEM768 PRIVATE KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_slice().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let inner = MlKem768PrivateKey::try_from(bytes).map_err(|message| {
MalformedStoredKeyError::MalformedData {
typ: Self::pem_type(),
message: message.to_string(),
}
})?;
Ok(StorablePrivateKey::Owned(Box::new(inner)))
}
}
impl<'a> PemStorableKey for StorablePublicKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"MLKEM768 PUBLIC KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_slice().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let inner = MlKem768PublicKey::try_from(bytes).map_err(|message| {
MalformedStoredKeyError::MalformedData {
typ: Self::pem_type(),
message: message.to_string(),
}
})?;
Ok(StorablePublicKey::Owned(Box::new(inner)))
}
}
}
pub mod x25519 {
use crate::keys::storage_wrappers::MalformedStoredKeyError;
use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey};
use nym_pemstore::traits::PemStorableKey;
declare_key_wrappers!(DHPublicKey, DHPrivateKey);
impl<'a> PemStorableKey for StorablePrivateKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"LP X25519 PRIVATE KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_ref().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let bytes =
bytes
.try_into()
.map_err(|_| MalformedStoredKeyError::InvalidKeyLength {
typ: Self::pem_type(),
})?;
Ok(StorablePrivateKey::Owned(Box::new(
DHPrivateKey::from_bytes(&bytes).map_err(|err| {
MalformedStoredKeyError::MalformedData {
typ: Self::pem_type(),
message: format!("{err:?}"),
}
})?,
)))
}
}
impl<'a> PemStorableKey for StorablePublicKey<'a> {
type Error = MalformedStoredKeyError;
fn pem_type() -> &'static str {
"LP X25519 PUBLIC KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.as_ref().as_ref().to_vec()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
let bytes =
bytes
.try_into()
.map_err(|_| MalformedStoredKeyError::InvalidKeyLength {
typ: Self::pem_type(),
})?;
Ok(StorablePublicKey::Owned(Box::new(DHPublicKey::from_bytes(
&bytes,
))))
}
}
}
}
-450
View File
@@ -1,450 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Convenience wrappers around KKT protocol functions for easier integration.
//!
//! This module provides simplified APIs for the common use case of exchanging
//! KEM public keys between a client (initiator) and gateway (responder).
//!
//! The underlying KKT protocol is implemented in the `session` module.
use nym_crypto::asymmetric::{ed25519, x25519};
use rand::{CryptoRng, RngCore};
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
context::{KKTContext, KKTMode},
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
error::KKTError,
};
// Re-export core session functions for advanced use cases
pub use crate::session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
};
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
use crate::frame::KKTFrame;
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
///
/// This is the client-side operation that initiates a KKT exchange.
/// The request will be signed with the provided signing key.
///
/// # Arguments
/// * `rng` - Random number generator
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
///
/// # Returns
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
/// * `KKTContext` - Context to use when validating the response
/// * `Vec<u8>` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder
///
/// # Example
/// ```ignore
/// let (session_secret, context, request_frame) = request_kem_key(
/// &mut rng,
/// ciphersuite,
/// client_signing_key,
/// responder_dh_public_key,
/// )?;
/// // Send request_frame to gateway
/// ```
pub fn request_kem_key<R: CryptoRng + RngCore>(
rng: &mut R,
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
responder_dh_public_key: &x25519::PublicKey,
) -> Result<(KKTSessionSecret, KKTContext, Vec<u8>), KKTError> {
// OneWay mode: client only wants responder's KEM key
// None: client doesn't send their own KEM key
let (initiator_context, initiator_frame) =
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?;
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?;
Ok((session_secret, initiator_context, encrypted_request_bytes))
}
/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key.
///
/// This is the client-side operation that processes the gateway's response.
/// It verifies the signature and validates the key hash against the expected value
/// (typically retrieved from a directory service).
///
/// # Arguments
/// * `context` - Context from the initial request
/// * `session_secret` - Session Secret Key (generated with request)
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
/// * `response_bytes` - Serialized response frame from responder
///
/// # Returns
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
///
/// # Example
/// ```ignore
/// let gateway_kem_key = validate_kem_response(
/// &mut context,
/// &session_secret,
/// &gateway_verification_key,
/// &expected_hash_from_directory,
/// &response_bytes,
/// )?;
/// // Use gateway_kem_key for PSQ
/// ```
pub fn validate_kem_response<'a>(
context: &mut KKTContext,
session_secret: &KKTSessionSecret,
responder_vk: &ed25519::PublicKey,
expected_key_hash: &[u8],
encrypted_response_bytes: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> {
let (responder_frame, responder_context) =
decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?;
initiator_ingest_response(
context,
&responder_frame,
&responder_context,
responder_vk,
expected_key_hash,
)
}
/// Decrypts and validates an *Encrypted* KKT response
///
/// This is the client-side operation that processes the gateway's response.
pub fn decrypt_kkt_response_frame(
session_secret: &KKTSessionSecret,
frame_ciphertext: &[u8],
) -> Result<(KKTFrame, KKTContext), KKTError> {
decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD)
}
/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key.
///
/// This is the gateway-side operation that processes a client's KKT request.
/// It validates the request signature (if authenticated) and responds with
/// the gateway's KEM public key, signed for authenticity.
///
/// # Arguments
/// * `encrypted_request_bytes` - encrypted KEM request
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
/// * `responder_signing_key` - Gateway's Ed25519 signing key
/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key
/// * `responder_kem_key` - Gateway's KEM public key to send
///
/// # Returns
/// * `KKTFrame` - Signed response frame containing the KEM public key
///
/// # Example
/// ```ignore
/// let response_frame = handle_kem_request(
/// &request_frame,
/// Some(client_verification_key), // or None for anonymous
/// gateway_signing_key,
/// &gateway_kem_public_key,
/// )?;
/// // Send response_frame back to client
/// ```
pub fn handle_kem_request<'a, R>(
rng: &mut R,
encrypted_request_bytes: &[u8],
initiator_vk: Option<&ed25519::PublicKey>,
responder_signing_key: &ed25519::PrivateKey,
responder_dh_private_key: &x25519::PrivateKey,
responder_kem_key: &EncapsulationKey<'a>,
) -> Result<Vec<u8>, KKTError>
where
R: RngCore + CryptoRng,
{
// Compute the session's shared secret, decrypt and parse context from the request frame
let (session_secret, request_frame, initiator_context) =
decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?;
// Validate the request (verifies signature if initiator_vk provided)
let (mut response_context, _) = responder_ingest_message(
&initiator_context,
initiator_vk,
None, // Not checking initiator's KEM key in OneWay mode
&request_frame,
)?;
// Generate signed response with our KEM public key
let responder_frame = responder_process(
&mut response_context,
request_frame.session_id(),
responder_signing_key,
responder_kem_key,
)?;
// Encrypt the responder's response with the session's shared secret
encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ciphersuite::{HashFunction, KEM, SignatureScheme},
key_utils::{generate_keypair_libcrux, hash_encapsulation_key},
};
fn random_x25519_key() -> x25519::PrivateKey {
let mut bytes = [0u8; 32];
let mut rng = rand::rng();
rng.fill_bytes(&mut bytes);
x25519::PrivateKey::from_secret(bytes)
}
#[test]
fn test_kkt_wrappers_oneway_authenticated() {
let mut rng = rand::rng();
// Generate Ed25519 keypairs for both parties
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0);
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = random_x25519_key();
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
// Generate responder's KEM keypair (X25519 for testing)
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// Create ciphersuite
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// Hash the KEM key (simulating directory storage)
let key_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&responder_kem_key.encode(),
);
// Client: Request KEM key
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
ed25519_init.private_key(),
&x25519_resp_pub,
)
.unwrap();
// Gateway: Handle request
let response_frame_ciphertext = handle_kem_request(
&mut rng,
&request_frame_ciphertext,
Some(ed25519_init.public_key()), // Authenticated
ed25519_resp.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
// Client: Validate response
let obtained_key = validate_kem_response(
&mut context,
&session_key,
ed25519_resp.public_key(),
&key_hash,
&response_frame_ciphertext,
)
.unwrap();
// Verify we got the correct KEM key
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
}
#[test]
fn test_kkt_wrappers_anonymous() {
let mut rng = rand::rng();
// Only responder has keys
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
let x25519_resp_priv = random_x25519_key();
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let key_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&responder_kem_key.encode(),
);
// Anonymous initiator
let (mut context, request_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap();
// Gateway: Handle anonymous request
let response_frame = handle_kem_request(
&mut rng,
&encrypted_request_bytes,
None, // Anonymous - no verification key
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
// Initiator: Validate response
let obtained_key = validate_kem_response(
&mut context,
&session_secret,
responder_keypair.public_key(),
&key_hash,
&response_frame,
)
.unwrap();
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
}
#[test]
fn test_invalid_signature_rejected() {
let mut rng = rand::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = random_x25519_key();
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
// Different keypair for wrong signature
let mut wrong_secret = [0u8; 32];
rng.fill_bytes(&mut wrong_secret);
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (_session_key, _context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
&x25519_resp_pub,
)
.unwrap();
// Gateway handles request but we provide WRONG verification key
let result = handle_kem_request(
&mut rng,
&request_frame_ciphertext,
Some(wrong_keypair.public_key()), // Wrong key!
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
);
// Should fail signature verification
assert!(result.is_err());
}
#[test]
fn test_hash_mismatch_rejected() {
let mut rng = rand::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = random_x25519_key();
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (session_key, mut context, request_frame) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
&x25519_resp_pub,
)
.unwrap();
let response_frame = handle_kem_request(
&mut rng,
&request_frame,
Some(initiator_keypair.public_key()),
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
// Client validates with WRONG hash
let result = validate_kem_response(
&mut context,
&session_key,
responder_keypair.public_key(),
&wrong_hash, // Wrong!
&response_frame,
);
// Should fail hash validation
assert!(result.is_err());
}
}
+185 -453
View File
@@ -1,498 +1,230 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pub mod ciphersuite; pub mod carrier;
pub mod context;
pub mod encryption;
pub mod error; pub mod error;
pub mod frame; pub mod frame;
pub mod initiator;
pub mod key_utils; pub mod key_utils;
// pub mod kkt; pub mod keys;
pub mod session; pub mod masked_byte;
pub mod message;
pub mod rekey;
pub mod responder;
// This must be less than 4 bits pub use nym_kkt_context as context;
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)] #[cfg(test)]
mod test { mod test {
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
use rand09::RngCore;
use crate::keys::KEMKeys;
use crate::{ use crate::{
KKT_RESPONSE_AAD, initiator::KKTInitiator,
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
encryption::{
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
encrypt_kkt_frame,
},
frame::KKTFrame,
key_utils::{ key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece, generate_keypair_mceliece, generate_keypair_mlkem, generate_lp_keypair_x25519,
generate_keypair_x25519, hash_encapsulation_key, hash_encapsulation_key,
},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
}, },
responder::KKTResponder,
}; };
#[test] #[test]
fn test_kkt_psq_e2e_clear() { fn test_kkt_psq_e2e_encrypted_carrier() {
let mut rng = rand::rng(); let mut rng = rand09::rng();
// generate ed25519 keys let mut payload: Vec<u8> = vec![0u8; 900_000];
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); rng.fill_bytes(&mut payload);
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 (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
}
}
}
#[test]
fn test_kkt_psq_e2e_encrypted() {
let mut rng = rand::rng();
// 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 // generate responder x25519 keys
let responder_x25519_keypair = generate_keypair_x25519(&mut rng); let responder_x25519_keypair = generate_lp_keypair_x25519(&mut rng);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { for hash_function in [
for hash_function in [ HashFunction::Blake3,
HashFunction::Blake3, HashFunction::SHA256,
HashFunction::SHA256, HashFunction::Shake128,
HashFunction::Shake128, HashFunction::Shake256,
HashFunction::Shake256, ] {
] { // generate kem public keys
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let responder_kem = KEMKeys::new(responder_mceliece_keypair, responder_mlkem_keypair);
let r_dir_hash_mlkem = hash_encapsulation_key(
hash_function,
HashLength::Default.value(),
responder_kem.ml_kem768_encapsulation_key().as_slice(),
);
let r_dir_hash_mceliece = hash_encapsulation_key(
hash_function,
HashLength::Default.value(),
responder_kem.mc_eliece_encapsulation_key().as_ref(),
);
let initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let _i_dir_hash_mlkem = hash_encapsulation_key(
hash_function,
HashLength::Default.value(),
initiator_mlkem_keypair.public_key().as_slice(),
);
let _i_dir_hash_mceliece = hash_encapsulation_key(
hash_function,
HashLength::Default.value(),
initiator_mceliece_keypair.pk.as_ref(),
);
let responder = KKTResponder::new(
&responder_x25519_keypair,
&responder_kem,
&[
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::Shake128,
HashFunction::Shake256,
],
&[SignatureScheme::Ed25519],
&[1],
)
.unwrap();
// OneWay - MlKem
{
let ciphersuite = Ciphersuite::resolve_ciphersuite( let ciphersuite = Ciphersuite::resolve_ciphersuite(
kem, KEM::MlKem768,
hash_function, hash_function,
crate::ciphersuite::SignatureScheme::Ed25519, SignatureScheme::Ed25519,
None, None,
) )
.unwrap(); .unwrap();
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
&mut rng,
ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
1u8,
Some(payload.clone()),
)
.unwrap();
// generate kem public keys let processed_request = responder.process_request(request, payload.len()).unwrap();
let (responder_kem_public_key, initiator_kem_public_key) = match kem { assert_eq!(processed_request.request_payload, payload);
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 result = initiator
.process_response(processed_request.response, 0)
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 (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
&mut rng,
responder_x25519_keypair.public_key(),
&i_frame,
)
.unwrap(); .unwrap();
// decryption - initiator frame assert_eq!(
result.encapsulation_key.as_bytes(),
responder_kem.ml_kem768_encapsulation_key().as_slice(),
)
}
// Mutual - MlKem
{
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::MlKem768,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
&mut rng,
ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
1u8,
Some(payload.clone()),
)
.unwrap();
let (r_session_secret, i_frame_r, i_context_r) = let processed_request = responder.process_request(request, payload.len()).unwrap();
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, _) = assert_eq!(processed_request.request_payload, payload);
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
let r_frame = responder_process( // if we keep unverified keys, this should change
&mut r_context, assert!(processed_request.remote_encapsulation_key.is_none());
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(), let processed_response = initiator
&responder_kem_public_key, .process_response(processed_request.response, 0)
)
.unwrap(); .unwrap();
// encryption - responder frame assert_eq!(
let r_bytes = processed_response.encapsulation_key.as_bytes(),
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) responder_kem.ml_kem768_encapsulation_key().as_slice(),
.unwrap(); )
}
// decryption - responder frame // OneWay - McEliece
{
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::McEliece,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
&mut rng,
ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mceliece,
1u8,
Some(payload.clone()),
)
.unwrap();
let (i_frame_r, i_context_r) = let processed_request = responder.process_request(request, payload.len()).unwrap();
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); assert_eq!(processed_request.request_payload, payload);
let i_obtained_key = initiator_ingest_response( let processed_response = initiator
&mut i_context, .process_response(processed_request.response, 0)
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap(); .unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) assert_eq!(
} processed_response.encapsulation_key.as_bytes(),
// Initiator, OneWay responder_kem.mc_eliece_encapsulation_key().as_ref()
{ )
let (mut i_context, i_frame) = initiator_process( }
&mut rng, // Mutual - MlKem
crate::context::KKTMode::OneWay, {
ciphersuite, let ciphersuite = Ciphersuite::resolve_ciphersuite(
initiator_ed25519_keypair.private_key(), KEM::McEliece,
None, hash_function,
) SignatureScheme::Ed25519,
None,
)
.unwrap();
let (mut initiator, request) = KKTInitiator::generate_one_way_request(
&mut rng,
ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mceliece,
1u8,
Some(payload.clone()),
)
.unwrap();
let processed_request = responder.process_request(request, payload.len()).unwrap();
assert_eq!(processed_request.request_payload, payload);
// if we keep unverified keys, this should change
assert!(processed_request.remote_encapsulation_key.is_none());
let processed_response = initiator
.process_response(processed_request.response, 0)
.unwrap(); .unwrap();
// encryption - initiator frame assert_eq!(
processed_response.encapsulation_key.as_bytes(),
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( responder_kem.mc_eliece_encapsulation_key().as_ref()
&mut rng, )
responder_x25519_keypair.public_key(),
&i_frame,
)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, r_context) =
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
// decryption - responder frame
let (i_frame_r, i_context_r) =
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
&mut rng,
responder_x25519_keypair.public_key(),
&i_frame,
)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, i_context_r) =
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&i_context_r,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
// decryption - responder frame
let (i_frame_r, i_context_r) =
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
} }
} }
} }
+189
View File
@@ -0,0 +1,189 @@
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
use crate::error::{
MaskedByteError,
MaskedByteError::{Failure, InvalidLength},
};
pub const MASKED_BYTE_LEN: usize = 16;
pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1";
const U8_RANGE: [u8; 256] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192,
193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
250, 251, 252, 253, 254, 255,
];
#[derive(Clone, Copy)]
pub struct MaskedByte([u8; MASKED_BYTE_LEN]);
impl MaskedByte {
/// Mask a byte by hashing it with some mask.
/// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte)
pub fn new(byte: u8, mask: &[u8]) -> Self {
let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
let mut hasher = blake3::Hasher::new();
hasher.update(MASKED_BYTE_CONTEXT_STR);
hasher.update(mask);
// avoid zero update
hasher.update(&[0xFF, byte]);
hasher.finalize_xof_into(&mut output);
Self(output)
}
/// Unmasks a byte by trial hashing.
/// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF).
/// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max).
/// If we find an `i` which yields back the hash input, then we found the masked byte.
/// Otherwise, the function returns an error.
pub fn unmask(&self, mask: &[u8]) -> Result<u8, MaskedByteError> {
self.unmask_check_version(mask, &U8_RANGE)
}
// This could be more efficient than unmask,
// because we just could check against a smaller list of supported versions.
pub fn unmask_check_version(
&self,
mask: &[u8],
supported_versions: &[u8],
) -> Result<u8, MaskedByteError> {
let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
let mut hasher = blake3::Hasher::new();
hasher.update(MASKED_BYTE_CONTEXT_STR);
hasher.update(mask);
// avoid zero update
hasher.update(&[0xFF]);
for i in supported_versions {
let mut t_hasher = hasher.clone();
t_hasher.update(&[*i]);
t_hasher.finalize_xof_into(&mut buf);
if buf == self.0 {
return Ok(*i);
}
}
Err(Failure)
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn to_bytes(self) -> [u8; MASKED_BYTE_LEN] {
self.0
}
}
impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte {
fn from(value: [u8; MASKED_BYTE_LEN]) -> Self {
MaskedByte(value)
}
}
impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte {
fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self {
MaskedByte(*value)
}
}
impl TryFrom<&[u8]> for MaskedByte {
type Error = MaskedByteError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let Ok(inner) = value.try_into() else {
return Err(InvalidLength {
expected: MASKED_BYTE_LEN,
actual: value.len(),
});
};
Ok(MaskedByte(inner))
}
}
#[cfg(test)]
mod test {
use crate::masked_byte::MASKED_BYTE_LEN;
use super::MaskedByte;
use rand09::{Rng, RngCore, rng};
#[test]
fn test_masking() {
let mut mask: [u8; 256] = [0u8; 256];
let mut wire_bytes: [u8; MASKED_BYTE_LEN];
// why not
for i in 0..=u8::MAX {
// gen mask
rng().fill_bytes(&mut mask);
let masked_byte = MaskedByte::new(i, &mask);
wire_bytes = masked_byte.to_bytes();
let decoded_masked_byte = MaskedByte::from(wire_bytes);
let output = decoded_masked_byte.unmask(&mask).unwrap();
assert_eq!(i, output);
// flip bit
let mut with_flipped_bit = decoded_masked_byte.to_bytes();
let byte_idx: usize = rng().random_range(0..MASKED_BYTE_LEN);
let bit_idx = rng().random_range(0..8);
with_flipped_bit[byte_idx] ^= 1 << bit_idx;
let decoded_masked_byte = MaskedByte::from(with_flipped_bit);
assert!(decoded_masked_byte.unmask(&mask).is_err());
}
}
#[test]
fn test_decoding() {
let mut mask: [u8; 256] = [0u8; 256];
// gen mask
rng().fill_bytes(&mut mask);
let byte = rng().random();
let masked_byte = MaskedByte::new(byte, &mask);
let wire_bytes: [u8; MASKED_BYTE_LEN] = masked_byte.to_bytes();
// should succeed
let decoded_masked_byte = MaskedByte::try_from(wire_bytes.as_slice()).unwrap();
let output = decoded_masked_byte.unmask(&mask).unwrap();
assert_eq!(byte, output);
let empty_slice: &[u8] = &[];
// should fail
assert!(MaskedByte::try_from(empty_slice).is_err());
let mut wire_bytes_messy = Vec::from(wire_bytes);
// add more one more byte
wire_bytes_messy.push(0x42);
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN + 1);
// should fail
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
// pop the added byte
_ = wire_bytes_messy.pop();
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN);
// should succeed
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_ok());
// pop one more byte
_ = wire_bytes_messy.pop();
assert_eq!(wire_bytes_messy.len(), MASKED_BYTE_LEN - 1);
// should fail
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
}
}
+265
View File
@@ -0,0 +1,265 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::carrier::Carrier;
use crate::context::{KKTContext, KKTMode, KKTRole};
use crate::error::KKTError;
use crate::frame::KKTFrame;
use crate::keys::EncapsulationKey;
use crate::masked_byte::{MASKED_BYTE_LEN, MaskedByte};
use libcrux_chacha20poly1305::TAG_LEN;
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
use nym_kkt_ciphersuite::{KEM, x25519};
pub struct KKTRequest {
/// The plaintext part of the request
pub(crate) plaintext: KKTRequestPlaintext,
/// Ciphertext of an initial request `KKTFrame`
pub(crate) encrypted_frame: Vec<u8>,
}
impl KKTRequest {
// the size of KKTRequest is the plaintext data followed by the frame and the encryption tag
pub const fn size_excluding_payload(mode: KKTMode, kem: KEM) -> usize {
KKTRequestPlaintext::SIZE
+ KKTFrame::size_excluding_payload(KKTRole::Initiator, mode, kem)
+ TAG_LEN
}
pub fn size(&self) -> usize {
self.encrypted_frame.len() + KKTRequestPlaintext::SIZE
}
pub fn into_bytes(mut self) -> Vec<u8> {
let mut out = self.plaintext.to_bytes();
out.append(&mut self.encrypted_frame);
out
}
pub fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
if b.len() < x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
return Err(KKTError::FrameDecodingError {
info: "the KKTRequest frame has invalid length".to_string(),
});
}
let plaintext =
KKTRequestPlaintext::try_from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN])?;
Ok(KKTRequest {
plaintext,
encrypted_frame: b[x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN..].to_vec(),
})
}
}
pub(crate) struct KKTRequestPlaintext {
/// Ephemeral Diffie-Hellman public key of the initiator
pub(crate) dh_pubkey: DHPublicKey,
/// Masked bytes representing the outer protocol version information
pub(crate) masked_version_bytes: MaskedByte,
}
impl KKTRequestPlaintext {
pub const SIZE: usize = x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN;
pub(crate) fn new(
initiator_pubkey: DHPublicKey,
responder_pubkey: &DHPublicKey,
outer_protocol_version: u8,
) -> Self {
let mask = Self::create_version_mask(&initiator_pubkey, responder_pubkey);
let masked_version_bytes = MaskedByte::new(outer_protocol_version, &mask);
KKTRequestPlaintext {
dh_pubkey: initiator_pubkey,
masked_version_bytes,
}
}
pub(crate) fn into_request(
self,
carrier: &mut Carrier,
frame: KKTFrame,
) -> Result<KKTRequest, KKTError> {
let frame_bytes = frame.try_to_bytes()?;
let frame_ciphertext = carrier.encrypt(&frame_bytes)?;
Ok(KKTRequest {
plaintext: self,
encrypted_frame: frame_ciphertext,
})
}
pub(crate) fn create_version_mask(
initiator_pubkey: &DHPublicKey,
responder_pubkey: &DHPublicKey,
) -> Vec<u8> {
let mut mask = Vec::with_capacity(2 * x25519::PUBLIC_KEY_LENGTH);
mask.extend_from_slice(initiator_pubkey.as_ref());
mask.extend_from_slice(responder_pubkey.as_ref());
mask
}
fn create_carrier_ctx(
masked_version: &MaskedByte,
initiator_pubkey: &DHPublicKey,
responder_pubkey: &DHPublicKey,
) -> Vec<u8> {
let mut context = Vec::new();
context.extend_from_slice(masked_version.as_slice());
context.extend_from_slice(crate::frame::KKT_CARRIER_CONTEXT);
context.extend_from_slice(initiator_pubkey.as_ref());
context.extend_from_slice(responder_pubkey.as_ref());
context
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN);
out.extend_from_slice(self.dh_pubkey.as_ref());
out.extend_from_slice(self.masked_version_bytes.as_slice());
out
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
if b.len() != x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
return Err(KKTError::FrameDecodingError {
info: "the KKTRequest frame has invalid length".to_string(),
});
}
// SAFETY: we're using exactly 32 byte
#[allow(clippy::unwrap_used)]
let dh_pubkey =
DHPublicKey::from_bytes(&b[..x25519::PUBLIC_KEY_LENGTH].try_into().unwrap());
let masked_version_bytes = MaskedByte::try_from(&b[x25519::PUBLIC_KEY_LENGTH..])?;
Ok(KKTRequestPlaintext {
dh_pubkey,
masked_version_bytes,
})
}
pub(crate) fn version_mask(&self, responder_pubkey: &DHPublicKey) -> Vec<u8> {
Self::create_version_mask(&self.dh_pubkey, responder_pubkey)
}
pub(crate) fn derive_initiator_carrier(
&self,
initiator_sk: &DHPrivateKey,
responder_pubkey: &DHPublicKey,
) -> Result<Carrier, KKTError> {
let ctx = Self::create_carrier_ctx(
&self.masked_version_bytes,
&self.dh_pubkey,
responder_pubkey,
);
let shared_secret = initiator_sk
.diffie_hellman(responder_pubkey)
.map_err(KKTError::shared_secret_derivation_failure)?;
Ok(Carrier::from_secret_slice(
shared_secret.as_ref(),
&ctx,
true,
))
}
pub(crate) fn derive_responder_carrier(
&self,
responder_keys: &DHKeyPair,
) -> Result<Carrier, KKTError> {
let ctx = Self::create_carrier_ctx(
&self.masked_version_bytes,
&self.dh_pubkey,
&responder_keys.pk,
);
let shared_secret = responder_keys
.sk()
.diffie_hellman(&self.dh_pubkey)
.map_err(KKTError::shared_secret_derivation_failure)?;
Ok(Carrier::from_secret_slice(
shared_secret.as_ref(),
&ctx,
false,
))
}
}
pub struct KKTRequestEncryptionResult {
/// Derived carrier used for decrypting this frame and encrypting the response
pub(crate) carrier: Carrier,
/// The underlying request that is going to get sent to the remote
pub(crate) request: KKTRequest,
}
pub struct DecryptedRequestFrame {
/// Derived carrier used for decrypting this frame and encrypting the response
pub(crate) carrier: Carrier,
/// The remote frame sent in the message
pub(crate) remote_frame: KKTFrame,
/// The unmasked byte representing the outer protocol version sent by the initiator
pub(crate) outer_protocol_version: u8,
}
impl DecryptedRequestFrame {
pub(crate) fn remote_context(&self) -> &KKTContext {
self.remote_frame.context()
}
}
pub struct ProcessedKKTRequest {
pub response: KKTResponse,
/// The obtained encapsulation key of the remote
pub remote_encapsulation_key: Option<EncapsulationKey>,
/// The KEM key requested in the original request
pub requested_kem: KEM,
/// The unmasked byte representing the outer protocol version sent by the initiator
pub outer_protocol_version: u8,
// Request payload data (Could be empty. Contents are unrelated to current KKT execution).
pub request_payload: Vec<u8>,
}
pub struct KKTResponse {
/// Encrypted KKT frame that is going to be sent back to the initiator
pub encrypted_frame: Vec<u8>,
}
impl KKTResponse {
// the size of KKTRequest is the plaintext data followed by the frame and the encryption tag
pub const fn size_excluding_payload(kem: KEM) -> usize {
// `KKTMode` argument makes no difference for the Responder role
KKTFrame::size_excluding_payload(KKTRole::Responder, KKTMode::OneWay, kem) + TAG_LEN
}
pub fn size(&self) -> usize {
self.encrypted_frame.len()
}
pub fn from_bytes(bytes: Vec<u8>) -> KKTResponse {
KKTResponse {
encrypted_frame: bytes,
}
}
pub fn into_bytes(self) -> Vec<u8> {
self.encrypted_frame
}
}
pub struct ProcessedKKTResponse {
/// The obtained encapsulation key of the remote
pub encapsulation_key: EncapsulationKey,
/// Indicates whether responder was able to verify the initiator's kem key,
pub verified_initiator_kem_key: bool,
/// Optional response payload (Could be empty. Contents are unrelated to current KKT execution).
pub response_payload: Vec<u8>,
}
+257
View File
@@ -0,0 +1,257 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Post-Quantum Re-Key Protocol
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 and XWing.
///
/// This protocol is safe if it runs under a trusted secure channel.
///
/// Bandwidth costs:
/// Request (MlKem768): 1216 bytes
/// Response (MlKem768): 1088 bytes
/// Request (XWing): 1248 bytes
/// Response (XWing): 1120 bytes
use libcrux_kem::*;
use nym_crypto::hkdf::blake3::derive_key_blake3;
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
use rand09::{CryptoRng, RngCore};
use std::fmt::{Debug, Formatter};
use zeroize::Zeroize;
use crate::error::KKTError;
/// Context string to be used with the Blake3 KDF.
const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1";
pub struct RekeyInitiator {
algorithm: Algorithm,
decapsulation_key: PrivateKey,
salt: [u8; 32],
}
impl Debug for RekeyInitiator {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let key_typ = match self.decapsulation_key {
PrivateKey::X25519(_) => "x25519",
PrivateKey::P256(_) => "p256",
PrivateKey::MlKem512(_) => "ml512",
PrivateKey::MlKem768(_) => "mlkem768",
PrivateKey::X25519MlKem768Draft00(_) => "x25519-mlkem768",
PrivateKey::XWingKemDraft06(_) => "xwing",
PrivateKey::MlKem1024(_) => "ml1024",
};
f.debug_struct("RekeyInitiator")
.field("algorithm", &self.algorithm)
.field("decapsulation_key", &key_typ)
.field("salt", &self.salt)
.finish()
}
}
impl RekeyInitiator {
/// The Initiator generates an ephemeral KEM keypair and a 32-byte salt.
/// The Initiator keeps the decapsulation key and generates a request message.
/// The request message contains the salt and an encoding of the encapsulation key as follows
/// salt encapsulation_key
/// [0 ........ 32 | 32 .............. ]
///
/// Inputs:
/// rng: something that implements CryptoRng + RngCore
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
///
/// Outputs:
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
/// Vec<u8>: The request message as explained above. This is to be sent to the responder as-is.
pub fn generate_request<R>(rng: &mut R, kem: KEM) -> Result<(RekeyInitiator, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let (algorithm, buffer_size) = match kem {
// KEM::XWing => (Algorithm::XWingKemDraft06, 32 + xwing::PUBLIC_KEY_LENGTH),
KEM::MlKem768 => (Algorithm::MlKem768, 32 + ml_kem768::PUBLIC_KEY_LENGTH),
// We don't support McEliece because the keys are massive.
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
KEM::McEliece => {
return Err(KKTError::UnsupportedAlgorithm {
info: "McEliece is not supported for re-keying",
});
}
};
// Generate the Initiator's salt
let mut salt = [0u8; 32];
rng.fill_bytes(&mut salt);
// Create the buffer for the request message and copy the salt into it.
let mut request_buffer = Vec::with_capacity(buffer_size);
request_buffer.extend_from_slice(&salt);
// Generate the ephemeral KEM keypair based on the algorithm from the function's input.
let (decapsulation_key, encapsulation_key) = key_gen(algorithm, rng)?;
// Append the encoding of the KEM encapsulation key to the initiator's randomness.
request_buffer.extend(encapsulation_key.encode());
Ok((
// The Initiator should store this until they use `RekeyInitiator::finalize`.
RekeyInitiator {
algorithm,
decapsulation_key,
salt,
},
// This is to be sent to the responder.
request_buffer,
))
}
/// The Initiator will attempt to decapsulate the `pre_key` generated by the responder
/// secret. This `pre_key` will be combined with the Initiator's previously generated salt
/// as input to a Blake3 KDF call to generate the new shared secret.
///
/// This function fails if the ciphertext cannot be decoded or decapsulated.
///
/// Input:
/// response_message: the responder's message which contains an encapsulation of `pre_key`.
/// Output:
/// [u8; 32]: the new shared secret.
pub fn finalize(mut self, response_message: &[u8]) -> Result<[u8; 32], KKTError> {
// Decode the responder's ciphertext.
let ciphertext = Ct::decode(self.algorithm, response_message)?;
// Decapsulate the `pre_key` using the Initiator's decapsulation key.
let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?;
// Encode the `pre_key` into bytes
let pre_key_bytes = pre_key.encode();
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &self.salt);
// Zeroize the Initiator's salt
self.salt.zeroize();
// TODO: zeroize the decapsulation key
Ok(new_secret)
}
}
/// The responder parses the request message.
/// The first 32 bytes are the Initiator's salt,
/// and the remainder is the encoding of the public key.
/// Given that XWing and MlKem768 have different key lengths,
/// we could deduce the algorithm from that.
///
/// If the message is badly formatted, or the encapsulation received is invalid,
/// this function will produce an error.
///
/// If everything is alright, the responder generates and encapsulates a key `pre_key` to send to the Initiator.
/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's salt to obtain
/// the new shared secret.
///
/// Inputs:
/// rng: something that implements CryptoRng + RngCore
/// request_message: the Initiator's request message (contains the salt and encapsulation key)
///
/// Outputs:
/// [u8; 32]: new shared secret
/// Vec<u8>: response which contains an encapsulation of a secret value generated by the responder.
/// This is to be sent back to the Initiator as-is.
pub fn responder_process<R>(
rng: &mut R,
mut request_message: Vec<u8>,
) -> Result<([u8; 32], Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
// Deduce the KEM algorithm from the message length
let algorithm = match request_message.len().checked_sub(32) {
//
Some(num) => match num {
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
// If message length is 1248 (32 + 1216) then the algorithm should be MlKem768
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
// We don't support McEliece because the keys are massive.
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
mceliece::PUBLIC_KEY_LENGTH => {
return Err(KKTError::UnsupportedAlgorithm {
info: "McEliece is not supported for re-keying",
});
}
// We don't support X25519 because it's not post-quantum secure.
x25519::PUBLIC_KEY_LENGTH => {
return Err(KKTError::UnsupportedAlgorithm {
info: "McEliece is not supported for re-keying",
});
}
// Reject if the size does not match any of the above.
_ => {
return Err(KKTError::UnsupportedAlgorithm {
info: "Unknown Algorithm",
});
}
},
// Reject if message length is less than 32.
None => {
return Err(KKTError::DecodingError {
info: "Invalid rekey request: size is too small",
});
}
};
// Split the message to get the Initiator's salt (first 32 bytes)
// and the encoding of the Initiator's public key.
let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32);
// Attempt to decode the Initiator's encapsulation key.
let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?;
// Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`.
let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?;
// Encode the ciphertext into bytes to send back to the initiator.
let message = ciphertext.encode();
// Encode the `pre_key` into bytes
let pre_key_bytes = pre_key.encode();
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, remote_salt);
// Zeroize the Initiator's salt
remote_salt.zeroize();
Ok((new_secret, message))
}
#[cfg(test)]
mod tests {
use crate::error::KKTError;
use crate::rekey::{RekeyInitiator, responder_process};
use nym_kkt_ciphersuite::KEM;
#[test]
fn rekey_test() {
let mut rng = rand09::rng();
let (rekey_state, request_message) =
RekeyInitiator::generate_request(&mut rng, KEM::MlKem768).unwrap();
let (responder_secret, response_message) =
responder_process(&mut rng, request_message).unwrap();
let initiator_secret = rekey_state.finalize(&response_message).unwrap();
assert_eq!(initiator_secret, responder_secret);
// mceliece should fail
let err = RekeyInitiator::generate_request(&mut rng, KEM::McEliece).unwrap_err();
assert_eq!(
err.to_string(),
KKTError::UnsupportedAlgorithm {
info: "McEliece is not supported for re-keying",
}
.to_string()
)
}
}
+196
View File
@@ -0,0 +1,196 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::key_utils::validate_encapsulation_key;
use crate::keys::{EncapsulationKey, KEMKeys};
use crate::message::{KKTRequest, KKTResponse, ProcessedKKTRequest};
use crate::{
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::KKTFrame,
};
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, SignatureScheme};
/// Representation of a KKT Responder
pub struct KKTResponder<'a> {
/// Long-term x25519 DH key pair of this Responder
x25519_keypair: &'a DHKeyPair,
/// KEM keys of this responder
kem_keys: &'a KEMKeys,
/// List of supported Hash Functions by this Responder
supported_hash_functions: Vec<HashFunction>,
/// List of supported Signature Schemes by this Responder
supported_signature_schemes: Vec<SignatureScheme>,
/// List of supported outer (LP) protocol version by this Responder
supported_outer_protocol_versions: Vec<u8>,
}
impl<'a> KKTResponder<'a> {
pub fn new(
x25519_keypair: &'a DHKeyPair,
kem_keys: &'a KEMKeys,
supported_hash_functions: &[HashFunction],
supported_signature_schemes: &[SignatureScheme],
supported_outer_protocol_versions: &[u8],
) -> Result<Self, KKTError> {
if supported_hash_functions.is_empty() {
return Err(KKTError::FunctionInputError {
info: "Did not provide a supported HashFunction when instantiating a KKTResponder",
});
}
if supported_signature_schemes.is_empty() {
return Err(KKTError::FunctionInputError {
info: "Did not provide a supported SignatureScheme when instantiating a KKTResponder",
});
}
if supported_outer_protocol_versions.is_empty() {
return Err(KKTError::FunctionInputError {
info: "Did not provide a supported outer protocol version when instantiating a KKTResponder",
});
}
Ok(Self {
x25519_keypair,
kem_keys,
supported_hash_functions: supported_hash_functions.to_vec(),
supported_signature_schemes: supported_signature_schemes.to_vec(),
supported_outer_protocol_versions: supported_outer_protocol_versions.to_vec(),
})
}
fn check_ciphersuite_compatiblity(
&self,
remote_ciphersuite: Ciphersuite,
) -> Result<(), KKTError> {
let r_hash = remote_ciphersuite.hash_function();
let r_sig = remote_ciphersuite.signature_scheme();
if !self.supported_hash_functions.contains(&r_hash) {
return Err(KKTError::IncompatibilityError {
info: "Unsupported HashFunction",
});
}
if !self.supported_signature_schemes.contains(&r_sig) {
return Err(KKTError::IncompatibilityError {
info: "Unsupported SignatureScheme",
});
}
Ok(())
}
// When this function fails, we do that silently (i.e. we don't generate a response to the initiator).
pub fn process_request(
&self,
request: KKTRequest,
request_payload_len: usize,
) -> Result<ProcessedKKTRequest, KKTError> {
let processed_req = KKTFrame::decrypt_initiator_frame(
self.x25519_keypair,
request,
&self.supported_outer_protocol_versions,
request_payload_len,
)?;
let remote_context = *processed_req.remote_context();
let remote_frame = processed_req.remote_frame;
let request_payload = remote_frame.payload().to_vec();
let mut carrier = processed_req.carrier;
self.check_ciphersuite_compatiblity(remote_context.ciphersuite())?;
let (local_context, remote_encapsulation_key) = match remote_context.mode() {
KKTMode::OneWay => responder_ingest_message(None, remote_frame)?,
KKTMode::Mutual => {
// So we can either fetch the remote hash here using some async call to the directory,
// which might make registration hang or accept the sent key then verify later.
// If we choose to not accept, the response's status will be KKTStatus::UnverifiedKEMKey.
// The response would still contain the responder's encapsulation key.
responder_ingest_message(None, remote_frame)?
}
};
let kem = local_context.ciphersuite().kem();
let Some(kem_key) = self.kem_keys.encoded_encapsulation_key(kem) else {
return Err(KKTError::IncompatibilityError {
info: "Unsupported KEM",
});
};
// for now the response payload is empty
let response_payload = Vec::with_capacity(0);
let frame = KKTFrame::new(local_context, kem_key, response_payload);
// encryption - responder frame
let encrypted_frame = carrier.encrypt(&frame.try_to_bytes()?)?;
Ok(ProcessedKKTRequest {
response: KKTResponse { encrypted_frame },
remote_encapsulation_key,
requested_kem: remote_context.ciphersuite().kem(),
outer_protocol_version: processed_req.outer_protocol_version,
request_payload,
})
}
}
pub fn responder_ingest_message(
expected_hash: Option<&[u8]>,
remote_frame: KKTFrame,
) -> Result<(KKTContext, Option<EncapsulationKey>), KKTError> {
let remote_context = remote_frame.context();
let mut own_context = remote_context.derive_responder_header()?;
let cs = own_context.ciphersuite();
match remote_context.role() {
KKTRole::Initiator => {
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
match own_context.mode() {
KKTMode::OneWay => Ok((own_context, None)),
KKTMode::Mutual => {
let Some(expected_hash) = expected_hash else {
own_context.update_status(KKTStatus::UnverifiedKEMKey);
// we don't store an unverified key
// changing the status notifies the initiator that we didn't
// we could still keep it here and then verify later...
// let received_encapsulation_key = EncapsulationKey::decode(
// own_context.ciphersuite().kem(),
// remote_frame.body_ref(),
// )?;
// Ok((own_context, Some(received_encapsulation_key)))
//
return Ok((own_context, None));
};
if !validate_encapsulation_key(
cs.hash_function(),
cs.hash_len(),
remote_frame.body_ref(),
expected_hash,
) {
// The key does not match the hash obtained from the directory
return Err(KKTError::MismatchedKEMHash);
}
let remote_key =
EncapsulationKey::try_from_bytes(remote_frame.body(), cs.kem())?;
Ok((own_context, Some(remote_key)))
}
}
}
KKTRole::Responder => Err(KKTError::IncompatibilityError {
info: "Responder received a request from another responder.",
}),
}
}
-230
View File
@@ -1,230 +0,0 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand::{CryptoRng, RngCore};
use crate::frame::KKTSessionId;
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::{KKT_SESSION_ID_LEN, KKTFrame},
key_utils::validate_encapsulation_key,
};
pub fn initiator_process<'a, R>(
rng: &mut R,
mode: KKTMode,
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
) -> Result<(KKTContext, KKTFrame), KKTError>
where
R: CryptoRng + RngCore,
{
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
let context_bytes = context.encode()?;
let mut session_id = [0; KKT_SESSION_ID_LEN];
// Generate Session ID
rng.fill_bytes(&mut session_id);
let body: &[u8] = match mode {
KKTMode::OneWay => &[],
KKTMode::Mutual => match own_encapsulation_key {
Some(encaps_key) => &encaps_key.encode(),
// Missing key
None => {
return Err(KKTError::FunctionInputError {
info: "KEM Key Not Provided",
});
}
},
};
let mut bytes_to_sign =
Vec::with_capacity(context.full_message_len() - context.signature_len());
bytes_to_sign.extend_from_slice(&context_bytes);
bytes_to_sign.extend_from_slice(body);
bytes_to_sign.extend_from_slice(&session_id);
let signature = signing_key.sign(bytes_to_sign).to_bytes();
Ok((
context,
KKTFrame::new(context_bytes, body, session_id, &signature),
))
}
pub fn anonymous_initiator_process<R>(
rng: &mut R,
ciphersuite: Ciphersuite,
) -> Result<(KKTContext, KKTFrame), KKTError>
where
R: CryptoRng + RngCore,
{
let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?;
let context_bytes = context.encode()?;
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
rng.fill_bytes(&mut session_id);
Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[])))
}
pub fn initiator_ingest_response<'a>(
own_context: &mut KKTContext,
remote_frame: &KKTFrame,
remote_context: &KKTContext,
remote_verification_key: &ed25519::PublicKey,
expected_hash: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> {
check_compatibility(own_context, remote_context)?;
match remote_context.status() {
KKTStatus::Ok => {
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
remote_context.full_message_len() - remote_context.signature_len(),
);
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
match Signature::from_bytes(remote_frame.signature_ref()) {
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
Ok(()) => {
let received_encapsulation_key = EncapsulationKey::decode(
own_context.ciphersuite().kem(),
remote_frame.body_ref(),
)?;
match validate_encapsulation_key(
&own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
remote_frame.body_ref(),
expected_hash,
) {
true => Ok(received_encapsulation_key),
// The key does not match the hash obtained from the directory
false => Err(KKTError::KEMError {
info: "Hash of received encapsulation key does not match the value stored on the directory.",
}),
}
}
Err(_) => Err(KKTError::SigVerifError),
},
Err(_) => Err(KKTError::SigConstructorError),
}
}
_ => Err(KKTError::ResponderFlaggedError {
status: remote_context.status(),
}),
}
}
// todo: figure out how to handle errors using status codes
pub fn responder_ingest_message<'a>(
remote_context: &KKTContext,
remote_verification_key: Option<&ed25519::PublicKey>,
expected_hash: Option<&[u8]>,
remote_frame: &KKTFrame,
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> {
let own_context = remote_context.derive_responder_header()?;
match remote_context.role() {
KKTRole::AnonymousInitiator => Ok((own_context, None)),
KKTRole::Initiator => {
match remote_verification_key {
Some(remote_verif_key) => {
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
own_context.full_message_len() - own_context.signature_len(),
);
bytes_to_verify.extend_from_slice(remote_frame.context_ref());
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
match Signature::from_bytes(remote_frame.signature_ref()) {
Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) {
Ok(()) => {
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
match own_context.mode() {
KKTMode::OneWay => Ok((own_context, None)),
KKTMode::Mutual => {
match expected_hash {
Some(expected_hash) => {
let received_encapsulation_key =
EncapsulationKey::decode(
own_context.ciphersuite().kem(),
remote_frame.body_ref(),
)?;
if validate_encapsulation_key(
&own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
remote_frame.body_ref(),
expected_hash,
) {
Ok((
own_context,
Some(received_encapsulation_key),
))
}
// The key does not match the hash obtained from the directory
else {
Err(KKTError::KEMError {
info: "Hash of received encapsulation key does not match the value stored on the directory.",
})
}
}
None => Err(KKTError::FunctionInputError {
info: "Expected hash of the remote encapsulation key is not provided.",
}),
}
}
}
}
Err(_) => Err(KKTError::SigVerifError),
},
Err(_) => Err(KKTError::SigConstructorError),
}
}
None => Err(KKTError::FunctionInputError {
info: "Remote Signature Verification Key Not Provided",
}),
}
}
KKTRole::Responder => Err(KKTError::IncompatibilityError {
info: "Responder received a request from another responder.",
}),
}
}
pub fn responder_process<'a>(
own_context: &mut KKTContext,
session_id: KKTSessionId,
signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>,
) -> Result<KKTFrame, KKTError> {
let body = encapsulation_key.encode();
let context_bytes = own_context.encode()?;
let mut bytes_to_sign =
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
bytes_to_sign.extend_from_slice(&own_context.encode()?);
bytes_to_sign.extend_from_slice(&body);
bytes_to_sign.extend_from_slice(&session_id);
let signature = signing_key.sign(bytes_to_sign).to_bytes();
Ok(KKTFrame::new(context_bytes, &body, session_id, &signature))
}
fn check_compatibility(
_own_context: &KKTContext,
_remote_context: &KKTContext,
) -> Result<(), KKTError> {
// todo: check ciphersuite/context compatibility
Ok(())
}
-8
View File
@@ -1,8 +0,0 @@
[package]
name = "nym-lp-common"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
publish = false
[dependencies]
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "io-mocks")]
use nym_test_utils::mocks::async_read_write::MockIOStream;
use std::net::SocketAddr;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
}
impl LpTransport for TcpStream {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self> {
TcpStream::connect(endpoint).await
}
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()> {
// Set TCP_NODELAY for low latency
self.set_nodelay(nodelay)
}
}
#[cfg(feature = "io-mocks")]
impl LpTransport for MockIOStream {
async fn connect(_endpoint: SocketAddr) -> std::io::Result<Self> {
Ok(MockIOStream::default())
}
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
Ok(())
}
}

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