Compare commits

..

44 Commits

Author SHA1 Message Date
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
Simon Wicky 46268edf9c [Feature] Fallback gateway listener and remove legacy key support (#6249)
* one commit to rule them all

* remove too aggressive copy pasting

* update details when outdated

* typo and serde alias

* no hostname option and fixes

* fix wasm client?

* non fallback fixed

* improve gateway details update

* better ws addresses

* PR review fixes

* improve type safety on update_gateway_published_data

* fix client gateway storage migration
2025-12-03 09:49:23 +01:00
Mark Sinclair f2091cc9d6 Data Observatory (#6172)
* rename nyxd-scraper to sqlite

wip: made storage mostly generic minus modules

changed error types to make modules dyn compatible

implemented traits for sqlite instance

using sqlite instance for rewarder and chain watcher

psql scaffolding

initial postgres support - missing some proto -> json parsing

use postgres in chain scraper

added message registry to block processor

message content parsing in psql

involved addresses

adding null value for logs

Revert "use postgres in chain scraper"

This reverts commit 83c84bfd2d.

using SignerInfo proto definitions for db serialisation

added ibc messages to MessageRegistry

* add the data observatory

* add the data observatory

* move message parsing and change webhook

* handle wasm messages in a module

* formatting and clippy

* copy shared migrations and add comments to ignore file to explain

* update offline queries

* change to clap args and use url::Url to parse args

* tidy up README, startup info, typos

* tidy up validator rewarder

* lock file

* change webhook module from msg to tx handler

* ignore profiler output

* add missing things and make clippy happy

* updated cosmrs version used by the nym wallet

* add glob to workspace dependencies

* rename migration files

* remove copying from shared migrations

* duplicate shared migrations to keep things simple

* add check for manual migration sync that will fail on `cargo build` in CI

* build.rs checks data observatory migrations have content of all shared scraper migrations and errors on changes or new files

* update runner

* add reset target to make file

* process events and logs

* migrations - remove unnecessary columns

* update offline queries

* chore: run cargo fmt

* fix up: inpsect_err instead of map_err

---------

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-12-02 21:27:22 +00:00
Jędrzej Stuczyński 0b58b6f728 remove run DKG migration (#6253) 2025-11-28 13:16:36 +00:00
benedetta davico e709e30e43 Merge pull request #6144 from nymtech/ns/weighted-scoring
Add weighted scoring to NS API
2025-11-28 02:05:44 -08:00
benedetta davico 29b405f813 Merge pull request #6236 from nymtech/master
sync master to develop
2025-11-27 03:23:28 -08:00
Jędrzej Stuczyński f5d22a66f6 bugfix: reexposed 'derive_extended_private_key' (#6247) 2025-11-27 10:28:53 +00:00
import this bf7cd15428 Merge pull request #6235 from nymtech/serinko/release-notes/v2025.21-mozzarella
[DOCs/operators]: Release notes for `v2025.21-mozzarella`
2025-11-26 13:58:40 +00:00
benedetta davico 757da6a456 Merge pull request #6234 from nymtech/release/2025.21-mozzarella
Release/2025.21 mozzarella
2025-11-26 04:52:46 -08:00
benedetta davico 62c581a9ae Merge pull request #6233 from nymtech/release/2025.21-mozzarella
Release/2025.21 mozzarella
2025-11-26 04:52:27 -08:00
serinko 32e06e19e7 bump stats 2025-11-26 13:18:47 +01:00
serinko fd1b524037 add operators news 2025-11-26 13:13:37 +01:00
serinko 36d0adfe92 add NTM info message 2025-11-26 12:15:50 +01:00
serinko fcf782674c bump up version 2025-11-26 12:08:27 +01:00
serinko 4b35c36299 add dev notes 2025-11-26 12:05:48 +01:00
Jędrzej Stuczyński ca7cbac320 chore: don't rederive wallet keys on every tx (#6213)
* chore: make 'DirectSecp256k1HdWallet' only derive its keys once on construction

Previously all the keys and account information was being derived for every transaction signed

* no longer keep account seed on the wallet struct
2025-11-26 10:45:01 +00:00
dependabot[bot] e410aecf40 build(deps): bump tower-http from 0.5.2 to 0.6.6 (#6030)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.5.2 to 0.6.6.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.5.2...tower-http-0.6.6)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.6.6
  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>
2025-11-26 10:43:33 +00:00
dependabot[bot] ae76335c31 build(deps): bump tracing-subscriber from 0.3.19 to 0.3.20 (#5993)
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:26:12 +00:00
dependabot[bot] dac7f1f83c build(deps): bump tracing-subscriber in /nym-wallet (#5994)
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:25:52 +00:00
dependabot[bot] 31ff3645c5 build(deps): bump actions/upload-pages-artifact from 3 to 4 (#5992)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:22:50 +00:00
dependabot[bot] 6dd3b78a74 build(deps): bump actions/setup-go from 5 to 6 (#6013)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:21:57 +00:00
dependabot[bot] 0ab14f7041 build(deps): bump ammonia from 4.1.1 to 4.1.2 (#6057)
Bumps [ammonia](https://github.com/rust-ammonia/ammonia) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/rust-ammonia/ammonia/releases)
- [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-ammonia/ammonia/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: ammonia
  dependency-version: 4.1.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:21:30 +00:00
dependabot[bot] 9c6310264e build(deps): bump tar-fs (#6063)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.9 to 3.1.1.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.9...v3.1.1)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:21:06 +00:00
dependabot[bot] aa37bfb7ff build(deps): bump mikefarah/yq from 4.47.1 to 4.48.1 (#6107)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.47.1 to 4.48.1.
- [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.47.1...v4.48.1)

---
updated-dependencies:
- dependency-name: mikefarah/yq
  dependency-version: 4.48.1
  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>
2025-11-26 10:20:42 +00:00
dependabot[bot] 66c2454775 Bump min-document from 2.19.0 to 2.19.1 (#6181)
Bumps [min-document](https://github.com/Raynos/min-document) from 2.19.0 to 2.19.1.
- [Commits](https://github.com/Raynos/min-document/compare/v2.19.0...v2.19.1)

---
updated-dependencies:
- dependency-name: min-document
  dependency-version: 2.19.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:20:22 +00:00
dependabot[bot] 670f383faa Bump js-yaml in /sdk/typescript/tests/integration-tests/mix-fetch (#6215)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:20:05 +00:00
dependabot[bot] 9714351fd8 Bump glob from 10.3.4 to 10.5.0 in /documentation/scripts/post-process (#6216)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.3.4 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.3.4...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:19:48 +00:00
dependabot[bot] 7352499328 Bump golang.org/x/crypto in /nym-gateway-probe/netstack_ping (#6220)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.39.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 10:19:30 +00:00
dependabot[bot] 06717037e5 Bump js-yaml in /sdk/typescript/codegen/contract-clients (#6231)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.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>
2025-11-26 10:18:41 +00:00
Tommy Verrall 1a7aa2ce90 Merge pull request #6068 from nymtech/dependabot/github_actions/dot-github/workflows/SonarSource/sonarqube-scan-action-6
build(deps): bump SonarSource/sonarqube-scan-action from 5 to 6 in /.github/workflows
2025-11-26 01:06:33 -08:00
Tommy Verrall c594361cd1 Merge pull request #6180 from nymtech/dependabot/npm_and_yarn/nym-node-status-api/nym-node-status-ui/next-15.4.7
Bump next from 15.4.1 to 15.4.7 in /nym-node-status-api/nym-node-status-ui
2025-11-26 01:05:53 -08:00
Tommy Verrall df52f10f52 Merge pull request #6219 from PaJaSoft/chain-link-fix
Update chain registry link
2025-11-26 00:41:55 -08:00
benedetta davico 6a96d8205b Bump version from 4.0.11-testing to 4.0.12 2025-11-26 09:33:17 +01:00
benedettadavico 22793bc45e update changelog 2025-11-25 15:16:42 +01:00
Simon Wicky 37f3ef58a3 [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly (#6225)
* return the handlefor a clean shutdown

* cargo lock
2025-11-21 16:39:12 +01:00
benedettadavico 54c7a01482 update version 2025-11-20 17:36:30 +01:00
benedettadavico b00e1f2fff addressing comment 2025-11-20 17:32:57 +01:00
benedettadavico 90e07d9980 weighted scoring and unit test 2025-11-20 12:49:11 +01:00
PaJaSoft b8479e1cde Update chain registry link 2025-11-20 01:24:16 +01:00
benedettadavico 1a4d64a0e5 bump versions 2025-11-14 11:23:47 +01:00
dependabot[bot] de27778192 Bump next in /nym-node-status-api/nym-node-status-ui
Bumps [next](https://github.com/vercel/next.js) from 15.4.1 to 15.4.7.
- [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.1...v15.4.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 21:19:35 +00:00
dependabot[bot] 37c4a3aa36 build(deps): bump SonarSource/sonarqube-scan-action
Bumps [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 5 to 6.
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: SonarSource/sonarqube-scan-action
  dependency-version: '6'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-26 13:13:05 +00:00
323 changed files with 11430 additions and 6681 deletions
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
components: rustfmt, clippy
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v5
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
# Upload entire repository
path: './ppa'
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+2 -2
View File
@@ -8,7 +8,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.47.1
uses: mikefarah/yq@v4.48.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+2
View File
@@ -63,3 +63,5 @@ nym-api/redocly/formatted-openapi.json
**/settings.sql
**/enter_db.sh
*.profraw
+22
View File
@@ -4,6 +4,28 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.21-mozzarella] (2025-11-25)
- [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly ([#6225])
- bugfix: fix credential proxy upgrade mode attestation url arg ([#6202])
- HTTP API resilience enable & domain rotation conditions ([#6200])
- Remove debug feature from http-macro spec in gateway probe ([#6195])
- DNS relibility and troubleshooting ([#6179])
- [bugfix] Distinguish authenticator errors by credential spent ([#6176])
- Typescript SDK 1.4.1 ([#6146])
- Enable URL rotation and retries for mixnet gateway init ([#6126])
- Feature/credential proxy jwt ([#5957])
[#6225]: https://github.com/nymtech/nym/pull/6225
[#6202]: https://github.com/nymtech/nym/pull/6202
[#6200]: https://github.com/nymtech/nym/pull/6200
[#6195]: https://github.com/nymtech/nym/pull/6195
[#6179]: https://github.com/nymtech/nym/pull/6179
[#6176]: https://github.com/nymtech/nym/pull/6176
[#6146]: https://github.com/nymtech/nym/pull/6146
[#6126]: https://github.com/nymtech/nym/pull/6126
[#5957]: https://github.com/nymtech/nym/pull/5957
## [2025.20-leerdammer] (2025-11-12)
- Max/tweak ts sdk actions ([#6185])
Generated
+521 -398
View File
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -87,7 +87,9 @@ members = [
"common/nymsphinx/params",
"common/nymsphinx/routing",
"common/nymsphinx/types",
"common/nyxd-scraper",
"common/nyxd-scraper-sqlite",
"common/nyxd-scraper-psql",
"common/nyxd-scraper-shared",
"common/pemstore",
"common/registration",
"common/serde-helpers",
@@ -124,6 +126,7 @@ members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-credential-proxy/vpn-api-lib-wasm",
"nym-data-observatory",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
@@ -263,6 +266,7 @@ futures = "0.3.31"
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-resolver = "0.25"
@@ -347,11 +351,11 @@ tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.15"
toml = "0.8.22"
tower = "0.5.2"
tower-http = "0.5.2"
tower-http = "0.6.6"
tracing = "0.1.41"
tracing-log = "0.2"
tracing-opentelemetry = "0.19.0"
tracing-subscriber = "0.3.19"
tracing-subscriber = "0.3.20"
tracing-tree = "0.2.2"
tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
@@ -398,7 +402,9 @@ cw-multi-test = "=2.3.2"
bip32 = { version = "0.5.3", default-features = false }
cosmrs = { version = "0.21.1" }
cosmrs = { version = "0.22.0" }
cosmos-sdk-proto = { version = "0.27.0" }
ibc-proto = { version = "0.52.0" }
tendermint = "0.40.4"
tendermint-rpc = "0.40.4"
prost = { version = "0.13", default-features = false }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.66"
version = "1.1.67"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.66"
version = "1.1.67"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -6,14 +6,14 @@
{
"name": "exists",
"ordinal": 0,
"type_info": "Int"
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
null
false
]
},
"hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32"
@@ -1,44 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?",
"describe": {
"columns": [
{
"name": "gateway_id_bs58",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "gateway_owner_address",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "gateway_listener",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "derived_aes128_ctr_blake3_hmac_keys_bs58",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "derived_aes256_gcm_siv_key",
"ordinal": 4,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
true
]
},
"hash": "0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE remote_gateway_details\n SET\n derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,\n derived_aes256_gcm_siv_key = ?\n WHERE gateway_id_bs58 = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data) \n VALUES (?, ?)\n ",
"query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data)\n VALUES (?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -8,5 +8,5 @@
},
"nullable": []
},
"hash": "b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4"
"hash": "2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210"
}
@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "SELECT\n rgd.gateway_id_bs58,\n derived_aes256_gcm_siv_key,\n gateway_listener,\n fallback_listener\n FROM\n remote_gateway_details AS rgd\n INNER JOIN\n remote_gateway_shared_keys AS rgsk\n ON\n rgd.gateway_id_bs58 = rgsk.gateway_id_bs58\n WHERE\n rgd.gateway_id_bs58 = ?",
"describe": {
"columns": [
{
"name": "gateway_id_bs58",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "derived_aes256_gcm_siv_key",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "gateway_listener",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "fallback_listener",
"ordinal": 3,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO remote_gateway_shared_keys(gateway_id_bs58, derived_aes256_gcm_siv_key)\n VALUES (?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type) \n VALUES (?, ?, ?)\n ",
"query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type)\n VALUES (?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -8,5 +8,5 @@
},
"nullable": []
},
"hash": "8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e"
"hash": "727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, gateway_listener, fallback_listener)\n VALUES (?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)\n VALUES (?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0"
}
@@ -9,7 +9,6 @@ rust-version.workspace = true
[dependencies]
async-trait.workspace = true
cosmrs.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
time.workspace = true
@@ -20,6 +19,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-crypto = { path = "../../crypto", features = ["asymmetric"] }
nym-gateway-requests = { path = "../../gateway-requests" }
nym-gateway-client = { path = "../../client-libs/gateway-client" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -0,0 +1,22 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE remote_gateway_details_temp
(
gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58),
derived_aes256_gcm_siv_key BLOB NOT NULL,
gateway_listener TEXT NOT NULL,
fallback_listener TEXT,
expiration_timestamp DATETIME NOT NULL
);
-- keep only registrations with a non null aes256 key
INSERT INTO remote_gateway_details_temp SELECT gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, NULL, datetime(0, 'unixepoch') FROM remote_gateway_details WHERE derived_aes256_gcm_siv_key IS NOT NULL;
DROP TABLE remote_gateway_details;
ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details;
-- delete registrations with no key
DELETE FROM registered_gateway WHERE gateway_id_bs58 NOT IN ( SELECT gateway_id_bs58 FROM remote_gateway_details);
@@ -6,6 +6,7 @@ use crate::{
types::{
RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails,
},
RawGatewayPublishedData,
};
use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
@@ -144,13 +145,11 @@ impl StorageManager {
&self,
gateway_id: &str,
) -> Result<RawRemoteGatewayDetails, sqlx::Error> {
sqlx::query_as!(
RawRemoteGatewayDetails,
"SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?",
gateway_id
)
.fetch_one(&self.connection_pool)
.await
// query_as! macro doesn't use fromRow
sqlx::query_as("SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?")
.bind(gateway_id)
.fetch_one(&self.connection_pool)
.await
}
pub(crate) async fn set_remote_gateway_details(
@@ -159,41 +158,36 @@ impl StorageManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, fallback_listener, expiration_timestamp)
VALUES (?, ?, ?, ?, ?)
"#,
remote.gateway_id_bs58,
remote.derived_aes128_ctr_blake3_hmac_keys_bs58,
remote.derived_aes256_gcm_siv_key,
remote.gateway_owner_address,
remote.gateway_listener,
remote.published_data.gateway_listener,
remote.published_data.fallback_listener,
remote.published_data.expiration_timestamp
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn update_remote_gateway_key(
pub(crate) async fn update_remote_gateway_published_data(
&self,
gateway_id_bs58: &str,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
derived_aes256_gcm_siv_key: Option<&[u8]>,
published_data: &RawGatewayPublishedData,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE remote_gateway_details
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE gateway_id_bs58 = ?
UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ?
"#,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
published_data.gateway_listener,
published_data.fallback_listener,
published_data.expiration_timestamp,
gateway_id_bs58
)
.execute(&self.connection_pool)
.await?;
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -2,18 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
ActiveGateway, BadGateway, GatewayDetails, GatewayRegistration, GatewayType,
GatewaysDetailsStore, StorageError,
ActiveGateway, BadGateway, GatewayDetails, GatewayPublishedData, GatewayRegistration,
GatewayType, GatewaysDetailsStore, StorageError,
};
use async_trait::async_trait;
use manager::StorageManager;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::SharedSymmetricKey;
use std::path::Path;
pub mod error;
mod manager;
mod models;
#[derive(Clone)]
pub struct OnDiskGatewaysDetails {
@@ -134,16 +132,15 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
async fn update_gateway_published_data(
&self,
gateway_id: ed25519::PublicKey,
updated_key: &SharedSymmetricKey,
gateway_id: &ed25519::PublicKey,
published_data: &GatewayPublishedData,
) -> Result<(), Self::StorageError> {
self.manager
.update_remote_gateway_key(
.update_remote_gateway_published_data(
&gateway_id.to_base58_string(),
None,
Some(updated_key.as_bytes()),
&published_data.into(),
)
.await?;
Ok(())
@@ -1,2 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
@@ -2,10 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::{ActiveGateway, GatewayRegistration};
use crate::{BadGateway, GatewayDetails, GatewaysDetailsStore};
use crate::{BadGateway, GatewayDetails, GatewayPublishedData, GatewaysDetailsStore};
use async_trait::async_trait;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_gateway_requests::{SharedGatewayKey, SharedSymmetricKey};
use nym_crypto::asymmetric::ed25519;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
@@ -96,26 +95,17 @@ impl GatewaysDetailsStore for InMemGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
async fn update_gateway_published_data(
&self,
gateway_id: PublicKey,
updated_key: &SharedSymmetricKey,
gateway_id: &ed25519::PublicKey,
published_data: &GatewayPublishedData,
) -> Result<(), Self::StorageError> {
let mut guard = self.inner.write().await;
#[allow(clippy::unwrap_used)]
if let Some(target) = guard.gateways.get_mut(&gateway_id.to_string()) {
let GatewayDetails::Remote(details) = &mut target.details else {
return Ok(());
};
assert_eq!(Arc::strong_count(&details.shared_key), 1);
// eh. that's nasty, but it's only ever used for ephemeral clients so should be fine for now...
details.shared_key = Arc::new(SharedGatewayKey::Current(
SharedSymmetricKey::try_from_bytes(updated_key.as_bytes()).unwrap(),
))
if let Some(gateway) = guard.gateways.get_mut(&gateway_id.to_base58_string()) {
if let GatewayDetails::Remote(ref mut remote_details) = gateway.details {
remote_details.published_data = published_data.clone();
}
}
Ok(())
}
@@ -18,16 +18,6 @@ pub enum BadGateway {
source: Ed25519RecoveryError,
},
#[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {source}")]
MalformedGatewayOwnerAccountAddress {
gateway_id: String,
raw_owner: String,
#[source]
source: cosmrs::ErrorReport,
},
#[error("the shared keys provided for gateway {gateway_id} are malformed: {source}")]
MalformedSharedKeys {
gateway_id: String,
@@ -50,4 +40,12 @@ pub enum BadGateway {
#[source]
source: url::ParseError,
},
#[error("the listening address ({raw_listener}) is malformed: {source}")]
MalformedListenerNoId {
raw_listener: String,
#[source]
source: url::ParseError,
},
}
@@ -6,7 +6,6 @@
use async_trait::async_trait;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::SharedSymmetricKey;
use std::error::Error;
pub mod backend;
@@ -60,10 +59,11 @@ pub trait GatewaysDetailsStore {
details: &GatewayRegistration,
) -> Result<(), Self::StorageError>;
async fn upgrade_stored_remote_gateway_key(
/// Update the gateway details
async fn update_gateway_published_data(
&self,
gateway_id: ed25519::PublicKey,
updated_key: &SharedSymmetricKey,
gateway_id: &ed25519::PublicKey,
published_data: &GatewayPublishedData,
) -> Result<(), Self::StorageError>;
/// Remove given gateway details from the underlying store.
@@ -2,20 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use cosmrs::AccountId;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_client::client::GatewayListeners;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
use time::Duration;
use time::OffsetDateTime;
use url::Url;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const REMOTE_GATEWAY_TYPE: &str = "remote";
pub const CUSTOM_GATEWAY_TYPE: &str = "custom";
const GATEWAY_DETAILS_TTL: Duration = Duration::days(7);
#[derive(Debug, Clone, Default)]
pub struct ActiveGateway {
@@ -65,15 +66,13 @@ impl From<GatewayDetails> for GatewayRegistration {
impl GatewayDetails {
pub fn new_remote(
gateway_id: ed25519::PublicKey,
shared_key: Arc<SharedGatewayKey>,
gateway_owner_address: Option<AccountId>,
gateway_listener: Url,
shared_key: Arc<SharedSymmetricKey>,
published_data: GatewayPublishedData,
) -> Self {
GatewayDetails::Remote(RemoteGatewayDetails {
gateway_id,
shared_key,
gateway_owner_address,
gateway_listener,
published_data,
})
}
@@ -88,13 +87,20 @@ impl GatewayDetails {
}
}
pub fn shared_key(&self) -> Option<&SharedGatewayKey> {
pub fn shared_key(&self) -> Option<&SharedSymmetricKey> {
match self {
GatewayDetails::Remote(details) => Some(&details.shared_key),
GatewayDetails::Custom(_) => None,
}
}
pub fn details_exipration(&self) -> Option<OffsetDateTime> {
match self {
GatewayDetails::Remote(details) => Some(details.published_data.expiration_timestamp),
GatewayDetails::Custom(_) => None,
}
}
pub fn is_custom(&self) -> bool {
matches!(self, GatewayDetails::Custom(..))
}
@@ -164,14 +170,78 @@ pub struct RegisteredGateway {
pub gateway_type: GatewayType,
}
#[derive(Debug, Clone)]
pub struct GatewayPublishedData {
pub listeners: GatewayListeners,
pub expiration_timestamp: OffsetDateTime,
}
impl GatewayPublishedData {
pub fn new(listeners: GatewayListeners) -> GatewayPublishedData {
GatewayPublishedData {
listeners,
expiration_timestamp: OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct RawGatewayPublishedData {
pub gateway_listener: String,
pub fallback_listener: Option<String>,
pub expiration_timestamp: OffsetDateTime,
}
impl<'a> From<&'a GatewayPublishedData> for RawGatewayPublishedData {
fn from(value: &'a GatewayPublishedData) -> Self {
Self {
gateway_listener: value.listeners.primary.to_string(),
fallback_listener: value.listeners.fallback.as_ref().map(|uri| uri.to_string()),
expiration_timestamp: value.expiration_timestamp,
}
}
}
impl TryFrom<RawGatewayPublishedData> for GatewayPublishedData {
type Error = BadGateway;
fn try_from(value: RawGatewayPublishedData) -> Result<Self, Self::Error> {
let gateway_listener: Url = Url::parse(&value.gateway_listener).map_err(|source| {
BadGateway::MalformedListenerNoId {
raw_listener: value.gateway_listener.clone(),
source,
}
})?;
let fallback_listener = value
.fallback_listener
.as_ref()
.map(|uri| {
Url::parse(uri).map_err(|source| BadGateway::MalformedListenerNoId {
raw_listener: uri.to_owned(),
source,
})
})
.transpose()?;
Ok(GatewayPublishedData {
listeners: GatewayListeners {
primary: gateway_listener,
fallback: fallback_listener,
},
expiration_timestamp: value.expiration_timestamp,
})
}
}
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct RawRemoteGatewayDetails {
pub gateway_id_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
pub gateway_owner_address: Option<String>,
pub gateway_listener: String,
pub derived_aes256_gcm_siv_key: Vec<u8>,
#[zeroize(skip)]
#[cfg_attr(feature = "sqlx", sqlx(flatten))]
pub published_data: RawGatewayPublishedData,
}
impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
@@ -186,81 +256,26 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
}
})?;
let shared_key =
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => {
return Err(BadGateway::MissingSharedKey {
gateway_id: value.gateway_id_bs58.clone(),
})
}
(Some(aes256gcm_siv), _) => {
let current_key =
SharedSymmetricKey::try_from_bytes(aes256gcm_siv).map_err(|source| {
BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
}
})?;
SharedGatewayKey::Current(current_key)
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
})?;
SharedGatewayKey::Legacy(legacy_key)
}
};
let gateway_owner_address = value
.gateway_owner_address
.as_ref()
.map(|raw_owner| {
AccountId::from_str(raw_owner).map_err(|source| {
BadGateway::MalformedGatewayOwnerAccountAddress {
gateway_id: value.gateway_id_bs58.clone(),
raw_owner: raw_owner.clone(),
source,
}
})
})
.transpose()?;
let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| {
BadGateway::MalformedListener {
let shared_key = SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
raw_listener: value.gateway_listener.clone(),
source,
}
})?;
})?;
Ok(RemoteGatewayDetails {
gateway_id,
shared_key: Arc::new(shared_key),
gateway_owner_address,
gateway_listener,
published_data: value.published_data.clone().try_into()?,
})
}
}
impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
fn from(value: &'a RemoteGatewayDetails) -> Self {
let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) =
match value.shared_key.deref() {
SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())),
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
};
RawRemoteGatewayDetails {
gateway_id_bs58: value.gateway_id.to_base58_string(),
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()),
gateway_listener: value.gateway_listener.to_string(),
derived_aes256_gcm_siv_key: value.shared_key.to_bytes(),
published_data: (&value.published_data).into(),
}
}
}
@@ -269,11 +284,9 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
pub struct RemoteGatewayDetails {
pub gateway_id: ed25519::PublicKey,
pub shared_key: Arc<SharedGatewayKey>,
pub shared_key: Arc<SharedSymmetricKey>,
pub gateway_owner_address: Option<AccountId>,
pub gateway_listener: Url,
pub published_data: GatewayPublishedData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -87,6 +87,7 @@ where
user_chosen_gateway_id.map(|id| id.to_base58_string()),
Some(common_args.latency_based_selection),
common_args.force_tls_gateway,
false,
);
tracing::debug!("Gateway selection specification: {selection_spec:?}");
@@ -167,6 +168,7 @@ where
identity: gateway_details.gateway_id,
active: common_args.set_active,
typ: gateway_registration.details.typ().to_string(),
endpoint: Some(gateway_details.gateway_listener.clone()),
endpoint: Some(gateway_details.published_data.listeners.primary.clone()),
fallback_endpoint: gateway_details.published_data.listeners.fallback.clone(),
})
}
@@ -140,6 +140,7 @@ where
user_chosen_gateway_id.map(|id| id.to_base58_string()),
Some(common_args.latency_based_selection),
common_args.force_tls_gateway,
false,
);
tracing::debug!("Gateway selection specification: {selection_spec:?}");
@@ -56,7 +56,8 @@ where
identity: remote_details.gateway_id,
active: active_gateway == Some(remote_details.gateway_id),
typ: GatewayType::Remote.to_string(),
endpoint: Some(remote_details.gateway_listener),
endpoint: Some(remote_details.published_data.listeners.primary.clone()),
fallback_endpoint: remote_details.published_data.listeners.fallback.clone(),
}),
GatewayDetails::Custom(_) => info.push(GatewayInfo {
registration: gateway.registration_timestamp,
@@ -64,6 +65,7 @@ where
active: active_gateway == Some(gateway.details.gateway_id()),
typ: gateway.details.typ().to_string(),
endpoint: None,
fallback_endpoint: None,
}),
};
}
@@ -15,6 +15,7 @@ pub struct GatewayInfo {
pub typ: String,
pub endpoint: Option<Url>,
pub fallback_endpoint: Option<Url>,
}
impl Display for GatewayInfo {
@@ -30,6 +31,9 @@ impl Display for GatewayInfo {
if let Some(endpoint) = &self.endpoint {
write!(f, " endpoint: {endpoint}")?;
}
if let Some(fallback_endpoint) = &self.fallback_endpoint {
write!(f, " fallback: {fallback_endpoint}")?;
}
Ok(())
}
}
@@ -529,7 +529,6 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
@@ -555,14 +554,7 @@ where
shutdown_tracker.clone_shutdown_token(),
)
} else {
let cfg = GatewayConfig::new(
details.gateway_id,
details
.gateway_owner_address
.as_ref()
.map(|o| o.to_string()),
details.gateway_listener.to_string(),
);
let cfg = GatewayConfig::new(details.gateway_id, details.published_data.listeners);
GatewayClient::new(
GatewayClientConfig::new_default()
.with_disabled_credentials_mode(config.client.disabled_credentials_mode)
@@ -592,32 +584,13 @@ where
// the gateway client startup procedure is slightly more complicated now
// we need to:
// - perform handshake (reg or auth)
// - check for key upgrade
// - maybe perform another upgrade handshake
// - check for bandwidth
// - start background tasks
let auth_res = gateway_client
let _ = gateway_client
.perform_initial_authentication()
.await
.map_err(gateway_failure)?;
if auth_res.requires_key_upgrade {
// drop the shared_key arc because we don't need it and we can't hold it for the purposes of upgrade
drop(auth_res);
let updated_key = gateway_client
.upgrade_key_authenticated()
.await
.map_err(gateway_failure)?;
details_store
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
.await.map_err(|err| {
tracing::error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
})?
}
gateway_client
.claim_initial_bandwidth()
.await
@@ -636,7 +609,6 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
@@ -667,7 +639,6 @@ where
config,
initialisation_result,
bandwidth_controller,
details_store,
packet_router,
stats_reporter,
#[cfg(unix)]
@@ -975,8 +946,7 @@ where
)
.await?;
let (reply_storage_backend, credential_store, details_store) =
self.client_store.into_runtime_stores();
let (reply_storage_backend, credential_store, _) = self.client_store.into_runtime_stores();
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
@@ -1069,7 +1039,6 @@ where
&self.config,
init_res,
bandwidth_controller,
&details_store,
gateway_packet_router,
stats_reporter.clone(),
#[cfg(unix)]
@@ -4,7 +4,9 @@
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::error::ClientCoreError;
use nym_client_core_gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore};
use nym_client_core_gateways_storage::{
ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore,
};
use nym_crypto::asymmetric::ed25519;
// helpers for error wrapping
@@ -85,6 +87,23 @@ where
})
}
pub async fn update_stored_published_data_gateway<D>(
details_store: &D,
gateway_id: &ed25519::PublicKey,
published_data: &GatewayPublishedData,
) -> Result<(), ClientCoreError>
where
D: GatewaysDetailsStore,
D::StorageError: Send + Sync + 'static,
{
details_store
.update_gateway_published_data(gateway_id, published_data)
.await
.map_err(|source| ClientCoreError::GatewaysDetailsStoreError {
source: Box::new(source),
})
}
pub async fn load_active_gateway_details<D>(
details_store: &D,
) -> Result<ActiveGateway, ClientCoreError>
@@ -2,210 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
pub mod v1_1_33 {
use crate::client::base_client::{
non_wasm_helpers::setup_fs_gateways_storage,
storage::helpers::{set_active_gateway, store_gateway_details},
};
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::disk_persistence::CommonClientPaths;
use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33;
use crate::error::ClientCoreError;
use nym_client_core_gateways_storage::{
CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails,
};
use nym_gateway_requests::shared_key::LegacySharedKeys;
use serde::{Deserialize, Serialize};
use sha2::{digest::Digest, Sha256};
use std::ops::Deref;
use std::path::Path;
use std::sync::Arc;
use zeroize::Zeroizing;
mod base64 {
use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&STANDARD.encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
let s = <String>::deserialize(deserializer)?;
STANDARD.decode(s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum PersistedGatewayDetails {
/// Standard details of a remote gateway
Default(PersistedGatewayConfig),
/// Custom gateway setup, such as for a client embedded inside gateway itself
Custom(PersistedCustomGatewayDetails),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct PersistedGatewayConfig {
/// The hash of the shared keys to ensure the correct ones are used with those gateway details.
#[serde(with = "base64")]
key_hash: Vec<u8>,
/// Actual gateway details being persisted.
details: OldGatewayEndpointConfigV1_1_33,
}
impl PersistedGatewayConfig {
fn verify(&self, shared_key: &LegacySharedKeys) -> bool {
let key_bytes = Zeroizing::new(shared_key.to_bytes());
let mut key_hasher = Sha256::new();
key_hasher.update(&key_bytes);
let key_hash = key_hasher.finalize();
self.key_hash == key_hash.deref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedCustomGatewayDetails {
gateway_id: String,
}
fn load_shared_key<P: AsRef<Path>>(path: P) -> Result<LegacySharedKeys, ClientCoreError> {
// the shared key was a simple pem file
Ok(nym_pemstore::load_key(path)?)
}
fn gateway_details_from_raw(
gateway_id: String,
gateway_owner: String,
gateway_listener: String,
gateway_shared_key: LegacySharedKeys,
) -> Result<GatewayDetails, ClientCoreError> {
Ok(GatewayDetails::Remote(RemoteGatewayDetails {
gateway_id: gateway_id
.parse()
.map_err(|err| ClientCoreError::UpgradeFailure {
message: format!("the stored gateway id was malformed: {err}"),
})?,
shared_key: Arc::new(gateway_shared_key.into()),
gateway_owner_address: Some(gateway_owner.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway owner address was malformed: {err}"),
}
})?),
gateway_listener: gateway_listener.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway listener address was malformed: {err}"),
}
})?,
}))
}
// helper to extract shared key and gateway details into the new GatewayRegistration
fn extract_gateway_registration(
storage_paths: &CommonClientPathsV1_1_33,
) -> Result<GatewayRegistration, ClientCoreError> {
let details_file = std::fs::File::open(&storage_paths.gateway_details).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!(
"failed to open gateway details file at {}: {err}",
storage_paths.gateway_details.display()
),
}
})?;
// in v1.1.33 of the clients, the gateway details struct was saved as json
let details: PersistedGatewayDetails =
serde_json::from_reader(details_file).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!(
"failed to deserialize gateway details from {}: {err}",
storage_paths.gateway_details.display()
),
}
})?;
let details = match details {
PersistedGatewayDetails::Default(config) => {
let gateway_shared_key =
load_shared_key(&storage_paths.keys.gateway_shared_key_file)?;
if !config.verify(&gateway_shared_key) {
return Err(ClientCoreError::UpgradeFailure {
message: "failed to verify consistency of the existing gateway details"
.to_string(),
});
}
gateway_details_from_raw(
config.details.gateway_id,
config.details.gateway_owner,
config.details.gateway_listener,
gateway_shared_key,
)?
}
PersistedGatewayDetails::Custom(custom) => {
GatewayDetails::Custom(CustomGatewayDetails {
gateway_id: custom.gateway_id.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway id was malformed: {err}"),
}
})?,
data: None,
})
}
};
Ok(details.into())
}
// it's responsibility of the caller to ensure this is called **after** new registration has already been saved
fn remove_old_gateway_details(storage_paths: &CommonClientPathsV1_1_33) -> std::io::Result<()> {
std::fs::remove_file(&storage_paths.gateway_details)?;
if storage_paths.keys.gateway_shared_key_file.exists() {
std::fs::remove_file(&storage_paths.keys.gateway_shared_key_file)?;
}
Ok(())
}
pub async fn migrate_gateway_details(
old_storage_paths: &CommonClientPathsV1_1_33,
new_storage_paths: &CommonClientPaths,
preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
_old_storage_paths: &CommonClientPathsV1_1_33,
_new_storage_paths: &CommonClientPaths,
_preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
) -> Result<(), ClientCoreError> {
let gateway_registration = match preloaded_config {
Some(config) => {
let gateway_shared_key =
load_shared_key(&old_storage_paths.keys.gateway_shared_key_file)?;
gateway_details_from_raw(
config.gateway_id,
config.gateway_owner,
config.gateway_listener,
gateway_shared_key,
)?
.into()
}
None => extract_gateway_registration(old_storage_paths)?,
};
// since we're migrating to a brand new store, the store should be empty
// and thus set the 'new' gateway as the active one
let details_store =
setup_fs_gateways_storage(&new_storage_paths.gateway_registrations).await?;
store_gateway_details(&details_store, &gateway_registration).await?;
set_active_gateway(
&details_store,
&gateway_registration.details.gateway_id().to_base58_string(),
)
.await?;
remove_old_gateway_details(old_storage_paths).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("failed to remove old data: {err}"),
}
})
Err(ClientCoreError::UnsupportedMigration(
"migration of legacy keys has been removed and is no longer supported".into(),
))
}
}
@@ -6,7 +6,7 @@ use nym_crypto::{
asymmetric::{ed25519, x25519},
hkdf::{DerivationMaterial, InvalidLength},
};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sphinx::acknowledgements::AckKey;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
@@ -106,7 +106,5 @@ fn _assert_keys_zeroize_on_drop() {
_assert_zeroize_on_drop::<ed25519::KeyPair>();
_assert_zeroize_on_drop::<x25519::KeyPair>();
_assert_zeroize_on_drop::<AckKey>();
_assert_zeroize_on_drop::<LegacySharedKeys>();
_assert_zeroize_on_drop::<SharedSymmetricKey>();
_assert_zeroize_on_drop::<SharedGatewayKey>();
}
+9
View File
@@ -16,6 +16,9 @@ use std::path::PathBuf;
#[derive(thiserror::Error, Debug)]
pub enum ClientCoreError {
#[error("could not perform the state migration: {0}")]
UnsupportedMigration(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
@@ -43,6 +46,9 @@ pub enum ClientCoreError {
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("node doesn't advertise ip addresses : {0}")]
MissingIpAddress(String),
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
@@ -163,6 +169,9 @@ pub enum ClientCoreError {
#[error("custom selection of gateway was expected")]
CustomGatewaySelectionExpected,
#[error("custom selection of gateway was unexpected")]
UnexpectedCustomGatewaySelection,
#[error("the persisted gateway details were set for a custom setup")]
UnexpectedPersistedCustomGatewayDetails,
+3 -10
View File
@@ -5,6 +5,7 @@ use crate::error::ClientCoreError;
use crate::init::types::RegistrationResult;
use futures::{SinkExt, StreamExt};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::GatewayListeners;
use nym_gateway_client::GatewayClient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
@@ -379,12 +380,12 @@ pub(super) fn get_specified_gateway(
pub(super) async fn register_with_gateway(
gateway_id: ed25519::PublicKey,
gateway_listener: Url,
gateway_listeners: GatewayListeners,
our_identity: Arc<ed25519::KeyPair>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<RegistrationResult, ClientCoreError> {
let mut gateway_client = GatewayClient::new_init(
gateway_listener,
gateway_listeners,
gateway_id,
our_identity.clone(),
#[cfg(unix)]
@@ -409,14 +410,6 @@ pub(super) async fn register_with_gateway(
}
})?;
// this should NEVER happen, if it did, it means the function was misused,
// because for any fresh **registration**, the derived key is always up to date
if auth_response.requires_key_upgrade {
return Err(ClientCoreError::UnexpectedKeyUpgrade {
gateway_id: gateway_id.to_base58_string(),
});
}
Ok(RegistrationResult {
shared_keys: auth_response.initial_shared_key,
authenticated_ephemeral_client: gateway_client,
+58 -12
View File
@@ -5,7 +5,7 @@
use crate::client::base_client::storage::helpers::{
has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details,
store_gateway_details,
store_gateway_details, update_stored_published_data_gateway,
};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
@@ -16,8 +16,8 @@ use crate::init::helpers::{
use crate::init::types::{
GatewaySelectionSpecification, GatewaySetup, InitialisationResult, SelectedGateway,
};
use nym_client_core_gateways_storage::GatewaysDetailsStore;
use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration};
use nym_client_core_gateways_storage::{GatewayPublishedData, GatewaysDetailsStore};
use nym_gateway_client::client::InitGatewayClient;
use nym_topology::node::RoutingNode;
use rand::rngs::OsRng;
@@ -71,21 +71,28 @@ where
let mut rng = OsRng;
let selected_gateway = match selection_specification {
GatewaySelectionSpecification::UniformRemote { must_use_tls } => {
GatewaySelectionSpecification::UniformRemote {
must_use_tls,
no_hostname,
} => {
let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?;
SelectedGateway::from_topology_node(gateway, must_use_tls)?
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
}
GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => {
GatewaySelectionSpecification::RemoteByLatency {
must_use_tls,
no_hostname,
} => {
let gateway =
choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?;
SelectedGateway::from_topology_node(gateway, must_use_tls)?
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
}
GatewaySelectionSpecification::Specified {
must_use_tls,
no_hostname,
identity,
} => {
let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?;
SelectedGateway::from_topology_node(gateway, must_use_tls)?
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
}
GatewaySelectionSpecification::Custom {
gateway_identity,
@@ -105,15 +112,15 @@ where
let (gateway_details, authenticated_ephemeral_client) = match selected_gateway {
SelectedGateway::Remote {
gateway_id,
gateway_owner_address,
gateway_listener,
gateway_listeners,
} => {
// if we're using a 'normal' gateway setup, do register
let our_identity = client_keys.identity_keypair();
let registration = helpers::register_with_gateway(
gateway_id,
gateway_listener.clone(),
gateway_listeners.clone(),
our_identity,
#[cfg(unix)]
connection_fd_callback,
@@ -123,8 +130,7 @@ where
GatewayDetails::new_remote(
gateway_id,
registration.shared_keys,
gateway_owner_address,
gateway_listener,
GatewayPublishedData::new(gateway_listeners),
),
Some(registration.authenticated_ephemeral_client),
)
@@ -150,6 +156,46 @@ where
})
}
pub async fn refresh_gateway_published_data<D>(
details_store: &D,
registration: GatewayRegistration,
available_gateways: Vec<RoutingNode>,
must_use_tls: bool,
no_hostname: bool,
) -> Result<(), ClientCoreError>
where
D: GatewaysDetailsStore,
D::StorageError: Send + Sync + 'static,
{
let gateway_id = registration.gateway_id().to_base58_string();
tracing::trace!("Updating gateway details : {gateway_id}");
let gateway = get_specified_gateway(&gateway_id, &available_gateways, must_use_tls)?;
let selected_gateway = SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?;
let new_gateway_listeners = match selected_gateway {
SelectedGateway::Remote {
gateway_listeners, ..
} => gateway_listeners,
SelectedGateway::Custom { .. } => {
// this should not happen, as `from_topology_node` returns a Remote
Err(ClientCoreError::UnexpectedCustomGatewaySelection)?
}
};
let new_published_data = GatewayPublishedData::new(new_gateway_listeners);
// update gateway details
update_stored_published_data_gateway(
details_store,
&registration.gateway_id(),
&new_published_data,
)
.await?;
Ok(())
}
async fn use_loaded_gateway_details<K, D>(
key_store: &K,
details_store: &D,
+70 -23
View File
@@ -10,12 +10,11 @@ use nym_client_core_gateways_storage::{
GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails,
};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::InitGatewayClient;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
use nym_gateway_client::SharedSymmetricKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKey;
use nym_validator_client::nyxd::AccountId;
use serde::Serialize;
use std::fmt::{Debug, Display};
#[cfg(unix)]
@@ -28,9 +27,7 @@ pub enum SelectedGateway {
Remote {
gateway_id: ed25519::PublicKey,
gateway_owner_address: Option<AccountId>,
gateway_listener: Url,
gateway_listeners: GatewayListeners,
},
Custom {
gateway_id: ed25519::PublicKey,
@@ -42,24 +39,40 @@ impl SelectedGateway {
pub fn from_topology_node(
node: RoutingNode,
must_use_tls: bool,
no_hostname: bool,
) -> Result<Self, ClientCoreError> {
// for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : )
let prefer_ipv6 = false;
let gateway_listener = if must_use_tls {
node.ws_entry_address_tls()
.ok_or(ClientCoreError::UnsupportedWssProtocol {
gateway: node.identity_key.to_base58_string(),
})?
let (gateway_listener, fallback_listener) = if must_use_tls {
// WSS main, no fallback
let primary =
node.ws_entry_address_tls()
.ok_or(ClientCoreError::UnsupportedWssProtocol {
gateway: node.identity_key.to_base58_string(),
})?;
(primary, None)
} else {
node.ws_entry_address(prefer_ipv6)
.ok_or(ClientCoreError::UnsupportedEntry {
let (maybe_primary, fallback) =
node.ws_entry_address_with_fallback(prefer_ipv6, no_hostname);
(
maybe_primary.ok_or(ClientCoreError::UnsupportedEntry {
id: node.node_id,
identity: node.identity_key.to_base58_string(),
})?
})?,
fallback,
)
};
let gateway_listener =
let fallback_listener_url = fallback_listener.and_then(|address| {
Url::parse(&address)
.inspect_err(|err| {
tracing::warn!("Malformed fallback listener, none will be used : {err}")
})
.ok()
});
let gateway_listener_url =
Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener {
gateway_id: node.identity_key.to_base58_string(),
raw_listener: gateway_listener,
@@ -68,8 +81,10 @@ impl SelectedGateway {
Ok(SelectedGateway::Remote {
gateway_id: node.identity_key,
gateway_owner_address: None,
gateway_listener,
gateway_listeners: GatewayListeners {
primary: gateway_listener_url,
fallback: fallback_listener_url,
},
})
}
@@ -98,7 +113,7 @@ impl SelectedGateway {
/// - shared keys derived between ourselves and the node
/// - an authenticated handle of an ephemeral handle created for the purposes of registration
pub struct RegistrationResult {
pub shared_keys: Arc<SharedGatewayKey>,
pub shared_keys: Arc<SharedSymmetricKey>,
pub authenticated_ephemeral_client: InitGatewayClient,
}
@@ -145,20 +160,36 @@ impl InitialisationResult {
pub fn gateway_id(&self) -> ed25519::PublicKey {
self.gateway_registration.details.gateway_id()
}
// indicates if the remote gateway details TTL has expired
pub fn exipred_details(&self) -> bool {
if let Some(expiration_timestamp) = self.gateway_registration.details.details_exipration() {
OffsetDateTime::now_utc() > expiration_timestamp
} else {
false
}
}
}
#[derive(Clone, Debug)]
pub enum GatewaySelectionSpecification {
/// Uniformly choose a random remote gateway.
UniformRemote { must_use_tls: bool },
UniformRemote {
must_use_tls: bool,
no_hostname: bool,
},
/// Should the new, remote, gateway be selected based on latency.
RemoteByLatency { must_use_tls: bool },
RemoteByLatency {
must_use_tls: bool,
no_hostname: bool,
},
/// Gateway with this specific identity should be chosen.
// JS: I don't really like the name of this enum variant but couldn't think of anything better at the time
Specified {
must_use_tls: bool,
no_hostname: bool,
identity: IdentityKey,
},
@@ -174,6 +205,7 @@ impl Default for GatewaySelectionSpecification {
fn default() -> Self {
GatewaySelectionSpecification::UniformRemote {
must_use_tls: false,
no_hostname: false,
}
}
}
@@ -183,16 +215,24 @@ impl GatewaySelectionSpecification {
gateway_identity: Option<String>,
latency_based_selection: Option<bool>,
must_use_tls: bool,
no_hostname: bool,
) -> Self {
if let Some(identity) = gateway_identity {
GatewaySelectionSpecification::Specified {
identity,
must_use_tls,
no_hostname,
}
} else if let Some(true) = latency_based_selection {
GatewaySelectionSpecification::RemoteByLatency { must_use_tls }
GatewaySelectionSpecification::RemoteByLatency {
must_use_tls,
no_hostname,
}
} else {
GatewaySelectionSpecification::UniformRemote { must_use_tls }
GatewaySelectionSpecification::UniformRemote {
must_use_tls,
no_hostname,
}
}
}
}
@@ -315,6 +355,7 @@ pub struct InitResults {
pub encryption_key: String,
pub gateway_id: String,
pub gateway_listener: String,
pub fallback_listener: Option<String>,
pub gateway_registration: OffsetDateTime,
pub address: Recipient,
}
@@ -332,7 +373,13 @@ impl InitResults {
identity_key: address.identity().to_base58_string(),
encryption_key: address.encryption_key().to_base58_string(),
gateway_id: gateway.gateway_id.to_base58_string(),
gateway_listener: gateway.gateway_listener.to_string(),
gateway_listener: gateway.published_data.listeners.primary.to_string(),
fallback_listener: gateway
.published_data
.listeners
.fallback
.as_ref()
.map(|uri| uri.to_string()),
gateway_registration: registration,
address,
}
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion,
GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse,
SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
GatewayProtocolVersionExt, GatewayRequestsError, ServerResponse, SharedSymmetricKey,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -47,43 +47,39 @@ use std::os::raw::c_int as RawFd;
use wasm_utils::websocket::JSWebsocket;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
use zeroize::Zeroizing;
pub mod config;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod websockets;
#[cfg(not(target_arch = "wasm32"))]
use websockets::connect_async;
use crate::client::websockets::connect_async_with_fallback;
pub struct GatewayConfig {
pub gateway_identity: ed25519::PublicKey,
// currently a dead field
pub gateway_owner: Option<String>,
pub gateway_listener: String,
pub gateway_listeners: GatewayListeners,
}
impl GatewayConfig {
pub fn new(
gateway_identity: ed25519::PublicKey,
gateway_owner: Option<String>,
gateway_listener: String,
) -> Self {
pub fn new(gateway_identity: ed25519::PublicKey, gateway_listeners: GatewayListeners) -> Self {
GatewayConfig {
gateway_identity,
gateway_owner,
gateway_listener,
gateway_listeners,
}
}
}
#[derive(Debug, Clone)]
pub struct GatewayListeners {
pub primary: Url,
pub fallback: Option<Url>,
}
#[must_use]
#[derive(Debug)]
pub struct AuthenticationResponse {
pub initial_shared_key: Arc<SharedGatewayKey>,
pub requires_key_upgrade: bool,
pub initial_shared_key: Arc<SharedSymmetricKey>,
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
@@ -92,10 +88,10 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
authenticated: bool,
bandwidth: ClientBandwidth,
gateway_address: String,
gateway_addresses: GatewayListeners,
gateway_identity: ed25519::PublicKey,
local_identity: Arc<ed25519::KeyPair>,
shared_key: Option<Arc<SharedGatewayKey>>,
shared_key: Option<Arc<SharedSymmetricKey>>,
connection: SocketState,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
@@ -118,7 +114,7 @@ impl<C, St> GatewayClient<C, St> {
gateway_config: GatewayConfig,
local_identity: Arc<ed25519::KeyPair>,
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
shared_key: Option<Arc<SharedGatewayKey>>,
shared_key: Option<Arc<SharedSymmetricKey>>,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
@@ -129,7 +125,7 @@ impl<C, St> GatewayClient<C, St> {
cfg,
authenticated: false,
bandwidth: ClientBandwidth::new_empty(),
gateway_address: gateway_config.gateway_listener,
gateway_addresses: gateway_config.gateway_listeners,
gateway_identity: gateway_config.gateway_identity,
local_identity,
shared_key,
@@ -148,7 +144,7 @@ impl<C, St> GatewayClient<C, St> {
self.gateway_identity
}
pub fn shared_key(&self) -> Option<Arc<SharedGatewayKey>> {
pub fn shared_key(&self) -> Option<Arc<SharedSymmetricKey>> {
self.shared_key.clone()
}
@@ -203,12 +199,19 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(not(target_arch = "wasm32"))]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
debug!(
"Attempting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(
&self.gateway_address,
if let Some(fallback_url) = &self.gateway_addresses.fallback {
debug!(
"Attempting to establish connection to gateway at: {}, with fallback at: {fallback_url}",
self.gateway_addresses.primary
);
} else {
debug!(
"Attempting to establish connection to gateway at: {}",
self.gateway_addresses.primary
);
}
let (ws_stream, _) = connect_async_with_fallback(
&self.gateway_addresses,
#[cfg(unix)]
self.connection_fd_callback.clone(),
)
@@ -221,7 +224,7 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(target_arch = "wasm32")]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
let ws_stream = match JSWebsocket::new(&self.gateway_address) {
let ws_stream = match JSWebsocket::new(self.gateway_addresses.primary.as_ref()) {
Ok(ws_stream) => ws_stream,
Err(e) => {
return Err(GatewayClientError::NetworkErrorWasm(e));
@@ -274,7 +277,7 @@ impl<C, St> GatewayClient<C, St> {
message: ClientRequest,
) -> Result<(), GatewayClientError> {
if let Some(shared_key) = self.shared_key() {
let encrypted = message.encrypt(&*shared_key)?;
let encrypted = message.encrypt(&shared_key)?;
Box::pin(self.send_websocket_message_without_response(encrypted)).await?;
Ok(())
} else {
@@ -463,19 +466,14 @@ impl<C, St> GatewayClient<C, St> {
async fn register(
&mut self,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
supported_gateway_protocol: GatewayProtocolVersion,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv();
debug_assert!(self.connection.is_available());
log::debug!(
"registering with gateway. using legacy key derivation: {}",
!derive_aes256_gcm_siv_key
);
log::debug!("registering with gateway");
// it's fine to instantiate it here as it's only used once (during authentication or registration)
// and putting it into the GatewayClient struct would be a hassle
@@ -525,75 +523,6 @@ impl<C, St> GatewayClient<C, St> {
Ok(())
}
pub async fn upgrade_key_authenticated(
&mut self,
) -> Result<Zeroizing<SharedSymmetricKey>, GatewayClientError> {
info!("*** STARTING AES128CTR-HMAC KEY UPGRADE INTO AES256GCM-SIV***");
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
if !shared_key.is_legacy() {
return Err(GatewayClientError::KeyAlreadyUpgraded);
}
// make sure we have the only reference, so we could safely swap it
if Arc::strong_count(shared_key) != 1 {
return Err(GatewayClientError::KeyAlreadyInUse);
}
assert!(shared_key.is_legacy());
let legacy_key = shared_key.unwrap_legacy();
let (updated_key, hkdf_salt) = legacy_key.upgrade();
let derived_key_digest = updated_key.digest();
let upgrade_request = ClientRequest::UpgradeKey {
hkdf_salt,
derived_key_digest,
}
.encrypt(legacy_key)?;
info!("sending upgrade request and awaiting the acknowledgement back");
let (ciphertext, nonce) = match self
.send_websocket_message_with_response(upgrade_request)
.await?
{
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
// attempt to decrypt it using NEW key
let Ok(response) = SensitiveServerResponse::decrypt(&ciphertext, &nonce, &updated_key)
else {
return Err(GatewayClientError::FatalKeyUpgradeFailure);
};
match response {
SensitiveServerResponse::KeyUpgradeAck { .. } => {
info!("received key upgrade acknowledgement")
}
_ => return Err(GatewayClientError::FatalKeyUpgradeFailure),
}
// perform in memory swap and make a copy for updating storage
let zeroizing_updated_key = updated_key.zeroizing_clone();
self.shared_key = Some(Arc::new(updated_key.into()));
Ok(zeroizing_updated_key)
}
async fn send_authenticate_request_and_handle_response(
&mut self,
msg: ClientControlRequest,
@@ -606,17 +535,14 @@ impl<C, St> GatewayClient<C, St> {
upgrade_mode,
} => {
if protocol_version.is_future_version() {
// SAFETY: future version is always defined
#[allow(clippy::unwrap_used)]
let version = protocol_version.unwrap();
error!("the gateway insists on using v{version} protocol which is not supported by this client");
error!("the gateway insists on using v{protocol_version} protocol which is not supported by this client");
return Err(GatewayClientError::AuthenticationFailure);
}
self.authenticated = status;
self.bandwidth
.update_and_maybe_log(bandwidth_remaining, upgrade_mode);
self.negotiated_protocol = protocol_version;
self.negotiated_protocol = Some(protocol_version);
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
if upgrade_mode {
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
@@ -629,27 +555,6 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> {
debug!("using v1 authentication");
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
let self_address = self
.local_identity
.public_key()
.derive_destination_address();
let msg = ClientControlRequest::new_legacy_authenticate(
self_address,
shared_key,
self.cfg.bandwidth.require_tickets,
)?;
self.send_authenticate_request_and_handle_response(msg)
.await
}
async fn authenticate_v2(
&mut self,
requested_protocol_version: GatewayProtocolVersion,
@@ -670,30 +575,22 @@ impl<C, St> GatewayClient<C, St> {
async fn authenticate(
&mut self,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
requested_protocol_version: GatewayProtocolVersion,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
if supported_gateway_protocol.supports_authenticate_v2() {
// use the highest possible protocol version the gateway has announced support for
// SAFETY: if announced protocol supports auth v2, it means it's properly set
#[allow(clippy::unwrap_used)]
self.authenticate_v2(supported_gateway_protocol.unwrap())
.await
} else {
self.authenticate_v1().await
}
// use the highest possible protocol version the gateway has announced support for
self.authenticate_v2(requested_protocol_version).await
}
/// Helper method to either call register or authenticate based on self.shared_key value
#[instrument(skip_all,
fields(
gateway = %self.gateway_identity,
gateway_address = %self.gateway_address
gateway_address = %self.gateway_addresses.primary
)
)]
pub async fn perform_initial_authentication(
@@ -704,15 +601,9 @@ impl<C, St> GatewayClient<C, St> {
}
// 1. check gateway's protocol version
let gw_protocol = match self.get_gateway_protocol().await {
Ok(protocol) => Some(protocol),
Err(_) => {
// if we failed to send the request, it means the gateway is running the old binary,
// so it has reset our connection - we have to reconnect
self.establish_connection().await?;
None
}
};
// if we failed to get this request resolved, it means the gateway is on an old version
// that definitely does not support auth v2 or aes256gcm, so we bail
let gw_protocol = self.get_gateway_protocol().await?;
debug!("supported gateway protocol: {gw_protocol:?}");
@@ -727,6 +618,16 @@ impl<C, St> GatewayClient<C, St> {
if !supports_auth_v2 {
warn!("this gateway is on an old version that doesn't support authentication v2")
}
// Dropping v1 support
if !supports_auth_v2 || !supports_aes_gcm_siv {
// we can't continue
return Err(GatewayClientError::IncompatibleProtocol {
gateway: gw_protocol,
current: CURRENT_PROTOCOL_VERSION,
});
}
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
@@ -736,7 +637,7 @@ impl<C, St> GatewayClient<C, St> {
let gw_protocol = if gw_protocol.is_future_version() {
warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT);
Some(GatewayProtocolVersion::CURRENT)
GatewayProtocolVersion::CURRENT
} else {
gw_protocol
};
@@ -746,7 +647,6 @@ impl<C, St> GatewayClient<C, St> {
return if let Some(shared_key) = &self.shared_key {
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv,
})
} else {
Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey)
@@ -761,11 +661,8 @@ impl<C, St> GatewayClient<C, St> {
#[allow(clippy::unwrap_used)]
let shared_key = self.shared_key.as_ref().unwrap();
let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv;
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade,
})
} else {
Err(GatewayClientError::AuthenticationFailure)
@@ -781,7 +678,6 @@ impl<C, St> GatewayClient<C, St> {
// so no upgrades are required
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: false,
})
}
}
@@ -1143,7 +1039,12 @@ impl<C, St> GatewayClient<C, St> {
}
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
self.authenticate(self.negotiated_protocol).await?;
if let Some(negotiated_protocol) = self.negotiated_protocol {
self.authenticate(negotiated_protocol).await?;
} else {
// This should never happen, because it would mean we're not registered
return Err(GatewayClientError::NotRegistered);
}
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
@@ -1188,7 +1089,7 @@ pub struct InitOnly;
impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
// for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic...
pub fn new_init(
gateway_listener: Url,
gateway_listeners: GatewayListeners,
gateway_identity: ed25519::PublicKey,
local_identity: Arc<ed25519::KeyPair>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
@@ -1207,7 +1108,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true),
authenticated: false,
bandwidth: ClientBandwidth::new_empty(),
gateway_address: gateway_listener.to_string(),
gateway_addresses: gateway_listeners,
gateway_identity,
local_identity,
shared_key: None,
@@ -1239,7 +1140,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
cfg: self.cfg,
authenticated: self.authenticated,
bandwidth: self.bandwidth,
gateway_address: self.gateway_address,
gateway_addresses: self.gateway_addresses,
gateway_identity: self.gateway_identity,
local_identity: self.local_identity,
shared_key: self.shared_key,
@@ -1,3 +1,5 @@
#[cfg(not(target_arch = "wasm32"))]
use crate::client::GatewayListeners;
use crate::error::GatewayClientError;
use nym_http_api_client::HickoryDnsResolver;
@@ -85,3 +87,35 @@ pub(crate) async fn connect_async(
source: Box::new(error),
})
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn connect_async_with_fallback(
endpoints: &GatewayListeners,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
match connect_async(
endpoints.primary.as_ref(),
#[cfg(unix)]
connection_fd_callback.clone(),
)
.await
{
Ok(inner) => Ok(inner),
Err(e) => {
if let Some(fallback) = &endpoints.fallback {
tracing::warn!(
"Main endpoint failed {} : {e}, trying fallback : {fallback}",
endpoints.primary
);
connect_async(
fallback.as_ref(),
#[cfg(unix)]
connection_fd_callback,
)
.await
} else {
Err(e)
}
}
}
}
@@ -83,6 +83,9 @@ pub enum GatewayClientError {
#[error("Client is not authenticated")]
NotAuthenticated,
#[error("Client is not registered")]
NotRegistered,
#[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")]
NotEnoughBandwidth(i64, i64),
@@ -116,8 +119,8 @@ pub enum GatewayClientError {
#[error("Failed to send mixnet message")]
MixnetMsgSenderFailedToSend,
#[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")]
IncompatibleProtocol { gateway: Option<u8>, current: u8 },
#[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway}")]
IncompatibleProtocol { gateway: u8, current: u8 },
#[error(
"The packet router hasn't been set - are you sure you started up the client correctly?"
+2 -4
View File
@@ -7,9 +7,7 @@ use tracing::{error, warn};
use tungstenite::{protocol::Message, Error as WsError};
pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig};
pub use nym_gateway_requests::shared_key::{
LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey,
};
pub use nym_gateway_requests::shared_key::SharedSymmetricKey;
pub use packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
PacketRouter,
@@ -47,7 +45,7 @@ pub(crate) fn cleanup_socket_messages(
pub(crate) fn try_decrypt_binary_message(
bin_msg: Vec<u8>,
shared_keys: &SharedGatewayKey,
shared_keys: &SharedSymmetricKey,
) -> Option<Vec<u8>> {
match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) {
Ok(bin_response) => match bin_response {
@@ -10,7 +10,7 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message};
use futures::channel::oneshot;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_gateway_requests::{
SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError,
};
@@ -66,7 +66,7 @@ pub(crate) struct PartiallyDelegatedHandle {
struct PartiallyDelegatedRouter {
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
@@ -76,7 +76,7 @@ struct PartiallyDelegatedRouter {
impl PartiallyDelegatedRouter {
fn new(
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
stream_return_requester: oneshot::Receiver<()>,
@@ -214,11 +214,6 @@ impl PartiallyDelegatedRouter {
SensitiveServerResponse::RememberMeAck {} => {
info!("received remember me acknowledgement");
}
SensitiveServerResponse::KeyUpgradeAck {} => {
warn!(
"received illegal key upgrade acknowledgement in an authenticated client"
);
}
_ => {
warn!("received unknown SensitiveServerResponse");
}
@@ -294,7 +289,7 @@ impl PartiallyDelegatedHandle {
pub(crate) fn split_and_listen_for_mixnet_messages(
conn: WsConn,
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
shutdown: ShutdownToken,
) -> Self {
@@ -75,6 +75,7 @@ workspace = true
features = ["json", "rustls-tls"]
[dev-dependencies]
anyhow = { workspace = true }
bip39 = { workspace = true }
cosmrs = { workspace = true, features = ["bip32"] }
ts-rs = { workspace = true }
@@ -7,6 +7,7 @@ use cosmrs::{tx, AccountId, Coin, Denom};
use nym_validator_client::http_client;
use nym_validator_client::nyxd::CosmWasmClient;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::signer::OfflineSigner;
use nym_validator_client::signing::tx_signer::TxSigner;
use nym_validator_client::signing::SignerData;
@@ -19,8 +20,8 @@ async fn main() {
let validator = "https://rpc.sandbox.nymtech.net";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
let signer = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, signer_mnemonic).unwrap();
let signer_address = signer.signer_addresses()[0].clone();
// local 'client' ONLY signing messages
let tx_signer = signer;
@@ -57,9 +58,15 @@ async fn main() {
100000u32,
);
let tx_raw = tx_signer
.sign_direct(&signer_address, vec![send_msg], fee, memo, signer_data)
.unwrap();
let tx_raw = TxSigner::sign_direct(
&tx_signer,
&signer_address,
vec![send_msg],
fee,
memo,
signer_data,
)
.unwrap();
let tx_bytes = tx_raw.to_bytes().unwrap();
// compare balances from before and after the tx
@@ -5,8 +5,7 @@ use crate::nyxd::{self, NyxdClient};
use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
use crate::signing::signer::{NoSigner, OfflineSigner};
use crate::{
DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient,
ValidatorClientError,
DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ValidatorClientError,
};
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
@@ -164,7 +163,7 @@ impl Client<HttpRpcClient, DirectSecp256k1HdWallet> {
) -> Result<DirectSigningHttpRpcValidatorClient, ValidatorClientError> {
let rpc_client = http_client(config.nyxd_url.as_str())?;
let prefix = &config.nyxd_config.chain_details.bech32_account_prefix;
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?;
Ok(Self::new_signing_with_rpc_client(
config, rpc_client, wallet,
@@ -177,12 +176,13 @@ impl Client<HttpRpcClient, DirectSecp256k1HdWallet> {
}
}
impl Client<ReqwestRpcClient, DirectSecp256k1HdWallet> {
#[allow(deprecated)]
impl Client<crate::ReqwestRpcClient, DirectSecp256k1HdWallet> {
pub fn new_reqwest_signing(
config: Config,
mnemonic: bip39::Mnemonic,
) -> DirectSigningReqwestRpcValidatorClient {
let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone());
let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone());
let prefix = &config.nyxd_config.chain_details.bech32_account_prefix;
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
@@ -203,9 +203,10 @@ impl Client<HttpRpcClient> {
}
}
impl Client<ReqwestRpcClient> {
#[allow(deprecated)]
impl Client<crate::ReqwestRpcClient> {
pub fn new_reqwest_query(config: Config) -> QueryReqwestRpcValidatorClient {
let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone());
let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone());
Self::new_with_rpc_client(config, rpc_client)
}
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nym_api;
use crate::signing::direct_wallet::DirectSecp256k1HdWalletError;
pub use tendermint_rpc::error::Error as TendermintRpcError;
use thiserror::Error;
@@ -26,6 +27,12 @@ pub enum ValidatorClientError {
#[error("No validator API url has been provided")]
NoAPIUrlAvailable,
#[error("failed to derive signing accounts: {source}")]
AccountDerivationFailure {
#[from]
source: DirectSecp256k1HdWalletError,
},
}
impl From<nym_api::error::NymAPIError> for ValidatorClientError {
@@ -12,6 +12,7 @@ pub mod rpc;
pub mod signing;
pub use crate::error::ValidatorClientError;
#[allow(deprecated)]
pub use crate::rpc::reqwest::ReqwestRpcClient;
pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
pub use client::{Client, Config, EcashApiClient};
@@ -38,9 +39,13 @@ pub type DirectSigningHttpRpcValidatorClient = Client<HttpRpcClient, DirectSecp2
#[cfg(feature = "http-client")]
pub type DirectSigningHttpRpcNyxdClient = nyxd::NyxdClient<HttpRpcClient, DirectSecp256k1HdWallet>;
#[allow(deprecated)]
pub type QueryReqwestRpcValidatorClient = Client<ReqwestRpcClient>;
#[allow(deprecated)]
pub type QueryReqwestRpcNyxdClient = nyxd::NyxdClient<ReqwestRpcClient>;
#[allow(deprecated)]
pub type DirectSigningReqwestRpcValidatorClient = Client<ReqwestRpcClient, DirectSecp256k1HdWallet>;
#[allow(deprecated)]
pub type DirectSigningReqwestRpcNyxdClient =
nyxd::NyxdClient<ReqwestRpcClient, DirectSecp256k1HdWallet>;
@@ -178,7 +178,7 @@ where
.ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(signer_address, dkg_contract_address, &msg, fee, memo, funds)
.await
@@ -99,7 +99,7 @@ where
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
@@ -95,7 +95,7 @@ where
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
group_contract_address,
@@ -667,7 +667,7 @@ where
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let memo = msg.default_memo();
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
mixnet_contract_address,
@@ -133,7 +133,7 @@ where
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
multisig_contract_address,
@@ -165,7 +165,7 @@ where
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
performance_contract_address,
@@ -375,7 +375,7 @@ where
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let memo = msg.name().to_string();
let signer_address = &self.signer_addresses()?[0];
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
vesting_contract_address,
@@ -324,7 +324,7 @@ where
{
type Error = S::Error;
fn get_accounts(&self) -> Result<Vec<AccountData>, Self::Error> {
fn get_accounts(&self) -> &[AccountData] {
self.signer.get_accounts()
}
@@ -19,7 +19,7 @@ use crate::signing::signer::NoSigner;
use crate::signing::signer::OfflineSigner;
use crate::signing::tx_signer::TxSigner;
use crate::signing::AccountData;
use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient, ReqwestRpcClient};
use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient};
use async_trait::async_trait;
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
use cosmrs::tx::{Raw, SignDoc};
@@ -158,12 +158,13 @@ impl NyxdClient<HttpClient> {
}
}
impl NyxdClient<ReqwestRpcClient> {
#[allow(deprecated)]
impl NyxdClient<crate::ReqwestRpcClient> {
pub fn connect_reqwest(
config: Config,
endpoint: Url,
) -> Result<QueryReqwestRpcNyxdClient, NyxdError> {
let client = ReqwestRpcClient::new(endpoint);
let client = crate::ReqwestRpcClient::new(endpoint);
Ok(NyxdClient {
client: MaybeSigningClient::new(client, (&config).into()),
@@ -195,18 +196,19 @@ impl NyxdClient<HttpClient, DirectSecp256k1HdWallet> {
let client = http_client(endpoint)?;
let prefix = &config.chain_details.bech32_account_prefix;
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?;
Ok(Self::connect_with_signer(config, client, wallet))
}
}
impl NyxdClient<ReqwestRpcClient, DirectSecp256k1HdWallet> {
#[allow(deprecated)]
impl NyxdClient<crate::ReqwestRpcClient, DirectSecp256k1HdWallet> {
pub fn connect_reqwest_with_mnemonic(
config: Config,
endpoint: Url,
mnemonic: bip39::Mnemonic,
) -> DirectSigningReqwestRpcNyxdClient {
let client = ReqwestRpcClient::new(endpoint);
let client = crate::ReqwestRpcClient::new(endpoint);
let prefix = &config.chain_details.bech32_account_prefix;
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
@@ -391,17 +393,12 @@ where
S: OfflineSigner + Send + Sync,
NyxdError: From<<S as OfflineSigner>::Error>,
{
pub fn signing_account(&self) -> Result<AccountData, NyxdError> {
pub fn signing_account(&self) -> Result<&AccountData, NyxdError> {
Ok(self.find_account(&self.address())?)
}
pub fn address(&self) -> AccountId {
match self.client.signer_addresses() {
Ok(addresses) => addresses[0].clone(),
Err(_) => {
panic!("key derivation failure")
}
}
self.client.signer_addresses()[0].clone()
}
pub fn mix_coin(&self, amount: u128) -> Coin {
@@ -867,7 +864,7 @@ where
{
type Error = S::Error;
fn get_accounts(&self) -> Result<Vec<AccountData>, Self::Error> {
fn get_accounts(&self) -> &[AccountData] {
self.client.get_accounts()
}
@@ -42,12 +42,15 @@ macro_rules! perform_with_compat {
}};
}
// the separate implementation is now completely redundant
#[deprecated(note = "use HttpClient directly instead")]
pub struct ReqwestRpcClient {
compat: CompatMode,
inner: reqwest::Client,
url: Url,
}
#[allow(deprecated)]
impl ReqwestRpcClient {
pub fn new(url: Url) -> Self {
ReqwestRpcClient {
@@ -131,6 +134,7 @@ impl TendermintRpcErrorMap for reqwest::Error {
}
}
#[allow(deprecated)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl TendermintRpcClient for ReqwestRpcClient {
@@ -2,18 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
use crate::signing::signer::{OfflineSigner, SigningError};
use crate::signing::{AccountData, Secp256k1Derivation};
use cosmrs::bip32::{DerivationPath, XPrv};
use cosmrs::crypto::secp256k1::SigningKey;
use cosmrs::crypto::PublicKey;
use crate::signing::{
derive_extended_private_key, derive_keypair, AccountData, Secp256k1Derivation, Secp256k1Keypair,
};
use bip32::XPrv;
use cosmrs::bip32::DerivationPath;
use cosmrs::tx;
use cosmrs::tx::SignDoc;
use nym_config::defaults;
use std::borrow::Cow;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
type Secp256k1Keypair = (SigningKey, PublicKey);
#[derive(Debug, Error)]
pub enum DirectSecp256k1HdWalletError {
#[error(transparent)]
@@ -36,28 +36,22 @@ pub enum DirectSecp256k1HdWalletError {
}
// TODO: maybe lock this one behind feature flag?
#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct DirectSecp256k1HdWallet {
/// Base secret
secret: bip39::Mnemonic,
/// BIP39 seed
seed: [u8; 64],
// An unfortunate result of immature rust async story is that async traits (only available in the separate package)
// can't yet figure out everything and if we stored our derived account data on the struct,
// that would include the secret key which is a dyn EcdsaSigner and hence not Sync making the wallet
// not Sync and if used on the signing client in an async trait, it wouldn't be Send
/// Derivation instructions
/// Derived accounts
#[zeroize(skip)]
accounts: Vec<Secp256k1Derivation>,
// unfortunately `dyn EcdsaSigner` does not guarantee Zeroize
accounts: Vec<AccountData>,
}
impl OfflineSigner for DirectSecp256k1HdWallet {
type Error = DirectSecp256k1HdWalletError;
fn get_accounts(&self) -> Result<Vec<AccountData>, Self::Error> {
self.try_derive_accounts()
fn get_accounts(&self) -> &[AccountData] {
&self.accounts
}
fn sign_direct_with_account(
@@ -77,55 +71,27 @@ impl DirectSecp256k1HdWallet {
}
/// Restores a wallet from the given BIP39 mnemonic using default options.
#[deprecated(
note = "this function can potentially panic if accounts can't be derived correctly. please use .checked_from_mnemonic() instead"
)]
pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self {
// unfortunately due to backwards compatibility requirements,
// we can't change signature of this method
#[allow(deprecated)]
DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic)
}
/// Restores a wallet from the given BIP39 mnemonic using default options.
pub fn checked_from_mnemonic(
prefix: &str,
mnemonic: bip39::Mnemonic,
) -> Result<Self, DirectSecp256k1HdWalletError> {
DirectSecp256k1HdWalletBuilder::new(prefix).try_build(mnemonic)
}
pub fn generate(prefix: &str, word_count: usize) -> Result<Self, DirectSecp256k1HdWalletError> {
let mneomonic = bip39::Mnemonic::generate(word_count)?;
Ok(Self::from_mnemonic(prefix, mneomonic))
}
fn derive_keypair(
&self,
hd_path: &DerivationPath,
) -> Result<Secp256k1Keypair, DirectSecp256k1HdWalletError> {
let extended_private_key = XPrv::derive_from_path(self.seed, hd_path)?;
let private_key: SigningKey = extended_private_key.into();
let public_key = private_key.public_key();
Ok((private_key, public_key))
}
pub fn derive_extended_private_key(
&self,
hd_path: &DerivationPath,
) -> Result<XPrv, DirectSecp256k1HdWalletError> {
Ok(XPrv::derive_from_path(self.seed, hd_path)?)
}
pub fn try_derive_accounts(&self) -> Result<Vec<AccountData>, DirectSecp256k1HdWalletError> {
let mut accounts = Vec::with_capacity(self.accounts.len());
for derivation_info in &self.accounts {
let keypair = self.derive_keypair(&derivation_info.hd_path)?;
// it seems this can only fail if the provided account prefix is invalid
let address = keypair
.1
.account_id(&derivation_info.prefix)
.map_err(
|source| DirectSecp256k1HdWalletError::AccountDerivationError { source },
)?;
accounts.push(AccountData {
address,
public_key: keypair.1,
private_key: keypair.0,
})
}
Ok(accounts)
Self::checked_from_mnemonic(prefix, mneomonic)
}
pub fn secret(&self) -> &bip39::Mnemonic {
@@ -142,6 +108,43 @@ impl DirectSecp256k1HdWallet {
pub fn mnemonic_string(&self) -> Zeroizing<String> {
Zeroizing::new(self.secret.to_string())
}
pub fn account_seed<'a, P: Into<Cow<'a, str>>>(
&self,
bip39_password: P,
) -> Zeroizing<[u8; 64]> {
Zeroizing::new(self.secret.to_seed(bip39_password))
}
/// Derive an extended private key from the stored account secret assuming no bip39 password
#[deprecated(
note = "use derive_extended_private_key_with_password to ensure correct derivation if used bip39 password"
)]
pub fn derive_extended_private_key(
&self,
hd_path: &DerivationPath,
) -> Result<XPrv, DirectSecp256k1HdWalletError> {
let seed = self.account_seed("");
derive_extended_private_key(seed, hd_path)
}
pub fn derive_keypair<'a, P: Into<Cow<'a, str>>>(
&self,
hd_path: &DerivationPath,
bip39_password: P,
) -> Result<Secp256k1Keypair, DirectSecp256k1HdWalletError> {
let seed = self.account_seed(bip39_password);
derive_keypair(seed, hd_path)
}
pub fn derive_extended_private_key_with_password<'a, P: Into<Cow<'a, str>>>(
&self,
hd_path: &DerivationPath,
bip39_password: P,
) -> Result<XPrv, DirectSecp256k1HdWalletError> {
let seed = self.account_seed(bip39_password);
derive_extended_private_key(seed, hd_path)
}
}
#[must_use]
@@ -188,23 +191,39 @@ impl DirectSecp256k1HdWalletBuilder {
self
}
#[deprecated(
note = "this function can potentially panic if accounts can't be derived correctly. please use .try_build() instead"
)]
pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet {
let seed = mnemonic.to_seed(&self.bip39_password);
// unfortunately due to backwards compatibility requirements,
// we can't change signature of this method
#[allow(clippy::expect_used)]
self.try_build(mnemonic)
.expect("account derivation failure")
}
pub fn try_build(
self,
mnemonic: bip39::Mnemonic,
) -> Result<DirectSecp256k1HdWallet, DirectSecp256k1HdWalletError> {
let seed = Zeroizing::new(mnemonic.to_seed(&self.bip39_password));
let prefix = self.prefix.clone();
let accounts = self
.hd_paths
.iter()
.map(|hd_path| Secp256k1Derivation {
hd_path: hd_path.clone(),
prefix: prefix.clone(),
.map(|hd_path| {
Secp256k1Derivation {
hd_path: hd_path.clone(),
prefix: prefix.clone(),
}
.try_derive_account(&seed)
})
.collect();
.collect::<Result<_, _>>()?;
DirectSecp256k1HdWallet {
Ok(DirectSecp256k1HdWallet {
accounts,
seed,
secret: mnemonic,
}
})
}
}
@@ -215,7 +234,7 @@ mod tests {
use super::*;
#[test]
fn generating_account_addresses() {
fn generating_account_addresses() -> anyhow::Result<()> {
// test vectors produced from our js wallet
let mnemonics = ["crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove",
"acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel",
@@ -230,11 +249,10 @@ mod tests {
"n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf",
];
for (idx, mnemonic) in mnemonics.iter().enumerate() {
let wallet = DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap());
assert_eq!(
wallet.try_derive_accounts().unwrap()[0].address,
addrs[idx].parse().unwrap()
)
let wallet =
DirectSecp256k1HdWallet::checked_from_mnemonic(&prefix, mnemonic.parse()?)?;
assert_eq!(wallet.signer_addresses()[0], addrs[idx].parse().unwrap());
}
Ok(())
}
}
@@ -1,6 +1,8 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::signing::direct_wallet::DirectSecp256k1HdWalletError;
use bip32::XPrv;
use cosmrs::bip32::DerivationPath;
use cosmrs::crypto::secp256k1::SigningKey;
use cosmrs::crypto::PublicKey;
@@ -12,14 +14,64 @@ pub mod direct_wallet;
pub mod signer;
pub mod tx_signer;
pub(crate) type Secp256k1Keypair = (SigningKey, PublicKey);
/// Derivation information required to derive a keypair and an address from a mnemonic.
#[derive(Debug, Clone)]
struct Secp256k1Derivation {
pub(crate) struct Secp256k1Derivation {
hd_path: DerivationPath,
prefix: String,
}
// TODO: is this struct going to be derivable with other signer types?
impl Secp256k1Derivation {
pub(crate) fn try_derive_account<S>(
&self,
seed: S,
) -> Result<AccountData, DirectSecp256k1HdWalletError>
where
S: AsRef<[u8]>,
{
let keypair = derive_keypair(seed, &self.hd_path)?;
// it seems this can only fail if the provided account prefix is invalid
let address = keypair
.1
.account_id(&self.prefix)
.map_err(|source| DirectSecp256k1HdWalletError::AccountDerivationError { source })?;
Ok(AccountData {
address,
public_key: keypair.1,
private_key: keypair.0,
})
}
}
pub fn derive_keypair<S>(
seed: S,
hd_path: &DerivationPath,
) -> Result<Secp256k1Keypair, DirectSecp256k1HdWalletError>
where
S: AsRef<[u8]>,
{
let extended_private_key = derive_extended_private_key(seed, hd_path)?;
let private_key: SigningKey = extended_private_key.into();
let public_key = private_key.public_key();
Ok((private_key, public_key))
}
pub fn derive_extended_private_key<S>(
seed: S,
hd_path: &DerivationPath,
) -> Result<XPrv, DirectSecp256k1HdWalletError>
where
S: AsRef<[u8]>,
{
Ok(XPrv::derive_from_path(seed, hd_path)?)
}
pub struct AccountData {
pub address: AccountId,
@@ -33,23 +33,18 @@ pub enum SignerType {
pub trait OfflineSigner {
type Error: From<SigningError>;
// I really dislike existence of this function because it makes you re-derive your key **twice** for each contract transaction
fn signer_addresses(&self) -> Result<Vec<AccountId>, Self::Error> {
let derived_addresses = self
.get_accounts()?
.into_iter()
.map(|account| account.address)
.collect();
Ok(derived_addresses)
fn signer_addresses(&self) -> Vec<AccountId> {
self.get_accounts()
.iter()
.map(|account| account.address.clone())
.collect()
}
fn get_accounts(&self) -> Result<Vec<AccountData>, Self::Error>;
fn get_accounts(&self) -> &[AccountData];
fn find_account(&self, signer_address: &AccountId) -> Result<AccountData, Self::Error> {
// TODO: we could really use some zeroize action here
let accounts = self.get_accounts()?;
accounts
.into_iter()
fn find_account(&self, signer_address: &AccountId) -> Result<&AccountData, Self::Error> {
self.get_accounts()
.iter()
.find(|account| &account.address == signer_address)
.ok_or_else(|| {
SigningError::AccountNotFound {
@@ -76,7 +71,7 @@ pub trait OfflineSigner {
message: M,
) -> Result<Signature, Self::Error> {
let signer = self.find_account(signer_address)?;
self.sign_raw_with_account(&signer, message)
self.sign_raw_with_account(signer, message)
}
fn sign_direct(
@@ -85,7 +80,7 @@ pub trait OfflineSigner {
sign_doc: SignDoc,
) -> Result<tx::Raw, Self::Error> {
let signer = self.find_account(signer_address)?;
self.sign_direct_with_account(&signer, sign_doc)
self.sign_direct_with_account(signer, sign_doc)
}
// unless explicitly defined, each signing method is unsupported
@@ -122,7 +117,7 @@ pub struct NoSigner;
// impl OfflineSigner for NoSigner {
// type Error = SignerUnavailable;
//
// fn get_accounts(&self) -> Result<Vec<AccountData>, Self::Error> {
// fn get_accounts(&self) -> &[AccountData] {
// return Err(SignerUnavailable);
// }
// }
@@ -50,7 +50,7 @@ pub trait TxSigner: OfflineSigner {
)
.map_err(|source| SigningError::SignDocFailure { source })?;
self.sign_direct_with_account(&account_from_signer, sign_doc)
self.sign_direct_with_account(account_from_signer, sign_doc)
}
}
@@ -3,6 +3,7 @@
use clap::Parser;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::signer::OfflineSigner;
#[derive(Debug, Parser)]
pub struct Args {
@@ -15,9 +16,10 @@ pub fn create_account(args: Args, prefix: &str) {
let word_count = args.word_count.unwrap_or(24);
let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!");
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)
.expect("failed to derive accounts!");
// Output address and mnemonics into separate lines for easier parsing
println!("{}", wallet.mnemonic_string().as_str());
println!("{}", wallet.try_derive_accounts().unwrap()[0].address());
println!("{}", wallet.signer_addresses()[0]);
}
+15 -16
View File
@@ -1,13 +1,13 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::QueryClient;
use crate::utils::show_error;
use clap::Parser;
use log::{error, info};
use nym_validator_client::nyxd::{AccountId, CosmWasmClient};
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use crate::context::QueryClient;
use crate::utils::show_error;
use nym_validator_client::signing::signer::OfflineSigner;
#[derive(Debug, Parser)]
pub struct Args {
@@ -50,20 +50,19 @@ pub async fn get_pubkey(
}
pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip39::Mnemonic) {
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
match wallet.try_derive_accounts() {
Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) {
Some(account) => {
println!("{}", account.public_key().to_string());
}
None => {
error!("Could not derive key that matches {address}")
}
},
Err(e) => {
error!("Failed to derive accounts. {e}");
let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) {
Ok(wallet) => wallet,
Err(err) => {
error!("Failed to derive accounts. {err}");
return;
}
}
};
let Ok(account) = wallet.find_account(&address) else {
error!("Could not derive key that matches {address}");
return;
};
println!("{}", account.public_key().to_string());
}
pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) {
+27 -24
View File
@@ -36,32 +36,35 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) {
return;
}
let wallet =
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set"));
match wallet.try_derive_accounts() {
Ok(accounts) => match accounts.first() {
Some(account) => {
let msg = args.message.into_bytes();
match wallet.sign_raw_with_account(account, msg) {
Ok(signature) => {
let output = SignatureOutputJson {
account_id: account.address().to_string(),
public_key: account.public_key(),
signature_as_hex: signature.to_string(),
};
println!("{}", json!(output));
}
Err(e) => {
error!("Failed to sign message. {e}");
}
let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic(
prefix,
mnemonic.expect("mnemonic not set"),
) {
Ok(wallet) => wallet,
Err(err) => {
error!("Could not derive an account key from the mnemonic: {err}");
return;
}
};
match wallet.get_accounts().first() {
Some(account) => {
let msg = args.message.into_bytes();
match wallet.sign_raw_with_account(account, msg) {
Ok(signature) => {
let output = SignatureOutputJson {
account_id: account.address().to_string(),
public_key: account.public_key(),
signature_as_hex: signature.to_string(),
};
println!("{}", json!(output));
}
Err(e) => {
error!("Failed to sign message. {e}");
}
}
None => {
error!("Could not derive an account key from the mnemonic",)
}
},
Err(e) => {
error!("Failed to derive accounts. {e}");
}
None => {
error!("Could not derive an account key from the mnemonic",)
}
}
}
@@ -1,73 +0,0 @@
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError};
use nym_sphinx::DestinationAddressBytes;
use thiserror::Error;
/// Replacement for what used to be an `AuthToken`.
///
/// Replacement for what used to be an `AuthToken`. We used to be generating an `AuthToken` based on
/// local secret and remote address in order to allow for authentication. Due to changes in registration
/// and the fact we are deriving a shared key, we are encrypting remote's address with the previously
/// derived shared key. If the value is as expected, then authentication is successful.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
// this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag)
pub struct EncryptedAddressBytes(Vec<u8>);
impl From<Vec<u8>> for EncryptedAddressBytes {
fn from(encrypted_address: Vec<u8>) -> Self {
EncryptedAddressBytes(encrypted_address)
}
}
#[derive(Debug, Error)]
pub enum EncryptedAddressConversionError {
#[error("Failed to decode the encrypted address - {0}")]
DecodeError(#[from] bs58::decode::Error),
}
impl EncryptedAddressBytes {
pub fn new(
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> Result<Self, SharedKeyUsageError> {
let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?;
Ok(EncryptedAddressBytes(ciphertext))
}
pub fn verify(
&self,
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> bool {
let Ok(reconstructed) = Self::new(address, key, nonce) else {
return false;
};
self == &reconstructed
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, EncryptedAddressConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
Ok(EncryptedAddressBytes(decoded))
}
pub fn to_base58_string(self) -> String {
bs58::encode(self.0).into_string()
}
}
impl From<EncryptedAddressBytes> for String {
fn from(val: EncryptedAddressBytes) -> Self {
val.to_base58_string()
}
}
@@ -1,4 +0,0 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod encrypted_address;
+2 -7
View File
@@ -7,17 +7,12 @@ use nym_sphinx::params::GatewayIntegrityHmacAlgorithm;
pub use types::*;
pub mod authentication;
pub mod models;
pub mod registration;
pub mod shared_key;
pub mod types;
pub use shared_key::helpers::SymmetricKey;
pub use shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys};
pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub use shared_key::{SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey};
pub type GatewayProtocolVersion = u8;
@@ -29,7 +24,7 @@ pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSIO
// 1 - initial release
// 2 - changes to client credentials structure
// 3 - change to AES-GCM-SIV and non-zero IVs
// 4 - introduction of v2 authentication protocol to prevent reply attacks
// 4 - introduction of v2 authentication protocol to prevent replay attacks
// 5 - add key rotation information to the serialised mix packet
// 6 - support for 'upgrade mode'
pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1;
@@ -5,7 +5,7 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExch
use crate::registration::handshake::state::State;
use crate::registration::handshake::HandshakeResult;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION};
use crate::GatewayProtocolVersionExt;
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
use tracing::info;
@@ -18,11 +18,11 @@ impl<S, R> State<'_, S, R> {
R: CryptoRng + RngCore,
{
// 1. if we're using non-legacy, i.e. aes256gcm-siv derivation, generate initiator salt for kdf
let maybe_hkdf_salt = self.maybe_generate_initiator_salt();
let hkdf_salt = self.generate_initiator_salt();
// 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a hkdf salt if we're using non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
let init_message = self.init_message(maybe_hkdf_salt.clone());
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT
let init_message = self.init_message(hkdf_salt.clone());
self.send_handshake_data(init_message).await?;
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
@@ -33,23 +33,20 @@ impl<S, R> State<'_, S, R> {
// NEGOTIATE PROTOCOL
if gateway_protocol.is_future_version() {
// SAFETY: future version means it's greater than CURRENT, which is always a `Some`
#[allow(clippy::unwrap_used)]
return Err(HandshakeError::UnsupportedProtocol {
version: gateway_protocol.unwrap(),
version: gateway_protocol,
});
}
let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION);
// that should never happen, but we're fine with that outcome
if Some(gateway_protocol) != self.proposed_protocol_version() {
if gateway_protocol != self.proposed_protocol_version() {
info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version());
self.set_protocol_version(gateway_protocol);
}
// 3. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
self.derive_shared_key(&mid_res.ephemeral_dh, &hkdf_salt);
// 4. verify the received signature using the locally derived keys
self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?;
@@ -56,10 +56,7 @@ impl<S, R> State<'_, S, R> {
// 2. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(
&init_message.ephemeral_dh,
init_message.initiator_salt.as_deref(),
);
self.derive_shared_key(&init_message.ephemeral_dh, &init_message.initiator_salt);
// 3. send ephemeral x25519 pubkey alongside the encrypted signature
// g^y || AES(k, sig(gate_priv, (g^y || g^x))
@@ -4,7 +4,7 @@
use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::KDF_SALT_LENGTH;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::{nonce_size, tag_size};
use nym_crypto::symmetric::aead::{nonce_size, tag_size, Nonce};
use nym_sphinx::params::GatewayEncryptionAlgorithm;
// it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason,
@@ -21,13 +21,13 @@ pub trait HandshakeMessage {
pub struct Initialisation {
pub identity: ed25519::PublicKey,
pub ephemeral_dh: x25519::PublicKey,
pub initiator_salt: Option<Vec<u8>>,
pub initiator_salt: Vec<u8>,
}
#[derive(Debug)]
pub struct MaterialExchange {
pub signature_ciphertext: Vec<u8>,
pub nonce: Option<Vec<u8>>,
pub nonce: Nonce<GatewayEncryptionAlgorithm>,
}
impl MaterialExchange {
@@ -61,21 +61,16 @@ impl Finalization {
}
impl HandshakeMessage for Initialisation {
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT
// Eventually the ID_PUBKEY prefix will get removed and recipient will know
// initializer's identity from another source.
fn into_bytes(self) -> Vec<u8> {
let bytes = self
.identity
self.identity
.to_bytes()
.into_iter()
.chain(self.ephemeral_dh.to_bytes());
if let Some(salt) = self.initiator_salt {
bytes.chain(salt).collect()
} else {
bytes.collect()
}
.chain(self.ephemeral_dh.to_bytes())
.chain(self.initiator_salt)
.collect()
}
// this will need to be adjusted when REMOTE_ID_PUBKEY is removed
@@ -83,9 +78,8 @@ impl HandshakeMessage for Initialisation {
where
Self: Sized,
{
let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE;
let current_len = legacy_len + KDF_SALT_LENGTH;
if bytes.len() != legacy_len && bytes.len() != current_len {
let current_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE + KDF_SALT_LENGTH;
if bytes.len() != current_len {
return Err(HandshakeError::MalformedRequest);
}
@@ -95,14 +89,13 @@ impl HandshakeMessage for Initialisation {
// SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// which is impossible
#[allow(clippy::unwrap_used)]
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
let ephemeral_dh = x25519::PublicKey::from_bytes(
&bytes
[ed25519::PUBLIC_KEY_LENGTH..ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE],
)
.unwrap();
let initiator_salt = if bytes.len() == legacy_len {
None
} else {
Some(bytes[legacy_len..].to_vec())
};
let initiator_salt = bytes[ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE..].to_vec();
Ok(Initialisation {
identity,
@@ -115,43 +108,31 @@ impl HandshakeMessage for Initialisation {
impl HandshakeMessage for MaterialExchange {
// AES(k, SIG(PRIV_GATE, G^y || G^x))
fn into_bytes(self) -> Vec<u8> {
if let Some(nonce) = self.nonce {
self.signature_ciphertext
.iter()
.cloned()
.chain(nonce)
.collect()
} else {
self.signature_ciphertext.to_vec()
}
self.signature_ciphertext
.iter()
.cloned()
.chain(self.nonce)
.collect()
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
// we expect to receive either:
// LEGACY: ed25519 signature ciphertext (64 bytes)
// CURRENT: ed25519 signature ciphertext (+ tag) + AES256-GCM-SIV nonce (76 bytes)
let legacy_len = ed25519::SIGNATURE_LENGTH;
let current_len = legacy_len
let current_len = ed25519::SIGNATURE_LENGTH
+ tag_size::<GatewayEncryptionAlgorithm>()
+ nonce_size::<GatewayEncryptionAlgorithm>();
if bytes.len() != legacy_len && bytes.len() != current_len {
if bytes.len() != current_len {
return Err(HandshakeError::MalformedResponse);
}
let (signature_ciphertext, nonce) = if bytes.len() == current_len {
let ciphertext_len =
ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
(
bytes[..ciphertext_len].to_vec(),
Some(bytes[ciphertext_len..].to_vec()),
)
} else {
(bytes.to_vec(), None)
};
let ciphertext_len = ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
let signature_ciphertext = bytes[..ciphertext_len].to_vec();
// SAFETY: we know the bytes have correct length
let nonce = Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(&bytes[ciphertext_len..]);
Ok(MaterialExchange {
signature_ciphertext,
@@ -3,7 +3,7 @@
use self::error::HandshakeError;
use crate::registration::handshake::state::State;
use crate::{GatewayProtocolVersion, SharedGatewayKey};
use crate::{GatewayProtocolVersion, SharedSymmetricKey};
use futures::future::BoxFuture;
use futures::{Sink, Stream};
use nym_crypto::asymmetric::ed25519;
@@ -48,7 +48,7 @@ impl Future for GatewayHandshake<'_> {
#[derive(Debug, PartialEq)]
pub struct HandshakeResult {
pub negotiated_protocol: GatewayProtocolVersion,
pub derived_key: SharedGatewayKey,
pub derived_key: SharedSymmetricKey,
}
pub fn client_handshake<'a, S, R>(
@@ -56,7 +56,7 @@ pub fn client_handshake<'a, S, R>(
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
gateway_pubkey: ed25519::PublicKey,
gateway_protocol: Option<GatewayProtocolVersion>,
gateway_protocol: GatewayProtocolVersion,
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
) -> GatewayHandshake<'a>
where
@@ -84,7 +84,7 @@ pub fn gateway_handshake<'a, S, R>(
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
received_init_payload: Vec<u8>,
requested_client_protocol: Option<GatewayProtocolVersion>,
requested_client_protocol: GatewayProtocolVersion,
shutdown_token: ShutdownToken,
) -> GatewayHandshake<'a>
where
@@ -125,7 +125,7 @@ DONE(status)
#[cfg(test)]
mod tests {
use super::*;
use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION};
use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION};
use anyhow::{bail, Context};
use futures::StreamExt;
use nym_test_utils::helpers::u64_seeded_rng;
@@ -221,7 +221,7 @@ mod tests {
client.socket,
client.keys,
*gateway.keys.public_key(),
Some(CURRENT_PROTOCOL_VERSION),
CURRENT_PROTOCOL_VERSION,
ShutdownToken::default(),
);
@@ -235,7 +235,7 @@ mod tests {
gateway.socket,
gateway.keys,
init_msg,
Some(CURRENT_PROTOCOL_VERSION),
CURRENT_PROTOCOL_VERSION,
ShutdownToken::default(),
);
@@ -261,7 +261,7 @@ mod tests {
client.socket,
client.keys,
*gateway.keys.public_key(),
Some(CURRENT_PROTOCOL_VERSION + 42),
CURRENT_PROTOCOL_VERSION + 42,
ShutdownToken::default(),
);
@@ -274,7 +274,7 @@ mod tests {
gateway.socket,
gateway.keys,
init_msg,
Some(CURRENT_PROTOCOL_VERSION + 42),
CURRENT_PROTOCOL_VERSION + 42,
ShutdownToken::default(),
);
@@ -292,46 +292,4 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn protocol_upgrade() -> anyhow::Result<()> {
let (client, gateway) = setup();
let handshake_client = client_handshake(
client.rng,
client.socket,
client.keys,
*gateway.keys.public_key(),
None,
ShutdownToken::default(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let init_msg = gateway.socket.get_handshake_init_data().await?;
let handshake_gateway = gateway_handshake(
gateway.rng,
gateway.socket,
gateway.keys,
init_msg,
None,
ShutdownToken::default(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_res = client???;
let gateway_res = gateway???;
// ensure the created keys are the same
assert_eq!(client_res, gateway_res);
// and the protocol got upgraded to the first known version
assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION);
Ok(())
}
}
@@ -5,12 +5,11 @@ use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
use crate::shared_key::SharedKeySize;
use crate::{
types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize,
LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION,
use crate::registration::handshake::{
HandshakeResult, SharedSymmetricKey, WsItem, KDF_SALT_LENGTH,
};
use crate::shared_key::SharedKeySize;
use crate::{types, GatewayProtocolVersion};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::random_nonce;
@@ -48,17 +47,15 @@ pub(crate) struct State<'a, S, R> {
ephemeral_keypair: x25519::KeyPair,
/// The derived shared key using the ephemeral keys of both parties.
derived_shared_keys: Option<SharedGatewayKey>,
derived_shared_keys: Option<SharedSymmetricKey>,
/// The known or received public identity key of the remote.
/// Ideally it would always be known before the handshake was initiated.
remote_pubkey: Option<ed25519::PublicKey>,
/// Version of the protocol to use during the handshake that also implicitly specifies
/// additional features such as the type of derived shared keys, i.e.
/// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
/// the above is decided by whether the specified protocol version supports the new variant or not.
protocol_version: Option<GatewayProtocolVersion>,
/// additional features
protocol_version: GatewayProtocolVersion,
// channel to receive shutdown signal
#[cfg(not(target_arch = "wasm32"))]
@@ -71,7 +68,7 @@ impl<'a, S, R> State<'a, S, R> {
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
remote_pubkey: Option<ed25519::PublicKey>,
protocol_version: Option<GatewayProtocolVersion>,
protocol_version: GatewayProtocolVersion,
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
) -> Self
where
@@ -96,31 +93,27 @@ impl<'a, S, R> State<'a, S, R> {
self.ephemeral_keypair.public_key()
}
pub(crate) fn proposed_protocol_version(&self) -> Option<GatewayProtocolVersion> {
pub(crate) fn proposed_protocol_version(&self) -> GatewayProtocolVersion {
self.protocol_version
}
pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) {
self.protocol_version = Some(protocol_version);
self.protocol_version = protocol_version;
}
pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option<Vec<u8>>
pub(crate) fn generate_initiator_salt(&mut self) -> Vec<u8>
where
R: CryptoRng + RngCore,
{
if self.protocol_version.supports_aes256_gcm_siv() {
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
Some(salt)
} else {
None
}
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
salt
}
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT
// Eventually the ID_PUBKEY prefix will get removed and recipient will know
// initializer's identity from another source.
pub(crate) fn init_message(&self, initiator_salt: Option<Vec<u8>>) -> Initialisation {
pub(crate) fn init_message(&self, initiator_salt: Vec<u8>) -> Initialisation {
Initialisation {
identity: *self.identity.public_key(),
ephemeral_dh: *self.ephemeral_keypair.public_key(),
@@ -138,23 +131,19 @@ impl<'a, S, R> State<'a, S, R> {
pub(crate) fn derive_shared_key(
&mut self,
remote_ephemeral_key: &x25519::PublicKey,
initiator_salt: Option<&[u8]>,
initiator_salt: &[u8],
) {
let dh_result = self
.ephemeral_keypair
.private_key()
.diffie_hellman(remote_ephemeral_key);
let key_size = if self.protocol_version.supports_aes256_gcm_siv() {
SharedKeySize::to_usize()
} else {
LegacySharedKeySize::to_usize()
};
let key_size = SharedKeySize::to_usize();
// SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
initiator_salt,
Some(initiator_salt),
&dh_result,
None,
key_size,
@@ -162,17 +151,10 @@ impl<'a, S, R> State<'a, S, R> {
.expect("somehow too long okm was provided");
// SAFETY: the okm has been expanded to the length expected by the corresponding keys
let shared_key = if self.protocol_version.supports_aes256_gcm_siv() {
#[allow(clippy::expect_used)]
let current_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Current(current_key)
} else {
#[allow(clippy::expect_used)]
let legacy_key = LegacySharedKeys::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Legacy(legacy_key)
};
#[allow(clippy::expect_used)]
let shared_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
self.derived_shared_keys = Some(shared_key)
}
@@ -191,12 +173,8 @@ impl<'a, S, R> State<'a, S, R> {
.collect();
let signature = self.identity.private_key().sign(plaintext);
let nonce = if self.protocol_version.supports_aes256_gcm_siv() {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
} else {
None
};
let mut rng = thread_rng();
let nonce = random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng);
// SAFETY: this function is only called after the local key has already been derived
#[allow(clippy::expect_used)]
@@ -204,7 +182,7 @@ impl<'a, S, R> State<'a, S, R> {
.derived_shared_keys
.as_ref()
.expect("shared key was not derived!")
.encrypt_naive(&signature.to_bytes(), nonce.as_deref())?;
.encrypt(&signature.to_bytes(), &nonce)?;
Ok(MaterialExchange {
signature_ciphertext,
@@ -224,15 +202,10 @@ impl<'a, S, R> State<'a, S, R> {
.as_ref()
.expect("shared key was not derived!");
// if the [client] init message contained non-legacy flag, the associated nonce MUST be present
if self.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() {
return Err(HandshakeError::MissingNonceForCurrentKey);
}
// first decrypt received data
let decrypted_signature = derived_shared_key.decrypt_naive(
let decrypted_signature = derived_shared_key.decrypt(
&remote_response.signature_ciphertext,
remote_response.nonce.as_deref(),
&remote_response.nonce,
)?;
// now verify signature itself
@@ -262,7 +235,7 @@ impl<'a, S, R> State<'a, S, R> {
#[allow(clippy::complexity)]
fn on_wg_msg(
msg: Option<WsItem>,
) -> Result<Option<(Vec<u8>, Option<GatewayProtocolVersion>)>, HandshakeError> {
) -> Result<Option<(Vec<u8>, GatewayProtocolVersion)>, HandshakeError> {
let Some(msg) = msg else {
return Err(HandshakeError::ClosedStream);
};
@@ -303,7 +276,7 @@ impl<'a, S, R> State<'a, S, R> {
#[cfg(not(target_arch = "wasm32"))]
async fn _receive_handshake_message_bytes(
&mut self,
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
) -> Result<(Vec<u8>, GatewayProtocolVersion), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -324,7 +297,7 @@ impl<'a, S, R> State<'a, S, R> {
#[cfg(target_arch = "wasm32")]
async fn _receive_handshake_message_bytes(
&mut self,
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
) -> Result<(Vec<u8>, GatewayProtocolVersion), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -339,7 +312,7 @@ impl<'a, S, R> State<'a, S, R> {
pub(crate) async fn receive_handshake_message<M>(
&mut self,
) -> Result<(M, Option<GatewayProtocolVersion>), HandshakeError>
) -> Result<(M, GatewayProtocolVersion), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
M: HandshakeMessage,
@@ -396,9 +369,7 @@ impl<'a, S, R> State<'a, S, R> {
// SAFETY: handshake can't be finalised without deriving the shared keys
#[allow(clippy::unwrap_used)]
HandshakeResult {
negotiated_protocol: self
.proposed_protocol_version()
.unwrap_or(INITIAL_PROTOCOL_VERSION),
negotiated_protocol: self.proposed_protocol_version(),
derived_key: self.derived_shared_keys.unwrap(),
}
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::blake3;
use nym_crypto::crypto_hash::compute_digest;
use nym_crypto::generic_array::{typenum::Unsigned, GenericArray};
use nym_crypto::symmetric::aead::{
self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce,
};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::GatewayEncryptionAlgorithm;
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub type SharedKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, Error)]
pub enum SharedKeyUsageError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("the provided nonce (or legacy IV) did not have the expected length")]
MalformedNonce,
#[error("did not provide a valid nonce for aead encryption")]
MissingAeadNonce,
#[error("failed to either encrypt or decrypt provided message")]
AeadFailure(#[from] AeadError),
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SharedSymmetricKey(AeadKey<GatewayEncryptionAlgorithm>);
type KeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, Clone, Copy, Error)]
pub enum SharedKeyConversionError {
#[error("the string representation of the shared key was malformed: {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error(
"the received shared keys had invalid size. Got: {received}, but expected: {expected}"
)]
InvalidSharedKeysSize { received: usize, expected: usize },
}
impl SharedSymmetricKey {
pub fn random_nonce(&self) -> Nonce<GatewayEncryptionAlgorithm> {
let mut rng = thread_rng();
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng)
}
pub fn nonce_size(&self) -> usize {
nonce_size::<GatewayEncryptionAlgorithm>()
}
pub fn validate_aead_nonce(
raw: &[u8],
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != KeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: KeySize::to_usize(),
});
}
Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes)))
}
pub fn zeroizing_clone(&self) -> Zeroizing<Self> {
Zeroizing::new(SharedSymmetricKey(self.0))
}
pub fn digest(&self) -> Vec<u8> {
compute_digest::<blake3::Hasher>(self.as_bytes()).to_vec()
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
pub fn to_bytes(&self) -> Vec<u8> {
self.0.iter().copied().collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let bs58_str = Zeroizing::new(val.into());
let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?);
Self::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
let bytes = Zeroizing::new(self.to_bytes());
bs58::encode(bytes).into_string()
}
pub fn encrypt(
&self,
plaintext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::encrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, plaintext).map_err(Into::into)
}
pub fn decrypt(
&self,
ciphertext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::decrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, ciphertext).map_err(Into::into)
}
}
impl PemStorableKey for SharedSymmetricKey {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
"AES-256-GCM-SIV GATEWAY SHARED KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
@@ -1,98 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LegacySharedKeys, SharedGatewayKey, SharedKeyUsageError, SharedSymmetricKey};
use nym_crypto::symmetric::aead::random_nonce;
use nym_crypto::symmetric::stream_cipher::random_iv;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use rand::thread_rng;
pub trait SymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8>;
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
}
impl SymmetricKey for SharedGatewayKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
self.random_nonce_or_iv()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.encrypt(plaintext, nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.decrypt(ciphertext, nonce)
}
}
impl SymmetricKey for SharedSymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.encrypt(plaintext, &nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.decrypt(ciphertext, &nonce)
}
}
impl SymmetricKey for LegacySharedKeys {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
Ok(self.encrypt_and_tag(plaintext, iv))
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
self.decrypt_tagged(ciphertext, iv)
}
}
@@ -1,246 +0,0 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::KDF_SALT_LENGTH;
use crate::shared_key::SharedSymmetricKey;
use crate::shared_key::{SharedKeyConversionError, SharedKeySize, SharedKeyUsageError};
use crate::LegacyGatewayMacSize;
use nym_crypto::generic_array::{
typenum::{Sum, Unsigned, U16},
GenericArray,
};
use nym_crypto::hkdf;
use nym_crypto::hmac::{compute_keyed_hmac, recompute_keyed_hmac_and_verify_tag};
use nym_crypto::symmetric::stream_cipher::{self, CipherKey, KeySizeUser, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{
GatewayIntegrityHmacAlgorithm, GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm,
};
use rand::{thread_rng, RngCore};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
// shared key is as long as the encryption key and the MAC key combined.
pub type LegacySharedKeySize = Sum<EncryptionKeySize, MacKeySize>;
// we're using 16 byte long key in sphinx, so let's use the same one here
type MacKeySize = U16;
type EncryptionKeySize = <LegacyGatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
/// Shared key used when computing MAC for messages exchanged between client and its gateway.
pub type MacKey = GenericArray<u8, MacKeySize>;
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct LegacySharedKeys {
encryption_key: CipherKey<LegacyGatewayEncryptionAlgorithm>,
mac_key: MacKey,
}
impl LegacySharedKeys {
pub fn upgrade(&self) -> (SharedSymmetricKey, Vec<u8>) {
let mut rng = thread_rng();
let mut salt = vec![0u8; KDF_SALT_LENGTH];
rng.fill_bytes(&mut salt);
let legacy_bytes = Zeroizing::new(self.to_bytes());
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(&salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
#[allow(clippy::expect_used)]
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
(key, salt)
}
pub fn upgrade_verify(
&self,
salt: &[u8],
expected_digest: &[u8],
) -> Option<SharedSymmetricKey> {
let legacy_bytes = Zeroizing::new(self.to_bytes());
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
#[allow(clippy::expect_used)]
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
if key.digest() != expected_digest {
// no need to zeroize that key since it's malformed and we won't be using it anyway
None
} else {
Some(key)
}
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != LegacySharedKeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: LegacySharedKeySize::to_usize(),
});
}
let encryption_key =
GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]);
let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]);
Ok(LegacySharedKeys {
encryption_key,
mac_key,
})
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given.
/// It does **NOT** attach any integrity macs on the produced ciphertext
pub fn encrypt_without_tagging(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
match iv {
Some(iv) => stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
data,
),
None => {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
&zero_iv,
data,
)
}
}
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given. Then it computes an integrity mac and concatenates it
/// with the previously produced ciphertext.
pub fn encrypt_and_tag(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
let ciphertext = self.encrypt_without_tagging(data, iv);
let mac = compute_keyed_hmac::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
&ciphertext,
);
mac.into_bytes().into_iter().chain(ciphertext).collect()
}
pub fn decrypt_without_tag(
&self,
ciphertext: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
Ok(stream_cipher::decrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
ciphertext,
))
}
pub fn decrypt_tagged(
&self,
enc_data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let mac_size = LegacyGatewayMacSize::to_usize();
if enc_data.len() < mac_size {
return Err(SharedKeyUsageError::TooShortRequest);
}
let mac_tag = &enc_data[..mac_size];
let message_bytes = &enc_data[mac_size..];
if !recompute_keyed_hmac_and_verify_tag::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
message_bytes,
mac_tag,
) {
return Err(SharedKeyUsageError::InvalidMac);
}
// couldn't have made the first borrow mutable as you can't have an immutable borrow
// together with a mutable one
let mut message_bytes_mut = message_bytes.to_vec();
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
stream_cipher::decrypt_in_place::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
&mut message_bytes_mut,
);
Ok(message_bytes_mut)
}
pub fn encryption_key(&self) -> &CipherKey<LegacyGatewayEncryptionAlgorithm> {
&self.encryption_key
}
pub fn mac_key(&self) -> &MacKey {
&self.mac_key
}
pub fn to_bytes(&self) -> Vec<u8> {
self.encryption_key
.iter()
.copied()
.chain(self.mac_key.iter().copied())
.collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
LegacySharedKeys::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}
impl From<LegacySharedKeys> for String {
fn from(keys: LegacySharedKeys) -> Self {
keys.to_base58_string()
}
}
impl PemStorableKey for LegacySharedKeys {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
// TODO: If common\nymsphinx\params\src\lib::GatewayIntegrityHmacAlgorithm changes
// the pem type needs updating!
"AES-128-CTR + HMAC-BLAKE3 GATEWAY SHARED KEYS"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
@@ -1,306 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::blake3;
use nym_crypto::crypto_hash::compute_digest;
use nym_crypto::generic_array::{typenum::Unsigned, GenericArray};
use nym_crypto::symmetric::aead::{
self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce,
};
use nym_crypto::symmetric::stream_cipher::{iv_size, random_iv, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub use legacy::LegacySharedKeys;
pub mod helpers;
pub mod legacy;
pub type SharedKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)]
pub enum SharedGatewayKey {
Current(SharedSymmetricKey),
Legacy(LegacySharedKeys),
}
impl SharedGatewayKey {
pub fn is_legacy(&self) -> bool {
matches!(self, SharedGatewayKey::Legacy(..))
}
pub fn aes128_ctr_hmac_bs58(&self) -> Option<Zeroizing<String>> {
match self {
SharedGatewayKey::Current(_) => None,
SharedGatewayKey::Legacy(key) => Some(Zeroizing::new(key.to_base58_string())),
}
}
pub fn aes256_gcm_siv(&self) -> Option<Zeroizing<Vec<u8>>> {
match self {
SharedGatewayKey::Current(key) => Some(Zeroizing::new(key.to_bytes())),
SharedGatewayKey::Legacy(_) => None,
}
}
// it is responsibility of the caller to ensure the correct variant is present
#[allow(clippy::panic)]
pub fn unwrap_legacy(&self) -> &LegacySharedKeys {
match self {
SharedGatewayKey::Current(_) => panic!("expected legacy key"),
SharedGatewayKey::Legacy(key) => key,
}
}
pub fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
if self.is_legacy() {
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
} else {
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
}
pub fn random_nonce_or_zero_iv(&self) -> Option<Vec<u8>> {
if self.is_legacy() {
None
} else {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
}
}
pub fn nonce_size(&self) -> usize {
match self {
SharedGatewayKey::Current(_) => nonce_size::<GatewayEncryptionAlgorithm>(),
SharedGatewayKey::Legacy(_) => iv_size::<LegacyGatewayEncryptionAlgorithm>(),
}
}
}
impl From<LegacySharedKeys> for SharedGatewayKey {
fn from(keys: LegacySharedKeys) -> Self {
SharedGatewayKey::Legacy(keys)
}
}
impl From<SharedSymmetricKey> for SharedGatewayKey {
fn from(keys: SharedSymmetricKey) -> Self {
SharedGatewayKey::Current(keys)
}
}
#[derive(Debug, Error)]
pub enum SharedKeyUsageError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("the provided nonce (or legacy IV) did not have the expected length")]
MalformedNonce,
#[error("did not provide a valid nonce for aead encryption")]
MissingAeadNonce,
#[error("failed to either encrypt or decrypt provided message")]
AeadFailure(#[from] AeadError),
}
impl SharedGatewayKey {
fn validate_aead_nonce(
raw: Option<&[u8]>,
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
let Some(raw) = raw else {
return Err(SharedKeyUsageError::MissingAeadNonce);
};
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
}
fn validate_cipher_iv(
raw: Option<&[u8]>,
) -> Result<Option<&IV<LegacyGatewayEncryptionAlgorithm>>, SharedKeyUsageError> {
let Some(raw) = raw else { return Ok(None) };
let iv = if raw.is_empty() {
None
} else {
if raw.len() != iv_size::<LegacyGatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Some(IV::<LegacyGatewayEncryptionAlgorithm>::from_slice(raw))
};
Ok(iv)
}
pub fn encrypt(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_and_tag(plaintext, iv))
}
}
}
pub fn decrypt(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_tagged(ciphertext, iv)
}
}
}
// for the legacy keys do not use integrity MAC
pub fn encrypt_naive(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_without_tagging(plaintext, iv))
}
}
}
// for the legacy keys do not use integrity MAC
pub fn decrypt_naive(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_without_tag(ciphertext, iv)
}
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SharedSymmetricKey(AeadKey<GatewayEncryptionAlgorithm>);
type KeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, Clone, Copy, Error)]
pub enum SharedKeyConversionError {
#[error("the string representation of the shared key was malformed: {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error(
"the received shared keys had invalid size. Got: {received}, but expected: {expected}"
)]
InvalidSharedKeysSize { received: usize, expected: usize },
}
impl SharedSymmetricKey {
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != KeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: KeySize::to_usize(),
});
}
Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes)))
}
pub fn zeroizing_clone(&self) -> Zeroizing<Self> {
Zeroizing::new(SharedSymmetricKey(self.0))
}
pub fn digest(&self) -> Vec<u8> {
compute_digest::<blake3::Hasher>(self.as_bytes()).to_vec()
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
pub fn to_bytes(&self) -> Vec<u8> {
self.0.iter().copied().collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let bs58_str = Zeroizing::new(val.into());
let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?);
Self::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
let bytes = Zeroizing::new(self.to_bytes());
bs58::encode(bytes).into_string()
}
pub fn encrypt(
&self,
plaintext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::encrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, plaintext).map_err(Into::into)
}
pub fn decrypt(
&self,
ciphertext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::decrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, ciphertext).map_err(Into::into)
}
}
impl PemStorableKey for SharedSymmetricKey {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
"AES-256-GCM-SIV GATEWAY SHARED KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use crate::{GatewayRequestsError, SharedSymmetricKey};
use nym_sphinx::forwarding::packet::MixPacket;
use strum::FromRepr;
use tungstenite::Message;
@@ -57,14 +57,14 @@ impl BinaryRequest {
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
@@ -78,7 +78,7 @@ impl BinaryRequest {
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use crate::{GatewayRequestsError, SharedSymmetricKey};
use strum::FromRepr;
use tungstenite::Message;
@@ -38,14 +38,14 @@ impl BinaryResponse {
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
@@ -58,7 +58,7 @@ impl BinaryResponse {
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
+24 -38
View File
@@ -3,12 +3,12 @@
use crate::{
BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError,
SharedGatewayKey,
SharedSymmetricKey,
};
use std::iter::once;
// each binary message consists of the following structure (for non-legacy messages)
// KIND || ENC_FLAG || MAYBE_NONCE || CIPHERTEXT/PLAINTEXT
// KIND || ENC_FLAG || NONCE || CIPHERTEXT/PLAINTEXT
// first byte is the kind of data to influence further serialisation/deseralisation
// second byte is a flag indicating whether the content is encrypted
// then it's followed by a pseudorandom nonce, assuming encryption is used
@@ -16,43 +16,25 @@ use std::iter::once;
pub struct BinaryData<'a> {
kind: u8,
encrypted: bool,
maybe_nonce: Option<&'a [u8]>,
nonce: &'a [u8],
data: &'a [u8],
}
impl<'a> BinaryData<'a> {
// serialises possibly encrypted data into bytes to be put on the wire
pub fn into_raw(self, legacy: bool) -> Vec<u8> {
if legacy {
return self.data.to_vec();
}
let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 }));
if let Some(nonce) = self.maybe_nonce {
i.chain(nonce.iter().copied())
.chain(self.data.iter().copied())
.collect()
} else {
i.chain(self.data.iter().copied()).collect()
}
pub fn into_raw(self) -> Vec<u8> {
once(self.kind)
.chain(once(if self.encrypted { 1 } else { 0 }))
.chain(self.nonce.iter().copied())
.chain(self.data.iter().copied())
.collect()
}
// attempts to perform basic parsing on bytes received from the wire
pub fn from_raw(
raw: &'a [u8],
available_key: &SharedGatewayKey,
available_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
// if we're using legacy key, it's quite simple:
// it's always encrypted with no nonce and the request/response kind is always 1
if available_key.is_legacy() {
return Ok(BinaryData {
kind: 1,
encrypted: true,
maybe_nonce: None,
data: raw,
});
}
if raw.len() < 2 {
return Err(GatewayRequestsError::TooShortRequest);
}
@@ -74,7 +56,7 @@ impl<'a> BinaryData<'a> {
Ok(BinaryData {
kind,
encrypted,
maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]),
nonce: &raw[2..2 + available_key.nonce_size()],
data: &raw[2 + available_key.nonce_size()..],
})
}
@@ -83,30 +65,32 @@ impl<'a> BinaryData<'a> {
pub fn make_encrypted_blob(
kind: u8,
plaintext: &[u8],
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let maybe_nonce = key.random_nonce_or_zero_iv();
let nonce = key.random_nonce();
let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?;
let ciphertext = key.encrypt(plaintext, &nonce)?;
Ok(BinaryData {
kind,
encrypted: true,
maybe_nonce: maybe_nonce.as_deref(),
nonce: &nonce,
data: &ciphertext,
}
.into_raw(key.is_legacy()))
.into_raw())
}
// attempts to parse previously recovered bytes into a [`BinaryRequest`]
pub fn into_request(
self,
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<BinaryRequest, GatewayRequestsError> {
let kind = BinaryRequestKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownRequestKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?;
&*key.decrypt(self.data, &nonce)?
} else {
self.data
};
@@ -117,13 +101,15 @@ impl<'a> BinaryData<'a> {
// attempts to parse previously recovered bytes into a [`BinaryResponse`]
pub fn into_response(
self,
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<BinaryResponse, GatewayRequestsError> {
let kind = BinaryResponseKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownResponseKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?;
&*key.decrypt(self.data, &nonce)?
} else {
self.data
};
@@ -10,7 +10,7 @@ use std::str::FromStr;
pub enum RegistrationHandshake {
HandshakePayload {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
protocol_version: GatewayProtocolVersion,
data: Vec<u8>,
},
HandshakeError {
@@ -19,7 +19,7 @@ pub enum RegistrationHandshake {
}
impl RegistrationHandshake {
pub fn new_payload(data: Vec<u8>, protocol_version: Option<GatewayProtocolVersion>) -> Self {
pub fn new_payload(data: Vec<u8>, protocol_version: GatewayProtocolVersion) -> Self {
RegistrationHandshake::HandshakePayload {
protocol_version,
data,
@@ -66,7 +66,7 @@ mod tests {
fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() {
let handshake_data = vec![1, 2, 3, 4, 5, 6];
let handshake_payload_with_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: Some(42),
protocol_version: 42,
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap();
@@ -77,25 +77,7 @@ mod tests {
protocol_version,
data,
} => {
assert_eq!(protocol_version, Some(42));
assert_eq!(data, handshake_data)
}
_ => panic!("this branch shouldn't have been reached!"),
}
let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: None,
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert!(protocol_version.is_none());
assert_eq!(protocol_version, 42);
assert_eq!(data, handshake_data)
}
_ => panic!("this branch shouldn't have been reached!"),
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey,
AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey,
};
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
@@ -23,7 +23,7 @@ pub struct AuthenticateRequest {
impl AuthenticateRequest {
pub fn new(
protocol_version: GatewayProtocolVersion,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
identity_keys: &ed25519::KeyPair,
) -> Result<AuthenticateRequest, GatewayRequestsError> {
let content = AuthenticateRequestContent::new(
@@ -72,14 +72,14 @@ impl AuthenticateRequest {
pub fn verify_ciphertext(
&self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<(), AuthenticationFailure> {
let expected = shared_key.encrypt(
self.content
.client_identity
.derive_destination_address()
.as_bytes_ref(),
Some(&self.content.nonce),
&SharedSymmetricKey::validate_aead_nonce(&self.content.nonce)?,
)?;
if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) {
@@ -117,20 +117,19 @@ pub struct AuthenticateRequestContent {
impl AuthenticateRequestContent {
fn new(
protocol_version: u8,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
client_identity: ed25519::PublicKey,
) -> Result<AuthenticateRequestContent, GatewayRequestsError> {
let nonce = shared_key.random_nonce_or_iv();
let nonce = shared_key.random_nonce();
let destination_address = client_identity.derive_destination_address();
let address_ciphertext =
shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?;
let address_ciphertext = shared_key.encrypt(destination_address.as_bytes_ref(), &nonce)?;
let now = OffsetDateTime::now_utc();
Ok(AuthenticateRequestContent {
protocol_version,
client_identity,
address_ciphertext,
nonce,
nonce: nonce.to_vec(),
request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970...
})
}
@@ -3,13 +3,9 @@
use crate::models::CredentialSpendingRequest;
use crate::text_request::authenticate::AuthenticateRequest;
use crate::{
GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey,
AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use crate::{GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::ed25519;
use nym_sphinx::DestinationAddressBytes;
use nym_statistics_common::types::SessionType;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
@@ -21,23 +17,14 @@ pub mod authenticate;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum ClientRequest {
UpgradeKey {
hkdf_salt: Vec<u8>,
derived_key_digest: Vec<u8>,
},
ForgetMe {
client: bool,
stats: bool,
},
RememberMe {
session_type: SessionType,
},
ForgetMe { client: bool, stats: bool },
RememberMe { session_type: SessionType },
}
impl ClientRequest {
pub fn encrypt<S: SymmetricKey>(
pub fn encrypt(
&self,
key: &S,
key: &SharedSymmetricKey,
) -> Result<ClientControlRequest, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
@@ -47,17 +34,21 @@ impl ClientRequest {
// SAFETY: the trait has been derived correctly with no weird variants
#[allow(clippy::unwrap_used)]
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ClientControlRequest::EncryptedRequest { ciphertext, nonce })
let nonce = key.random_nonce();
let ciphertext = key.encrypt(&plaintext, &nonce)?;
Ok(ClientControlRequest::EncryptedRequest {
ciphertext,
nonce: nonce.to_vec(),
})
}
pub fn decrypt<S: SymmetricKey>(
pub fn decrypt(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
let plaintext = key.decrypt(ciphertext, &nonce)?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
@@ -68,35 +59,20 @@ impl ClientRequest {
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum ClientControlRequest {
// TODO: should this also contain a MAC considering that at this point we already
// have the shared key derived?
Authenticate {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
address: String,
enc_address: String,
iv: String,
},
AuthenticateV2(Box<AuthenticateRequest>),
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
protocol_version: GatewayProtocolVersion,
data: Vec<u8>,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
EcashCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
// Old gateways only understand `iv` so rename the field, but have nonce as an alias for next version update, to then phase out `iv`
#[serde(rename = "iv")]
#[serde(alias = "nonce")]
nonce: Vec<u8>,
},
UpgradeModeJWT {
// no need to encrypt it as it's public anyway
@@ -109,40 +85,29 @@ pub enum ClientControlRequest {
},
SupportedProtocol {},
// if you're adding new variants here, consider putting them inside `ClientRequest` instead
// NO LONGER SUPPORTED
Authenticate {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
address: String,
enc_address: String,
iv: String,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
}
impl ClientControlRequest {
pub fn new_legacy_authenticate(
address: DestinationAddressBytes,
shared_key: &SharedGatewayKey,
uses_credentials: bool,
) -> Result<Self, GatewayRequestsError> {
// if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV
// since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade,
// so use the lowest possible version we can
let protocol_version = if !shared_key.is_legacy() {
Some(AES_GCM_SIV_PROTOCOL_VERSION)
} else if uses_credentials {
Some(CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION)
} else {
// if we're not going to be using credentials, advertise lower protocol version to allow connection
// to wider range of gateways
Some(INITIAL_PROTOCOL_VERSION)
};
let nonce = shared_key.random_nonce_or_iv();
let ciphertext = shared_key.encrypt_naive(address.as_bytes_ref(), Some(&nonce))?;
Ok(ClientControlRequest::Authenticate {
protocol_version,
address: address.as_base58_string(),
enc_address: bs58::encode(&ciphertext).into_string(),
iv: bs58::encode(&nonce).into_string(),
})
}
pub fn new_authenticate_v2(
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
identity_keys: &ed25519::KeyPair,
protocol_version: GatewayProtocolVersion,
) -> Result<Self, GatewayRequestsError> {
@@ -174,26 +139,27 @@ impl ClientControlRequest {
pub fn new_enc_ecash_credential(
credential: CredentialSpendingData,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let nonce = shared_key.random_nonce_or_iv();
let enc_credential = shared_key.encrypt(&serialized_credential, Some(&nonce))?;
let nonce = shared_key.random_nonce();
let enc_credential = shared_key.encrypt(&serialized_credential, &nonce)?;
Ok(ClientControlRequest::EcashCredential {
enc_credential,
iv: nonce,
nonce: nonce.to_vec(),
})
}
pub fn try_from_enc_ecash_credential(
enc_credential: Vec<u8>,
shared_key: &SharedGatewayKey,
iv: Vec<u8>,
shared_key: &SharedSymmetricKey,
nonce: Vec<u8>,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(&nonce)?;
let credential_bytes = shared_key.decrypt(&enc_credential, &nonce)?;
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey,
GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey, SimpleGatewayRequestsError,
};
use serde::{Deserialize, Serialize};
use tungstenite::Message;
@@ -12,15 +12,14 @@ use tungstenite::Message;
#[derive(Serialize, Deserialize, Debug)]
#[non_exhaustive]
pub enum SensitiveServerResponse {
KeyUpgradeAck {},
ForgetMeAck {},
RememberMeAck {},
}
impl SensitiveServerResponse {
pub fn encrypt<S: SymmetricKey>(
pub fn encrypt(
&self,
key: &S,
key: &SharedSymmetricKey,
) -> Result<ServerResponse, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
@@ -30,17 +29,21 @@ impl SensitiveServerResponse {
// SAFETY: the trait has been derived correctly with no weird variants
#[allow(clippy::unwrap_used)]
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ServerResponse::EncryptedResponse { ciphertext, nonce })
let nonce = key.random_nonce();
let ciphertext = key.encrypt(&plaintext, &nonce)?;
Ok(ServerResponse::EncryptedResponse {
ciphertext,
nonce: nonce.to_vec(),
})
}
pub fn decrypt<S: SymmetricKey>(
pub fn decrypt(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
let plaintext = key.decrypt(ciphertext, &nonce)?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
@@ -72,7 +75,7 @@ pub struct SendResponse {
pub enum ServerResponse {
Authenticate {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
protocol_version: GatewayProtocolVersion,
status: bool,
bandwidth_remaining: i64,
@@ -83,7 +86,7 @@ pub enum ServerResponse {
},
Register {
#[serde(default)]
protocol_version: Option<GatewayProtocolVersion>,
protocol_version: GatewayProtocolVersion,
status: bool,
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
@@ -0,0 +1,23 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
-- make aes256gcm column non-nullable and drop any clients that still use the legacy keys
CREATE TABLE shared_keys_tmp
(
client_id INTEGER NOT NULL PRIMARY KEY REFERENCES clients (id),
client_address_bs58 TEXT NOT NULL UNIQUE,
derived_aes256_gcm_siv_key BLOB NOT NULL,
last_used_authentication TIMESTAMP WITHOUT TIME ZONE
);
INSERT INTO shared_keys_tmp (client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication)
SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication
FROM shared_keys
WHERE derived_aes256_gcm_siv_key IS NOT NULL;
DROP TABLE shared_keys;
ALTER TABLE shared_keys_tmp
RENAME TO shared_keys;
+3 -4
View File
@@ -9,7 +9,7 @@ use models::{
VerifiedTicket, WireguardPeer,
};
use nym_credentials_interface::ClientTicket;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sphinx::DestinationAddressBytes;
use shared_keys::SharedKeysManager;
use sqlx::{
@@ -165,7 +165,7 @@ impl SharedKeyGatewayStorage for GatewayStorage {
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedGatewayKey,
shared_keys: &SharedSymmetricKey,
) -> Result<i64, GatewayStorageError> {
let client_address_bs58 = client_address.as_base58_string();
let client_id = match self
@@ -184,8 +184,7 @@ impl SharedKeyGatewayStorage for GatewayStorage {
.insert_shared_keys(
client_id,
client_address_bs58,
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
shared_keys.aes256_gcm_siv().as_deref(),
shared_keys.to_bytes().as_ref(),
)
.await?;
Ok(client_id)
+5 -22
View File
@@ -3,7 +3,7 @@
use crate::{error::GatewayStorageError, make_bincode_serializer};
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use sqlx::FromRow;
use time::OffsetDateTime;
@@ -18,33 +18,16 @@ pub struct PersistedSharedKeys {
#[allow(dead_code)]
pub client_address_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
pub derived_aes256_gcm_siv_key: Vec<u8>,
pub last_used_authentication: Option<OffsetDateTime>,
}
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
impl TryFrom<PersistedSharedKeys> for SharedSymmetricKey {
type Error = GatewayStorageError;
fn try_from(value: PersistedSharedKeys) -> Result<Self, Self::Error> {
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => Err(GatewayStorageError::MissingSharedKey {
id: value.client_id,
}),
(Some(aes256gcm_siv), _) => {
let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Current(current_key))
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Legacy(legacy_key))
}
}
SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))
}
}
+2 -6
View File
@@ -42,26 +42,22 @@ impl SharedKeysManager {
&self,
client_id: i64,
client_address_bs58: String,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>,
derived_aes256_gcm_siv_key: Option<&Vec<u8>>,
derived_aes256_gcm_siv_key: &Vec<u8>,
) -> Result<(), sqlx::Error> {
// https://stackoverflow.com/a/20310838
// we don't want to be using `INSERT OR REPLACE INTO` due to the foreign key on `available_bandwidth` if the entry already exists
sqlx::query!(
r#"
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?);
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58,derived_aes256_gcm_siv_key) VALUES (?, ?, ?);
UPDATE shared_keys
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE client_address_bs58 = ?
"#,
client_id,
client_address_bs58,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
client_address_bs58,
).execute(&self.connection_pool).await?;
+2 -2
View File
@@ -3,7 +3,7 @@
use async_trait::async_trait;
use nym_credentials_interface::ClientTicket;
use nym_gateway_requests::SharedGatewayKey;
use nym_gateway_requests::SharedSymmetricKey;
use nym_sphinx::DestinationAddressBytes;
use time::OffsetDateTime;
@@ -25,7 +25,7 @@ pub trait SharedKeyGatewayStorage {
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedGatewayKey,
shared_keys: &SharedSymmetricKey,
) -> Result<i64, GatewayStorageError>;
async fn get_shared_keys(
&self,

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