Compare commits

..

137 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
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
512 changed files with 27259 additions and 36356 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
+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/**'
@@ -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
+1997 -2370
View File
File diff suppressed because it is too large Load Diff
+127 -117
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",
@@ -172,10 +170,10 @@ members = [
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below # "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch", "wasm/mix-fetch",
"wasm/node-tester", "wasm/node-tester",
"wasm/libp2p-nym",
"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 = [
@@ -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
@@ -544,11 +559,6 @@ web-sys = "0.3.76"
#[patch.crates-io] #[patch.crates-io]
#sphinx-packet = { path = "../sphinx" } #sphinx-packet = { path = "../sphinx" }
# Patch multiaddr with Protocol::Nym support for libp2p-nym transport
[patch.crates-io.multiaddr]
git = "https://github.com/mfahampshire/rust-multiaddr"
branch = "nym-protocol"
# Profile settings for individual crates # Profile settings for individual crates
# Compile-time verified queries do quite a bit of work at compile time. Incremental # Compile-time verified queries do quite a bit of work at compile time. Incremental
+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,7 +19,6 @@ 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, SystemTime>;
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>; pub type HmacSha256 = Hmac<Sha256>;
@@ -53,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)
} }
} }
+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 -2
View File
@@ -14,7 +14,6 @@ documentation.workspace = true
[dependencies] [dependencies]
async-trait = { workspace = true } async-trait = { workspace = true }
bincode = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
bs58 = { workspace = true } bs58 = { workspace = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
@@ -30,7 +29,6 @@ sha2 = { workspace = true }
si-scale = { workspace = true } si-scale = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] } url = { workspace = true, features = ["serde"] }
tokio-util = { workspace = true, features = ["codec"] }
time = { workspace = true } time = { workspace = true }
tokio = { workspace = true, features = ["sync", "macros"] } tokio = { workspace = true, features = ["sync", "macros"] }
tracing = { workspace = true } tracing = { workspace = true }
@@ -123,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 }
@@ -36,7 +36,6 @@ use crate::init::{
types::{GatewaySetup, InitialisationResult}, types::{GatewaySetup, InitialisationResult},
}; };
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::SinkExt;
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
use nym_client_core_config_types::{ForgetMe, RememberMe}; use nym_client_core_config_types::{ForgetMe, RememberMe};
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore}; use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
@@ -67,7 +66,7 @@ use std::os::raw::c_int as RawFd;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio_util::sync::{PollSendError, PollSender}; use tokio::sync::mpsc::Sender;
use url::Url; use url::Url;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@@ -113,7 +112,10 @@ pub struct ClientInput {
} }
impl ClientInput { impl ClientInput {
pub async fn send(&mut self, message: InputMessage) -> Result<(), PollSendError<InputMessage>> { pub async fn send(
&self,
message: InputMessage,
) -> Result<(), tokio::sync::mpsc::error::SendError<InputMessage>> {
self.input_sender.send(message).await self.input_sender.send(message).await
} }
} }
@@ -743,7 +745,7 @@ where
config: &Config, config: &Config,
user_agent: Option<UserAgent>, user_agent: Option<UserAgent>,
client_stats_id: String, client_stats_id: String,
input_sender: PollSender<InputMessage>, input_sender: Sender<InputMessage>,
shutdown_tracker: &ShutdownTracker, shutdown_tracker: &ShutdownTracker,
) -> ClientStatsSender { ) -> ClientStatsSender {
tracing::info!("Starting statistics control..."); tracing::info!("Starting statistics control...");
@@ -1011,7 +1013,7 @@ where
&self.config, &self.config,
self.user_agent.clone(), self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()), generate_client_stats_id(*self_address.identity()),
tokio_util::sync::PollSender::new(input_sender.clone()), input_sender.clone(),
&shutdown_tracker.clone(), &shutdown_tracker.clone(),
); );
@@ -1137,7 +1139,7 @@ where
client_input: ClientInputStatus::AwaitingProducer { client_input: ClientInputStatus::AwaitingProducer {
client_input: ClientInput { client_input: ClientInput {
connection_command_sender: client_connection_tx, connection_command_sender: client_connection_tx,
input_sender: PollSender::new(input_sender), input_sender,
client_request_sender, client_request_sender,
}, },
}, },
@@ -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)
}
@@ -1,36 +1,22 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> // Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::error::ClientCoreError;
use crate::make_bincode_serializer;
use bincode::Options;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType; use nym_sphinx::params::PacketType;
use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::connections::TransmissionLane; use nym_task::connections::TransmissionLane;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tokio_util::{
bytes::Buf,
bytes::BytesMut,
codec::{Decoder, Encoder},
};
pub type InputMessageSender = tokio_util::sync::PollSender<InputMessage>; pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>; pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
const LENGHT_ENCODING_PREFIX_SIZE: usize = 4; #[derive(Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub enum InputMessage { pub enum InputMessage {
/// Fire an already prepared mix packets into the network. /// Fire an already prepared mix packets into the network.
/// No guarantees are made about it. For example no retransmssion /// No guarantees are made about it. For example no retransmssion
/// will be attempted if it gets dropped. /// will be attempted if it gets dropped.
///
/// Packets are stored as pre-serialized bytes, which avoids
/// requiring Serde on MixPacket.
Premade { Premade {
packet_bytes: Vec<Vec<u8>>, msgs: Vec<MixPacket>,
lane: TransmissionLane, lane: TransmissionLane,
}, },
@@ -79,16 +65,12 @@ pub enum InputMessage {
} }
impl InputMessage { impl InputMessage {
pub fn simple(data: &[u8], recipient: Recipient) -> Self {
InputMessage::new_regular(recipient, data.to_vec(), TransmissionLane::General, None)
}
pub fn new_premade( pub fn new_premade(
packet_bytes: Vec<Vec<u8>>, msgs: Vec<MixPacket>,
lane: TransmissionLane, lane: TransmissionLane,
packet_type: PacketType, packet_type: PacketType,
) -> Self { ) -> Self {
let message = InputMessage::Premade { packet_bytes, lane }; let message = InputMessage::Premade { msgs, lane };
if packet_type == PacketType::Mix { if packet_type == PacketType::Mix {
message message
} else { } else {
@@ -203,394 +185,4 @@ impl InputMessage {
self.set_max_retransmissions(max_retransmissions); self.set_max_retransmissions(max_retransmissions);
self self
} }
#[allow(clippy::expect_used)]
pub fn serialized_size(&self) -> u64 {
make_bincode_serializer()
.serialized_size(self)
.expect("failed to get serialized InputMessage size")
+ LENGHT_ENCODING_PREFIX_SIZE as u64
}
}
pub struct AdressedInputMessageCodec(pub Recipient);
impl Encoder<&[u8]> for AdressedInputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: &[u8], buf: &mut BytesMut) -> Result<(), Self::Error> {
let mut codec = InputMessageCodec;
let input_message = InputMessage::simple(item, self.0);
codec.encode(input_message, buf)?;
Ok(())
}
}
pub struct InputMessageCodec;
impl Encoder<InputMessage> for InputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: InputMessage, buf: &mut BytesMut) -> Result<(), Self::Error> {
#[allow(clippy::expect_used)]
let encoded = make_bincode_serializer().serialize(&item)?;
let encoded_len = encoded.len() as u32;
let mut encoded_with_len = encoded_len.to_le_bytes().to_vec();
encoded_with_len.extend(encoded);
buf.reserve(encoded_with_len.len());
buf.extend_from_slice(&encoded_with_len);
Ok(())
}
}
impl Decoder for InputMessageCodec {
type Item = InputMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
#[allow(clippy::expect_used)]
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = make_bincode_serializer()
.deserialize(&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE])?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
/// Codec for encoding/decoding `ReconstructedMessage` for the AsyncRead interface.
///
/// This codec was moved from `nymsphinx::receiver` to keep bincode serialization
/// out of the core sphinx crate.
///
/// Uses a simple length-prefixed binary format:
/// - 4 bytes: length of encoded message
/// - N bytes: encoded message using `ReconstructedMessage::encode()`
///
/// The message encoding format is:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub struct ReconstructedMessageCodec;
impl Encoder<ReconstructedMessage> for ReconstructedMessageCodec {
type Error = ClientCoreError;
fn encode(
&mut self,
item: ReconstructedMessage,
buf: &mut BytesMut,
) -> Result<(), Self::Error> {
let encoded = item.encode();
let encoded_len = encoded.len() as u32;
buf.reserve(LENGHT_ENCODING_PREFIX_SIZE + encoded.len());
buf.extend_from_slice(&encoded_len.to_le_bytes());
buf.extend_from_slice(&encoded);
Ok(())
}
}
impl Decoder for ReconstructedMessageCodec {
type Item = ReconstructedMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = ReconstructedMessage::decode(
&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE],
)
.map_err(|e| ClientCoreError::CodecError(e.to_string()))?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::PacketType;
use rand::SeedableRng;
fn test_recipient() -> Recipient {
Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap()
}
fn test_sender_tag() -> AnonymousSenderTag {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
AnonymousSenderTag::new_random(&mut rng)
}
#[test]
fn encode_decode_all_variants() {
let mut codec = InputMessageCodec;
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
3,
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Expected Anonymous variant"),
}
}
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_reply(
test_sender_tag(),
vec![6, 7, 8],
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Reply { data, .. } => {
assert_eq!(data, vec![6, 7, 8]);
}
_ => panic!("Expected Reply variant"),
}
}
{
let mut buf = BytesMut::new();
let inner = InputMessage::new_anonymous(
test_recipient(),
vec![9, 10],
2,
TransmissionLane::General,
None,
);
let msg = InputMessage::new_wrapper(inner, PacketType::Mix);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::MessageWrapper {
message,
packet_type,
} => {
assert_eq!(packet_type, PacketType::Mix);
match *message {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![9, 10]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Expected Anonymous inner message"),
}
}
_ => panic!("Expected MessageWrapper variant"),
}
}
}
#[test]
fn encode_decode_sequential_messages() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3],
1,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![4, 5, 6, 7],
2,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![8, 9],
3,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
let decoded1 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode first message");
match decoded1 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3]);
assert_eq!(reply_surbs, 1);
}
_ => panic!("Wrong variant"),
}
let decoded2 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode second message");
match decoded2 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![4, 5, 6, 7]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Wrong variant"),
}
let decoded3 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode third message");
match decoded3 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![8, 9]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Wrong variant"),
}
// Buffer should be empty
let decoded4 = codec.decode(&mut buf).unwrap();
assert!(decoded4.is_none(), "Should have no more messages");
assert_eq!(buf.len(), 0, "Buffer should be empty");
}
#[test]
fn partial_message_handling() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
// Empty @ beginning
assert!(codec.decode(&mut buf).unwrap().is_none());
let mut buf = BytesMut::from(&[0x10, 0x00][..]);
assert!(codec.decode(&mut buf).unwrap().is_none());
assert_eq!(buf.len(), 2, "Buffer should be unchanged");
let mut full_buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
2,
TransmissionLane::General,
None,
),
&mut full_buf,
)
.unwrap();
// Only first half of the message
let partial_len = full_buf.len() / 2;
let mut partial_buf = full_buf.split_to(partial_len);
assert!(codec.decode(&mut partial_buf).unwrap().is_none());
assert_eq!(partial_buf.len(), partial_len, "Buffer should be unchanged");
partial_buf.unsplit(full_buf);
let decoded = codec.decode(&mut partial_buf).unwrap();
assert!(decoded.is_some(), "Should decode complete message");
match decoded.unwrap() {
InputMessage::Anonymous { data, .. } => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
}
_ => panic!("Expected Anonymous variant"),
}
}
#[test]
fn addressed_codec_compatibility() {
let recipient = test_recipient();
let data = b"test message payload";
let mut addressed_codec = AdressedInputMessageCodec(recipient);
let mut buf = BytesMut::new();
addressed_codec.encode(data.as_ref(), &mut buf).unwrap();
let mut input_codec = InputMessageCodec;
let decoded = input_codec
.decode(&mut buf)
.unwrap()
.expect("Should decode");
match decoded {
InputMessage::Regular {
data: decoded_data,
recipient: decoded_recipient,
lane,
..
} => {
assert_eq!(decoded_data, data, "Data should match");
assert_eq!(decoded_recipient, recipient, "Recipient should match");
assert_eq!(lane, TransmissionLane::General, "Should use General lane");
}
_ => panic!("Expected Regular variant"),
}
}
} }
@@ -45,25 +45,7 @@ where
} }
} }
async fn handle_premade_packets(&mut self, packet_bytes: Vec<Vec<u8>>, lane: TransmissionLane) { async fn handle_premade_packets(&mut self, packets: Vec<MixPacket>, lane: TransmissionLane) {
// Deserialize packet bytes back to MixPacket
let packets: Vec<MixPacket> = packet_bytes
.into_iter()
.filter_map(|bytes| {
MixPacket::try_from_v2_bytes(&bytes)
.map_err(|e| {
warn!("Failed to deserialize premade packet: {}", e);
e
})
.ok()
})
.collect();
if packets.is_empty() {
warn!("No valid premade packets to send");
return;
}
self.message_handler self.message_handler
.send_premade_mix_packets( .send_premade_mix_packets(
packets packets
@@ -174,9 +156,7 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions) self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await; .await;
} }
InputMessage::Premade { packet_bytes, lane } => { InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
self.handle_premade_packets(packet_bytes, lane).await
}
InputMessage::MessageWrapper { InputMessage::MessageWrapper {
message, message,
packet_type, packet_type,
@@ -222,8 +202,8 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions) self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await; .await;
} }
InputMessage::Premade { packet_bytes, lane } => { InputMessage::Premade { msgs, lane } => {
self.handle_premade_packets(packet_bytes, lane).await self.handle_premade_packets(msgs, lane).await
} }
// MessageWrappers can't be nested // MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => { InputMessage::MessageWrapper { .. } => {
@@ -17,7 +17,7 @@
#![warn(clippy::dbg_macro)] #![warn(clippy::dbg_macro)]
use crate::client::inbound_messages::{InputMessage, InputMessageSender}; use crate::client::inbound_messages::{InputMessage, InputMessageSender};
use futures::{SinkExt, StreamExt}; use futures::StreamExt;
use nym_client_core_config_types::StatsReporting; use nym_client_core_config_types::StatsReporting;
use nym_sphinx::addressing::Recipient; use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{ use nym_statistics_common::clients::{
-9
View File
@@ -255,15 +255,6 @@ pub enum ClientCoreError {
#[error("Could not access task registry, {0}")] #[error("Could not access task registry, {0}")]
RegistryAccess(#[from] RegistryAccessError), RegistryAccess(#[from] RegistryAccessError),
#[error("Serialization error: {0}")]
BincodeError(#[from] Box<bincode::ErrorKind>),
#[error("Could not coarce to array")]
ArrayCreationFailure(#[from] std::array::TryFromSliceError),
#[error("Codec error: {0}")]
CodecError(String),
} }
impl From<tungstenite::Error> for ClientCoreError { impl From<tungstenite::Error> for ClientCoreError {
-8
View File
@@ -1,4 +1,3 @@
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
use std::future::Future; use std::future::Future;
#[cfg(all( #[cfg(all(
@@ -40,10 +39,3 @@ where
{ {
tokio::spawn(future); tokio::spawn(future);
} }
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -1,7 +1,6 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
pub use backend::*; pub use backend::*;
pub use combined::CombinedReplyStorage; pub use combined::CombinedReplyStorage;
pub use key_storage::SentReplyKeys; pub use key_storage::SentReplyKeys;
+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,8 +1,6 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#![allow(clippy::derivable_impls)]
// MAX: surpressing warning for the moment, will be dealt with in a different PR (TODO)
use cosmwasm_schema::cw_serde; use cosmwasm_schema::cw_serde;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
+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);
}
}
-4
View File
@@ -1,8 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
#[cfg(feature = "asymmetric")] #[cfg(feature = "asymmetric")]
pub mod asymmetric; pub mod asymmetric;
pub mod bech32_address_validation; pub mod bech32_address_validation;
@@ -12,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
View File
@@ -1,6 +1,5 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub use nym_crypto::generic_array; pub use nym_crypto::generic_array;
use nym_crypto::OutputSizeUser; use nym_crypto::OutputSizeUser;
@@ -70,7 +70,7 @@ impl BinaryRequest {
let plaintext = match self { let plaintext = match self {
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?, BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.to_v2_bytes()?, BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
}; };
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
+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(())
}
} }
} }
@@ -72,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());
+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");
+179 -93
View File
@@ -1,9 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
// silences clippy warning: use of deprecated tuple variant `HttpClientError::GenericRequestFailure`: use another more strongly typed variant - this variant is only left for compatibility reasons - TODO
//! Nym HTTP API Client //! Nym HTTP API Client
//! //!
//! Centralizes and implements the core API client functionality. This crate provides custom, //! Centralizes and implements the core API client functionality. This crate provides custom,
@@ -139,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;
@@ -150,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;
@@ -164,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;
@@ -198,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;
@@ -209,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
@@ -330,6 +347,12 @@ 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: Box<reqwest::Url>, url: Box<reqwest::Url>,
@@ -557,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
@@ -565,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 {
@@ -645,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)
@@ -662,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);
@@ -726,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
} }
@@ -736,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
} }
@@ -764,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,
@@ -807,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,
@@ -865,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(),
@@ -900,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()
@@ -929,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();
@@ -951,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
@@ -984,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!(
@@ -1011,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!(
@@ -1051,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 => {
@@ -1099,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),
@@ -1124,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 {
@@ -1161,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.
@@ -1313,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
@@ -1330,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
@@ -1352,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
@@ -1372,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
@@ -1394,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"`.
@@ -1406,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"`.
@@ -1423,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.
@@ -1435,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"`.
@@ -1453,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
} }
} }
+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 }
@@ -28,7 +28,7 @@ impl MixnetConnectionBeacon {
} }
} }
async fn send_mixnet_self_ping(&mut self) -> Result<u64> { async fn send_mixnet_self_ping(&self) -> Result<u64> {
trace!("Sending mixnet self ping"); trace!("Sending mixnet self ping");
let (input_message, request_id) = create_self_ping(self.our_address); let (input_message, request_id) = create_self_ping(self.our_address);
self.mixnet_client_sender self.mixnet_client_sender
@@ -38,7 +38,7 @@ impl MixnetConnectionBeacon {
Ok(request_id) Ok(request_id)
} }
pub async fn run(mut self, shutdown: CancellationToken) -> Result<()> { pub async fn run(self, shutdown: CancellationToken) -> Result<()> {
debug!("Mixnet connection beacon is running"); debug!("Mixnet connection beacon is running");
let mut ping_interval = tokio::time::interval(MIXNET_SELF_PING_INTERVAL); let mut ping_interval = tokio::time::interval(MIXNET_SELF_PING_INTERVAL);
loop { loop {
@@ -3,6 +3,7 @@
use std::time::Duration; use std::time::Duration;
use futures::StreamExt;
use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, Recipient}; use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, Recipient};
use tracing::{debug, error}; use tracing::{debug, error};
@@ -21,25 +22,23 @@ pub async fn self_ping_and_wait(
wait_for_self_ping_return(mixnet_client, &request_ids).await wait_for_self_ping_return(mixnet_client, &request_ids).await
} }
async fn send_self_pings( async fn send_self_pings(our_address: Recipient, mixnet_client: &MixnetClient) -> Result<Vec<u64>> {
our_address: Recipient, // Send pings
mixnet_client: &mut MixnetClient, let request_ids = futures::stream::iter(1..=3)
) -> Result<Vec<u64>> { .then(|_| async {
let sender = mixnet_client.split_sender();
let futures = (1..=3).map(|_| {
let mut sender = sender.clone();
async move {
let (input_message, request_id) = create_self_ping(our_address); let (input_message, request_id) = create_self_ping(our_address);
sender mixnet_client
.send(input_message) .send(input_message)
.await .await
.map_err(|e| Error::NymSdkError(Box::new(e)))?; .map_err(|err| Error::NymSdkError(Box::new(err)))?;
Ok::<_, Error>(request_id) Ok::<u64, Error>(request_id)
} })
}); .collect::<Vec<_>>()
.await;
futures::future::try_join_all(futures).await // Check the vec of results and return the first error, if any. If there are not errors, unwrap
// all the results into a vec of u64s.
request_ids.into_iter().collect::<Result<Vec<_>>>()
} }
async fn wait_for_self_ping_return( async fn wait_for_self_ping_return(
+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,15 +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", "io-util"] } num_enum = { workspace = true }
nym-test-utils = { path = "../test-utils", optional = true } thiserror = { workspace = true }
tracing = { 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());
}
}

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