Compare commits

..

104 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu b83fdd34af Add fd callback for initial authentication 2025-03-21 15:07:54 +02:00
Bogdan-Ștefan Neacşu 356cd2eeac WIP 2025-03-21 13:53:37 +02:00
Jędrzej Stuczyński 0f6ec8610e hotfix: correctly increment ws connection counter (#5620) 2025-03-14 15:47:17 +00:00
benedetta davico c3b8c4b2f7 Merge pull request #5616 from nymtech/bd/remove-explorer-api-ci
Remove explorer-api from ci-build-binaries
2025-03-13 13:36:30 +01:00
benedettadavico 271b9e545c remove bump to explorer-api 2025-03-13 13:35:06 +01:00
benedetta davico 9641f01670 remove explorer-api from ci-build-binaries 2025-03-13 13:31:46 +01:00
benedettadavico a7bb3e8d91 bump versions for chokito 2025-03-13 13:19:37 +01:00
Jack Wampler 79ce611d21 Server Side internal DoT/DoH opt out (#5577) 2025-03-12 10:14:04 -06:00
benedetta davico 960e817b8f Merge pull request #5578 from nymtech/yana/fix-double-memo
delete double memo field in send modal
2025-03-12 15:03:04 +01:00
dependabot[bot] 8b03e66ba7 build(deps): bump braces in /sdk/typescript/packages/nodejs-client (#5611)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 13:41:18 +00:00
dependabot[bot] 6a35581299 build(deps-dev): bump webpack-dev-middleware (#5610)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 13:40:54 +00:00
Jędrzej Stuczyński ce124a29a7 Chore/more payment watcher debug endpoints (#5608)
* add new endpoints for health and build information

* fixed timestamp serialisation in api responses

* status routes for price scraper

* state for processing bank msg

* clippy
2025-03-12 12:12:28 +00:00
Jędrzej Stuczyński f62d8813e0 chore: start sending v2 sphinx packets (#5554)
* chore: start sending v2 sphinx packets

* updated surb construction to use current format
2025-03-12 12:01:58 +00:00
dependabot[bot] a9cf016af2 build(deps-dev): bump ws in /wasm/mix-fetch/internal-dev (#5593)
Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.18.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.18.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 11:58:19 +00:00
dependabot[bot] a8403b585b build(deps-dev): bump webpack in /wasm/mix-fetch/internal-dev (#5597)
Bumps [webpack](https://github.com/webpack/webpack) from 5.77.0 to 5.98.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.77.0...v5.98.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 11:58:12 +00:00
Jon Häggblad e9a7b48da0 Export lane queue lengths in sdk (#5609) 2025-03-12 12:57:17 +01:00
dependabot[bot] 66792f57ed build(deps): bump @babel/helpers from 7.24.4 to 7.26.10 (#5606)
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.24.4 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 11:02:53 +00:00
Jędrzej Stuczyński f8d863249e Merge pull request #5605 from nymtech/chore/update-bls12_381-fork
Chore/update bls12 381 fork
2025-03-12 11:02:34 +00:00
Jędrzej Stuczyński 7d59a2477a chore: change auth v2 timestamp skew and allow values from the future (#5604)
* chore: change auth v2 timestamp skew and allow values from the future

* made the if statement more readable
2025-03-12 11:02:19 +00:00
Jędrzej Stuczyński eca88b0fa4 introduce internal tool for checking signer status (#5598)
* introduce internal tool for checking signer status

* fixed nym-api types due to moving values around

* added abci version
2025-03-12 11:02:03 +00:00
dependabot[bot] b80a4c8614 build(deps): bump body-parser and express (#5596)
Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

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

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 11:00:38 +00:00
dependabot[bot] ec5d342e3a build(deps): bump serve-static and express (#5594)
Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

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

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 11:00:21 +00:00
dependabot[bot] 6565655861 build(deps): bump cookie and express in /wasm/client/internal-dev (#5592)
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

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

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:59:36 +00:00
dependabot[bot] 5aba886f14 build(deps): bump cookie and express in /wasm/mix-fetch/internal-dev (#5591)
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

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

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:59:20 +00:00
dependabot[bot] 3ee73d541e build(deps): bump braces in /wasm/zknym-lib/internal-dev (#5590)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:58:56 +00:00
dependabot[bot] 4588a3036e build(deps): bump webpack-dev-middleware in /wasm/zknym-lib/internal-dev (#5589)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:58:41 +00:00
dependabot[bot] 6194ac07b8 build(deps): bump ring from 0.17.3 to 0.17.13 in /nym-wallet (#5582)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.3 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:57:02 +00:00
Jędrzej Stuczyński a7fcfef5a3 Merge pull request #5601 from nymtech/chore/payment-watcher-debug-endpoints
Chore/payment watcher debug endpoints
2025-03-11 16:47:30 +00:00
dependabot[bot] fa927b82d8 Merge pull request #5541 from nymtech/dependabot/cargo/rs_merkle-1.5.0
build(deps): bump rs_merkle from 1.4.2 to 1.5.0
2025-03-11 16:02:00 +01:00
import this f724478763 [DOCs/operators]: Add steps to synchronize server time, using NTP (#5603) 2025-03-11 11:18:18 +00:00
Jędrzej Stuczyński 040f4f2500 Merge pull request #5602 from nymtech/merge/release/2025.4-dorina-patched
merge release/2025.4-dorina-patched into develop
2025-03-11 10:36:50 +00:00
Jędrzej Stuczyński 63002e784a Merge branch 'develop' into merge/release/2025.4-dorina-patched 2025-03-11 09:53:56 +00:00
Jon Häggblad 4a0b683b70 Merge pull request #5583 from nymtech/dependabot/cargo/ring-0.17.13
build(deps): bump ring from 0.17.9 to 0.17.13
2025-03-11 10:37:21 +01:00
Jędrzej Stuczyński 9e84b1f0c1 ci clippy 2025-03-11 09:33:44 +00:00
Jon Häggblad bf031ad6de Merge pull request #5587 from nymtech/dependabot/cargo/tokio-1.44.0
build(deps): bump tokio from 1.43.0 to 1.44.0
2025-03-11 09:36:43 +01:00
dependabot[bot] 933769401c build(deps): bump tokio from 1.43.0 to 1.44.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.43.0 to 1.44.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.44.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-11 08:07:30 +00:00
Jon Häggblad ddd85704bb Merge pull request #5576 from nymtech/max/update-surb-example-tempdir2
Rust SDK SURB example: change hardcoded file to tempdir
2025-03-11 09:05:25 +01:00
Jon Häggblad 17860c809f Merge pull request #5588 from nymtech/dependabot/cargo/tempfile-3.18.0
build(deps): bump tempfile from 3.17.1 to 3.18.0
2025-03-11 08:38:11 +01:00
Jon Häggblad 2d00fcd934 Allow resetting all SURB sender tags (#5600)
* Allow resetting all SURB sender tags

* wasm fixes

* More wasm fixes
2025-03-11 08:35:40 +01:00
Jędrzej Stuczyński c2c3df98cb updated payment watcher version 2025-03-10 17:28:24 +00:00
Jędrzej Stuczyński f429092e21 added basic payment listener information to status api 2025-03-10 17:28:12 +00:00
Jędrzej Stuczyński d7ef68d8d1 remove fallback to env values for watched addresses 2025-03-10 17:28:12 +00:00
Jędrzej Stuczyński 1a334b575d feat: make sure any terminated task kills the watcher and write run info to db (#5517)
* feat: make sure any terminated task kills the watcher and write run info to db

* updated chain watcher version
2025-03-10 13:34:08 +00:00
dependabot[bot] 2126736aff build(deps): bump tempfile from 3.17.1 to 3.18.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.17.1 to 3.18.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.17.1...v3.18.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 10:37:03 +00:00
dependabot[bot] a69aa23609 build(deps): bump the patch-updates group with 8 updates (#5585)
Bumps the patch-updates group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [bytes](https://github.com/tokio-rs/bytes) | `1.10.0` | `1.10.1` |
| [semver](https://github.com/dtolnay/semver) | `1.0.25` | `1.0.26` |
| [serde](https://github.com/serde-rs/serde) | `1.0.218` | `1.0.219` |
| [serde_bytes](https://github.com/serde-rs/bytes) | `0.11.16` | `0.11.17` |
| [serde_derive](https://github.com/serde-rs/serde) | `1.0.218` | `1.0.219` |
| [serde_repr](https://github.com/dtolnay/serde-repr) | `0.1.19` | `0.1.20` |
| [time](https://github.com/time-rs/time) | `0.3.37` | `0.3.39` |
| [ff](https://github.com/zkcrypto/ff) | `0.13.0` | `0.13.1` |


Updates `bytes` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.0...v1.10.1)

Updates `semver` from 1.0.25 to 1.0.26
- [Release notes](https://github.com/dtolnay/semver/releases)
- [Commits](https://github.com/dtolnay/semver/compare/1.0.25...1.0.26)

Updates `serde` from 1.0.218 to 1.0.219
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

Updates `serde_bytes` from 0.11.16 to 0.11.17
- [Release notes](https://github.com/serde-rs/bytes/releases)
- [Commits](https://github.com/serde-rs/bytes/compare/0.11.16...0.11.17)

Updates `serde_derive` from 1.0.218 to 1.0.219
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

Updates `serde_repr` from 0.1.19 to 0.1.20
- [Release notes](https://github.com/dtolnay/serde-repr/releases)
- [Commits](https://github.com/dtolnay/serde-repr/compare/0.1.19...0.1.20)

Updates `time` from 0.3.37 to 0.3.39
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.37...v0.3.39)

Updates `ff` from 0.13.0 to 0.13.1
- [Changelog](https://github.com/zkcrypto/ff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkcrypto/ff/commits)

---
updated-dependencies:
- dependency-name: bytes
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: serde_bytes
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: serde_derive
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: serde_repr
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: time
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: ff
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 11:35:14 +01:00
dependabot[bot] 8a2d98e3ce build(deps): bump ring from 0.17.9 to 0.17.13
Bumps [ring](https://github.com/briansmith/ring) from 0.17.9 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 17:16:05 +00:00
mfahampshire 9c4243914e Max/ns api docs (#5544)
* first pass

* cleanup

* added qu

* add readme

* more verbose err

* reword explainer @ top

* rename private-key.public to public-key

* move instructions to own file + add _meta.json files

* first pass probe

* remove unnecessary doubled notice to developers

* added extra debug log to version()

* include PR suggestions

* remove commented out function
2025-03-07 09:57:52 +00:00
import this 143ede268d [DOCs/operators]: Fix typo (#5581) 2025-03-07 09:56:45 +00:00
import this 81bddb5f6d [DOCs/operators]: Second patch version changelog (#5580) 2025-03-07 09:46:08 +00:00
benedettadavico 247ebb7c43 update changelog 2025-03-06 21:26:16 +01:00
Jędrzej Stuczyński 01c052e9a4 use legacy crypto for constructing SURB headers (#5579) 2025-03-06 20:13:16 +00:00
Yana 3880971e57 delete double memo field in send modal 2025-03-06 21:34:22 +02:00
benedettadavico 6bd31b9521 bump nym-node version 2025-03-06 18:08:58 +01:00
Jon Häggblad 430c33eb04 Set DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE to 50 2025-03-06 18:03:08 +01:00
mfahampshire d45d1eb313 change hardcoded file to tempdir 2025-03-06 17:37:19 +01:00
import this 3cb3ebd79b [DOCs/operators]: Release ntoes for patched version (#5573) 2025-03-06 14:56:40 +00:00
benedettadavico b42e5b063e bump api version 2025-03-06 15:45:02 +01:00
benedettadavico f6b30d0db6 update changelog for patched-dorina 2025-03-06 15:06:24 +01:00
benedettadavico c33e4c0836 bumping versions dorina patched 2025-03-06 15:03:43 +01:00
Jędrzej Stuczyński be92ccf0da bugfix: make sure to correctly decode response content when putting it into error message (#5571) 2025-03-06 11:24:16 +00:00
Jędrzej Stuczyński 35bf49c48c chore: additional logs when attempting to load ecash keys (#5567) 2025-03-06 11:24:03 +00:00
Jędrzej Stuczyński 7335a3dad4 fix: gateway protocol negotation for v3/v4 2025-03-06 11:08:52 +00:00
Jędrzej Stuczyński 698883c03f feature: v2 authentication request (#5537) (#5563)
* introduced v2 authentication request between clients and gateways

* client to send v2 auth when possible

* added persistence to last used authentication timestamp

* added clients identity to signed plaintext
2025-03-06 09:18:39 +00:00
Jon Häggblad 8ddef08c72 Tweak surb management to be more conservative (#5570)
To reduce the risk of the IPR DoS the client:

- Lower the timeout until the IPR will disconnect a client
- Reduce fewer surbs at a time. Large surb requests increases the
  latency until all fragments in the response have been delivered. The
  efficiency gains of having large surb requests dimishes quickly for
  large sizes as well
2025-03-06 10:09:15 +01:00
Jon Häggblad 0d8b3abc6f Deserialize v5 authenticator requests (#5568) 2025-03-05 23:07:32 +01:00
Jędrzej Stuczyński aa2f336904 hotfix: ensure we bail on merkle leaves insertion upon missing data (#5565)
* hotfix: ensure we bail on merkle leaves insertion upon missing data

* Update Cargo.toml

---------

Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
2025-03-05 16:44:35 +00:00
Jędrzej Stuczyński eacaf84430 add full response body to error message upon decoding failure (#5566) 2025-03-05 16:43:56 +00:00
Jon Häggblad c284b1e8b1 Create authenticator v5 request/response types (#5561)
* Create authenticator v5 request/response types

* Support v5 in the authenticator

* Fix tests

* Bump nym-node version
2025-03-05 15:41:44 +01:00
Jon Häggblad 7785d085cf Handle disconnect in IPR (#5547)
* Implement disconnect in the IPR

* Remove unused async
2025-03-05 15:17:51 +01:00
Jon Häggblad bb5b2eafcf Allow IPR reconnect to session (#5562) 2025-03-05 15:02:07 +01:00
mfahampshire 09ea406c02 DOCS v2025.4-dorina release notes (#5552)
* WIP changelog

* [DOCs/operators]: Adding operators notes to new changelog PR(#5564)

---------

Co-authored-by: import this <97586125+serinko@users.noreply.github.com>
2025-03-05 11:39:55 +00:00
Jędrzej Stuczyński 8c6f84b3fe Merge pull request #5550 from nymtech/merge/release/2025.4-dorina
Merge/release/2025.4 dorina
2025-03-04 12:55:45 +00:00
Jędrzej Stuczyński 27dc9c8024 Merge branch 'develop' into merge/release/2025.4-dorina 2025-03-04 11:00:24 +00:00
Jędrzej Stuczyński 42d559bc69 fix prometheus metric naming test due to changes to packet version scheme 2025-03-04 10:46:12 +00:00
benedettadavico 41b9b0e5bd update changelog 2025-03-04 10:40:08 +01:00
dependabot[bot] 6c781a0064 build(deps): bump itertools from 0.13.0 to 0.14.0 (#5509)
Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.13.0 to 0.14.0.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: itertools
  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-03-04 00:37:35 +01:00
dependabot[bot] 080ec80722 build(deps): bump uuid from 1.13.2 to 1.15.1 (#5542)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.13.2 to 1.15.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.13.2...v1.15.1)

---
updated-dependencies:
- dependency-name: uuid
  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-03-04 00:36:24 +01:00
dependabot[bot] 9c17239831 build(deps): bump flate2 from 1.0.35 to 1.1.0 (#5510)
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.0.35 to 1.1.0.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Changelog](https://github.com/rust-lang/flate2-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.0.35...1.1.0)

---
updated-dependencies:
- dependency-name: flate2
  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-03-04 00:35:12 +01:00
dependabot[bot] f6c19ec02b build(deps): bump the patch-updates group across 1 directory with 14 updates (#5549) 2025-03-03 20:05:21 +01:00
Jędrzej Stuczyński 94ff8a79ee feature: disallow routing mix packets to nodes not present in the topology (#5526)
* new NymNodeTopologyProvider to also keep track of ips of all nodes

* added nym-api endpoint for nodes existence by ip

* change behaviour of updating allowed nodes alongside the topology

* clippy

* license fix

* fix default filtering limit
2025-03-03 18:03:47 +00:00
Jędrzej Stuczyński 155c4d37ef feature: v2 authentication request (#5537)
* introduced v2 authentication request between clients and gateways

* client to send v2 auth when possible

* added persistence to last used authentication timestamp

* added clients identity to signed plaintext
2025-03-03 17:51:30 +00:00
Jędrzej Stuczyński 7060fa6dad fixed sphinx version metrics registration (#5546) 2025-03-03 17:24:10 +00:00
Jon Häggblad 9be9c04f52 Add SURBs soft threshold (#5535)
* Add surbs soft threshold

* wip

* Proactively request more SURBs than needed

* fmt

* cleanup

* wip logging

* wip

* debugging

* wip

* Tidy

* tidy

* Set threshold buffer default for IPR

* rustfmt

* wasm fixes

* debug

* Tweak debug message

* Set default min buffer to 0

* Tweak backlog message

* Restore debug message

* tweak

* tweak

* wasm
2025-03-03 14:06:20 +01:00
import this 2a6fe6624d [DOCs/operators]: Advanced server setup: install KVM, virtualise machines, prep VMs for nym-node (#5493)
* initialise KVM docs

* initialise steps for KVM installation and setup

* document guide to setup KVM network bridge

* add new page with KVM installation

* add disclaimer

* add VM configuration guide

* first version finalised, ready for testing and review

* finish VM guide

* setup guide finished

* add last sentence
2025-03-03 11:49:09 +00:00
Jędrzej Stuczyński 4f7124e661 Feature/chain status api (#5539)
* nym-api endpoint to return latest block information

* attached chain health to health query

* fixed serde casing

* one of the most nastiest work arounds in test code
2025-03-03 10:47:40 +00:00
mfahampshire f52f07f6ec Max/tcp proxy bin sdk readme (#5354)
* removed old todos
* add bin files to proxy
* add readme to sdk
* fmt
2025-03-03 07:39:17 +00:00
Fran Arbanas b709d3ba0b Fix/pull from harbor (#5521)
* fix: pull from harbor instead of dockerhub

* add remaining

* add comments saying that these changes will only work with VPN
2025-02-28 14:01:33 +01:00
Jon Häggblad 128f69a5d6 Simplify IPR v8 (#5532)
* Purge stuff from v8

* Adapt to v8 changes

* Use protocol in ipr header

* Remove commented out code

* Remove unused error
2025-02-28 13:04:53 +01:00
Jon Häggblad 40dd7dc95e Add RUSTUP_PERMIT_COPY_RENAME to ci-build (#5533) 2025-02-28 10:55:30 +01:00
Jack Wampler f13ce6bf2d HickoryDnsResolver use a shared instance by default to limit fd use (#5523) 2025-02-27 09:05:10 -07:00
Jon Häggblad 856dbfe1ac IPR request types v8 (#5498)
* IPR v8 request/response types

* Remove signature for when we use sender tags

* Remove unused

* Address some review comments

* Update license to GPL-3.0 for IPR

Since the IPR can run as a binary, make sure it's license is GPL-3.0

* update cargo deny

* Add back support for v6

* Tidy responses

* Clippy

* Fix compilation

* Conversions

* Conversions

* Split response conversion

* request split

* Complete conversion switch

* Remove commented out code

* rustfmt

* Remove unused conversions

* Remove unused TryFrom

* use from
2025-02-27 15:21:55 +01:00
Tommy Verrall b2f6836756 Merge pull request #5465 from pedrofaustino/patch-1
Display error messages if IPv4 or IPv6 address not found on nymtun0
2025-02-27 11:11:41 +01:00
Tommy Verrall 87e429d78a Merge pull request #5524 from nymtech/yana/memo-and-links
Make "Memo" visible per default on send NYM
2025-02-27 10:32:38 +01:00
Yana 4178809555 Make "Memo" visible per default on send NYM 2025-02-26 18:53:08 +02:00
benedetta davico e6f6e1342f Update ns-api version 2025-02-26 12:25:46 +01:00
Jędrzej Stuczyński 65175fee09 merge #5512 again after reverting due to incorrect rebase (#5520)
* setup workspace global lints to prevent needless panics

* removed sources of panic in nym-crypto, nym-node and nym-api

* adjusted test code
2025-02-26 10:52:09 +00:00
Jędrzej Stuczyński 69b2448500 chore: removed all old coconut code (#5500) 2025-02-26 10:02:55 +00:00
Jędrzej Stuczyński 8ba5322997 bugfix: bound check when recovering a reply SURB (#5502) 2025-02-26 09:48:21 +00:00
Jędrzej Stuczyński 2cb3817b2c feat: add config option for maximum number of client connections (#5513) 2025-02-26 09:48:13 +00:00
Jędrzej Stuczyński 80b395cd8e feat: use ct_eq for checking bearer token (#5501) (#5519) 2025-02-26 09:48:05 +00:00
Jędrzej Stuczyński 8f5457e698 feature: allow nym-nodes to understand future version of sphinx packets (#5496) (#5518)
* use updated sphinx crate

* updated outfox usage of keygen in tests

* use x25519 in outfox

* remove redundant constructor

* adjusted key convertion traits
2025-02-26 09:47:57 +00:00
Jędrzej Stuczyński 26f97d3c34 dont query for ecash apis unless necessary (#5508) 2025-02-24 10:59:06 +00:00
benedettadavico 63a8f96ea5 bump versions 2025-02-19 12:13:24 +01:00
pedrofaustino 0d397ab5cc Display error messages if IPv4 or IPv6 address not found on nymtun0 (issue #5461) 2025-02-14 12:47:34 +01:00
333 changed files with 12702 additions and 9811 deletions
@@ -26,6 +26,7 @@ jobs:
runs-on: ${{ matrix.platform }}
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- uses: actions/checkout@v4
@@ -99,7 +100,6 @@ jobs:
cp target/release/nymvisor $OUTPUT_DIR
cp target/release/nym-node $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
cp target/release/explorer-api $OUTPUT_DIR
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR
fi
@@ -12,6 +12,7 @@ jobs:
runs-on: arc-ubuntu-22.04
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Check out repository code
uses: actions/checkout@v4
+1
View File
@@ -37,6 +37,7 @@ jobs:
env:
CARGO_TERM_COLOR: always
IPINFO_API_TOKEN: ${{ secrets.IPINFO_API_TOKEN }}
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
+112
View File
@@ -4,6 +4,118 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.4-dorina-patched] (2025-03-06)
- use legacy crypto for constructing SURB headers ([#5579])
- bugfix: make sure to correctly decode response content when putting it into error message ([#5571])
- Tweak surb management to be more conservative ([#5570])
- Deserialize v5 authenticator requests ([#5568])
- chore: additional logs when attempting to load ecash keys ([#5567])
- add full response body to error message upon decoding failure ([#5566])
- hotfix: ensure we bail on merkle leaves insertion upon missing data ([#5565])
- feature: v2 authentication request (#5537) ([#5563])
- Create authenticator v5 request/response types ([#5561])
[#5579]: https://github.com/nymtech/nym/pull/5579
[#5571]: https://github.com/nymtech/nym/pull/5571
[#5570]: https://github.com/nymtech/nym/pull/5570
[#5568]: https://github.com/nymtech/nym/pull/5568
[#5567]: https://github.com/nymtech/nym/pull/5567
[#5566]: https://github.com/nymtech/nym/pull/5566
[#5565]: https://github.com/nymtech/nym/pull/5565
[#5563]: https://github.com/nymtech/nym/pull/5563
[#5561]: https://github.com/nymtech/nym/pull/5561
## [2025.4-dorina] (2025-03-04)
- fixed sphinx version metrics registration ([#5546])
- Feature/chain status api ([#5539])
- Add SURBs soft threshold ([#5535])
- Simplify IPR v8 ([#5532])
- Shared instance for DNS AsyncResolver ([#5523])
- merge #5512 again after reverting due to incorrect rebase ([#5520])
- cherry-pick 17d3ff2d775f61aee381d90a304ed416c08f33fc onto dorina ([#5519])
- cherry-pick 6e5d0dac1b75413c5f09122b0d953f8ec6ef48df onto dorina ([#5518])
- chore: workspace global panic preventing lints ([#5512])
- bugfix: dont query for ecash apis unless necessary when spending ticketbooks ([#5508])
- bugfix: bound check when recovering a reply SURB ([#5502])
- chore: removed all old coconut code ([#5500])
- IPR request types v8 ([#5498])
- Support static routes for HTTP requests ([#5487])
- build(deps): bump the patch-updates group across 1 directory with 3 updates ([#5482])
- added missing import to doctest ([#5480])
- adjusted TestSetup::new_complex to ensure bonded node's existence ([#5478])
- Trigger contracts CI on main workspace Cargo changes ([#5477])
- build(deps): bump http from 1.1.0 to 1.2.0 ([#5472])
- build(deps): bump utoipa-swagger-ui from 8.0.3 to 8.1.0 ([#5471])
- build(deps): bump colored from 2.1.0 to 2.2.0 ([#5470])
- build(deps): bump celes from 2.4.0 to 2.5.0 ([#5469])
- build(deps): bump the patch-updates group with 2 updates ([#5467])
- build(deps): bump elliptic from 6.5.4 to 6.6.1 in /docker/typescript_client/upload_contract ([#5463])
- Run cargo autoinherit ([#5460])
- Fix clippy::precedence ([#5457])
- Provide Interval context with node descriptor endpoints ([#5456])
- fix: update fx average rate calcs to ignore 0 values ([#5454])
- Feature/add gbp currency ([#5453])
- Add helper to extract a list of sqlite files with journal files wal/shm ([#5452])
- Add a middleware layer to the nym api allowing for data compression ([#5451])
- Condense core API functionalities and enable gzip decompression for reqwest payloads ([#5450])
- build(deps): bump uniffi_build from 0.25.3 to 0.29.0 ([#5448])
- Upgrade tower to 0.5.2 ([#5446])
- build(deps): bump hickory-proto from 0.24.2 to 0.24.3 ([#5444])
- Seedable clients ([#5440])
- build(deps): bump the patch-updates group across 1 directory with 10 updates ([#5439])
- Remove all recv_with_delay and add shutdown condition to loops in client-core ([#5435])
- Disable the test for checking the remaining bandwidth in nym-node-status-api ([#5425])
- Dz nym node stats ([#5418])
- build(deps): bump hyper from 1.4.1 to 1.6.0 ([#5416])
- build(deps): bump publicsuffix from 2.2.3 to 2.3.0 ([#5367])
- Nymnode entrypoint docker ([#5300])
[#5546]: https://github.com/nymtech/nym/pull/5546
[#5539]: https://github.com/nymtech/nym/pull/5539
[#5535]: https://github.com/nymtech/nym/pull/5535
[#5532]: https://github.com/nymtech/nym/pull/5532
[#5523]: https://github.com/nymtech/nym/pull/5523
[#5520]: https://github.com/nymtech/nym/pull/5520
[#5519]: https://github.com/nymtech/nym/pull/5519
[#5518]: https://github.com/nymtech/nym/pull/5518
[#5512]: https://github.com/nymtech/nym/pull/5512
[#5508]: https://github.com/nymtech/nym/pull/5508
[#5502]: https://github.com/nymtech/nym/pull/5502
[#5500]: https://github.com/nymtech/nym/pull/5500
[#5498]: https://github.com/nymtech/nym/pull/5498
[#5487]: https://github.com/nymtech/nym/pull/5487
[#5482]: https://github.com/nymtech/nym/pull/5482
[#5480]: https://github.com/nymtech/nym/pull/5480
[#5478]: https://github.com/nymtech/nym/pull/5478
[#5477]: https://github.com/nymtech/nym/pull/5477
[#5472]: https://github.com/nymtech/nym/pull/5472
[#5471]: https://github.com/nymtech/nym/pull/5471
[#5470]: https://github.com/nymtech/nym/pull/5470
[#5469]: https://github.com/nymtech/nym/pull/5469
[#5467]: https://github.com/nymtech/nym/pull/5467
[#5463]: https://github.com/nymtech/nym/pull/5463
[#5460]: https://github.com/nymtech/nym/pull/5460
[#5457]: https://github.com/nymtech/nym/pull/5457
[#5456]: https://github.com/nymtech/nym/pull/5456
[#5454]: https://github.com/nymtech/nym/pull/5454
[#5453]: https://github.com/nymtech/nym/pull/5453
[#5452]: https://github.com/nymtech/nym/pull/5452
[#5451]: https://github.com/nymtech/nym/pull/5451
[#5450]: https://github.com/nymtech/nym/pull/5450
[#5448]: https://github.com/nymtech/nym/pull/5448
[#5446]: https://github.com/nymtech/nym/pull/5446
[#5444]: https://github.com/nymtech/nym/pull/5444
[#5440]: https://github.com/nymtech/nym/pull/5440
[#5439]: https://github.com/nymtech/nym/pull/5439
[#5435]: https://github.com/nymtech/nym/pull/5435
[#5425]: https://github.com/nymtech/nym/pull/5425
[#5418]: https://github.com/nymtech/nym/pull/5418
[#5416]: https://github.com/nymtech/nym/pull/5416
[#5367]: https://github.com/nymtech/nym/pull/5367
[#5300]: https://github.com/nymtech/nym/pull/5300
## [2025.3-ruta] (2025-02-10)
- Push down forget me to client configs ([#5431])
Generated
+262 -251
View File
File diff suppressed because it is too large Load Diff
+35 -25
View File
@@ -66,7 +66,6 @@ members = [
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymcoconut",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -138,7 +137,7 @@ members = [
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
"tools/nym-nr-query",
@@ -192,10 +191,10 @@ aes = "0.8.1"
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
ammonia = "4"
anyhow = "1.0.95"
anyhow = "1.0.97"
arc-swap = "1.7.1"
argon2 = "0.5.0"
async-trait = "0.1.86"
async-trait = "0.1.87"
axum = "0.7.5"
axum-client-ip = "0.6.1"
axum-extra = "0.9.4"
@@ -206,24 +205,24 @@ bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
bitvec = "1.0.0"
blake3 = "1.5.5"
blake3 = "1.6.1"
bloomfilter = "1.0.14"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.7.2"
bytes = "1.10.1"
cargo_metadata = "0.18.1"
celes = "2.5.0"
cfg-if = "1.0.0"
chacha20 = "0.9.0"
chacha20poly1305 = "0.10.1"
chrono = "0.4.39"
chrono = "0.4.40"
cipher = "0.4.3"
clap = "4.5.30"
clap = "4.5.31"
clap_complete = "4.5"
clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.15.10"
console = "0.15.11"
console-subscriber = "0.1.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
@@ -242,22 +241,23 @@ doc-comment = "0.3"
dotenvy = "0.15.6"
ecdsa = "0.16"
ed25519-dalek = "2.1"
encoding_rs = "0.8.35"
env_logger = "0.11.6"
envy = "0.4"
etherparse = "0.13.0"
eyre = "0.6.9"
fastrand = "2.1.1"
flate2 = "1.0.35"
flate2 = "1.1.0"
futures = "0.3.31"
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getset = "0.1.4"
getset = "0.1.5"
handlebars = "3.5.5"
headers = "0.4.0"
hex = "0.4.3"
hex-literal = "0.3.3"
hickory-resolver = "0.24.3"
hickory-resolver = "0.24.4"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -273,7 +273,7 @@ inquire = "0.6.2"
ip_network = "0.4.1"
ipnetwork = "0.20"
isocountry = "0.3.2"
itertools = "0.13.0"
itertools = "0.14.0"
k256 = "0.13"
lazy_static = "1.5.0"
ledger-transport = "0.10.0"
@@ -308,21 +308,21 @@ reqwest = { version = "0.12.4", default-features = false }
rocket = "0.5.0"
rocket_cors = "0.6.0"
rocket_okapi = "0.8.0"
rs_merkle = "1.4.2"
rs_merkle = "1.5.0"
safer-ffi = "0.1.13"
schemars = "0.8.21"
semver = "1.0.25"
serde = "1.0.217"
serde_bytes = "0.11.15"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
serde_bytes = "0.11.17"
serde_derive = "1.0"
serde_json = "1.0.138"
serde_json = "1.0.140"
serde_json_path = "0.7.2"
serde_repr = "0.1"
serde_with = "3.9.0"
serde_yaml = "0.9.25"
sha2 = "0.10.8"
si-scale = "0.2.3"
sphinx-packet = "0.3.1"
sphinx-packet = "=0.3.2"
sqlx = "0.7.4"
strum = "0.26"
strum_macros = "0.26"
@@ -331,10 +331,10 @@ syn = "1"
sysinfo = "0.33.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.15"
tempfile = "3.18"
thiserror = "2.0"
time = "0.3.37"
tokio = "1.43"
time = "0.3.39"
tokio = "1.44"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
@@ -370,9 +370,9 @@ prometheus = { version = "0.13.0" }
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" }
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect-updated" }
group = { version = "0.13.0", default-features = false }
ff = { version = "0.13.0", default-features = false }
ff = { version = "0.13.1", default-features = false }
subtle = "2.5.0"
# cosmwasm-related
@@ -438,3 +438,13 @@ opt-level = 'z'
[profile.release.package.mix-fetch-wasm]
# lto = true
opt-level = 'z'
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
todo = "deny"
dbg_macro = "deny"
exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.48"
version = "1.1.51"
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.48"
version = "1.1.51"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+1
View File
@@ -1,2 +1,3 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
+3 -2
View File
@@ -6,14 +6,15 @@ pub mod v1;
pub mod v2;
pub mod v3;
pub mod v4;
pub mod v5;
mod error;
mod util;
pub use error::Error;
pub use v4 as latest;
pub use v5 as latest;
pub const CURRENT_VERSION: u8 = 4;
pub const CURRENT_VERSION: u8 = 5;
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
+92 -20
View File
@@ -8,8 +8,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
v1, v2, v3,
v4::{self, registration::IpPair},
v1, v2, v3, v4,
v5::{self, registration::IpPair},
Error,
};
@@ -19,6 +19,7 @@ pub enum AuthenticatorVersion {
V2,
V3,
V4,
V5,
UNKNOWN,
}
@@ -34,6 +35,8 @@ impl From<Protocol> for AuthenticatorVersion {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
@@ -68,6 +71,12 @@ impl InitMessage for v4::registration::InitMessage {
}
}
impl InitMessage for v5::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
pub trait FinalMessage {
fn pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
@@ -138,6 +147,24 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.gateway_client.private_ips.into()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v5::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.gateway_client.private_ips
}
@@ -182,29 +209,39 @@ impl TopUpMessage for v4::topup::TopUpMessage {
}
}
impl TopUpMessage for v5::topup::TopUpMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn credential(&self) -> CredentialSpendingData {
self.credential.clone()
}
}
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
reply_to: Option<Recipient>,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
reply_to: Option<Recipient>,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
reply_to: Option<Recipient>,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
reply_to: Option<Recipient>,
request_id: u64,
},
}
@@ -218,7 +255,7 @@ impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
@@ -227,7 +264,7 @@ impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
@@ -237,7 +274,7 @@ impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
@@ -251,20 +288,20 @@ impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
@@ -278,20 +315,20 @@ impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
@@ -299,7 +336,7 @@ impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
@@ -313,20 +350,20 @@ impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
@@ -334,7 +371,42 @@ impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: value.reply_to,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v5::request::AuthenticatorRequest) -> Self {
match value.data {
v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
@@ -0,0 +1,478 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use crate::{v4, v5};
impl From<v4::request::AuthenticatorRequest> for v5::request::AuthenticatorRequest {
fn from(authenticator_request: v4::request::AuthenticatorRequest) -> Self {
Self {
protocol: Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator,
},
data: authenticator_request.data.into(),
request_id: authenticator_request.request_id,
}
}
}
impl From<v4::request::AuthenticatorRequestData> for v5::request::AuthenticatorRequestData {
fn from(authenticator_request_data: v4::request::AuthenticatorRequestData) -> Self {
match authenticator_request_data {
v4::request::AuthenticatorRequestData::Initial(init_msg) => {
v5::request::AuthenticatorRequestData::Initial(init_msg.into())
}
v4::request::AuthenticatorRequestData::Final(final_msg) => {
v5::request::AuthenticatorRequestData::Final(Box::new((*final_msg).into()))
}
v4::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => {
v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key)
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into())
}
}
}
}
impl From<v4::registration::InitMessage> for v5::registration::InitMessage {
fn from(init_msg: v4::registration::InitMessage) -> Self {
Self {
pub_key: init_msg.pub_key,
}
}
}
impl From<v4::registration::FinalMessage> for v5::registration::FinalMessage {
fn from(final_msg: v4::registration::FinalMessage) -> Self {
Self {
gateway_client: final_msg.gateway_client.into(),
credential: final_msg.credential,
}
}
}
impl From<v4::registration::GatewayClient> for v5::registration::GatewayClient {
fn from(gateway_client: v4::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v5::registration::GatewayClient> for v4::registration::GatewayClient {
fn from(gateway_client: v5::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v4::registration::ClientMac> for v5::registration::ClientMac {
fn from(client_mac: v4::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<v5::registration::ClientMac> for v4::registration::ClientMac {
fn from(client_mac: v5::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<Box<v4::topup::TopUpMessage>> for Box<v5::topup::TopUpMessage> {
fn from(top_up_message: Box<v4::topup::TopUpMessage>) -> Self {
Box::new(v5::topup::TopUpMessage {
pub_key: top_up_message.pub_key,
credential: top_up_message.credential,
})
}
}
impl From<v4::response::AuthenticatorResponse> for v5::response::AuthenticatorResponse {
fn from(value: v4::response::AuthenticatorResponse) -> Self {
Self {
protocol: Protocol {
version: 5,
service_provider_type: value.protocol.service_provider_type,
},
data: value.data.into(),
}
}
}
impl From<v4::response::AuthenticatorResponseData> for v5::response::AuthenticatorResponseData {
fn from(authenticator_response_data: v4::response::AuthenticatorResponseData) -> Self {
match authenticator_response_data {
v4::response::AuthenticatorResponseData::PendingRegistration(pending_response) => {
v5::response::AuthenticatorResponseData::PendingRegistration(
pending_response.into(),
)
}
v4::response::AuthenticatorResponseData::Registered(registered_response) => {
v5::response::AuthenticatorResponseData::Registered(registered_response.into())
}
v4::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => v5::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response.into(),
),
v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => {
v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into())
}
}
}
}
impl From<v4::response::RegisteredResponse> for v5::response::RegisteredResponse {
fn from(value: v4::response::RegisteredResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
}
}
}
impl From<v4::response::PendingRegistrationResponse> for v5::response::PendingRegistrationResponse {
fn from(value: v4::response::PendingRegistrationResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
}
}
}
impl From<v4::registration::RegistrationData> for v5::registration::RegistrationData {
fn from(value: v4::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v5::registration::RegistrationData> for v4::registration::RegistrationData {
fn from(value: v5::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v4::response::RemainingBandwidthResponse> for v5::response::RemainingBandwidthResponse {
fn from(value: v4::response::RemainingBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.map(Into::into),
}
}
}
impl From<v4::response::TopUpBandwidthResponse> for v5::response::TopUpBandwidthResponse {
fn from(value: v4::response::TopUpBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
}
}
}
impl From<v4::registration::RegistredData> for v5::registration::RegistredData {
fn from(value: v4::registration::RegistredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ips.into(),
wg_port: value.wg_port,
}
}
}
impl From<v4::registration::RemainingBandwidthData> for v5::registration::RemainingBandwidthData {
fn from(value: v4::registration::RemainingBandwidthData) -> Self {
Self {
available_bandwidth: value.available_bandwidth,
}
}
}
impl From<v4::registration::IpPair> for v5::registration::IpPair {
fn from(value: v4::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
impl From<v5::registration::IpPair> for v4::registration::IpPair {
fn from(value: v5::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
#[cfg(test)]
mod tests {
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::encryption::PrivateKey;
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::PeerPublicKey;
use x25519_dalek::PublicKey;
use super::*;
use crate::{
util::tests::{CREDENTIAL_BYTES, RECIPIENT},
v4,
};
#[test]
fn upgrade_initial_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let (msg, _) = v4::request::AuthenticatorRequest::new_initial_request(
v4::registration::InitMessage::new(pub_key),
reply_to,
);
let upgraded_msg = v5::request::AuthenticatorRequest::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::request::AuthenticatorRequestData::Initial(v5::registration::InitMessage {
pub_key
})
);
}
#[test]
fn upgrade_final_req() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v4::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let gateway_client = v4::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let credential = Some(CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap());
let final_message = v4::registration::FinalMessage {
gateway_client: gateway_client.clone(),
credential: credential.clone(),
};
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let (msg, _) =
v4::request::AuthenticatorRequest::new_final_request(final_message, reply_to);
let upgraded_msg = v5::request::AuthenticatorRequest::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::request::AuthenticatorRequestData::Final(Box::new(
v5::registration::FinalMessage {
gateway_client: v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v5::registration::IpPair::new(ipv4, ipv6),
nonce
),
credential
}
))
);
}
#[test]
fn upgrade_query_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let (msg, _) = v4::request::AuthenticatorRequest::new_query_request(pub_key, reply_to);
let upgraded_msg = v5::request::AuthenticatorRequest::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key)
);
}
#[test]
fn upgrade_pending_reg_resp() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v4::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let wg_port = 51822;
let gateway_data = v4::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let registration_data = v4::registration::RegistrationData {
nonce,
gateway_data,
wg_port,
};
let request_id = 123;
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let msg = v4::response::AuthenticatorResponse::new_pending_registration_success(
registration_data,
request_id,
reply_to,
);
let upgraded_msg = v5::response::AuthenticatorResponse::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::response::AuthenticatorResponseData::PendingRegistration(
v5::response::PendingRegistrationResponse {
request_id,
reply: v5::registration::RegistrationData {
nonce,
gateway_data: v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v5::registration::IpPair::new(ipv4, ipv6),
nonce
),
wg_port
}
}
)
);
}
#[test]
fn upgrade_registered_resp() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let private_ips = v4::registration::IpPair::new(ipv4, ipv6);
let wg_port = 51822;
let registred_data = v4::registration::RegistredData {
pub_key,
private_ips,
wg_port,
};
let request_id = 123;
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let msg = v4::response::AuthenticatorResponse::new_registered(
registred_data,
reply_to,
request_id,
);
let upgraded_msg = v5::response::AuthenticatorResponse::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse {
request_id,
reply: v5::registration::RegistredData {
wg_port,
pub_key,
private_ips: v5::registration::IpPair::new(ipv4, ipv6)
}
})
);
}
#[test]
fn upgrade_remaining_bandwidth_resp() {
let available_bandwidth = 42;
let remaining_bandwidth_data = Some(v4::registration::RemainingBandwidthData {
available_bandwidth,
});
let request_id = 123;
let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap();
let msg = v4::response::AuthenticatorResponse::new_remaining_bandwidth(
remaining_bandwidth_data,
reply_to,
request_id,
);
let upgraded_msg = v5::response::AuthenticatorResponse::from(msg);
assert_eq!(
upgraded_msg.protocol,
Protocol {
version: 5,
service_provider_type: ServiceProviderType::Authenticator
}
);
assert_eq!(
upgraded_msg.data,
v5::response::AuthenticatorResponseData::RemainingBandwidth(
v5::response::RemainingBandwidthResponse {
request_id,
reply: Some(v5::registration::RemainingBandwidthData {
available_bandwidth,
})
}
)
);
}
}
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod conversion;
pub mod registration;
pub mod request;
pub mod response;
pub mod topup;
pub const VERSION: u8 = 5;
@@ -0,0 +1,287 @@
// -2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use base64::{engine::general_purpose, Engine};
use nym_credentials_interface::CredentialSpendingData;
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
use hmac::{Hmac, Mac};
#[cfg(feature = "verify")]
use nym_crypto::asymmetric::encryption::PrivateKey;
#[cfg(feature = "verify")]
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
}
impl IpPair {
pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl From<(Ipv4Addr, Ipv6Addr)> for IpPair {
fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl fmt::Display for IpPair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.ipv4, self.ipv6)
}
}
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InitMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
}
impl InitMessage {
pub fn new(pub_key: PeerPublicKey) -> Self {
InitMessage { pub_key }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct FinalMessage {
/// Gateway client data
pub gateway_client: GatewayClient,
/// Ecash credential
pub credential: Option<CredentialSpendingData>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistrationData {
pub nonce: u64,
pub gateway_data: GatewayClient,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RemainingBandwidthData {
pub available_bandwidth: i64,
}
/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret.
/// Gateway/Nym node can then verify pub_key payload using the same process
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct GatewayClient {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Assigned private IPs (v4 and v6)
pub private_ips: IpPair,
/// Sha256 hmac on the data (alongside the prior nonce)
pub mac: ClientMac,
}
impl GatewayClient {
#[cfg(feature = "verify")]
pub fn new(
local_secret: &PrivateKey,
remote_public: x25519_dalek::PublicKey,
private_ips: IpPair,
nonce: u64,
) -> Self {
// convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek
#[allow(clippy::expect_used)]
let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes());
let local_public: x25519_dalek::PublicKey = (&static_secret).into();
let dh = static_secret.diffie_hellman(&remote_public);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(dh.as_bytes())
.expect("x25519 shared secret is always 32 bytes long");
mac.update(local_public.as_bytes());
mac.update(private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
GatewayClient {
pub_key: PeerPublicKey::new(local_public),
private_ips,
mac: ClientMac(mac.finalize().into_bytes().to_vec()),
}
}
// Reusable secret should be gateways Wireguard PK
// Client should perform this step when generating its payload, using its own WG PK
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek
#[allow(clippy::expect_used)]
let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes());
let dh = static_secret.diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(dh.as_bytes())
.expect("x25519 shared secret is always 32 bytes long");
mac.update(self.pub_key.as_bytes());
mac.update(self.private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
mac.verify_slice(&self.mac)
.map_err(|source| Error::FailedClientMacVerification {
client: self.pub_key.to_string(),
source,
})
}
pub fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
// TODO: change the inner type into generic array of size HmacSha256::OutputSize
// TODO2: rely on our internal crypto/hmac
#[derive(Debug, Clone, PartialEq)]
pub struct ClientMac(Vec<u8>);
impl fmt::Display for ClientMac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", general_purpose::STANDARD.encode(&self.0))
}
}
impl ClientMac {
#[allow(dead_code)]
pub fn new(mac: Vec<u8>) -> Self {
ClientMac(mac)
}
}
impl Deref for ClientMac {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for ClientMac {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mac_bytes: Vec<u8> =
general_purpose::STANDARD
.decode(s)
.map_err(|source| Error::MalformedClientMac {
mac: s.to_string(),
source,
})?;
Ok(ClientMac(mac_bytes))
}
}
impl Serialize for ClientMac {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let encoded_key = general_purpose::STANDARD.encode(self.0.clone());
serializer.serialize_str(&encoded_key)
}
}
impl<'de> Deserialize<'de> for ClientMac {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let encoded_key = String::deserialize(deserializer)?;
ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::encryption;
#[test]
fn create_ip_pair() {
let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into();
let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into();
assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6));
}
#[test]
#[cfg(feature = "verify")]
fn client_request_roundtrip() {
let mut rng = rand::thread_rng();
let gateway_key_pair = encryption::KeyPair::new(&mut rng);
let client_key_pair = encryption::KeyPair::new(&mut rng);
let nonce = 1234567890;
let client = GatewayClient::new(
client_key_pair.private_key(),
x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()),
IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()),
nonce,
);
assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok())
}
}
@@ -0,0 +1,132 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{
registration::{FinalMessage, InitMessage},
topup::TopUpMessage,
};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorRequest {
pub protocol: Protocol,
pub data: AuthenticatorRequestData,
pub request_id: u64,
}
impl AuthenticatorRequest {
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::Initial(init_message),
request_id,
},
request_id,
)
}
pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::Final(Box::new(final_message)),
request_id,
},
request_id,
)
}
pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::QueryBandwidth(peer_public_key),
request_id,
},
request_id,
)
}
pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)),
request_id,
},
request_id,
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorRequestData {
Initial(InitMessage),
Final(Box<FinalMessage>),
QueryBandwidth(PeerPublicKey),
TopUpBandwidth(Box<TopUpMessage>),
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn check_first_bytes_protocol() {
let version = 5;
let data = AuthenticatorRequest {
protocol: Protocol {
version,
service_provider_type: ServiceProviderType::Authenticator,
},
data: AuthenticatorRequestData::Initial(InitMessage::new(
PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(),
)),
request_id: 1,
};
let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap();
assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]);
}
}
@@ -0,0 +1,132 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorResponse {
pub protocol: Protocol,
pub data: AuthenticatorResponseData,
}
impl AuthenticatorResponse {
pub fn new_pending_registration_success(
registration_data: RegistrationData,
request_id: u64,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse {
reply: registration_data,
request_id,
}),
}
}
pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::Registered(RegisteredResponse {
reply: registred_data,
request_id,
}),
}
}
pub fn new_remaining_bandwidth(
remaining_bandwidth_data: Option<RemainingBandwidthData>,
request_id: u64,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
}),
}
}
pub fn new_topup_bandwidth(
remaining_bandwidth_data: RemainingBandwidthData,
request_id: u64,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
}),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn id(&self) -> Option<u64> {
match &self.data {
AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id),
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id),
AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorResponseData {
PendingRegistration(PendingRegistrationResponse),
Registered(RegisteredResponse),
RemainingBandwidth(RemainingBandwidthResponse),
TopUpBandwidth(TopUpBandwidthResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingRegistrationResponse {
pub request_id: u64,
pub reply: RegistrationData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply: RegistredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RemainingBandwidthResponse {
pub request_id: u64,
pub reply: Option<RemainingBandwidthData>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TopUpBandwidthResponse {
pub request_id: u64,
pub reply: RemainingBandwidthData,
}
@@ -0,0 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TopUpMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Ecash credential
pub credential: CredentialSpendingData,
}
+4 -7
View File
@@ -105,26 +105,24 @@ impl<C, St: Storage> BandwidthController<C, St> {
async fn get_aggregate_verification_key(
&self,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
}
async fn get_coin_index_signatures(
&self,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await
}
@@ -132,13 +130,12 @@ impl<C, St: Storage> BandwidthController<C, St> {
&self,
epoch_id: EpochId,
expiration_date: Date,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await
}
@@ -154,7 +151,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
{
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
let mut api_clients = Default::default();
let mut api_clients = ApiClientsWrapper::new(&self.client, epoch_id);
let verification_key = self
.get_aggregate_verification_key(epoch_id, &mut api_clients)
+64 -21
View File
@@ -21,30 +21,67 @@ use rand::thread_rng;
use std::fmt::Display;
use std::future::Future;
// it really doesn't need the RwLock because it's never moved across tasks,
// but we need all the Send/Sync action
#[derive(Default)]
pub(crate) struct ApiClientsWrapper(Option<Vec<EcashApiClient>>);
impl ApiClientsWrapper {
pub(crate) async fn get_or_init<C>(
pub(crate) trait EcashClientsProvider {
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>;
}
impl EcashClientsProvider for Vec<EcashApiClient> {
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
Ok(self.clone())
}
}
impl<C> EcashClientsProvider for &mut ApiClientsWrapper<'_, C>
where
C: DkgQueryClient + Sync + Send,
{
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
self.clients().await
}
}
pub(crate) enum ApiClientsWrapper<'a, C> {
Uninitialised {
query_client: &'a C,
epoch_id: EpochId,
dkg_client: &C,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
},
Cached {
clients: Vec<EcashApiClient>,
},
}
impl<'a, C> ApiClientsWrapper<'a, C> {
pub(crate) fn new(query_client: &'a C, epoch_id: EpochId) -> Self {
ApiClientsWrapper::Uninitialised {
query_client,
epoch_id,
}
}
async fn clients(&mut self) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
{
if let Some(cached) = &self.0 {
return Ok(cached.clone());
match self {
ApiClientsWrapper::Uninitialised {
query_client,
epoch_id,
} => {
let clients = all_ecash_api_clients(*query_client, *epoch_id).await?;
*self = ApiClientsWrapper::Cached {
clients: clients.clone(),
};
Ok(clients)
}
ApiClientsWrapper::Cached { clients } => Ok(clients.clone()),
}
let clients = all_ecash_api_clients(dkg_client, epoch_id).await?;
// technically we don't have to be cloning all the clients here, but it's way simpler than
// dealing with locking and whatnot given the performance penalty is negligible
self.0 = Some(clients.clone());
Ok(clients)
}
}
@@ -76,7 +113,7 @@ where
pub(crate) async fn get_aggregate_verification_key<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
St: Storage,
@@ -90,6 +127,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let master_vk = query_random_apis_until_success(
ecash_apis,
|api| async move { api.api_client.master_verification_key(Some(epoch_id)).await },
@@ -115,7 +154,7 @@ where
pub(crate) async fn get_coin_index_signatures<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
St: Storage,
@@ -129,6 +168,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let index_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
@@ -159,7 +200,7 @@ pub(crate) async fn get_expiration_date_signatures<St>(
storage: &St,
epoch_id: EpochId,
expiration_date: Date,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
St: Storage,
@@ -173,6 +214,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let expiration_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
+11 -1
View File
@@ -45,11 +45,12 @@ const DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO: f64 = 0.70;
// clients/client-core/src/client/replies/reply_storage/surb_storage.rs
const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10;
const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200;
const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0;
// define how much to request at once
// clients/client-core/src/client/replies/reply_controller.rs
const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10;
const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100;
const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 50;
const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500;
@@ -621,6 +622,10 @@ pub struct ReplySurbs {
/// Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
pub maximum_reply_surb_storage_threshold: usize,
/// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client
/// should proactively request additional reply surbs.
pub minimum_reply_surb_threshold_buffer: usize,
/// Defines the minimum number of reply surbs the client would request.
pub minimum_reply_surb_request_size: u32,
@@ -653,6 +658,9 @@ pub struct ReplySurbs {
/// Specifies the number of mixnet hops the packet should go through. If not specified, then
/// the default value is used.
pub surb_mix_hops: Option<u8>,
/// Specifies if we should reset all the sender tags on startup
pub fresh_sender_tags: bool,
}
impl Default for ReplySurbs {
@@ -660,6 +668,7 @@ impl Default for ReplySurbs {
ReplySurbs {
minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD,
maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD,
minimum_reply_surb_threshold_buffer: DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER,
minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE,
maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE,
maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE,
@@ -669,6 +678,7 @@ impl Default for ReplySurbs {
maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE,
maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE,
surb_mix_hops: None,
fresh_sender_tags: false,
}
}
}
@@ -181,6 +181,7 @@ impl From<ConfigV5> for Config {
maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age,
maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age,
surb_mix_hops: value.debug.reply_surbs.surb_mix_hops,
..Default::default()
},
..Default::default()
},
@@ -139,6 +139,8 @@ where
let gateway_setup = GatewaySetup::New {
specification: selection_spec,
available_gateways,
#[cfg(unix)]
connection_fd_callback: None,
};
let init_details =
@@ -187,6 +187,8 @@ where
let gateway_setup = GatewaySetup::New {
specification: selection_spec,
available_gateways,
#[cfg(unix)]
connection_fd_callback: None,
};
let init_details =
@@ -88,7 +88,7 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path).await {
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
@@ -33,10 +33,12 @@ pub enum PreparationError {
#[error(transparent)]
NymTopologyError(#[from] NymTopologyError),
#[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")]
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
MessageTooLongForSingleSurb { fragments: usize },
#[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")]
#[error(
"not enough reply SURBs to send the message, available: {available} required: {required}."
)]
NotEnoughSurbs { available: usize, required: usize },
}
@@ -517,17 +517,25 @@ where
use crate::error::ClientCoreStatusMessage;
let packets = self.transmission_buffer.total_size();
let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0;
let lanes = self.transmission_buffer.num_lanes();
let lanes = self.transmission_buffer.lanes();
let mult = self.sending_delay_controller.current_multiplier();
let delay = self.current_average_message_sending_delay().as_millis();
let lane_status = lanes
.iter()
.map(|lane_name| {
let lane_length = self.transmission_buffer.lane_length(lane_name).unwrap_or(0);
format!("{lane_name:?}: {lane_length}")
})
.collect::<Vec<String>>()
.join(", ");
let status_str = if self.config.traffic.disable_main_poisson_packet_distribution {
format!("Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, no delay")
format!("Packet backlog: {lane_status}, no delay")
} else {
format!(
"Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, avg delay: {delay}ms ({mult})"
)
format!("Packet backlog: {lane_status}, avg delay: {delay}ms ({mult})")
};
if packets > 1000 {
log::warn!("{status_str}");
} else if packets > 0 {
@@ -102,6 +102,7 @@ where
fragments: I,
lane: TransmissionLane,
) {
trace!("buffering pending replies for {recipient}");
self.pending_replies
.entry(*recipient)
.or_insert_with(TransmissionBuffer::new)
@@ -113,6 +114,7 @@ where
recipient: &AnonymousSenderTag,
fragments: Vec<(TransmissionLane, Fragment)>,
) {
trace!("re-inserting pending replies for {recipient}");
// the buffer should ALWAYS exist at this point, if it doesn't, it's a bug...
self.pending_replies
.entry(*recipient)
@@ -125,6 +127,7 @@ where
recipient: &AnonymousSenderTag,
data: Vec<Arc<PendingAcknowledgement>>,
) {
trace!("re-inserting pending retransmissions for {recipient}");
// the underlying entry MUST exist as we've just got data from there
let map_entry = self
.pending_retransmissions
@@ -142,7 +145,7 @@ where
}
fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool {
trace!("checking if we should request more surbs from {:?}", target);
trace!("checking if we should request more surbs from {target}");
let pending_queue_size = self
.pending_replies
@@ -158,11 +161,6 @@ where
let total_queue = pending_queue_size + retransmission_queue;
// simple as that - there's absolutely nothing to retransmit
if total_queue == 0 {
return false;
}
let available_surbs = self
.full_reply_storage
.surbs_storage_ref()
@@ -179,11 +177,27 @@ where
.full_reply_storage
.surbs_storage_ref()
.max_surb_threshold();
let min_surbs_threshold_buffer =
self.config.reply_surbs.minimum_reply_surb_threshold_buffer;
debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}");
// After clearing the queue, we want to have at least `min_surbs_threshold` surbs available
// and reserved for requesting additional surbs, and in addition to that we also want to
// have `min_surbs_threshold_buffer` surbs available proactively.
let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer;
(pending_surbs + available_surbs) < max_surbs_threshold
&& (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold)
// Check if we have enough surbs to handle the total queue and maintain minimum thresholds
let total_required_surbs = total_queue + target_surbs_after_clearing_queue;
let total_available_surbs = pending_surbs + available_surbs;
debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}");
// We should request more surbs if:
// 1. We haven't hit the maximum surb threshold, and
// 2. We don't have enough surbs to handle the queue plus minimum thresholds
let is_below_max_threshold = total_available_surbs < max_surbs_threshold;
let is_below_required_surbs = total_available_surbs < total_required_surbs;
is_below_max_threshold && is_below_required_surbs
}
async fn handle_send_reply(
@@ -244,6 +258,10 @@ where
&recipient_tag,
);
warn!("failed to send reply to {recipient_tag}: {err}");
info!(
"buffering {no_fragments} fragments for {recipient_tag}",
no_fragments = to_send.len()
);
self.insert_pending_replies(&recipient_tag, to_send, lane);
}
}
@@ -251,6 +269,13 @@ where
// if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it
if !fragments.is_empty() {
// Ideally we should have enough surbs above the minimum threshold to handle sending
// new replies without having to first request more surbs. That's why I'd like to log
// these cases as they might indicate a problem with the surb management.
debug!(
"buffering {no_fragments} fragments for {recipient_tag}",
no_fragments = fragments.len()
);
self.insert_pending_replies(&recipient_tag, fragments, lane);
}
@@ -265,6 +290,7 @@ where
target: AnonymousSenderTag,
amount: u32,
) -> Result<(), PreparationError> {
debug!("requesting {amount} additional reply surbs for {target}");
let reply_surb = self
.full_reply_storage
.surbs_storage_ref()
@@ -686,7 +712,7 @@ where
// it should take into consideration the average latency, sending rate and queue size.
// it should request as many surbs as it takes to saturate its sending rate before next batch arrives
async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) {
trace!("requesting surbs for queues clearing");
trace!("requesting surbs for queue clearing");
let pending_queue_size = self
.pending_replies
@@ -700,17 +726,18 @@ where
.map(|pending_queue| pending_queue.len())
.unwrap_or_default();
let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32;
let total_queue = (pending_queue_size + retransmission_queue) as u32;
if total_queue == 0 {
trace!("the pending queues for {:?} are already empty", target);
return;
}
// To proactively request additional surbs, we aim to have a buffer of extra surbs in our
// storage.
let total_queue_with_buffer = total_queue + min_surbs_buffer;
let request_size = min(
self.config.reply_surbs.maximum_reply_surb_request_size,
max(
total_queue,
total_queue_with_buffer,
self.config.reply_surbs.minimum_reply_surb_request_size,
),
);
@@ -719,7 +746,7 @@ where
.request_additional_reply_surbs(target, request_size)
.await
{
warn!("failed to request additional surbs... - {err}")
info!("{err}")
}
}
@@ -58,8 +58,8 @@ impl<T> TransmissionBuffer<T> {
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn num_lanes(&self) -> usize {
self.buffer.keys().count()
pub(crate) fn lanes(&self) -> Vec<TransmissionLane> {
self.buffer.keys().cloned().collect()
}
pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option<usize> {
@@ -83,6 +83,7 @@ impl<T> TransmissionBuffer<T> {
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused)]
pub(crate) fn total_size_in_bytes(&self) -> usize
where
T: SizedData,
+10 -2
View File
@@ -11,6 +11,8 @@ use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKeyRef;
use nym_validator_client::UserAgent;
use rand::{seq::SliceRandom, Rng};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::{sync::Arc, time::Duration};
use tungstenite::Message;
use url::Url;
@@ -313,9 +315,15 @@ pub(super) async fn register_with_gateway(
gateway_id: identity::PublicKey,
gateway_listener: Url,
our_identity: Arc<identity::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_id, our_identity.clone());
let mut gateway_client = GatewayClient::new_init(
gateway_listener,
gateway_id,
our_identity.clone(),
#[cfg(unix)]
connection_fd_callback,
);
gateway_client.establish_connection().await.map_err(|err| {
log::warn!("Failed to establish connection with gateway!");
+22 -4
View File
@@ -23,6 +23,8 @@ use nym_topology::node::RoutingNode;
use rand::rngs::OsRng;
use rand::{CryptoRng, RngCore};
use serde::Serialize;
#[cfg(unix)]
use std::{os::fd::RawFd, sync::Arc};
pub mod helpers;
pub mod types;
@@ -53,6 +55,7 @@ async fn setup_new_gateway<K, D>(
details_store: &D,
selection_specification: GatewaySelectionSpecification,
available_gateways: Vec<RoutingNode>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<InitialisationResult, ClientCoreError>
where
K: KeyStore,
@@ -108,9 +111,14 @@ where
// 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(), our_identity)
.await?;
let registration = helpers::register_with_gateway(
gateway_id,
gateway_listener.clone(),
our_identity,
#[cfg(unix)]
connection_fd_callback,
)
.await?;
(
GatewayDetails::new_remote(
gateway_id,
@@ -203,9 +211,19 @@ where
GatewaySetup::New {
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback,
} => {
log::debug!("GatewaySetup::New with spec: {specification:?}");
setup_new_gateway(key_store, details_store, specification, available_gateways).await
setup_new_gateway(
key_store,
details_store,
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback,
)
.await
}
GatewaySetup::ReuseConnection {
authenticated_ephemeral_client,
+10
View File
@@ -18,6 +18,8 @@ use nym_validator_client::client::IdentityKey;
use nym_validator_client::nyxd::AccountId;
use serde::Serialize;
use std::fmt::{Debug, Display};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::sync::Arc;
use time::OffsetDateTime;
use url::Url;
@@ -208,6 +210,10 @@ pub enum GatewaySetup {
// TODO: seems to be a bit inefficient to pass them by value
available_gateways: Vec<RoutingNode>,
/// Callback useful for allowing initial connection to gateway
#[cfg(unix)]
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
},
ReuseConnection {
@@ -231,6 +237,8 @@ impl Debug for GatewaySetup {
GatewaySetup::New {
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback: _,
} => f
.debug_struct("GatewaySetup::New")
.field("specification", specification)
@@ -270,6 +278,8 @@ impl GatewaySetup {
additional_data: None,
},
available_gateways: vec![],
#[cfg(unix)]
connection_fd_callback: None,
}
}
@@ -10,7 +10,7 @@ use crate::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
};
use async_trait::async_trait;
use log::{error, info, warn};
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
@@ -52,7 +52,10 @@ impl Backend {
Ok(backend)
}
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
pub async fn try_load<P: AsRef<Path>>(
database_path: P,
fresh_sender_tags: bool,
) -> Result<Self, StorageError> {
let owned_path: PathBuf = database_path.as_ref().into();
if owned_path.file_name().is_none() {
return Err(StorageError::DatabasePathWithoutFilename {
@@ -118,6 +121,9 @@ impl Backend {
if days > 2 {
info!("it's been over {days} days and {hours} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.");
manager.delete_all_tags().await?;
} else if fresh_sender_tags {
debug!("starting with fresh sender tags");
manager.delete_all_tags().await?;
}
Ok(Backend {
@@ -20,8 +20,8 @@ use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse,
SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
@@ -563,28 +563,10 @@ impl<C, St> GatewayClient<C, St> {
Ok(zeroizing_updated_key)
}
async fn authenticate(&mut self) -> Result<(), GatewayClientError> {
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
let self_address = self
.local_identity
.as_ref()
.public_key()
.derive_destination_address();
let msg = ClientControlRequest::new_authenticate(
self_address,
shared_key,
self.cfg.bandwidth.require_tickets,
)?;
async fn send_authenticate_request_and_handle_response(
&mut self,
msg: ClientControlRequest,
) -> Result<(), GatewayClientError> {
match self.send_websocket_message(msg).await? {
ServerResponse::Authenticate {
protocol_version,
@@ -608,6 +590,51 @@ 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_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) -> Result<(), GatewayClientError> {
debug!("using v2 authentication");
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?;
self.send_authenticate_request_and_handle_response(msg)
.await
}
async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
if use_v2 {
self.authenticate_v2().await
} else {
self.authenticate_v1().await
}
}
/// Helper method to either call register or authenticate based on self.shared_key value
#[instrument(skip_all,
fields(
@@ -623,19 +650,25 @@ impl<C, St> GatewayClient<C, St> {
}
// 1. check gateway's protocol version
let supports_aes_gcm_siv = match self.get_gateway_protocol().await {
Ok(protocol) => protocol >= AES_GCM_SIV_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?;
false
None
}
};
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
}
if !supports_auth_v2 {
warn!("this gateway is on an old version that doesn't support authentication v2")
}
if self.authenticated {
debug!("Already authenticated");
@@ -650,7 +683,7 @@ impl<C, St> GatewayClient<C, St> {
}
if self.shared_key.is_some() {
self.authenticate().await?;
self.authenticate(supports_auth_v2).await?;
if self.authenticated {
// if we are authenticated it means we MUST have an associated shared_key
@@ -983,7 +1016,8 @@ impl<C, St> GatewayClient<C, St> {
}
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
self.authenticate().await?;
self.authenticate(self.negotiated_protocol.supports_authenticate_v2())
.await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
@@ -1031,6 +1065,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
gateway_listener: Url,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Self {
log::trace!("Initialising gateway client");
use futures::channel::mpsc;
@@ -1056,7 +1091,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
stats_reporter: ClientStatsSender::new(None, task_client.clone()),
negotiated_protocol: None,
#[cfg(unix)]
connection_fd_callback: None,
connection_fd_callback,
task_client,
}
}
@@ -38,7 +38,8 @@ pub(crate) async fn connect_async(
// Do a DNS lookup for the domain using our custom DNS resolver
resolver
.resolve_str(domain)
.await?
.await
.inspect_err(|err| tracing::error!("Resolve error {err}"))?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
@@ -49,20 +50,27 @@ pub(crate) async fn connect_async(
address: endpoint.to_owned(),
});
for sock_addr in sock_addrs {
tracing::info!("Trying with {sock_addr}");
let socket = if sock_addr.is_ipv4() {
TcpSocket::new_v4()
} else {
TcpSocket::new_v6()
}
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
.map_err(|err| {
tracing::error!("Couldn't create the socket");
GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
}
})?;
tracing::info!("Preparing to call callback");
#[cfg(unix)]
if let Some(callback) = connection_fd_callback.as_ref() {
tracing::info!("Calling callback");
callback.as_ref()(socket.as_raw_fd());
}
tracing::info!("Preparing to connect");
match socket.connect(sock_addr).await {
Ok(s) => {
@@ -23,11 +23,12 @@ use nym_api_requests::models::{
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
use nym_coconut_dkg_common::types::EpochId;
use nym_ecash_contract_common::deposit::DepositId;
use nym_http_api_client::UserAgent;
use nym_network_defaults::NymNetworkDetails;
use std::net::IpAddr;
use time::Date;
use url::Url;
@@ -710,4 +711,11 @@ impl NymApiClient {
.issued_ticketbooks_challenge(expiration_date, deposits)
.await?)
}
pub async fn nodes_by_addresses(
&self,
addresses: Vec<IpAddr>,
) -> Result<NodesByAddressesResponse, ValidatorClientError> {
Ok(self.nym_api.nodes_by_addresses(addresses).await?)
}
}
@@ -83,6 +83,12 @@ impl TryFrom<ContractVKShare> for EcashApiClient {
let url_address = Url::parse(&share.announce_address)?;
// The NymApiClient constructed here uses the default (hickory DoT/DoH) resolver because
// this EcashApiClient is used by both client and non-client applications.
//
// In non-client applications this resolver can cause warning logs about H2 connection
// failure. This indicates that the long lived https connection was closed by the remote
// peer and the resolver will have to reconnect. It should not impact actual functionality
Ok(EcashApiClient {
api_client: NymApiClient::new(url_address),
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
@@ -12,10 +12,13 @@ use nym_api_requests::ecash::models::{
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription,
PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
@@ -40,6 +43,7 @@ pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
use std::net::IpAddr;
use time::format_description::BorrowedFormatItem;
use time::Date;
use tracing::instrument;
@@ -66,6 +70,19 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[instrument(level = "debug", skip(self))]
async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::API_STATUS_ROUTES,
routes::BUILD_INFORMATION,
],
NO_PARAMS,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
@@ -1015,6 +1032,23 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn nodes_by_addresses(
&self,
addresses: Vec<IpAddr>,
) -> Result<NodesByAddressesResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
routes::nym_nodes::BY_ADDRESSES,
],
NO_PARAMS,
&NodesByAddressesRequestBody { addresses },
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
self.get_json(
@@ -1023,6 +1057,15 @@ pub trait NymApiClientExt: ApiClient {
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
NO_PARAMS,
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -43,11 +43,14 @@ pub mod nym_nodes {
pub const NYM_NODES_BONDED: &str = "bonded";
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
pub const BY_ADDRESSES: &str = "by-addresses";
}
pub const STATUS_ROUTES: &str = "status";
pub const API_STATUS_ROUTES: &str = "api-status";
pub const HEALTH: &str = "health";
pub const BUILD_INFORMATION: &str = "build-information";
pub const MIXNODE: &str = "mixnode";
pub const GATEWAY: &str = "gateway";
pub const NYM_NODES: &str = "nym-nodes";
@@ -69,4 +72,5 @@ pub const SUBMIT_NODE: &str = "submit-node-monitoring-results";
pub const SERVICE_PROVIDERS: &str = "services";
pub const DETAILS: &str = "details";
pub const CHAIN_STATUS: &str = "chain-status";
pub const NETWORK: &str = "network";
@@ -28,7 +28,6 @@ use nym_network_defaults::{ChainDetails, NymNetworkDetails};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
use std::time::SystemTime;
use tendermint_rpc::endpoint::block::Response as BlockResponse;
use tendermint_rpc::endpoint::*;
use tendermint_rpc::{Error as TendermintRpcError, Order};
use url::Url;
@@ -63,6 +62,8 @@ pub use cw3;
pub use cw4;
pub use cw_controllers;
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
pub use prost::Name;
pub use tendermint_rpc::endpoint::block::Response as BlockResponse;
pub use tendermint_rpc::{
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
query::Query,
-9
View File
@@ -25,15 +25,6 @@ pub fn in6addr_any_init() -> IpAddr {
IpAddr::V6(Ipv6Addr::UNSPECIFIED)
}
/// Helper for providing binding warnings if node tries to bind to any of those
pub const SPECIAL_ADDRESSES: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
IpAddr::V4(Ipv4Addr::BROADCAST),
IpAddr::V6(Ipv6Addr::LOCALHOST),
IpAddr::V6(Ipv6Addr::UNSPECIFIED),
];
// TODO: is it really part of 'Config'?
pub trait OptionalSet {
/// If the value is available (i.e. `Some`), the provided closure is applied.
+4 -1
View File
@@ -43,4 +43,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"]
sphinx = ["nym-sphinx-types/sphinx"]
[lints]
workspace = true
+10 -4
View File
@@ -16,8 +16,11 @@ pub fn compute_keyed_hmac<D>(key: &[u8], data: &[u8]) -> HmacOutput<D>
where
D: Digest + BlockSizeUser,
{
let mut hmac = SimpleHmac::<D>::new_from_slice(key)
.expect("HMAC was instantiated with a key of an invalid size!");
// SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying
// digest, they're padded with 0. if they're larger they're hashed and padded
// the reason for `Result` return type is due to the trait definition
#[allow(clippy::unwrap_used)]
let mut hmac = SimpleHmac::<D>::new_from_slice(key).unwrap();
hmac.update(data);
hmac.finalize()
}
@@ -27,8 +30,11 @@ pub fn recompute_keyed_hmac_and_verify_tag<D>(key: &[u8], data: &[u8], tag: &[u8
where
D: Digest + BlockSizeUser,
{
let mut hmac = SimpleHmac::<D>::new_from_slice(key)
.expect("HMAC was instantiated with a key of an invalid size!");
// SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying
// digest, they're padded with 0. if they're larger they're hashed and padded
// the reason for `Result` return type is due to the trait definition
#[allow(clippy::unwrap_used)]
let mut hmac = SimpleHmac::<D>::new_from_slice(key).unwrap();
hmac.update(data);
let tag_arr = Output::<D>::from_slice(tag);
+14 -5
View File
@@ -27,12 +27,16 @@ where
// after performing diffie-hellman we don't care about the private component anymore
let dh_result = ephemeral_keypair.private_key().diffie_hellman(remote_key);
// there is no reason for this to fail as our okm is expected to be only C::KeySize bytes
// SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()`
// larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used)
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<D>(None, &dh_result, None, C::key_size())
.expect("somehow too long okm was provided");
let derived_shared_key =
Key::<C>::from_exact_iter(okm).expect("okm was expanded to incorrect length!");
// SAFETY: the generated okm has exactly `C::key_size()` elements,
// so this call is safe
#[allow(clippy::unwrap_used)]
let derived_shared_key = Key::<C>::from_exact_iter(okm).unwrap();
(ephemeral_keypair, derived_shared_key)
}
@@ -48,9 +52,14 @@ where
{
let dh_result = local_key.diffie_hellman(remote_key);
// there is no reason for this to fail as our okm is expected to be only C::KeySize bytes
// SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()`
// larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used)
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<D>(None, &dh_result, None, C::key_size())
.expect("somehow too long okm was provided");
Key::<C>::from_exact_iter(okm).expect("okm was expanded to incorrect length!")
// SAFETY: the generated okm has exactly `C::key_size()` elements,
// so this call is safe
#[allow(clippy::unwrap_used)]
Key::<C>::from_exact_iter(okm).unwrap()
}
+4 -9
View File
@@ -60,20 +60,15 @@ where
Iv::<C>::default()
}
pub fn iv_from_slice<C>(b: &[u8]) -> &IV<C>
pub fn try_iv_from_slice<C>(b: &[u8]) -> Option<&IV<C>>
where
C: IvSizeUser,
{
if b.len() != C::iv_size() {
// `from_slice` would have caused a panic about this issue anyway.
// Now we at least have slightly more information
panic!(
"Tried to convert {} bytes to IV. Expected {}",
b.len(),
C::iv_size()
)
None
} else {
Some(IV::<C>::from_slice(b))
}
IV::<C>::from_slice(b)
}
// TODO: there's really no way to use more parts of the keystream if it was required at some point.
+1 -1
View File
@@ -21,7 +21,7 @@ lazy_static = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
rand_core = { workspace = true }
sha2 = "0.9"
sha2 = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
thiserror = { workspace = true }
+96 -2
View File
@@ -54,12 +54,12 @@ pub(crate) fn hash_to_scalar<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> Scalar {
pub(crate) fn hash_to_scalars<M: AsRef<[u8]>>(msg: M, domain: &[u8], n: usize) -> Vec<Scalar> {
let mut output = vec![Scalar::zero(); n];
Scalar::hash_to_field::<ExpandMsgXmd<Sha256>>(msg.as_ref(), domain, &mut output);
Scalar::hash_to_field::<ExpandMsgXmd<Sha256>, _>([msg], domain, &mut output);
output
}
pub(crate) fn hash_g2<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> G2Projective {
<G2Projective as HashToCurve<ExpandMsgXmd<Sha256>>>::hash_to_curve(msg, domain)
<G2Projective as HashToCurve<ExpandMsgXmd<Sha256>>>::hash_to_curve([msg], domain)
}
pub(crate) fn combine_scalar_chunks(chunks: &[Scalar]) -> Scalar {
@@ -112,3 +112,97 @@ pub(crate) fn deserialize_g2(b: &[u8]) -> Option<G2Projective> {
G2Projective::from_bytes(&encoding).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bls12_381::G2Affine;
#[test]
fn test_hash_to_scalar() {
let msg1 = "foo";
let expected1 = Scalar::from_bytes(&[
253, 57, 224, 227, 175, 195, 226, 82, 46, 175, 33, 126, 171, 239, 255, 92, 108, 168, 6,
79, 90, 11, 235, 236, 221, 10, 85, 133, 42, 81, 95, 30,
])
.unwrap();
let msg2 = "bar";
let expected2 = Scalar::from_bytes(&[
48, 83, 69, 52, 42, 18, 135, 244, 211, 190, 160, 196, 118, 154, 24, 126, 0, 125, 72,
201, 170, 225, 123, 201, 52, 120, 171, 132, 235, 182, 20, 26,
])
.unwrap();
let msg3 = [
33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123,
250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27,
115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101,
60, 42, 92, 128, 131, 161, 43,
];
let expected3 = Scalar::from_bytes(&[
128, 189, 8, 43, 186, 55, 52, 61, 171, 196, 159, 177, 162, 100, 27, 143, 85, 83, 218,
171, 91, 220, 155, 25, 7, 38, 2, 36, 4, 93, 136, 4,
])
.unwrap();
assert_eq!(
hash_to_scalar(msg1, b"NYMECASH-V01-CS02-with-expander-SHA256"),
expected1
);
assert_eq!(
hash_to_scalar(msg2, b"NYMECASH-V01-CS02-with-expander-SHA256"),
expected2
);
assert_eq!(
hash_to_scalar(msg3, b"NYMECASH-V01-CS02-with-expander-SHA256"),
expected3
);
}
#[test]
fn test_hash_g2() {
let msg1 = "foo";
let expected1 = G2Affine::from_compressed(&[
175, 187, 62, 7, 29, 17, 42, 93, 28, 93, 234, 253, 101, 166, 158, 187, 153, 82, 93, 18,
11, 233, 36, 107, 51, 117, 30, 127, 32, 254, 210, 77, 133, 12, 253, 255, 84, 128, 36,
214, 234, 103, 50, 21, 26, 78, 112, 49, 20, 69, 19, 109, 7, 78, 33, 227, 196, 180, 168,
219, 73, 251, 192, 221, 41, 138, 160, 131, 191, 186, 156, 117, 179, 179, 191, 235, 171,
26, 219, 148, 170, 179, 11, 38, 137, 14, 95, 115, 171, 186, 163, 82, 158, 6, 239, 88,
])
.unwrap()
.into();
let msg2 = "bar";
let expected2 = G2Affine::from_compressed(&[
183, 25, 90, 187, 34, 184, 30, 182, 215, 242, 158, 83, 116, 34, 210, 96, 188, 79, 83,
255, 100, 122, 90, 188, 196, 93, 164, 253, 20, 106, 205, 33, 48, 140, 60, 149, 66, 246,
121, 244, 146, 66, 170, 60, 113, 95, 102, 237, 25, 231, 8, 42, 121, 124, 180, 140, 34,
104, 173, 251, 89, 189, 28, 196, 49, 66, 101, 38, 68, 44, 40, 235, 21, 35, 204, 123,
218, 238, 216, 92, 134, 217, 212, 246, 176, 77, 187, 0, 245, 134, 132, 73, 31, 44, 137,
197,
])
.unwrap()
.into();
let msg3 = [
33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123,
250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27,
115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101,
60, 42, 92, 128, 131, 161, 43,
];
let expected3 = G2Affine::from_compressed(&[
151, 185, 8, 123, 223, 150, 192, 192, 115, 10, 3, 129, 49, 179, 31, 108, 0, 17, 46,
231, 184, 164, 247, 228, 22, 142, 87, 70, 120, 111, 154, 15, 245, 110, 32, 84, 53, 117,
239, 93, 89, 119, 32, 17, 39, 250, 198, 137, 6, 95, 137, 202, 54, 244, 238, 190, 11,
217, 237, 95, 72, 59, 140, 56, 3, 42, 61, 195, 192, 101, 46, 204, 207, 75, 70, 176,
207, 48, 24, 195, 248, 234, 178, 168, 54, 109, 19, 189, 51, 52, 120, 69, 248, 226, 102,
91,
])
.unwrap()
.into();
assert_eq!(hash_g2(msg1, b"DUMMY_TEST_DOMAIN"), expected1);
assert_eq!(hash_g2(msg2, b"DUMMY_TEST_DOMAIN"), expected2);
assert_eq!(hash_g2(msg3, b"DUMMY_TEST_DOMAIN"), expected3);
}
}
+3
View File
@@ -20,11 +20,14 @@ serde_json = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["log"] }
time = { workspace = true }
subtle = { workspace = true }
zeroize = { workspace = true }
nym-crypto = { path = "../crypto", features = ["aead", "hashing"] }
nym-pemstore = { path = "../pemstore" }
nym-sphinx = { path = "../nymsphinx" }
nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] }
nym-task = { path = "../task" }
nym-credentials = { path = "../credentials" }
@@ -15,6 +15,12 @@ use thiserror::Error;
// 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}")]
+20 -1
View File
@@ -19,7 +19,7 @@ pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION;
pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
@@ -27,10 +27,29 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION;
// 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
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
// TODO: could using `Mac` trait here for OutputSize backfire?
// Should hmac itself be exposed, imported and used instead?
pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
pub trait GatewayProtocolVersionExt {
fn supports_aes256_gcm_siv(&self) -> bool;
fn supports_authenticate_v2(&self) -> bool;
}
impl GatewayProtocolVersionExt for Option<u8> {
fn supports_aes256_gcm_siv(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AES_GCM_SIV_PROTOCOL_VERSION
}
fn supports_authenticate_v2(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
}
}
@@ -3,12 +3,14 @@
use crate::SharedKeyUsageError;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::asymmetric::ed25519::SignatureError;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
use nym_sphinx::params::packet_sizes::PacketSize;
use serde::{Deserialize, Serialize};
use std::string::FromUtf8Error;
use thiserror::Error;
use time::OffsetDateTime;
// specific errors (that should not be nested!!) for clients to match on
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)]
@@ -92,7 +94,34 @@ pub enum GatewayRequestsError {
#[error("the provided [v1] credential has invalid number of parameters - {0}")]
InvalidNumberOfEmbededParameters(u32),
#[error("failed to authenticate the client: {0}")]
Authentication(#[from] AuthenticationFailure),
// variant to catch legacy errors
#[error("{0}")]
Other(String),
}
#[derive(Debug, Error)]
pub enum AuthenticationFailure {
#[error(transparent)]
KeyUsageFailure(#[from] SharedKeyUsageError),
#[error("failed to verify provided address ciphertext")]
MalformedCiphertext,
#[error("failed to verify request signature")]
InvalidSignature(#[from] SignatureError),
#[error("the client is not registered")]
NotRegistered,
#[error("the provided request timestamp is excessively skewed. got {received} whilst the server time is {server}")]
ExcessiveTimestampSkew {
received: OffsetDateTime,
server: OffsetDateTime,
},
#[error("the provided request timestamp is smaller or equal to one previously used")]
RequestReuse,
}
@@ -0,0 +1,151 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey};
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::iter;
use std::time::Duration;
use subtle::ConstantTimeEq;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateRequest {
#[serde(flatten)]
pub content: AuthenticateRequestContent,
pub request_signature: ed25519::Signature,
}
impl AuthenticateRequest {
pub fn new(
protocol_version: u8,
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
) -> Result<AuthenticateRequest, GatewayRequestsError> {
let content = AuthenticateRequestContent::new(
protocol_version,
shared_key,
*identity_keys.public_key(),
)?;
let plaintext = content.plaintext();
let request_signature = identity_keys.private_key().sign(&plaintext);
Ok(AuthenticateRequest {
content,
request_signature,
})
}
pub fn verify_timestamp(
&self,
max_request_timestamp_skew: Duration,
) -> Result<(), AuthenticationFailure> {
let now = OffsetDateTime::now_utc();
if self.content.request_timestamp() < now - max_request_timestamp_skew {
return Err(AuthenticationFailure::ExcessiveTimestampSkew {
received: self.content.request_timestamp(),
server: now,
});
}
if self.content.request_timestamp() - max_request_timestamp_skew > now {
return Err(AuthenticationFailure::ExcessiveTimestampSkew {
received: self.content.request_timestamp(),
server: now,
});
}
Ok(())
}
pub fn ensure_timestamp_not_reused(
&self,
previous: OffsetDateTime,
) -> Result<(), AuthenticationFailure> {
if self.content.request_timestamp() <= previous {
return Err(AuthenticationFailure::RequestReuse);
}
Ok(())
}
pub fn verify_ciphertext(
&self,
shared_key: &SharedGatewayKey,
) -> Result<(), AuthenticationFailure> {
let expected = shared_key.encrypt(
self.content
.client_identity
.derive_destination_address()
.as_bytes_ref(),
Some(&self.content.nonce),
)?;
if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) {
return Err(AuthenticationFailure::MalformedCiphertext);
}
Ok(())
}
pub fn verify_signature(&self) -> Result<(), AuthenticationFailure> {
let plaintext = self.content.plaintext();
self.content
.client_identity
.verify(plaintext, &self.request_signature)
.map_err(Into::into)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateRequestContent {
pub protocol_version: u8,
// this is identical to the client's address
pub client_identity: ed25519::PublicKey,
#[serde(with = "nym_serde_helpers::base64")]
pub address_ciphertext: Vec<u8>,
#[serde(with = "nym_serde_helpers::base64")]
pub nonce: Vec<u8>,
pub request_unix_timestamp: u64,
}
impl AuthenticateRequestContent {
fn new(
protocol_version: u8,
shared_key: &SharedGatewayKey,
client_identity: ed25519::PublicKey,
) -> Result<AuthenticateRequestContent, GatewayRequestsError> {
let nonce = shared_key.random_nonce_or_iv();
let destination_address = client_identity.derive_destination_address();
let address_ciphertext =
shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?;
let now = OffsetDateTime::now_utc();
Ok(AuthenticateRequestContent {
protocol_version,
client_identity,
address_ciphertext,
nonce,
request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970...
})
}
}
impl AuthenticateRequestContent {
pub fn plaintext(&self) -> Vec<u8> {
iter::once(self.protocol_version)
.chain(self.client_identity.to_bytes())
.chain(self.address_ciphertext.iter().copied())
.chain(self.nonce.iter().copied())
.chain(self.request_unix_timestamp.to_be_bytes())
.collect()
}
pub fn request_timestamp(&self) -> OffsetDateTime {
OffsetDateTime::from_unix_timestamp(self.request_unix_timestamp as i64)
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
}
}
@@ -2,16 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
use crate::models::CredentialSpendingRequest;
use crate::text_request::authenticate::AuthenticateRequest;
use crate::{
GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
INITIAL_PROTOCOL_VERSION,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::ed25519;
use nym_sphinx::DestinationAddressBytes;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tungstenite::Message;
pub mod authenticate;
// wrapper for all encrypted requests for ease of use
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
@@ -68,6 +73,9 @@ pub enum ClientControlRequest {
enc_address: String,
iv: String,
},
AuthenticateV2(Box<AuthenticateRequest>),
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
@@ -123,9 +131,22 @@ impl ClientControlRequest {
})
}
pub fn new_authenticate_v2(
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
) -> Result<Self, GatewayRequestsError> {
// if we're using v2 authentication, we must announce at least that protocol version
let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION;
Ok(ClientControlRequest::AuthenticateV2(Box::new(
AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?,
)))
}
pub fn name(&self) -> String {
match self {
ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(),
ClientControlRequest::AuthenticateV2(..) => "AuthenticateV2".to_string(),
ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
"RegisterHandshakeInitRequest".to_string()
}
@@ -0,0 +1,7 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
ALTER TABLE shared_keys
ADD COLUMN last_used_authentication TIMESTAMP WITHOUT TIME ZONE;
+14
View File
@@ -200,6 +200,20 @@ impl GatewayStorage {
Ok(())
}
pub async fn update_last_used_authentication_timestamp(
&self,
client_id: i64,
last_used_authentication_timestamp: OffsetDateTime,
) -> Result<(), GatewayStorageError> {
self.shared_key_manager
.update_last_used_authentication_timestamp(
client_id,
last_used_authentication_timestamp,
)
.await?;
Ok(())
}
pub async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
let client = self.client_manager.get_client(client_id).await?;
Ok(client)
+1 -1
View File
@@ -14,13 +14,13 @@ pub struct Client {
#[derive(FromRow)]
pub struct PersistedSharedKeys {
#[allow(dead_code)]
pub client_id: i64,
#[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 last_used_authentication: Option<OffsetDateTime>,
}
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
+21 -7
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::models::PersistedSharedKeys;
use time::OffsetDateTime;
#[derive(Clone)]
pub(crate) struct SharedKeysManager {
@@ -68,6 +69,22 @@ impl SharedKeysManager {
Ok(())
}
pub(crate) async fn update_last_used_authentication_timestamp(
&self,
client_id: i64,
last_used: OffsetDateTime,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;",
last_used,
client_id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve shared keys stored for the particular client.
///
/// # Arguments
@@ -77,13 +94,10 @@ impl SharedKeysManager {
&self,
client_address_bs58: &str,
) -> Result<Option<PersistedSharedKeys>, sqlx::Error> {
sqlx::query_as!(
PersistedSharedKeys,
"SELECT * FROM shared_keys WHERE client_address_bs58 = ?",
client_address_bs58
)
.fetch_optional(&self.connection_pool)
.await
sqlx::query_as("SELECT * FROM shared_keys WHERE client_address_bs58 = ?")
.bind(client_address_bs58)
.fetch_optional(&self.connection_pool)
.await
}
/// Removes from the database shared keys derived with the particular client.
+7 -1
View File
@@ -21,6 +21,12 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }
mime = { workspace = true }
nym-bin-common = { path = "../bin-common" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
@@ -32,4 +38,4 @@ workspace = true
features = ["tokio"]
[dev-dependencies]
tokio = { workspace = true, features=["rt", "macros"] }
tokio = { workspace = true, features = ["rt", "macros"] }
+112 -11
View File
@@ -1,3 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! DNS resolver configuration for internal lookups.
//!
//! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints
@@ -9,19 +12,35 @@
//!
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the
//! `hickory-resolver` crate
//!
//!
//! Note: The hickory DoH resolver can cause warning logs about H2 connection failure. This
//! indicates that the long lived https connection was closed by the remote peer and the resolver
//! will have to reconnect. It should not impact actual functionality.
//!
//! code ref: https://github.com/hickory-dns/hickory-dns/blob/06a8b1ce9bd9322d8e6accf857d30257e1274427/crates/proto/src/h2/h2_client_stream.rs#L534
//!
//! example log:
//!
//! ```txt
//! WARN /home/ubuntu/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hickory-proto-0.24.3/src/h2/h2_client_stream.rs:493: h2 connection failed: unexpected end of file
//! ```
#![deny(missing_docs)]
use crate::ClientBuilder;
use std::{net::SocketAddr, sync::Arc};
use std::{
net::SocketAddr,
sync::{Arc, LazyLock},
};
use hickory_resolver::lookup_ip::LookupIp;
use hickory_resolver::{
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts},
error::ResolveError,
lookup_ip::LookupIpIntoIter,
TokioAsyncResolver,
};
use hickory_resolver::{error::ResolveErrorKind, lookup_ip::LookupIp};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tracing::warn;
@@ -30,6 +49,13 @@ impl ClientBuilder {
/// Override the DNS resolver implementation used by the underlying http client.
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
self.use_secure_dns = false;
self
}
/// Override the DNS resolver implementation used by the underlying http client.
pub fn no_hickory_dns(mut self) -> Self {
self.use_secure_dns = false;
self
}
}
@@ -38,6 +64,14 @@ struct SocketAddrs {
iter: LookupIpIntoIter,
}
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
tracing::debug!("Initializing shared DNS resolver");
HickoryDnsResolver::default()
});
#[derive(Debug, thiserror::Error)]
#[error("hickory-dns resolver error: {hickory_error}")]
/// Error occurring while resolving a hostname into an IP address.
@@ -47,29 +81,62 @@ pub struct HickoryDnsError {
}
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
///
/// Typical use involves instantiating using the `Default` implementation and then resolving using
/// methods or trait implementations.
///
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
/// internal `AsyncResolver`.
#[derive(Debug, Default, Clone)]
pub struct HickoryDnsResolver {
/// Since we might not have been called in the context of a
/// Tokio Runtime in initialization, so we must delay the actual
/// construction of the resolver.
// Since we might not have been called in the context of a
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioAsyncResolver>>,
fallback: Arc<OnceCell<TokioAsyncResolver>>,
dont_use_shared: bool,
}
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let fallback = self.fallback.clone();
let independent = self.dont_use_shared;
Box::pin(async move {
let resolver = resolver.get_or_try_init(new_resolver)?;
let resolver = resolver.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
})?;
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name.as_str()).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
warn!("primary DNS failed w/ error {e}: using system fallback");
let resolver = fallback.get_or_try_init(new_resolver_system)?;
match e.kind() {
ResolveErrorKind::NoRecordsFound { .. } => {}
_ => {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
}
let resolver = fallback.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.get_or_try_init(new_resolver_system)?
.clone())
}
})?;
resolver.lookup_ip(name.as_str()).await?
}
};
@@ -93,21 +160,55 @@ impl Iterator for SocketAddrs {
impl HickoryDnsResolver {
/// Attempt to resolve a domain name to a set of ['IpAddr']s
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
let resolver = self.state.get_or_try_init(new_resolver)?;
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
warn!("primary DNS failed w/ error {e}: using system fallback");
let resolver = self.fallback.get_or_try_init(new_resolver_system)?;
match e.kind() {
ResolveErrorKind::NoRecordsFound { .. } => {}
_ => {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
}
let resolver = self
.fallback
.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
}
};
Ok(lookup)
}
/// Create a (lazy-initialized) resolver that is not shared across threads.
pub fn thread_resolver() -> Self {
Self {
dont_use_shared: true,
..Default::default()
}
}
fn new_resolver(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
if self.dont_use_shared {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
}
fn new_resolver_system(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
if self.dont_use_shared {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.get_or_try_init(new_resolver_system)?
.clone())
}
}
}
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
+84 -34
View File
@@ -147,13 +147,13 @@ use thiserror::Error;
use tracing::{instrument, warn};
use url::Url;
use http::HeaderMap;
pub use reqwest::IntoUrl;
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
pub use reqwest::IntoUrl;
mod user_agent;
pub use user_agent::UserAgent;
@@ -210,6 +210,12 @@ pub enum HttpClientError<E: Display = String> {
#[error("failed to resolve request. status: '{status}', additional error message: {error}")]
EndpointFailure { status: StatusCode, error: E },
#[error("failed to decode response body: {source} from {content}")]
ResponseDecodeFailure {
source: serde_json::Error,
content: String,
},
#[cfg(target_arch = "wasm32")]
#[error("the request has timed out")]
RequestTimeout,
@@ -222,6 +228,8 @@ pub struct ClientBuilder {
timeout: Option<Duration>,
custom_user_agent: bool,
reqwest_client_builder: reqwest::ClientBuilder,
#[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool,
}
impl ClientBuilder {
@@ -233,37 +241,46 @@ impl ClientBuilder {
U: IntoUrl,
E: Display,
{
// a naive check: if the provided URL does not start with http(s), add that scheme
let str_url = url.as_str();
// a naive check: if the provided URL does not start with http(s), add that scheme
if !str_url.starts_with("http") {
let alt = format!("http://{str_url}");
warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ...");
// TODO: or should we maybe default to https?
Self::new(alt)
} else {
#[cfg(target_arch = "wasm32")]
let reqwest_client_builder = reqwest::ClientBuilder::new();
Ok(Self::new_with_url(url.into_url()?))
}
}
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = {
let r = reqwest::ClientBuilder::new()
.dns_resolver(Arc::new(HickoryDnsResolver::default()));
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_url(url: Url) -> Self {
if !url.scheme().starts_with("http") {
warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
}
// Note this is extra as the `gzip` feature for `reqwest` crate should be enabled which
// `"Enable[s] auto gzip decompression by checking the Content-Encoding response header."`
//
// I am going to leave it here anyways so that gzip decompression is attempted even if
// that feature is removed.
r.gzip(true)
};
#[cfg(target_arch = "wasm32")]
let reqwest_client_builder = reqwest::ClientBuilder::new();
Ok(ClientBuilder {
url: url.into_url()?,
timeout: None,
custom_user_agent: false,
reqwest_client_builder,
})
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = {
let r = reqwest::ClientBuilder::new();
// Note this is extra as the `gzip` feature for `reqwest` crate should be enabled which
// `"Enable[s] auto gzip decompression by checking the Content-Encoding response header."`
//
// I am going to leave it here anyways so that gzip decompression is attempted even if
// that feature is removed.
r.gzip(true)
};
ClientBuilder {
url,
timeout: None,
custom_user_agent: false,
reqwest_client_builder,
use_secure_dns: true,
}
}
@@ -319,10 +336,18 @@ impl ClientBuilder {
let mut builder = self
.reqwest_client_builder
.timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT));
// if no custom user agent was set, use a default
if !self.custom_user_agent {
builder =
builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")))
}
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
builder.build()?
};
@@ -349,6 +374,9 @@ pub struct Client {
impl Client {
/// Create a new http `Client`
// no timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed
//
// In order to prevent interference in API requests at the DNS phase we default to a resolver
// that uses DoT and DoH.
pub fn new(base_url: Url, timeout: Option<Duration>) -> Self {
Self::new_url::<_, String>(base_url, timeout).expect(
"we provided valid url and we were unwrapping previous construction errors anyway",
@@ -849,6 +877,26 @@ fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
url
}
fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String {
use encoding_rs::{Encoding, UTF_8};
use mime::Mime;
let content_type = headers
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let encoding_name = content_type
.as_ref()
.and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
.unwrap_or("utf-8");
let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
let (text, _, _) = encoding.decode(bytes);
text.into_owned()
}
/// Attempt to parse a json object from an HTTP response
#[instrument(level = "debug", skip_all)]
pub async fn parse_response<T, E>(res: Response, allow_empty: bool) -> Result<T, HttpClientError<E>>
@@ -864,21 +912,23 @@ where
return Err(HttpClientError::EmptyResponse { status });
}
}
let headers = res.headers().clone();
tracing::trace!("headers: {:?}", headers);
if res.status().is_success() {
#[cfg(debug_assertions)]
{
let text = res.text().await.inspect_err(|err| {
tracing::error!("Couldn't even get response text: {err}");
})?;
tracing::trace!("Result:\n{:#?}", text);
serde_json::from_str(&text)
.map_err(|err| HttpClientError::GenericRequestFailure(err.to_string()))
// internally reqwest is first retrieving bytes and then performing parsing via serde_json
// (and similarly does the same thing for text())
let full = res.bytes().await?;
match serde_json::from_slice(&full) {
Ok(data) => Ok(data),
Err(err) => {
let content = decode_as_text(&full, headers);
Err(HttpClientError::ResponseDecodeFailure {
source: err,
content,
})
}
}
#[cfg(not(debug_assertions))]
Ok(res.json().await?)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound)
} else {
+1
View File
@@ -13,6 +13,7 @@ bincode = { workspace = true }
bytes = { workspace = true }
nym-bin-common = { path = "../bin-common" }
nym-crypto = { path = "../crypto" }
nym-service-provider-requests-common = { path = "../service-provider-requests-common" }
nym-sphinx = { path = "../nymsphinx" }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+9 -9
View File
@@ -2,24 +2,18 @@ use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
// The current version of the protocol.
// The idea here is that we add new request response types at least one version before we start
// using them.
// Also, depending on the version in the client connect message the IPR could respond with a
// matching older version.
pub use v6::request;
pub use v6::response;
pub mod codec;
pub mod sign;
pub mod v6;
pub mod v7;
pub mod v8;
// version 3: initial version
// version 4: IPv6 support
// version 5: Add severity level to info response
// version 6: Increase the available IPs
// version 7: Add signature support (for the future)
pub const CURRENT_VERSION: u8 = 6;
// version 8: Anonymous sends
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
@@ -45,3 +39,9 @@ fn make_bincode_serializer() -> impl bincode::Options {
.with_big_endian()
.with_varint_encoding()
}
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
@@ -1,6 +1,7 @@
use std::time::Duration;
use nym_crypto::asymmetric::identity;
use nym_crypto::asymmetric::ed25519;
use time::OffsetDateTime;
// For reply protection, if a request is older than this, it will be rejected
const MAX_REQUEST_AGE: Duration = Duration::from_secs(10);
@@ -22,29 +23,37 @@ pub enum SignatureError {
#[error("signature verification failed")]
VerificationFailed {
message: String,
error: identity::SignatureError,
error: ed25519::SignatureError,
},
}
pub trait SignedRequest {
fn identity(&self) -> &identity::PublicKey;
fn identity(&self) -> Option<&ed25519::PublicKey>;
fn request(&self) -> Result<Vec<u8>, SignatureError>;
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError>;
fn signature(&self) -> Option<&identity::Signature>;
fn signature(&self) -> Option<&ed25519::Signature>;
fn timestamp(&self) -> time::OffsetDateTime;
fn timestamp(&self) -> OffsetDateTime;
fn verify(&self) -> Result<(), SignatureError> {
let identity = match self.identity() {
Some(identity) => identity,
None => {
// If we are not revealing our identity, we don't need to verify anything
return Ok(());
}
};
if let Some(signature) = self.signature() {
// First check that the request is recent enough
if time::OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE {
if OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE {
return Err(SignatureError::RequestOutOfDate);
}
let request_as_bytes = self.request()?;
let request_as_bytes = self.request_as_bytes()?;
self.identity()
identity
.verify(request_as_bytes, signature)
.map_err(|error| SignatureError::VerificationFailed {
message: "signature verification failed".to_string(),
@@ -1,69 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{v6, v7};
impl From<v7::response::StaticConnectFailureReason> for v6::response::StaticConnectFailureReason {
fn from(failure: v7::response::StaticConnectFailureReason) -> Self {
match failure {
v7::response::StaticConnectFailureReason::RequestedIpAlreadyInUse => {
v6::response::StaticConnectFailureReason::RequestedIpAlreadyInUse
}
v7::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse => {
v6::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse
}
v7::response::StaticConnectFailureReason::OutOfDateTimestamp => {
v6::response::StaticConnectFailureReason::Other("out of date timestamp".to_string())
}
v7::response::StaticConnectFailureReason::Other(reason) => {
v6::response::StaticConnectFailureReason::Other(reason)
}
}
}
}
impl From<v7::response::DynamicConnectFailureReason> for v6::response::DynamicConnectFailureReason {
fn from(failure: v7::response::DynamicConnectFailureReason) -> Self {
match failure {
v7::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse => {
v6::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse
}
v7::response::DynamicConnectFailureReason::NoAvailableIp => {
v6::response::DynamicConnectFailureReason::NoAvailableIp
}
v7::response::DynamicConnectFailureReason::Other(err) => {
v6::response::DynamicConnectFailureReason::Other(err)
}
}
}
}
impl From<v7::response::InfoResponseReply> for v6::response::InfoResponseReply {
fn from(reply: v7::response::InfoResponseReply) -> Self {
match reply {
v7::response::InfoResponseReply::Generic { msg } => {
v6::response::InfoResponseReply::Generic { msg }
}
v7::response::InfoResponseReply::VersionMismatch {
request_version,
response_version,
} => v6::response::InfoResponseReply::VersionMismatch {
request_version,
response_version,
},
v7::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => {
v6::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst }
}
}
}
}
impl From<v7::response::InfoLevel> for v6::response::InfoLevel {
fn from(level: v7::response::InfoLevel) -> Self {
match level {
v7::response::InfoLevel::Info => v6::response::InfoLevel::Info,
v7::response::InfoLevel::Warn => v6::response::InfoLevel::Warn,
v7::response::InfoLevel::Error => v6::response::InfoLevel::Error,
}
}
}
-1
View File
@@ -1,4 +1,3 @@
pub mod conversion;
pub mod request;
pub mod response;
@@ -1,125 +0,0 @@
use time::OffsetDateTime;
use crate::{v6, v7};
impl From<v6::request::IpPacketRequest> for v7::request::IpPacketRequest {
fn from(ip_packet_request: v6::request::IpPacketRequest) -> Self {
Self {
version: 7,
data: ip_packet_request.data.into(),
}
}
}
impl From<v6::request::IpPacketRequestData> for v7::request::IpPacketRequestData {
fn from(ip_packet_request_data: v6::request::IpPacketRequestData) -> Self {
match ip_packet_request_data {
v6::request::IpPacketRequestData::StaticConnect(r) => {
v7::request::IpPacketRequestData::StaticConnect(
v7::request::SignedStaticConnectRequest {
request: r.into(),
signature: None,
},
)
}
v6::request::IpPacketRequestData::DynamicConnect(r) => {
v7::request::IpPacketRequestData::DynamicConnect(
v7::request::SignedDynamicConnectRequest {
request: r.into(),
signature: None,
},
)
}
v6::request::IpPacketRequestData::Disconnect(r) => {
v7::request::IpPacketRequestData::Disconnect(v7::request::SignedDisconnectRequest {
request: r.into(),
signature: None,
})
}
v6::request::IpPacketRequestData::Data(r) => {
v7::request::IpPacketRequestData::Data(r.into())
}
v6::request::IpPacketRequestData::Ping(r) => {
v7::request::IpPacketRequestData::Ping(r.into())
}
v6::request::IpPacketRequestData::Health(r) => {
v7::request::IpPacketRequestData::Health(r.into())
}
}
}
}
impl From<v6::request::StaticConnectRequest> for v7::request::StaticConnectRequest {
fn from(static_connect_request: v6::request::StaticConnectRequest) -> Self {
Self {
request_id: static_connect_request.request_id,
ips: static_connect_request.ips,
reply_to: static_connect_request.reply_to,
reply_to_hops: static_connect_request.reply_to_hops,
reply_to_avg_mix_delays: static_connect_request.reply_to_avg_mix_delays,
buffer_timeout: static_connect_request.buffer_timeout,
timestamp: OffsetDateTime::now_utc(),
}
}
}
#[allow(deprecated)]
impl From<v6::request::DynamicConnectRequest> for v7::request::DynamicConnectRequest {
fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self {
Self {
request_id: dynamic_connect_request.request_id,
reply_to: dynamic_connect_request.reply_to,
reply_to_hops: dynamic_connect_request.reply_to_hops,
reply_to_avg_mix_delays: dynamic_connect_request.reply_to_avg_mix_delays,
buffer_timeout: dynamic_connect_request.buffer_timeout,
timestamp: OffsetDateTime::now_utc(),
}
}
}
impl From<v6::request::DisconnectRequest> for v7::request::SignedDisconnectRequest {
fn from(disconnect_request: v6::request::DisconnectRequest) -> Self {
Self {
request: disconnect_request.into(),
signature: None,
}
}
}
impl From<v6::request::DisconnectRequest> for v7::request::DisconnectRequest {
fn from(disconnect_request: v6::request::DisconnectRequest) -> Self {
Self {
request_id: disconnect_request.request_id,
reply_to: disconnect_request.reply_to,
timestamp: OffsetDateTime::now_utc(),
}
}
}
impl From<v6::request::DataRequest> for v7::request::DataRequest {
fn from(data_request: v6::request::DataRequest) -> Self {
Self {
ip_packets: data_request.ip_packets,
}
}
}
impl From<v6::request::PingRequest> for v7::request::PingRequest {
fn from(ping_request: v6::request::PingRequest) -> Self {
Self {
request_id: ping_request.request_id,
reply_to: ping_request.reply_to,
timestamp: OffsetDateTime::now_utc(),
}
}
}
impl From<v6::request::HealthRequest> for v7::request::HealthRequest {
fn from(health_request: v6::request::HealthRequest) -> Self {
Self {
request_id: health_request.request_id,
reply_to: health_request.reply_to,
timestamp: OffsetDateTime::now_utc(),
}
}
}
-2
View File
@@ -1,6 +1,4 @@
pub mod conversion;
pub mod request;
pub mod response;
pub mod signature;
pub const VERSION: u8 = 7;
+53 -33
View File
@@ -1,22 +1,18 @@
use std::fmt;
use nym_crypto::asymmetric::identity;
use nym_sphinx::addressing::clients::Recipient;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::{make_bincode_serializer, IpPair};
use super::{
signature::{SignatureError, SignedRequest},
VERSION,
use crate::{
sign::{SignatureError, SignedRequest},
IpPair,
};
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct IpPacketRequest {
pub version: u8,
pub data: IpPacketRequestData,
@@ -30,7 +26,7 @@ impl IpPacketRequest {
reply_to_avg_mix_delays: Option<f64>,
buffer_timeout: Option<u64>,
) -> (Self, u64) {
let request_id = generate_random();
let request_id = crate::generate_random();
(
Self {
version: VERSION,
@@ -58,7 +54,7 @@ impl IpPacketRequest {
reply_to_avg_mix_delays: Option<f64>,
buffer_timeout: Option<u64>,
) -> (Self, u64) {
let request_id = generate_random();
let request_id = crate::generate_random();
(
Self {
version: VERSION,
@@ -79,7 +75,7 @@ impl IpPacketRequest {
}
pub fn new_disconnect_request(reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
let request_id = crate::generate_random();
(
Self {
version: VERSION,
@@ -104,7 +100,7 @@ impl IpPacketRequest {
}
pub fn new_ping(reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
let request_id = crate::generate_random();
(
Self {
version: VERSION,
@@ -119,7 +115,7 @@ impl IpPacketRequest {
}
pub fn new_health_request(reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
let request_id = crate::generate_random();
(
Self {
version: VERSION,
@@ -155,16 +151,27 @@ impl IpPacketRequest {
}
}
pub fn verify(&self) -> Result<(), SignatureError> {
match &self.data {
IpPacketRequestData::StaticConnect(request) => request.verify(),
IpPacketRequestData::DynamicConnect(request) => request.verify(),
IpPacketRequestData::Disconnect(request) => request.verify(),
IpPacketRequestData::Data(_) => Ok(()),
IpPacketRequestData::Ping(_) => Ok(()),
IpPacketRequestData::Health(_) => Ok(()),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
crate::make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
crate::make_bincode_serializer().deserialize(&message.message)
}
}
@@ -179,6 +186,19 @@ pub enum IpPacketRequestData {
Health(HealthRequest),
}
impl fmt::Display for IpPacketRequestData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IpPacketRequestData::StaticConnect(_) => write!(f, "StaticConnect"),
IpPacketRequestData::DynamicConnect(_) => write!(f, "DynamicConnect"),
IpPacketRequestData::Disconnect(_) => write!(f, "Disconnect"),
IpPacketRequestData::Data(_) => write!(f, "Data"),
IpPacketRequestData::Ping(_) => write!(f, "Ping"),
IpPacketRequestData::Health(_) => write!(f, "Health"),
}
}
}
impl IpPacketRequestData {
pub fn add_signature(&mut self, signature: identity::Signature) -> Option<identity::Signature> {
match self {
@@ -202,9 +222,9 @@ impl IpPacketRequestData {
pub fn signable_request(&self) -> Option<Result<Vec<u8>, SignatureError>> {
match self {
IpPacketRequestData::StaticConnect(request) => Some(request.request()),
IpPacketRequestData::DynamicConnect(request) => Some(request.request()),
IpPacketRequestData::Disconnect(request) => Some(request.request()),
IpPacketRequestData::StaticConnect(request) => Some(request.request_as_bytes()),
IpPacketRequestData::DynamicConnect(request) => Some(request.request_as_bytes()),
IpPacketRequestData::Disconnect(request) => Some(request.request_as_bytes()),
IpPacketRequestData::Data(_) => None,
IpPacketRequestData::Ping(_) => None,
IpPacketRequestData::Health(_) => None,
@@ -242,7 +262,7 @@ pub struct StaticConnectRequest {
impl StaticConnectRequest {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
crate::make_bincode_serializer().serialize(self)
}
}
@@ -253,11 +273,11 @@ pub struct SignedStaticConnectRequest {
}
impl SignedRequest for SignedStaticConnectRequest {
fn identity(&self) -> &identity::PublicKey {
self.request.reply_to.identity()
fn identity(&self) -> Option<&identity::PublicKey> {
Some(self.request.reply_to.identity())
}
fn request(&self) -> Result<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
self.request
.to_bytes()
.map_err(|error| SignatureError::RequestSerializationError {
@@ -306,7 +326,7 @@ pub struct DynamicConnectRequest {
impl DynamicConnectRequest {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
crate::make_bincode_serializer().serialize(self)
}
}
@@ -317,11 +337,11 @@ pub struct SignedDynamicConnectRequest {
}
impl SignedRequest for SignedDynamicConnectRequest {
fn identity(&self) -> &identity::PublicKey {
self.request.reply_to.identity()
fn identity(&self) -> Option<&identity::PublicKey> {
Some(self.request.reply_to.identity())
}
fn request(&self) -> Result<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
self.request
.to_bytes()
.map_err(|error| SignatureError::RequestSerializationError {
@@ -355,7 +375,7 @@ pub struct DisconnectRequest {
impl DisconnectRequest {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
crate::make_bincode_serializer().serialize(self)
}
}
@@ -366,11 +386,11 @@ pub struct SignedDisconnectRequest {
}
impl SignedRequest for SignedDisconnectRequest {
fn identity(&self) -> &identity::PublicKey {
self.request.reply_to.identity()
fn identity(&self) -> Option<&identity::PublicKey> {
Some(self.request.reply_to.identity())
}
fn request(&self) -> Result<Vec<u8>, SignatureError> {
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
self.request
.to_bytes()
.map_err(|error| SignatureError::RequestSerializationError {
+4
View File
@@ -0,0 +1,4 @@
pub mod request;
pub mod response;
pub const VERSION: u8 = 8;
+304
View File
@@ -0,0 +1,304 @@
use std::fmt;
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct IpPacketRequest {
pub protocol: Protocol,
pub data: IpPacketRequestData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum IpPacketRequestData {
Data(DataRequest),
Control(Box<ControlRequest>),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ControlRequest {
Connect(ConnectRequest),
Disconnect(DisconnectRequest),
Ping(PingRequest),
Health(HealthRequest),
}
// A data request is when the client wants to send an IP packet to a destination.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DataRequest {
pub ip_packets: bytes::Bytes,
}
// A dynamic connect request is when the client does not provide the internal IP address it will use
// on the ip packet router, and instead requests one to be assigned to it.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ConnectRequest {
pub request_id: u64,
// The maximum time in milliseconds the IPR should wait when filling up a mix packet
// with ip packets.
pub buffer_timeout: Option<u64>,
// Timestamp of when the request was sent by the client.
pub timestamp: OffsetDateTime,
}
// A disconnect request is when the client wants to disconnect from the ip packet router and free
// up the allocated IP address.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DisconnectRequest {
pub request_id: u64,
// Timestamp of when the request was sent by the client.
pub timestamp: OffsetDateTime,
}
// A ping request is when the client wants to check if the ip packet router is still alive.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PingRequest {
pub request_id: u64,
// Timestamp of when the request was sent by the client.
pub timestamp: OffsetDateTime,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct HealthRequest {
pub request_id: u64,
// Timestamp of when the request was sent by the client.
pub timestamp: OffsetDateTime,
}
impl IpPacketRequest {
pub fn new_connect_request(buffer_timeout: Option<u64>) -> (Self, u64) {
let protocol = Protocol {
version: VERSION,
service_provider_type: ServiceProviderType::IpPacketRouter,
};
let request_id = rand::random();
let timestamp = OffsetDateTime::now_utc();
let connect = ConnectRequest {
request_id,
buffer_timeout,
timestamp,
};
let request = Self {
protocol,
data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(connect))),
};
(request, request_id)
}
pub fn new_disconnect_request() -> (Self, u64) {
let protocol = Protocol {
version: VERSION,
service_provider_type: ServiceProviderType::IpPacketRouter,
};
let request_id = rand::random();
let timestamp = OffsetDateTime::now_utc();
let disconnect = DisconnectRequest {
request_id,
timestamp,
};
let request = Self {
protocol,
data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect(disconnect))),
};
(request, request_id)
}
pub fn new_data_request(ip_packets: bytes::Bytes) -> Self {
Self {
protocol: Protocol {
version: VERSION,
service_provider_type: ServiceProviderType::IpPacketRouter,
},
data: IpPacketRequestData::Data(DataRequest { ip_packets }),
}
}
pub fn new_ping() -> (Self, u64) {
let protocol = Protocol {
version: VERSION,
service_provider_type: ServiceProviderType::IpPacketRouter,
};
let request_id = rand::random();
let timestamp = OffsetDateTime::now_utc();
let ping_request = PingRequest {
request_id,
timestamp,
};
let request = Self {
protocol,
data: IpPacketRequestData::Control(Box::new(ControlRequest::Ping(ping_request))),
};
(request, request_id)
}
pub fn new_health_request() -> (Self, u64) {
let protocol = Protocol {
version: VERSION,
service_provider_type: ServiceProviderType::IpPacketRouter,
};
let request_id = rand::random();
let timestamp = OffsetDateTime::now_utc();
let health_request = HealthRequest {
request_id,
timestamp,
};
let request = Self {
protocol,
data: IpPacketRequestData::Control(Box::new(ControlRequest::Health(health_request))),
};
(request, request_id)
}
pub fn id(&self) -> Option<u64> {
match self.data {
IpPacketRequestData::Control(ref c) => Some(c.id()),
IpPacketRequestData::Data(_) => None,
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
crate::make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
crate::make_bincode_serializer().deserialize(&message.message)
}
}
impl fmt::Display for IpPacketRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"IpPacketRequest {{ version: {}, data: {} }}",
self.protocol.version, self.data
)
}
}
impl fmt::Display for IpPacketRequestData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IpPacketRequestData::Data(_) => write!(f, "Data"),
IpPacketRequestData::Control(c) => write!(f, "Control({})", c),
}
}
}
impl ControlRequest {
fn id(&self) -> u64 {
match self {
ControlRequest::Connect(request) => request.request_id,
ControlRequest::Disconnect(request) => request.request_id,
ControlRequest::Ping(request) => request.request_id,
ControlRequest::Health(request) => request.request_id,
}
}
}
impl fmt::Display for ControlRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ControlRequest::Connect(_) => write!(f, "Connect"),
ControlRequest::Disconnect(_) => write!(f, "Disconnect"),
ControlRequest::Ping(_) => write!(f, "Ping"),
ControlRequest::Health(_) => write!(f, "Health"),
}
}
}
impl ConnectRequest {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
crate::make_bincode_serializer().serialize(self)
}
}
impl DisconnectRequest {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
crate::make_bincode_serializer().serialize(self)
}
}
#[cfg(test)]
mod tests {
use time::macros::datetime;
use super::*;
#[test]
fn check_size_of_request() {
let connect = IpPacketRequest {
protocol: Protocol {
version: 4,
service_provider_type: ServiceProviderType::IpPacketRouter,
},
data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(ConnectRequest {
request_id: 123,
buffer_timeout: None,
timestamp: datetime!(2024-01-01 12:59:59.5 UTC),
}))),
};
assert_eq!(connect.to_bytes().unwrap().len(), 21);
}
#[test]
fn check_size_of_data() {
let data = IpPacketRequest {
protocol: Protocol {
version: 4,
service_provider_type: ServiceProviderType::IpPacketRouter,
},
data: IpPacketRequestData::Data(DataRequest {
ip_packets: bytes::Bytes::from(vec![1u8; 32]),
}),
};
assert_eq!(data.to_bytes().unwrap().len(), 36);
}
#[test]
fn serialize_and_deserialize_data_request() {
let data = IpPacketRequest {
protocol: Protocol {
version: 4,
service_provider_type: ServiceProviderType::IpPacketRouter,
},
data: IpPacketRequestData::Data(DataRequest {
ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
}),
};
let serialized = data.to_bytes().unwrap();
let deserialized = IpPacketRequest::from_reconstructed_message(
&nym_sphinx::receiver::ReconstructedMessage {
message: serialized,
sender_tag: None,
},
)
.unwrap();
assert_eq!(deserialized.protocol.version, 4);
assert_eq!(
deserialized.protocol.service_provider_type,
ServiceProviderType::IpPacketRouter
);
assert_eq!(
deserialized.data,
IpPacketRequestData::Data(DataRequest {
ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
})
);
}
}
@@ -0,0 +1,219 @@
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use serde::{Deserialize, Serialize};
use crate::{make_bincode_serializer, IpPair};
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IpPacketResponse {
pub version: u8,
pub data: IpPacketResponseData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum IpPacketResponseData {
Data(DataResponse),
Control(Box<ControlResponse>),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DataResponse {
pub ip_packet: bytes::Bytes,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ControlResponse {
// Response for a connect request
Connect(ConnectResponse),
// Response for a disconnect initiqated by the client
Disconnect(DisconnectResponse),
// Message from the server that the client got disconnected without the client initiating it
UnrequestedDisconnect(UnrequestedDisconnect),
// Response to ping request
Pong(PongResponse),
// Response for a health request
Health(Box<HealthResponse>),
// Info response. This can be anything from informative messages to errors
Info(InfoResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConnectResponse {
pub request_id: u64,
pub reply: ConnectResponseReply,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ConnectResponseReply {
Success(ConnectSuccess),
Failure(ConnectFailureReason),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConnectSuccess {
pub ips: IpPair,
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum ConnectFailureReason {
#[error("client is already connected")]
ClientAlreadyConnected,
#[error("no available ip address")]
NoAvailableIp,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DisconnectResponse {
pub request_id: u64,
pub reply: DisconnectResponseReply,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DisconnectResponseReply {
Success,
Failure(DisconnectFailureReason),
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum DisconnectFailureReason {
#[error("client is not connected")]
ClientNotConnected,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UnrequestedDisconnect {
pub reason: UnrequestedDisconnectReason,
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum UnrequestedDisconnectReason {
#[error("client mixnet traffic timeout")]
ClientMixnetTrafficTimeout,
#[error("client tun traffic timeout")]
ClientTunTrafficTimeout,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PongResponse {
pub request_id: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HealthResponse {
pub request_id: u64,
pub reply: HealthResponseReply,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HealthResponseReply {
// Return the binary build information of the IPR
pub build_info: BinaryBuildInformationOwned,
// Return if the IPR has performed a successful routing test.
pub routable: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InfoResponse {
pub request_id: u64,
pub reply: InfoResponseReply,
pub level: InfoLevel,
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum InfoResponseReply {
#[error("{msg}")]
Generic { msg: String },
#[error(
"version mismatch: response is v{request_version} and response is v{response_version}"
)]
VersionMismatch {
request_version: u8,
response_version: u8,
},
#[error("destination failed exit policy filter check: {dst}")]
ExitPolicyFilterCheckFailed { dst: String },
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InfoLevel {
Info,
Warn,
Error,
}
impl IpPacketResponse {
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
Self {
version: VERSION,
data: IpPacketResponseData::Data(DataResponse { ip_packet }),
}
}
pub fn id(&self) -> Option<u64> {
match &self.data {
IpPacketResponseData::Data(_) => None,
IpPacketResponseData::Control(response) => response.id(),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
}
impl IpPacketResponseData {
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
impl ControlResponse {
fn id(&self) -> Option<u64> {
match self {
ControlResponse::Connect(response) => Some(response.request_id),
ControlResponse::Disconnect(response) => Some(response.request_id),
ControlResponse::UnrequestedDisconnect(_) => None,
ControlResponse::Pong(response) => Some(response.request_id),
ControlResponse::Health(response) => Some(response.request_id),
ControlResponse::Info(response) => Some(response.request_id),
}
}
}
impl ConnectResponseReply {
pub fn is_success(&self) -> bool {
match self {
ConnectResponseReply::Success(_) => true,
ConnectResponseReply::Failure(_) => false,
}
}
}
+2 -2
View File
@@ -15,10 +15,10 @@ bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental",
bincode.workspace = true
cfg-if.workspace = true
itertools = { workspace = true }
digest = "0.9"
digest = { workspace = true }
rand = { workspace = true }
thiserror = { workspace = true }
sha2 = "0.9"
sha2 = { workspace = true }
bs58 = { workspace = true }
serde = { workspace = true, features = ["derive"] }
rayon = { workspace = true, optional = true }
+73 -6
View File
@@ -113,17 +113,13 @@ const G1_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-BLS12381G1_XMD:SHA-256_SS
const SCALAR_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-expander-SHA256";
pub fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve([msg], G1_HASH_DOMAIN)
}
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
let mut output = vec![Scalar::zero()];
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
msg.as_ref(),
SCALAR_HASH_DOMAIN,
&mut output,
);
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>, _>([msg], SCALAR_HASH_DOMAIN, &mut output);
output[0]
}
@@ -401,4 +397,75 @@ mod tests {
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
}
#[test]
fn test_hash_to_scalar() {
let msg1 = "foo";
let expected1 = Scalar::from_bytes(&[
253, 57, 224, 227, 175, 195, 226, 82, 46, 175, 33, 126, 171, 239, 255, 92, 108, 168, 6,
79, 90, 11, 235, 236, 221, 10, 85, 133, 42, 81, 95, 30,
])
.unwrap();
let msg2 = "bar";
let expected2 = Scalar::from_bytes(&[
48, 83, 69, 52, 42, 18, 135, 244, 211, 190, 160, 196, 118, 154, 24, 126, 0, 125, 72,
201, 170, 225, 123, 201, 52, 120, 171, 132, 235, 182, 20, 26,
])
.unwrap();
let msg3 = [
33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123,
250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27,
115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101,
60, 42, 92, 128, 131, 161, 43,
];
let expected3 = Scalar::from_bytes(&[
128, 189, 8, 43, 186, 55, 52, 61, 171, 196, 159, 177, 162, 100, 27, 143, 85, 83, 218,
171, 91, 220, 155, 25, 7, 38, 2, 36, 4, 93, 136, 4,
])
.unwrap();
assert_eq!(hash_to_scalar(msg1), expected1);
assert_eq!(hash_to_scalar(msg2), expected2);
assert_eq!(hash_to_scalar(msg3), expected3);
}
#[test]
fn test_hash_to_g1() {
let msg1 = "foo";
let expected1 = G1Affine::from_compressed(&[
161, 109, 186, 0, 192, 221, 83, 87, 71, 31, 120, 201, 185, 35, 62, 239, 46, 120, 117,
150, 191, 227, 128, 161, 78, 201, 207, 167, 86, 181, 229, 115, 2, 6, 178, 16, 251, 118,
219, 115, 184, 96, 2, 10, 31, 63, 150, 70,
])
.unwrap()
.into();
let msg2 = "bar";
let expected2 = G1Affine::from_compressed(&[
135, 102, 204, 42, 221, 49, 209, 192, 250, 87, 59, 255, 197, 93, 37, 113, 38, 2, 154,
233, 68, 234, 206, 182, 121, 212, 166, 210, 74, 155, 190, 33, 203, 237, 176, 60, 249,
241, 53, 170, 18, 168, 49, 35, 1, 151, 205, 174,
])
.unwrap()
.into();
let msg3 = [
33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123,
250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27,
115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101,
60, 42, 92, 128, 131, 161, 43,
];
let expected3 = G1Affine::from_compressed(&[
184, 200, 211, 115, 47, 45, 39, 185, 105, 9, 222, 247, 132, 241, 121, 130, 238, 224,
155, 109, 105, 201, 137, 154, 132, 149, 214, 233, 136, 69, 77, 132, 174, 30, 46, 123,
20, 92, 219, 18, 45, 29, 208, 127, 158, 145, 130, 41,
])
.unwrap()
.into();
assert_eq!(hash_g1(msg1), expected1);
assert_eq!(hash_g1(msg2), expected2);
assert_eq!(hash_g1(msg3), expected3);
}
}
-48
View File
@@ -1,48 +0,0 @@
[package]
name = "nym-coconut"
version = "0.5.0"
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>", "Ania Piotrowska <ania@nymtech.net>"]
edition = "2021"
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] }
itertools = { workspace = true }
digest = "0.9"
rand = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
bs58 = { workspace = true }
sha2 = "0.9"
zeroize = { workspace = true, optional = true }
nym-dkg = { path = "../dkg" }
nym-pemstore = { path = "../pemstore" }
[dependencies.ff]
workspace = true
default-features = false
[dependencies.group]
workspace = true
default-features = false
[dev-dependencies]
criterion = { workspace = true, features = ["html_reports"] }
doc-comment = { workspace = true }
rand_chacha = { workspace = true }
[[bench]]
name = "benchmarks"
harness = false
[features]
key-zeroize = ["zeroize", "bls12_381/zeroize"]
default = []
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
getrandom = { version="0.2", features=["js"] }
-1
View File
@@ -1 +0,0 @@
This project was partially funded through the NGI0 PET Fund, a fund established by NL.net with financial support from the European Commission's NGI programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310.
-360
View File
@@ -1,360 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, Scalar};
use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field;
use group::{Curve, Group};
use nym_coconut::{
aggregate_signature_shares_and_verify, aggregate_verification_keys, blind_sign,
prepare_blind_sign, prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen,
verify_credential, verify_partial_blind_signature, Attribute, BlindedSignature, Parameters,
Signature, SignatureShare, VerificationKey,
};
use rand::seq::SliceRandom;
use std::ops::Neg;
use std::time::Duration;
#[allow(unused)]
fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let gt1 = bls12_381::pairing(g11, g21);
let gt2 = bls12_381::pairing(g12, g22);
assert_eq!(gt1, gt2)
}
#[allow(unused)]
fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let miller_loop_result = multi_miller_loop(&[
(g11, &G2Prepared::from(*g21)),
(&g12.neg(), &G2Prepared::from(*g22)),
]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
#[allow(unused)]
fn bench_pairings(c: &mut Criterion) {
let mut rng = rand::thread_rng();
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let r = Scalar::random(&mut rng);
let s = Scalar::random(&mut rng);
let g11 = (g1 * r).to_affine();
let g21 = (g2 * s).to_affine();
let g21_prep = G2Prepared::from(g21);
let g12 = (g1 * s).to_affine();
let g22 = (g2 * r).to_affine();
let g22_prep = G2Prepared::from(g22);
c.bench_function("double pairing", |b| {
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller in affine", |b| {
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller with prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
});
c.bench_function("multi miller with semi-prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
});
}
#[allow(unused)]
fn multi_miller_pairing_with_prepared(
g11: &G1Affine,
g21: &G2Prepared,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
// the case of being able to prepare G2 generator
#[allow(unused)]
fn multi_miller_pairing_with_semi_prepared(
g11: &G1Affine,
g21: &G2Affine,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result =
multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
#[allow(clippy::too_many_arguments)]
fn unblind_and_aggregate(
params: &Parameters,
blinded_signatures: &[BlindedSignature],
partial_verification_keys: &[VerificationKey],
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
commitment_hash: &G1Projective,
pedersen_commitments_openings: &[Scalar],
verification_key: &VerificationKey,
) -> Signature {
// Unblind all partial signatures
let unblinded_signatures: Vec<Signature> = blinded_signatures
.iter()
.zip(partial_verification_keys.iter())
.map(|(signature, partial_verification_key)| {
signature
.unblind_and_verify(
params,
partial_verification_key,
private_attributes,
public_attributes,
commitment_hash,
pedersen_commitments_openings,
)
.unwrap()
})
.collect();
let unblinded_signature_shares: Vec<SignatureShare> = unblinded_signatures
.iter()
.enumerate()
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
.collect();
let mut attributes = vec![];
attributes.extend_from_slice(private_attributes);
attributes.extend_from_slice(public_attributes);
aggregate_signature_shares_and_verify(
params,
verification_key,
&attributes,
&unblinded_signature_shares,
)
.unwrap()
}
struct BenchCase {
num_authorities: u64,
threshold_p: f32,
num_public_attrs: u32,
num_private_attrs: u32,
}
impl BenchCase {
fn threshold(&self) -> u64 {
(self.num_authorities as f32 * self.threshold_p).round() as u64
}
fn num_attrs(&self) -> u32 {
self.num_public_attrs + self.num_private_attrs
}
}
fn bench_coconut(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-coconut");
group.measurement_time(Duration::from_secs(1000));
let case = BenchCase {
num_authorities: 100,
threshold_p: 0.7,
num_public_attrs: 2,
num_private_attrs: 2,
};
let params = setup(case.num_public_attrs + case.num_private_attrs).unwrap();
random_scalars_refs!(public_attributes, params, case.num_public_attrs as usize);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
// The prepare blind sign is performed by the user
let (pedersen_commitments_openings, blind_sign_request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
// CLIENT BENCHMARK: Data needed to ask for a credential
// Let's benchmark the operations the client has to perform
// to ask for a credential
group.bench_function(
format!(
"[Client] prepare_blind_sign_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap())
},
);
// keys for the validators
let coconut_keypairs = ttp_keygen(&params, case.threshold(), case.num_authorities).unwrap();
// VALIDATOR BENCHMARK: Issue partial credential
// we pick only one key pair, as we want to validate how much does it
// take for a single validator to issue a partial credential
let mut rng = rand::thread_rng();
let keypair = coconut_keypairs.choose(&mut rng).unwrap();
group.bench_function(
format!(
"[Validator] compute_single_blind_sign_for_credential_with_{}_attributes",
case.num_attrs(),
),
|b| {
b.iter(|| {
blind_sign(
&params,
keypair.secret_key(),
&blind_sign_request,
&public_attributes,
)
.unwrap()
})
},
);
// computing all partial credentials
// NOTE: in reality, each validator computes only single signature
let mut blinded_signatures = Vec::new();
for keypair in coconut_keypairs.iter() {
let blinded_signature = blind_sign(
&params,
keypair.secret_key(),
&blind_sign_request,
&public_attributes,
)
.unwrap();
blinded_signatures.push(blinded_signature)
}
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key().clone())
.collect();
// verify a random partial blind signature
let rand_idx = 1;
let random_blind_signature = blinded_signatures.get(rand_idx).unwrap();
let partial_verification_key = verification_keys.get(rand_idx).unwrap();
group.bench_function(
format!(
"verify_partial_blind_signature_{}_private_attributes_{}_public_attributes",
case.num_private_attrs, case.num_public_attrs
),
|b| {
b.iter(|| {
verify_partial_blind_signature(
&params,
blind_sign_request.get_private_attributes_pedersen_commitments(),
&public_attributes,
random_blind_signature,
partial_verification_key,
)
})
},
);
// Lets bench worse case, ie aggregating all
let indices: Vec<u64> = (1..=case.num_authorities).collect();
// aggregate verification keys
let aggr_verification_key =
aggregate_verification_keys(&verification_keys, Some(&indices)).unwrap();
// CLIENT OPERATION: Unblind partial singatures and aggregate into single signature
let aggregated_signature = unblind_and_aggregate(
&params,
&blinded_signatures,
&verification_keys,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
&pedersen_commitments_openings,
&aggr_verification_key,
);
// CLIENT BENCHMARK: aggregate all partial credentials
group.bench_function(
format!(
"[Client] unblind_and_aggregate_partial_credentials_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
unblind_and_aggregate(
&params,
&blinded_signatures,
&verification_keys,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
&pedersen_commitments_openings,
&aggr_verification_key)
})
},
);
// CLIENT OPERATION: Randomize credentials and generate any cryptographic material to verify them
let theta = prove_bandwidth_credential(
&params,
&aggr_verification_key,
&aggregated_signature,
&serial_number,
&binding_number,
)
.unwrap();
// CLIENT BENCHMARK
group.bench_function(
format!(
"[Client] randomize_and_prove_credential_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
prove_bandwidth_credential(
&params,
&aggr_verification_key,
&aggregated_signature,
&serial_number,
&binding_number,
)
.unwrap()
})
},
);
// VERIFIER OPERATION
// Verify credentials
verify_credential(&params, &aggr_verification_key, &theta, &public_attributes);
// VERIFICATION BENCHMARK
group.bench_function(
format!(
"[Verifier] verify_credentials_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
verify_credential(&params, &aggr_verification_key, &theta, &public_attributes)
})
},
);
}
criterion_group!(benches, bench_coconut);
criterion_main!(benches);
-354
View File
@@ -1,354 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::ops::{Deref, Mul};
use bls12_381::{G1Projective, Scalar};
use group::Curve;
use serde_derive::{Deserialize, Serialize};
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::traits::{Base58, Bytable};
use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar};
use crate::Attribute;
/// Type alias for the ephemeral key generated during ElGamal encryption
pub type EphemeralKey = Scalar;
/// Two G1 points representing ElGamal ciphertext
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective);
impl TryFrom<&[u8]> for Ciphertext {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Ciphertext> {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"Ciphertext must be exactly 96 bytes, got {}",
bytes.len()
)));
}
// safety: we just checked for the length so the unwraps are fine
#[allow(clippy::unwrap_used)]
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
#[allow(clippy::unwrap_used)]
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
let c1 = try_deserialize_g1_projective(
c1_bytes,
CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()),
)?;
let c2 = try_deserialize_g1_projective(
c2_bytes,
CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()),
)?;
Ok(Ciphertext(c1, c2))
}
}
impl Ciphertext {
pub fn c1(&self) -> &G1Projective {
&self.0
}
pub fn c2(&self) -> &G1Projective {
&self.1
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Ciphertext> {
Ciphertext::try_from(bytes)
}
}
/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PrivateKey(pub(crate) Scalar);
impl PrivateKey {
/// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve
/// that represents original h^m.
pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective {
let (c1, c2) = &(ciphertext.0, ciphertext.1);
// (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d
c2 - c1 * self.0
}
pub fn public_key(&self, params: &Parameters) -> PublicKey {
PublicKey(params.gen1() * self.0)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
pub fn from_bytes(bytes: &[u8; 32]) -> Result<PrivateKey> {
try_deserialize_scalar(
bytes,
CoconutError::Deserialization(
"Failed to deserialize ElGamal private key - it was not in the canonical form"
.to_string(),
),
)
.map(PrivateKey)
}
}
impl Bytable for PrivateKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
let received = slice.len();
let Ok(arr) = slice.try_into() else {
return Err(CoconutError::UnexpectedArrayLength {
typ: "elgamal::PrivateKey".to_string(),
received,
expected: 32,
});
};
PrivateKey::from_bytes(arr)
}
}
impl Base58 for PrivateKey {}
// TODO: perhaps be more explicit and apart from gamma also store generator and group order?
/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PublicKey(G1Projective);
impl PublicKey {
/// Encrypt encrypts the given message in the form of h^m,
/// where h is a point on the G1 curve using the given public key.
/// The random k is returned alongside the encryption
/// as it is required by the Coconut Scheme to create proofs of knowledge.
pub fn encrypt(
&self,
params: &Parameters,
h: &G1Projective,
msg: &Scalar,
) -> (Ciphertext, EphemeralKey) {
let k = params.random_scalar();
// c1 = g1^k
let c1 = params.gen1() * k;
// c2 = gamma^k * h^m
let c2 = self.0 * k + h * msg;
(Ciphertext(c1, c2), k)
}
pub fn to_bytes(&self) -> [u8; 48] {
self.0.to_affine().to_compressed()
}
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
try_deserialize_g1_projective(
bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed ElGamal public key".to_string(),
),
)
.map(PublicKey)
}
}
impl Bytable for PublicKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().into()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
let received = slice.len();
let Ok(arr) = slice.try_into() else {
return Err(CoconutError::UnexpectedArrayLength {
typ: "elgamal::PublicKey".to_string(),
received,
expected: 48,
});
};
PublicKey::from_bytes(arr)
}
}
impl TryFrom<&[u8]> for PublicKey {
type Error = CoconutError;
fn try_from(slice: &[u8]) -> Result<PublicKey> {
PublicKey::try_from_byte_slice(slice)
}
}
impl Base58 for PublicKey {}
impl Deref for PublicKey {
type Target = G1Projective;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Mul<&'a Scalar> for &PublicKey {
type Output = G1Projective;
fn mul(self, rhs: &'a Scalar) -> Self::Output {
self.0 * rhs
}
}
#[derive(Serialize, Deserialize)]
/// A convenient wrapper for both keys of the ElGamal keypair
pub struct ElGamalKeyPair {
private_key: PrivateKey,
public_key: PublicKey,
}
impl ElGamalKeyPair {
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
pub fn private_key(&self) -> &PrivateKey {
&self.private_key
}
}
/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters]
pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
let private_key = params.random_scalar();
let gamma = params.gen1() * private_key;
ElGamalKeyPair {
private_key: PrivateKey(private_key),
public_key: PublicKey(gamma),
}
}
pub fn compute_attribute_encryption(
params: &Parameters,
private_attributes: &[&Attribute],
pub_key: &PublicKey,
commitment_hash: G1Projective,
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
private_attributes
.iter()
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
.unzip()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keygen() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let expected = params.gen1() * keypair.private_key.0;
let gamma = keypair.public_key.0;
assert_eq!(
expected, gamma,
"Public key, gamma, should be equal to g1^d, where d is the private key"
);
}
#[test]
fn encryption() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(&params, &h, &m);
let expected_c1 = params.gen1() * ephemeral_key;
assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k");
let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m;
assert_eq!(
expected_c2, ciphertext.1,
"c2 should be equal to gamma^k * h^m"
);
}
#[test]
fn decryption() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, _) = keypair.public_key.encrypt(&params, &h, &m);
let dec = keypair.private_key.decrypt(&ciphertext);
let expected = h * m;
assert_eq!(
expected, dec,
"after ElGamal decryption, original h^m should be obtained"
);
}
#[test]
fn private_key_bytes_roundtrip() {
let params = Parameters::default();
let private_key = PrivateKey(params.random_scalar());
let bytes = private_key.to_bytes();
// also make sure it is equivalent to the internal scalar's bytes
assert_eq!(private_key.0.to_bytes(), bytes);
assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap())
}
#[test]
fn public_key_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let public_key = PublicKey(params.gen1() * r);
let bytes = public_key.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes
assert_eq!(public_key.0.to_affine().to_compressed(), bytes);
assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap())
}
#[test]
fn ciphertext_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s);
let bytes = ciphertext.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
ciphertext.0.to_affine().to_compressed(),
ciphertext.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap())
}
}
-69
View File
@@ -1,69 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
/// A `Result` alias where the `Err` case is `coconut_rs::Error`.
pub type Result<T> = std::result::Result<T, CoconutError>;
#[derive(Error, Debug)]
pub enum CoconutError {
#[error("Setup error: {0}")]
Setup(String),
#[error("encountered error during keygen")]
Keygen,
#[error("Issuance related error: {0}")]
Issuance(String),
#[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)]
IssuanceMaxAttributes { max: usize, requested: usize },
#[error("Interpolation error: {0}")]
Interpolation(String),
#[error("Aggregation error: {0}")]
Aggregation(String),
#[error("Unblind error: {0}")]
Unblind(String),
#[error("Verification error: {0}")]
Verification(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error(
"Deserailization error, expected at least {} bytes, got {}",
min,
actual
)]
DeserializationMinLength { min: usize, actual: usize },
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")]
DeserializationInvalidLength {
actual: usize,
target: usize,
modulus_target: usize,
modulus: usize,
object: String,
},
#[error("received an array of unexpected size for deserialization of {typ}. got {received} but expected {expected}")]
UnexpectedArrayLength {
typ: String,
received: usize,
expected: usize,
},
#[error("failed to decode the base58 representation: {0}")]
Base58DecodingFailure(#[from] bs58::decode::Error),
#[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")]
ScalarDeserializationFailure,
#[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")]
G1ProjectiveDeserializationFailure,
}
-15
View File
@@ -1,15 +0,0 @@
use crate::{BlindSignRequest, BlindedSignature, Bytable, VerifyCredentialRequest};
macro_rules! impl_clone {
($struct:ident) => {
impl Clone for $struct {
fn clone(&self) -> Self {
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
}
}
};
}
impl_clone!(BlindSignRequest);
impl_clone!(BlindedSignature);
impl_clone!(VerifyCredentialRequest);
-2
View File
@@ -1,2 +0,0 @@
mod clone;
mod serde;
-57
View File
@@ -1,57 +0,0 @@
use crate::elgamal::PrivateKey;
use crate::scheme::SecretKey;
use crate::{
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, VerificationKey,
VerifyCredentialRequest,
};
use serde::de::Unexpected;
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
macro_rules! impl_serde {
($struct:ident, $visitor:ident) => {
pub struct $visitor {}
impl Serialize for $struct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_bs58())
}
}
impl<'de> Visitor<'de> for $visitor {
type Value = $struct;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "A base58 encoded struct")
}
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
match $struct::try_from_bs58(s) {
Ok(x) => Ok(x),
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
}
}
}
impl<'de> Deserialize<'de> for $struct {
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str($visitor {})
}
}
};
}
impl_serde!(SecretKey, V1);
impl_serde!(VerificationKey, V2);
impl_serde!(PublicKey, V3);
impl_serde!(PrivateKey, V4);
impl_serde!(BlindSignRequest, V5);
impl_serde!(BlindedSignature, V6);
impl_serde!(Signature, V7);
impl_serde!(VerifyCredentialRequest, V8);
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub use bls12_381::Scalar;
pub use elgamal::elgamal_keygen;
pub use elgamal::ElGamalKeyPair;
pub use elgamal::PublicKey;
pub use error::CoconutError;
pub use scheme::aggregation::aggregate_key_shares;
pub use scheme::aggregation::aggregate_signature_shares;
pub use scheme::aggregation::aggregate_signature_shares_and_verify;
pub use scheme::aggregation::aggregate_verification_keys;
pub use scheme::issuance::blind_sign;
pub use scheme::issuance::prepare_blind_sign;
pub use scheme::issuance::sign;
pub use scheme::issuance::verify_partial_blind_signature;
pub use scheme::issuance::BlindSignRequest;
pub use scheme::keygen::keygen;
pub use scheme::keygen::ttp_keygen;
pub use scheme::keygen::KeyPair;
pub use scheme::keygen::SecretKey;
pub use scheme::keygen::VerificationKey;
pub use scheme::keygen::VerificationKeyShare;
pub use scheme::setup::setup;
pub use scheme::setup::Parameters;
pub use scheme::verification::check_vk_pairing;
pub use scheme::verification::prove_bandwidth_credential;
pub use scheme::verification::verify;
pub use scheme::verification::verify_credential;
pub use scheme::verification::BlindedSerialNumber;
pub use scheme::verification::VerifyCredentialRequest;
pub use scheme::BlindedSignature;
pub use scheme::Signature;
pub use scheme::SignatureShare;
pub use scheme::SignerIndex;
pub use traits::Base58;
pub use traits::Bytable;
pub use utils::hash_to_scalar;
pub mod elgamal;
mod error;
mod impls;
mod proofs;
mod scheme;
pub mod tests;
mod traits;
pub mod utils;
pub type Attribute = bls12_381::Scalar;
pub type PrivateAttribute = Attribute;
pub type PublicAttribute = Attribute;
pub use bls12_381::G1Projective;
-619
View File
@@ -1,619 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: look at https://crates.io/crates/merlin to perhaps use it instead?
use std::borrow::Borrow;
use bls12_381::{G1Projective, G2Projective, Scalar};
use digest::generic_array::typenum::Unsigned;
use digest::Digest;
use group::GroupEncoding;
use itertools::izip;
use sha2::Sha256;
use crate::error::{CoconutError, Result};
use crate::scheme::issuance::compute_hash;
use crate::scheme::setup::Parameters;
use crate::scheme::VerificationKey;
use crate::utils::{try_deserialize_scalar, try_deserialize_scalar_vec};
use crate::Attribute;
// as per the reference python implementation
type ChallengeDigest = Sha256;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ProofCmCs {
challenge: Scalar,
response_opening: Scalar,
response_openings: Vec<Scalar>,
response_attributes: Vec<Scalar>,
}
// note: this is slightly different from the reference python implementation
// as we omit the unnecessary string conversion. Instead we concatenate byte
// representations together and hash that.
// note2: G1 and G2 elements are using their compressed representations
// and as per the bls12-381 library all elements are using big-endian form
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
fn compute_challenge<D, I, B>(iter: I) -> Scalar
where
D: Digest,
I: Iterator<Item = B>,
B: AsRef<[u8]>,
{
let mut h = D::new();
for point_representation in iter {
h.update(point_representation);
}
let digest = h.finalize();
// TODO: I don't like the 0 padding here (though it's what we've been using before,
// but we never had a security audit anyway...)
// instead we could maybe use the `from_bytes` variant and adding some suffix
// when computing the digest until we produce a valid scalar.
let mut bytes = [0u8; 64];
let pad_size = 64usize
.checked_sub(D::OutputSize::to_usize())
.unwrap_or_default();
bytes[pad_size..].copy_from_slice(&digest);
Scalar::from_bytes_wide(&bytes)
}
fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
witness - challenge * secret
}
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
where
S: Borrow<Scalar>,
{
debug_assert_eq!(witnesses.len(), secrets.len());
witnesses
.iter()
.zip(secrets.iter())
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
.collect()
}
impl ProofCmCs {
/// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment
/// using the Fiat-Shamir heuristic.
pub(crate) fn construct(
params: &Parameters,
commitment: &G1Projective,
commitment_opening: &Scalar,
commitments: &[G1Projective],
pedersen_commitments_openings: &[Scalar],
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
) -> Self {
// note: this is only called from `prepare_blind_sign` that already checks
// whether private attributes are non-empty and whether we don't have too many
// attributes in total to sign.
// we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len()
// witness creation
let witness_commitment_opening = params.random_scalar();
let witness_pedersen_commitments_openings =
params.n_random_scalars(pedersen_commitments_openings.len());
let witness_attributes = params.n_random_scalars(private_attributes.len());
// recompute h
let h = compute_hash(*commitment, public_attributes);
let hs_bytes = params
.gen_hs()
.iter()
.map(|h| h.to_bytes())
.collect::<Vec<_>>();
let g1 = params.gen1();
// compute commitments
// zkp commitment for the attributes commitment cm
// Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i])
let commitment_attributes = g1 * witness_commitment_opening
+ witness_attributes
.iter()
.zip(params.gen_hs().iter())
.map(|(wm_i, hs_i)| hs_i * wm_i)
.sum::<G1Projective>();
// zkp commitments for the individual attributes
let commitments_attributes = witness_pedersen_commitments_openings
.iter()
.zip(witness_attributes.iter())
.map(|(o_j, m_j)| g1 * o_j + h * m_j)
.collect::<Vec<_>>();
let commitments_bytes = commitments
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
let commitments_attributes_bytes = commitments_attributes
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// compute challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
.chain(std::iter::once(h.to_bytes().as_ref()))
.chain(std::iter::once(commitment.to_bytes().as_ref()))
.chain(commitments_bytes.iter().map(|cm| cm.as_ref()))
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
.chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())),
);
// Responses
let response_opening =
produce_response(&witness_commitment_opening, &challenge, commitment_opening);
let response_openings = produce_responses(
&witness_pedersen_commitments_openings,
&challenge,
&pedersen_commitments_openings.iter().collect::<Vec<_>>(),
);
let response_attributes =
produce_responses(&witness_attributes, &challenge, private_attributes);
ProofCmCs {
challenge,
response_opening,
response_openings,
response_attributes,
}
}
pub(crate) fn verify(
&self,
params: &Parameters,
commitment: &G1Projective,
commitments: &[G1Projective],
public_attributes: &[&Attribute],
) -> bool {
if self.response_attributes.len() != commitments.len() {
return false;
}
// recompute h
let h = compute_hash(*commitment, public_attributes);
let g1 = params.gen1();
let hs_bytes = params
.gen_hs()
.iter()
.map(|h| h.to_bytes())
.collect::<Vec<_>>();
// recompute witnesses commitments
// Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n])
let commitment_attributes = (commitment
- public_attributes
.iter()
.zip(params.gen_hs().iter().skip(self.response_attributes.len()))
.map(|(&pub_attr, hs)| hs * pub_attr)
.sum::<G1Projective>())
* self.challenge
+ g1 * self.response_opening
+ self
.response_attributes
.iter()
.zip(params.gen_hs().iter())
.map(|(res_attr, hs)| hs * res_attr)
.sum::<G1Projective>();
let commitments_attributes = izip!(
commitments.iter(),
self.response_openings.iter(),
self.response_attributes.iter()
)
.map(|(cm_j, r_o_j, r_m_j)| cm_j * self.challenge + g1 * r_o_j + h * r_m_j)
.collect::<Vec<_>>();
let commitments_bytes = commitments
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
let commitments_attributes_bytes = commitments_attributes
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// re-compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
.chain(std::iter::once(h.to_bytes().as_ref()))
.chain(std::iter::once(commitment.to_bytes().as_ref()))
.chain(commitments_bytes.iter().map(|cm| cm.as_ref()))
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
.chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())),
);
challenge == self.challenge
}
// challenge || response opening || openings len || response openings || attributes len ||
// response attributes
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let openings_len = self.response_openings.len() as u64;
let attributes_len = self.response_attributes.len() as u64;
let mut bytes = Vec::with_capacity(16 + (2 + openings_len + attributes_len) as usize * 32);
bytes.extend_from_slice(&self.challenge.to_bytes());
bytes.extend_from_slice(&self.response_opening.to_bytes());
bytes.extend_from_slice(&openings_len.to_le_bytes());
for ro in &self.response_openings {
bytes.extend_from_slice(&ro.to_bytes());
}
bytes.extend_from_slice(&attributes_len.to_le_bytes());
for rm in &self.response_attributes {
bytes.extend_from_slice(&rm.to_bytes());
}
bytes
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
// at the very minimum there must be a single attribute being proven
if bytes.len() < 32 * 4 + 16 || (bytes.len() - 16) % 32 != 0 {
return Err(CoconutError::Deserialization(
"tried to deserialize proof of commitments with bytes of invalid length"
.to_string(),
));
}
let mut idx = 0;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let challenge = try_deserialize_scalar(
&challenge_bytes,
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
let response_opening = try_deserialize_scalar(
&response_opening_bytes,
CoconutError::Deserialization(
"Failed to deserialize the response to the random".to_string(),
),
)?;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let ro_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
idx += 8;
if bytes[idx..].len() < ro_len as usize * 32 + 8 {
return Err(
CoconutError::Deserialization(
"tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()),
);
}
let ro_end = idx + ro_len as usize * 32;
let response_openings = try_deserialize_scalar_vec(
ro_len,
&bytes[idx..ro_end],
CoconutError::Deserialization("Failed to deserialize openings response".to_string()),
)?;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let rm_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap());
let response_attributes = try_deserialize_scalar_vec(
rm_len,
&bytes[ro_end + 8..],
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
)?;
Ok(ProofCmCs {
challenge,
response_opening,
response_openings,
response_attributes,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ProofKappaZeta {
// c
challenge: Scalar,
// responses
response_serial_number: Scalar,
response_binding_number: Scalar,
response_blinder: Scalar,
}
impl ProofKappaZeta {
pub(crate) fn construct(
params: &Parameters,
verification_key: &VerificationKey,
serial_number: &Attribute,
binding_number: &Attribute,
blinding_factor: &Scalar,
blinded_message: &G2Projective,
blinded_serial_number: &G2Projective,
) -> Self {
// create the witnesses
let witness_blinder = params.random_scalar();
let witness_serial_number = params.random_scalar();
let witness_binding_number = params.random_scalar();
let witness_attributes = [witness_serial_number, witness_binding_number];
let beta_bytes = verification_key
.beta_g2
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
// witnesses commitments
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
let commitment_kappa = params.gen2() * witness_blinder
+ verification_key.alpha
+ witness_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(wm_i, beta_i)| beta_i * wm_i)
.sum::<G2Projective>();
// zeta is the public value associated with the serial number
let commitment_zeta = params.gen2() * witness_serial_number;
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen2().to_bytes().as_ref())
.chain(std::iter::once(blinded_message.to_bytes().as_ref()))
.chain(std::iter::once(blinded_serial_number.to_bytes().as_ref()))
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
.chain(beta_bytes.iter().map(|b| b.as_ref()))
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
);
// responses
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
let response_serial_number =
produce_response(&witness_serial_number, &challenge, serial_number);
let response_binding_number =
produce_response(&witness_binding_number, &challenge, binding_number);
ProofKappaZeta {
challenge,
response_serial_number,
response_binding_number,
response_blinder,
}
}
pub(crate) fn private_attributes_len(&self) -> usize {
2
}
pub(crate) fn verify(
&self,
params: &Parameters,
verification_key: &VerificationKey,
kappa: &G2Projective,
zeta: &G2Projective,
) -> bool {
let beta_bytes = verification_key
.beta_g2
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
let response_attributes = [self.response_serial_number, self.response_binding_number];
// re-compute witnesses commitments
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
let commitment_kappa = kappa * self.challenge
+ params.gen2() * self.response_blinder
+ verification_key.alpha * (Scalar::one() - self.challenge)
+ response_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>();
// zeta is the public value associated with the serial number
let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number;
// compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen2().to_bytes().as_ref())
.chain(std::iter::once(kappa.to_bytes().as_ref()))
.chain(std::iter::once(zeta.to_bytes().as_ref()))
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
.chain(beta_bytes.iter().map(|b| b.as_ref()))
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
);
challenge == self.challenge
}
// challenge || response serial number || response binding number || repose blinder
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let attributes_len = 2; // because we have serial number and the binding number
let mut bytes = Vec::with_capacity((1 + attributes_len + 1) as usize * 32);
bytes.extend_from_slice(&self.challenge.to_bytes());
bytes.extend_from_slice(&self.response_serial_number.to_bytes());
bytes.extend_from_slice(&self.response_binding_number.to_bytes());
bytes.extend_from_slice(&self.response_blinder.to_bytes());
bytes
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
// at the very minimum there must be a single attribute being proven
if bytes.len() != 128 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len(),
modulus: 32,
object: "kappa and zeta".to_string(),
target: 32 * 4,
});
}
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let challenge_bytes = bytes[..32].try_into().unwrap();
let challenge = try_deserialize_scalar(
&challenge_bytes,
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
let response_serial_number = try_deserialize_scalar(
serial_number_bytes,
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
)?;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
let response_binding_number = try_deserialize_scalar(
binding_number_bytes,
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
)?;
// safety: bound checked + constant offset
#[allow(clippy::unwrap_used)]
let blinder_bytes = bytes[96..].try_into().unwrap();
let response_blinder = try_deserialize_scalar(
&blinder_bytes,
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
)?;
Ok(ProofKappaZeta {
challenge,
response_serial_number,
response_binding_number,
response_blinder,
})
}
}
// proof builder:
// - commitment
// - challenge
// - responses
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::keygen::keygen;
use crate::scheme::setup::setup;
use crate::scheme::verification::{compute_kappa, compute_zeta};
use crate::tests::helpers::random_scalars_refs;
use group::Group;
use rand::thread_rng;
#[test]
fn proof_cm_cs_bytes_roundtrip() {
let mut rng = thread_rng();
let params = setup(1).unwrap();
let cm = G1Projective::random(&mut rng);
let r = params.random_scalar();
let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)];
let rs = params.n_random_scalars(1);
random_scalars_refs!(private_attributes, params, 1);
// 0 public 1 private
let pi_s = ProofCmCs::construct(&params, &cm, &r, &cms, &rs, &private_attributes, &[]);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
let params = setup(2).unwrap();
let cm = G1Projective::random(&mut rng);
let r = params.random_scalar();
let cms: [G1Projective; 2] = [
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
];
let rs = params.n_random_scalars(2);
random_scalars_refs!(private_attributes, params, 2);
// 0 public 2 privates
let pi_s = ProofCmCs::construct(&params, &cm, &r, &cms, &rs, &private_attributes, &[]);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
}
#[test]
fn proof_kappa_zeta_bytes_roundtrip() {
let params = setup(4).unwrap();
let keypair = keygen(&params);
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
let serial_number = &params.random_scalar();
let binding_number = &params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let r = params.random_scalar();
let kappa = compute_kappa(&params, keypair.verification_key(), &private_attributes, r);
let zeta = compute_zeta(&params, serial_number);
// 0 public 2 private
let pi_v = ProofKappaZeta::construct(
&params,
keypair.verification_key(),
serial_number,
binding_number,
&r,
&kappa,
&zeta,
);
let proof_bytes = pi_v.to_bytes();
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
assert_eq!(proof_from_bytes, pi_v);
// 2 public 2 private
let params = setup(4).unwrap();
let keypair = keygen(&params);
let pi_v = ProofKappaZeta::construct(
&params,
keypair.verification_key(),
serial_number,
binding_number,
&r,
&kappa,
&zeta,
);
let proof_bytes = pi_v.to_bytes();
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
assert_eq!(proof_from_bytes, pi_v);
}
}
-432
View File
@@ -1,432 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::iter::Sum;
use core::ops::Mul;
use bls12_381::{G2Prepared, G2Projective, Scalar};
use group::Curve;
use itertools::Itertools;
use crate::error::{CoconutError, Result};
use crate::scheme::verification::check_bilinear_pairing;
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
use crate::utils::perform_lagrangian_interpolation_at_origin;
use crate::{Attribute, Parameters, VerificationKeyShare};
pub(crate) trait Aggregatable: Sized {
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
// if aggregation is a threshold one, all indices should be unique
indices.iter().unique_by(|&index| index).count() == indices.len()
}
}
// includes `VerificationKey`
impl<T> Aggregatable for T
where
T: Sum,
for<'a> T: Sum<&'a T>,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
if aggregatable.is_empty() {
return Err(CoconutError::Aggregation("Empty set of values".to_string()));
}
if let Some(indices) = indices {
if !Self::check_unique_indices(indices) {
return Err(CoconutError::Aggregation("Non-unique indices".to_string()));
}
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
} else {
// non-threshold
Ok(aggregatable.iter().sum())
}
}
}
impl Aggregatable for PartialSignature {
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
let h = sigs
.first()
.ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))?
.sig1();
// TODO: is it possible to avoid this allocation?
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
Ok(Signature(*h, aggr_sigma))
}
}
/// Ensures all provided verification keys were generated to verify the same number of attributes.
fn check_same_key_size(keys: &[VerificationKey]) -> bool {
keys.iter().map(|vk| vk.beta_g1.len()).all_equal()
&& keys.iter().map(|vk| vk.beta_g2.len()).all_equal()
}
pub fn aggregate_verification_keys(
keys: &[VerificationKey],
indices: Option<&[SignerIndex]>,
) -> Result<VerificationKey> {
if !check_same_key_size(keys) {
return Err(CoconutError::Aggregation(
"Verification keys are of different sizes".to_string(),
));
}
Aggregatable::aggregate(keys, indices)
}
pub fn aggregate_key_shares(shares: &[VerificationKeyShare]) -> Result<VerificationKey> {
let (keys, indices): (Vec<_>, Vec<_>) = shares
.iter()
.map(|share| (share.key.clone(), share.index))
.unzip();
aggregate_verification_keys(&keys, Some(&indices))
}
pub fn aggregate_signatures(
signatures: &[PartialSignature],
indices: Option<&[SignerIndex]>,
) -> Result<Signature> {
Aggregatable::aggregate(signatures, indices)
}
pub fn aggregate_signatures_and_verify(
params: &Parameters,
verification_key: &VerificationKey,
attributes: &[&Attribute],
signatures: &[PartialSignature],
indices: Option<&[SignerIndex]>,
) -> Result<Signature> {
// aggregate the signature
let signature = aggregate_signatures(signatures, indices)?;
// Verify the signature
let alpha = verification_key.alpha;
let tmp = attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(&attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
if bool::from(signature.0.is_identity()) {
return Err(CoconutError::Aggregation(
"Verification of the aggregated signature failed - h is an identity point".to_string(),
));
}
if !check_bilinear_pairing(
&signature.0.to_affine(),
&G2Prepared::from((alpha + tmp).to_affine()),
&signature.1.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CoconutError::Aggregation(
"Verification of the aggregated signature failed".to_string(),
));
}
Ok(signature)
}
pub fn aggregate_signature_shares(shares: &[SignatureShare]) -> Result<Signature> {
let (signatures, indices): (Vec<_>, Vec<_>) = shares
.iter()
.map(|share| (*share.signature(), share.index()))
.unzip();
aggregate_signatures(&signatures, Some(&indices))
}
pub fn aggregate_signature_shares_and_verify(
params: &Parameters,
verification_key: &VerificationKey,
attributes: &[&Attribute],
shares: &[SignatureShare],
) -> Result<Signature> {
let (signatures, indices): (Vec<_>, Vec<_>) = shares
.iter()
.map(|share| (*share.signature(), share.index()))
.unzip();
aggregate_signatures_and_verify(
params,
verification_key,
attributes,
&signatures,
Some(&indices),
)
}
#[cfg(test)]
mod tests {
use crate::scheme::issuance::sign;
use crate::scheme::keygen::ttp_keygen;
use crate::scheme::verification::verify;
use crate::tests::helpers::random_scalars_refs;
use bls12_381::G1Projective;
use group::Group;
use super::*;
#[test]
fn key_aggregation_works_for_any_subset_of_keys() {
let params = Parameters::new(2).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let vks = keypairs
.into_iter()
.map(|keypair| keypair.verification_key().clone())
.collect::<Vec<_>>();
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
assert_eq!(aggr_vk1, aggr_vk2);
// TODO: should those two actually work or not?
// aggregating threshold+1
let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
assert_eq!(aggr_vk1, aggr_more);
// aggregating all
let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
assert_eq!(aggr_all, aggr_vk1);
// not taking enough points (threshold was 3)
let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
assert_ne!(aggr_not_enough, aggr_vk1);
// taking wrong index
let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap();
assert_ne!(aggr_vk1, aggr_bad);
}
#[test]
fn key_aggregation_doesnt_work_for_empty_set_of_keys() {
let keys: Vec<VerificationKey> = vec![];
assert!(aggregate_verification_keys(&keys, None).is_err());
}
#[test]
fn key_aggregation_doesnt_work_if_indices_have_invalid_length() {
let keys = vec![VerificationKey::identity(3)];
assert!(aggregate_verification_keys(&keys, Some(&[])).is_err());
assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err());
}
#[test]
fn key_aggregation_doesnt_work_for_non_unique_indices() {
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)];
assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err());
}
#[test]
fn key_aggregation_doesnt_work_for_keys_of_different_size() {
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)];
assert!(aggregate_verification_keys(&keys, None).is_err())
}
#[test]
fn signature_aggregation_works_for_any_subset_of_signatures() {
let params = Parameters::new(2).unwrap();
random_scalars_refs!(attributes, params, 2);
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| {
(
keypair.secret_key().clone(),
keypair.verification_key().clone(),
)
})
.unzip();
let sigs = sks
.iter()
.map(|sk| sign(sk, &attributes).unwrap())
.collect::<Vec<_>>();
// aggregating (any) threshold works
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
let aggr_sig1 = aggregate_signatures_and_verify(
&params,
&aggr_vk_1,
&attributes,
&sigs[..3],
Some(&[1, 2, 3]),
)
.unwrap();
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
let aggr_sig2 = aggregate_signatures_and_verify(
&params,
&aggr_vk_1,
&attributes,
&sigs[2..],
Some(&[3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_sig1, aggr_sig2);
// verify credential for good measure
assert!(verify(&params, &aggr_vk_1, &attributes, &aggr_sig1));
assert!(verify(&params, &aggr_vk_2, &attributes, &aggr_sig2));
// aggregating threshold+1 works
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
let aggr_more = aggregate_signatures_and_verify(
&params,
&aggr_vk_more,
&attributes,
&sigs[1..],
Some(&[2, 3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_sig1, aggr_more);
// aggregating all
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
let aggr_all = aggregate_signatures_and_verify(
&params,
&aggr_vk_all,
&attributes,
&sigs,
Some(&[1, 2, 3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_all, aggr_sig1);
// not taking enough points (threshold was 3) should fail
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
let aggr_not_enough = aggregate_signatures_and_verify(
&params,
&aggr_vk_not_enough,
&attributes,
&sigs[..2],
Some(&[1, 2]),
)
.unwrap();
assert_ne!(aggr_not_enough, aggr_sig1);
// taking wrong index should fail
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
assert!(aggregate_signatures_and_verify(
&params,
&aggr_vk_bad,
&attributes,
&sigs[2..],
Some(&[42, 123, 100]),
)
.is_err());
}
fn random_signature() -> Signature {
let mut rng = rand::thread_rng();
Signature(
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
)
}
#[test]
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
let signatures: Vec<Signature> = vec![];
let params = Parameters::new(2).unwrap();
random_scalars_refs!(attributes, params, 2);
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| {
(
keypair.secret_key().clone(),
keypair.verification_key().clone(),
)
})
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(aggregate_signatures_and_verify(
&params,
&aggr_vk_all,
&attributes,
&signatures,
None
)
.is_err());
}
#[test]
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
let signatures = vec![random_signature()];
let params = Parameters::new(2).unwrap();
random_scalars_refs!(attributes, params, 2);
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| {
(
keypair.secret_key().clone(),
keypair.verification_key().clone(),
)
})
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(aggregate_signatures_and_verify(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[])
)
.is_err());
assert!(aggregate_signatures_and_verify(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[1, 2]),
)
.is_err());
}
#[test]
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
let signatures = vec![random_signature(), random_signature()];
let params = Parameters::new(2).unwrap();
random_scalars_refs!(attributes, params, 2);
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| {
(
keypair.secret_key().clone(),
keypair.verification_key().clone(),
)
})
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(aggregate_signatures_and_verify(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[1, 1]),
)
.is_err());
}
// TODO: test for aggregating non-threshold keys
}
@@ -1,79 +0,0 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::{CoconutError, Result};
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g2_projective;
use bls12_381::{G2Affine, G2Projective};
use group::Curve;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct BlindedSerialNumber(G2Projective);
// use custom Debug implementation to show base58 encoding (rather than raw curve elements)
impl Debug for BlindedSerialNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("BlindedSerialNumber")
.field(&self.to_bs58())
.finish()
}
}
impl From<G2Projective> for BlindedSerialNumber {
fn from(value: G2Projective) -> Self {
BlindedSerialNumber(value)
}
}
impl From<G2Affine> for BlindedSerialNumber {
fn from(value: G2Affine) -> Self {
BlindedSerialNumber(value.into())
}
}
impl Deref for BlindedSerialNumber {
type Target = G2Projective;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&[u8]> for BlindedSerialNumber {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Self> {
if bytes.len() != 96 {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize blinded serial number with incorrect number of bytes, expected 96, got {}", bytes.len()),
));
}
// safety: we've just made a check for 96 bytes
#[allow(clippy::unwrap_used)]
let inner = try_deserialize_g2_projective(
&bytes.try_into().unwrap(),
CoconutError::Deserialization(
"failed to deserialize the blinded serial number (zeta)".to_string(),
),
)?;
Ok(BlindedSerialNumber(inner))
}
}
impl Bytable for BlindedSerialNumber {
fn to_byte_vec(&self) -> Vec<u8> {
self.0.to_affine().to_compressed().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Self::try_from(slice)
}
}
impl Base58 for BlindedSerialNumber {}
-660
View File
@@ -1,660 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::ops::Neg;
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Prepared, Scalar};
use group::{Curve, Group, GroupEncoding};
use crate::error::{CoconutError, Result};
use crate::proofs::ProofCmCs;
use crate::scheme::keygen::VerificationKey;
use crate::scheme::setup::Parameters;
use crate::scheme::BlindedSignature;
use crate::scheme::SecretKey;
use crate::Attribute;
use crate::Signature;
// TODO: possibly completely remove those two functions.
// They only exist to have a simpler and smaller code snippets to test
// basic functionalities.
use crate::traits::{Base58, Bytable};
use crate::utils::{hash_g1, try_deserialize_g1_projective};
// TODO NAMING: double check this one
// Lambda
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct BlindSignRequest {
// cm
commitment: G1Projective,
// h
commitment_hash: G1Projective,
// c
private_attributes_commitments: Vec<G1Projective>,
// pi_s
pi_s: ProofCmCs,
}
impl TryFrom<&[u8]> for BlindSignRequest {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
if bytes.len() < 48 + 48 + 8 + 48 {
return Err(CoconutError::DeserializationMinLength {
min: 48 + 48 + 8 + 48,
actual: bytes.len(),
});
}
let mut j = 0;
let commitment_bytes_len = 48;
let commitment_hash_bytes_len = 48;
// safety: we made bound check and we're using constant offest
#[allow(clippy::unwrap_used)]
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
let commitment = try_deserialize_g1_projective(
&cm_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
),
)?;
j += commitment_bytes_len;
// safety: we made bound check and we're using constant offest
#[allow(clippy::unwrap_used)]
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
let commitment_hash = try_deserialize_g1_projective(
&cm_hash_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment hash".to_string(),
),
)?;
j += commitment_hash_bytes_len;
// safety: we made bound check and we're using constant offest
#[allow(clippy::unwrap_used)]
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < c_len as usize * 48 {
return Err(CoconutError::DeserializationMinLength {
min: c_len as usize * 48,
actual: bytes[56..].len(),
});
}
let mut private_attributes_commitments = Vec::with_capacity(c_len as usize);
for i in 0..c_len as usize {
let start = j + i * 48;
let end = start + 48;
if bytes.len() < end {
return Err(CoconutError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
));
}
// safety: we made bound check and we're using constant offest
#[allow(clippy::unwrap_used)]
let private_attributes_commitment_bytes = bytes[start..end].try_into().unwrap();
let private_attributes_commitment = try_deserialize_g1_projective(
&private_attributes_commitment_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
),
)?;
private_attributes_commitments.push(private_attributes_commitment)
}
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 48..])?;
Ok(BlindSignRequest {
commitment,
commitment_hash,
private_attributes_commitments,
pi_s,
})
}
}
impl Bytable for BlindSignRequest {
fn to_byte_vec(&self) -> Vec<u8> {
let cm_bytes = self.commitment.to_affine().to_compressed();
let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed();
let c_len = self.private_attributes_commitments.len() as u64;
let proof_bytes = self.pi_s.to_bytes();
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 48 + proof_bytes.len());
bytes.extend_from_slice(&cm_bytes);
bytes.extend_from_slice(&cm_hash_bytes);
bytes.extend_from_slice(&c_len.to_le_bytes());
for c in &self.private_attributes_commitments {
bytes.extend_from_slice(&c.to_affine().to_compressed());
}
bytes.extend_from_slice(&proof_bytes);
bytes
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
BlindSignRequest::from_bytes(slice)
}
}
impl Base58 for BlindSignRequest {}
impl BlindSignRequest {
fn verify_proof(&self, params: &Parameters, public_attributes: &[&Attribute]) -> bool {
self.pi_s.verify(
params,
&self.commitment,
&self.private_attributes_commitments,
public_attributes,
)
}
pub fn verify_commitment_hash(&self, public_attributes: &[&Attribute]) -> bool {
self.commitment_hash == compute_hash(self.commitment, public_attributes)
}
pub fn get_commitment_hash(&self) -> G1Projective {
self.commitment_hash
}
pub fn get_private_attributes_pedersen_commitments(&self) -> &[G1Projective] {
&self.private_attributes_commitments
}
pub fn to_bytes(&self) -> Vec<u8> {
self.to_byte_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
BlindSignRequest::try_from(bytes)
}
pub fn num_private_attributes(&self) -> usize {
self.private_attributes_commitments.len()
}
}
pub fn compute_attributes_commitment(
params: &Parameters,
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
hs: &[G1Affine],
) -> (Scalar, G1Projective) {
let commitment_opening = params.random_scalar();
// Produces h0 ^ m0 * h1^m1 * .... * hn^mn
// where m0, m1, ...., mn are attributes
let attr_cm = private_attributes
.iter()
.chain(public_attributes.iter())
.zip(hs)
.map(|(&m, h)| h * m)
.sum::<G1Projective>();
// Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn
let commitment = params.gen1() * commitment_opening + attr_cm;
(commitment_opening, commitment)
}
pub fn compute_pedersen_commitments_for_private_attributes(
params: &Parameters,
private_attributes: &[&Attribute],
h: &G1Projective,
) -> (Vec<Scalar>, Vec<G1Projective>) {
// Generate openings for Pedersen commitment for each private attribute
let commitments_openings = params.n_random_scalars(private_attributes.len());
// Compute Pedersen commitment for each private attribute
let pedersen_commitments = commitments_openings
.iter()
.zip(private_attributes.iter())
.map(|(o_j, &m_j)| params.gen1() * o_j + h * m_j)
.collect::<Vec<_>>();
(commitments_openings, pedersen_commitments)
}
pub fn compute_hash(commitment: G1Projective, public_attributes: &[&Attribute]) -> G1Projective {
let mut buff = Vec::new();
buff.extend_from_slice(commitment.to_bytes().as_ref());
for attr in public_attributes {
buff.extend_from_slice(attr.to_bytes().as_ref());
}
hash_g1(buff)
}
/// Builds cryptographic material required for blind sign.
pub fn prepare_blind_sign(
params: &Parameters,
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
) -> Result<(Vec<Scalar>, BlindSignRequest)> {
if private_attributes.is_empty() {
return Err(CoconutError::Issuance(
"Tried to prepare blind sign request for an empty set of private attributes"
.to_string(),
));
}
let hs = params.gen_hs();
if private_attributes.len() + public_attributes.len() > hs.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: hs.len(),
requested: private_attributes.len() + public_attributes.len(),
});
}
let mut commitment_hash;
let mut commitment;
let mut commitment_opening;
loop {
// Compute the attributes commitment
let (c_opening, c) =
compute_attributes_commitment(params, private_attributes, public_attributes, hs);
commitment_opening = c_opening;
commitment = c;
// Compute the commitment hash
commitment_hash = compute_hash(commitment, public_attributes);
// Check if the commitment hash is not the identity point
if !bool::from(commitment_hash.is_identity()) {
break;
}
}
let (pedersen_commitments_openings, pedersen_commitments) =
compute_pedersen_commitments_for_private_attributes(
params,
private_attributes,
&commitment_hash,
);
let pi_s = ProofCmCs::construct(
params,
&commitment,
&commitment_opening,
&pedersen_commitments,
&pedersen_commitments_openings,
private_attributes,
public_attributes,
);
Ok((
pedersen_commitments_openings,
BlindSignRequest {
commitment,
commitment_hash,
private_attributes_commitments: pedersen_commitments,
pi_s,
},
))
}
pub fn blind_sign(
params: &Parameters,
signing_secret_key: &SecretKey,
blind_sign_request: &BlindSignRequest,
public_attributes: &[&Attribute],
) -> Result<BlindedSignature> {
let num_private = blind_sign_request.private_attributes_commitments.len();
let hs = params.gen_hs();
if num_private + public_attributes.len() > hs.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: hs.len(),
requested: num_private + public_attributes.len(),
});
}
// Verify the commitment hash
let h = compute_hash(blind_sign_request.commitment, public_attributes);
if bool::from(blind_sign_request.commitment_hash.is_identity()) {
return Err(CoconutError::Issuance(
"Commitment hash should not be an identity point".to_string(),
));
}
if !(h == blind_sign_request.commitment_hash) {
return Err(CoconutError::Issuance(
"Failed to verify the commitment hash".to_string(),
));
}
// Verify the ZK proof
if !blind_sign_request.verify_proof(params, public_attributes) {
return Err(CoconutError::Issuance(
"Failed to verify the proof of knowledge".to_string(),
));
}
// in python implementation there are n^2 G1 multiplications, let's do it with a single one instead.
// i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes)
// rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately
let signed_public = h * public_attributes
.iter()
.zip(signing_secret_key.ys.iter().skip(num_private))
.map(|(&attr, yi)| attr * yi)
.sum::<Scalar>();
// h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
let sig = blind_sign_request
.private_attributes_commitments
.iter()
.zip(signing_secret_key.ys.iter())
.map(|(c, yi)| c * yi)
.chain(std::iter::once(h * signing_secret_key.x))
.chain(std::iter::once(signed_public))
.sum();
Ok(BlindedSignature(h, sig))
}
/// Verifies a partial blind signature using the provided parameters and validator's verification key.
///
/// # Arguments
///
/// * `params` - A reference to the cryptographic parameters.
/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
/// * `public_attributes` - A reference to the public attributes included in the client's request.
/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
/// * `partial_verification_key` - A reference to the validator's partial verification key.
///
/// # Returns
///
/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
///
/// # Remarks
///
/// This function verifies the correctness and validity of a partial blind signature using
/// the provided cryptographic parameters, blind signature request, blinded signature,
/// and partial verification key.
/// It calculates pairings based on the provided values and checks whether the partial blind signature
/// is consistent with the verification key and commitments in the blind signature request.
/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
pub fn verify_partial_blind_signature(
params: &Parameters,
private_attribute_commitments: &[G1Projective],
public_attributes: &[&Attribute],
blind_sig: &BlindedSignature,
partial_verification_key: &VerificationKey,
) -> bool {
let num_private_attributes = private_attribute_commitments.len();
if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
return false;
}
if bool::from(blind_sig.0.is_identity()) {
return false;
}
// TODO: we're losing some memory here due to extra allocation,
// but worst-case scenario (given SANE amount of attributes), it's just few kb at most
let c_neg = blind_sig.1.to_affine().neg();
let g2_prep = params.prepared_miller_g2();
let mut terms = vec![
// (c^{-1}, g2)
(c_neg, g2_prep.clone()),
// (s, alpha)
(
blind_sig.0.to_affine(),
G2Prepared::from(partial_verification_key.alpha.to_affine()),
),
];
// for each private attribute, add (cm_i, beta_i) to the miller terms
for (private_attr_commit, beta_g2) in private_attribute_commitments
.iter()
.zip(&partial_verification_key.beta_g2)
{
// (cm_i, beta_i)
terms.push((
private_attr_commit.to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
for (&pub_attr, beta_g2) in public_attributes.iter().zip(
partial_verification_key
.beta_g2
.iter()
.skip(num_private_attributes),
) {
// (s^pub_j, beta_j)
terms.push((
(blind_sig.0 * pub_attr).to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// get the references to all the terms to get the arguments the miller loop expects
#[allow(clippy::map_identity)]
let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
// since checking whether e(a, b) == e(c, d)
// is equivalent to checking e(a, b) • e(c, d)^{-1} == id
// and thus to e(a, b) • e(c^{-1}, d) == id
//
// compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
multi_miller_loop(&terms_refs)
.final_exponentiation()
.is_identity()
.into()
}
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
pub fn sign(secret_key: &SecretKey, public_attributes: &[&Attribute]) -> Result<Signature> {
if public_attributes.len() > secret_key.ys.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: secret_key.ys.len(),
requested: public_attributes.len(),
});
}
//Serialize the array structure of the public attributes into a byte array
let mut serialized_attributes = Vec::new();
//Prepend the length of the entire array (in bytes)
let array_len = public_attributes.len() as u64;
serialized_attributes.extend_from_slice(&array_len.to_le_bytes());
//Serialize each attribute with its length
for &attribute in public_attributes.iter() {
let attr_bytes = attribute.to_bytes();
let attr_len = attr_bytes.len() as u64;
// Prefix the attribute with its length
serialized_attributes.extend_from_slice(&attr_len.to_le_bytes());
serialized_attributes.extend_from_slice(&attr_bytes);
}
//Hash the resulting byte array to derive the point H
let h = hash_g1(serialized_attributes);
// x + m0 * y0 + m1 * y1 + ... mn * yn
let exponent = secret_key.x
+ public_attributes
.iter()
.zip(secret_key.ys.iter())
.map(|(&m_i, y_i)| m_i * y_i)
.sum::<Scalar>();
let sig2 = h * exponent;
Ok(Signature(h, sig2))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::keygen::keygen;
use crate::tests::helpers::random_scalars_refs;
#[test]
fn blind_sign_request_bytes_roundtrip() {
// 0 public and 1 private attribute
let params = Parameters::new(1).unwrap();
random_scalars_refs!(private_attributes, params, 1);
random_scalars_refs!(public_attributes, params, 0);
let (_commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let bytes = lambda.to_bytes();
assert_eq!(
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
lambda
);
// 2 public and 2 private attributes
let params = Parameters::new(4).unwrap();
random_scalars_refs!(private_attributes, params, 2);
random_scalars_refs!(public_attributes, params, 2);
let (_commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let bytes = lambda.to_bytes();
assert_eq!(
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
lambda
);
}
#[test]
fn test_prepare_blind_sign_non_identity_commitment_hash() {
let params = Parameters::new(1).unwrap();
random_scalars_refs!(private_attributes, params, 1);
random_scalars_refs!(public_attributes, params, 0);
// Call the function to prepare the blind sign
let result = prepare_blind_sign(&params, &private_attributes, &public_attributes);
// Ensure the result is Ok
assert!(result.is_ok(), "prepare_blind_sign should succeed");
let (_, blind_sign_request) = result.unwrap();
// Ensure the commitment_hash is not the identity point
assert!(
!bool::from(blind_sign_request.commitment_hash.is_identity()),
"commitment_hash should not be the identity point"
);
}
#[test]
fn test_blind_sign_with_identity_commitment_hash() {
let params = Parameters::new(1).unwrap();
random_scalars_refs!(private_attributes, params, 1);
random_scalars_refs!(public_attributes, params, 0);
// Call the function to prepare the blind sign
let (_commitments_openings, blind_sign_request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let blind_sign_request = BlindSignRequest {
commitment_hash: G1Projective::identity(),
..blind_sign_request // This copies the other fields from the existing instance
};
let signing_secret_key = SecretKey {
x: params.random_scalar(),
ys: vec![params.random_scalar()],
};
// Call blind_sign and ensure it returns an error due to identity commitment hash
let result = blind_sign(
&params,
&signing_secret_key,
&blind_sign_request,
&public_attributes,
);
// The result should be an error
assert!(
result.is_err(),
"blind_sign should return an error when commitment_hash is the identity point"
);
}
#[test]
fn successful_verify_partial_blind_signature() {
let params = Parameters::new(4).unwrap();
random_scalars_refs!(private_attributes, params, 2);
random_scalars_refs!(public_attributes, params, 2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let validator_keypair = keygen(&params);
let blind_sig = blind_sign(
&params,
validator_keypair.secret_key(),
&request,
&public_attributes,
)
.unwrap();
assert!(verify_partial_blind_signature(
&params,
&request.private_attributes_commitments,
&public_attributes,
&blind_sig,
validator_keypair.verification_key()
));
}
#[test]
fn successful_verify_partial_blind_signature_no_public_attributes() {
let params = Parameters::new(4).unwrap();
random_scalars_refs!(private_attributes, params, 2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let validator_keypair = keygen(&params);
let blind_sig = blind_sign(&params, validator_keypair.secret_key(), &request, &[]).unwrap();
assert!(verify_partial_blind_signature(
&params,
&request.private_attributes_commitments,
&[],
&blind_sig,
validator_keypair.verification_key()
));
}
#[test]
fn fail_verify_partial_blind_signature_with_wrong_key() {
let params = Parameters::new(4).unwrap();
random_scalars_refs!(private_attributes, params, 2);
random_scalars_refs!(public_attributes, params, 2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let validator_keypair = keygen(&params);
let validator2_keypair = keygen(&params);
let blind_sig = blind_sign(
&params,
validator_keypair.secret_key(),
&request,
&public_attributes,
)
.unwrap();
// this assertion should fail, as we try to verify with a wrong validator key
assert!(!verify_partial_blind_signature(
&params,
&request.private_attributes_commitments,
&public_attributes,
&blind_sig,
validator2_keypair.verification_key()
),);
}
}
-722
View File
@@ -1,722 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::borrow::Borrow;
use core::iter::Sum;
use core::ops::{Add, Mul};
use bls12_381::{G1Projective, G2Projective, Scalar};
use group::Curve;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use serde_derive::{Deserialize, Serialize};
use crate::error::{CoconutError, Result};
use crate::scheme::aggregation::aggregate_verification_keys;
use crate::scheme::setup::Parameters;
use crate::scheme::SignerIndex;
use crate::traits::Bytable;
use crate::utils::{
try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar,
try_deserialize_scalar_vec, Polynomial,
};
use crate::Base58;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
#[cfg_attr(
feature = "key-zeroize",
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
)]
pub struct SecretKey {
pub(crate) x: Scalar,
pub(crate) ys: Vec<Scalar>,
}
impl PemStorableKey for SecretKey {
type Error = CoconutError;
fn pem_type() -> &'static str {
"COCONUT SECRET KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
impl TryFrom<&[u8]> for SecretKey {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
// There should be x and at least one y
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 32 * 2 + 8,
modulus: 32,
object: "secret key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
#[allow(clippy::unwrap_used)]
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
#[allow(clippy::unwrap_used)]
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
let actual_ys_len = (bytes.len() - 40) / 32;
if ys_len as usize != actual_ys_len {
return Err(CoconutError::Deserialization(format!(
"Tried to deserialize secret key with inconsistent ys len (expected {ys_len}, got {actual_ys_len})"
)));
}
let x = try_deserialize_scalar(
&x_bytes,
CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()),
)?;
let ys = try_deserialize_scalar_vec(
ys_len,
&bytes[40..],
CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()),
)?;
Ok(SecretKey { x, ys })
}
}
impl SecretKey {
/// Following a (distributed) key generation process, scalar values can be obtained
/// outside of the normal key generation process.
pub fn create_from_raw(x: Scalar, ys: Vec<Scalar>) -> Self {
Self { x, ys }
}
/// Extract the Scalar copy of the underlying secrets.
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
(self.x, self.ys.clone())
}
pub fn size(&self) -> usize {
self.ys.len()
}
/// Derive verification key using this secret key.
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
let g1 = params.gen1();
let g2 = params.gen2();
VerificationKey {
alpha: g2 * self.x,
beta_g1: self.ys.iter().map(|y| g1 * y).collect(),
beta_g2: self.ys.iter().map(|y| g2 * y).collect(),
}
}
// x || ys.len() || ys
pub fn to_bytes(&self) -> Vec<u8> {
let ys_len = self.ys.len();
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
bytes.extend_from_slice(&self.x.to_bytes());
bytes.extend_from_slice(&ys_len.to_le_bytes());
for y in self.ys.iter() {
bytes.extend_from_slice(&y.to_bytes())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey> {
SecretKey::try_from(bytes)
}
}
impl Bytable for SecretKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
SecretKey::try_from(slice)
}
}
impl Base58 for SecretKey {}
// TODO: perhaps change points to affine representation
// to make verification slightly more efficient?
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct VerificationKey {
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
pub(crate) alpha: G2Projective,
pub(crate) beta_g1: Vec<G1Projective>,
pub(crate) beta_g2: Vec<G2Projective>,
}
impl PemStorableKey for VerificationKey {
type Error = CoconutError;
fn pem_type() -> &'static str {
"COCONUT VERIFICATION KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
impl TryFrom<&[u8]> for VerificationKey {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<VerificationKey> {
// There should be at least alpha, one betaG1 and one betaG2 and their length
if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8 - 96,
target: 96 * 2 + 48 + 8,
modulus: 96 + 48,
object: "verification key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
#[allow(clippy::unwrap_used)]
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
#[allow(clippy::unwrap_used)]
let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
let actual_betas_len = (bytes.len() - 104) / (96 + 48);
if betas_len as usize != actual_betas_len {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize verification key with inconsistent betas len (expected {betas_len}, got {actual_betas_len})"
)));
}
let alpha = try_deserialize_g2_projective(
&alpha_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G2 point (alpha)".to_string(),
),
)?;
let mut beta_g1 = Vec::with_capacity(betas_len as usize);
let mut beta_g1_end: u64 = 0;
for i in 0..betas_len {
let start = (104 + i * 48) as usize;
let end = start + 48;
// we're using a constant 48 byte offset (which is the size of G1 compressed) so unwrap is fine
#[allow(clippy::unwrap_used)]
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g1_projective(
&beta_i_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G1 point (beta)".to_string(),
),
)?;
beta_g1_end = end as u64;
beta_g1.push(beta_i)
}
let mut beta_g2 = Vec::with_capacity(betas_len as usize);
for i in 0..betas_len {
let start = (beta_g1_end + i * 96) as usize;
let end = start + 96;
// we're using a constant 96 byte offset (which is the size of G2 compressed) so unwrap is fine
#[allow(clippy::unwrap_used)]
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g2_projective(
&beta_i_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G2 point (beta)".to_string(),
),
)?;
beta_g2.push(beta_i)
}
Ok(VerificationKey {
alpha,
beta_g1,
beta_g2,
})
}
}
impl<'b> Add<&'b VerificationKey> for VerificationKey {
type Output = VerificationKey;
#[inline]
fn add(self, rhs: &'b VerificationKey) -> VerificationKey {
// If you're trying to add two keys together that were created
// for different number of attributes, just panic as it's a
// nonsense operation.
assert_eq!(
self.beta_g1.len(),
rhs.beta_g1.len(),
"trying to add verification keys generated for different number of attributes [G1]"
);
assert_eq!(
self.beta_g2.len(),
rhs.beta_g2.len(),
"trying to add verification keys generated for different number of attributes [G2]"
);
assert_eq!(
self.beta_g1.len(),
self.beta_g2.len(),
"this key is incorrect - the number of elements G1 and G2 does not match"
);
assert_eq!(
rhs.beta_g1.len(),
rhs.beta_g2.len(),
"they key you want to add is incorrect - the number of elements G1 and G2 does not match"
);
VerificationKey {
alpha: self.alpha + rhs.alpha,
beta_g1: self
.beta_g1
.iter()
.zip(rhs.beta_g1.iter())
.map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1)
.collect(),
beta_g2: self
.beta_g2
.iter()
.zip(rhs.beta_g2.iter())
.map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2)
.collect(),
}
}
}
impl Mul<Scalar> for &VerificationKey {
type Output = VerificationKey;
#[inline]
fn mul(self, rhs: Scalar) -> Self::Output {
VerificationKey {
alpha: self.alpha * rhs,
beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(),
beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(),
}
}
}
impl<T> Sum<T> for VerificationKey
where
T: Borrow<VerificationKey>,
{
#[inline]
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = T>,
{
let mut peekable = iter.peekable();
let head_attributes = match peekable.peek() {
Some(head) => head.borrow().beta_g2.len(),
None => {
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
// of VerificationKey. So should it panic here or just return some nonsense value?
return VerificationKey::identity(0);
}
};
peekable.fold(VerificationKey::identity(head_attributes), |acc, item| {
acc + item.borrow()
})
}
}
impl VerificationKey {
/// Create a (kinda) identity verification key using specified
/// number of 'beta' elements
pub(crate) fn identity(beta_size: usize) -> Self {
VerificationKey {
alpha: G2Projective::identity(),
beta_g1: vec![G1Projective::identity(); beta_size],
beta_g2: vec![G2Projective::identity(); beta_size],
}
}
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
aggregate_verification_keys(sigs, indices)
}
pub fn alpha(&self) -> &G2Projective {
&self.alpha
}
pub fn beta_g1(&self) -> &Vec<G1Projective> {
&self.beta_g1
}
pub fn beta_g2(&self) -> &Vec<G2Projective> {
&self.beta_g2
}
pub fn to_bytes(&self) -> Vec<u8> {
let beta_g1_len = self.beta_g1.len();
let beta_g2_len = self.beta_g2.len();
let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96);
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
for beta_g1 in self.beta_g1.iter() {
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
}
for beta_g2 in self.beta_g2.iter() {
bytes.extend_from_slice(&beta_g2.to_affine().to_compressed())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKey> {
VerificationKey::try_from(bytes)
}
}
impl Bytable for VerificationKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
VerificationKey::try_from(slice)
}
}
impl Base58 for VerificationKey {}
#[derive(Debug, Clone)]
pub struct VerificationKeyShare {
pub key: VerificationKey,
pub index: SignerIndex,
}
impl From<(VerificationKey, SignerIndex)> for VerificationKeyShare {
fn from(value: (VerificationKey, SignerIndex)) -> Self {
VerificationKeyShare {
key: value.0,
index: value.1,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
pub struct KeyPair {
secret_key: SecretKey,
verification_key: VerificationKey,
/// Optional index value specifying polynomial point used during threshold key generation.
pub index: Option<SignerIndex>,
}
impl From<KeyPair> for (SecretKey, VerificationKey) {
fn from(value: KeyPair) -> Self {
(value.secret_key, value.verification_key)
}
}
impl PemStorableKeyPair for KeyPair {
type PrivatePemKey = SecretKey;
type PublicPemKey = VerificationKey;
fn private_key(&self) -> &Self::PrivatePemKey {
&self.secret_key
}
fn public_key(&self) -> &Self::PublicPemKey {
&self.verification_key
}
fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self {
Self::from_keys(secret_key, verification_key)
}
}
impl KeyPair {
const MARKER_BYTES: &'static [u8] = b"coconutkeypair";
pub fn from_keys(secret_key: SecretKey, verification_key: VerificationKey) -> Self {
Self {
secret_key,
verification_key,
index: None,
}
}
pub fn secret_key(&self) -> &SecretKey {
&self.secret_key
}
pub fn verification_key(&self) -> &VerificationKey {
&self.verification_key
}
pub fn to_verification_key_share(&self) -> Option<VerificationKeyShare> {
self.index.map(|index| VerificationKeyShare {
key: self.verification_key.clone(),
index,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
self.to_byte_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
KeyPair::try_from_byte_slice(bytes)
}
}
impl Bytable for KeyPair {
fn to_byte_vec(&self) -> Vec<u8> {
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
let mut byts = vec![];
let secret_key_bytes = self.secret_key.to_bytes();
let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes();
let verification_key_bytes = self.verification_key.to_bytes();
let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes();
byts.extend_from_slice(Self::MARKER_BYTES);
byts.extend_from_slice(&secret_key_len);
byts.extend_from_slice(&secret_key_bytes);
byts.extend_from_slice(&verification_key_len);
byts.extend_from_slice(&verification_key_bytes);
if let Some(index) = self.index {
byts.extend_from_slice(&index.to_le_bytes())
}
byts
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
KeyPair::try_from(slice)
}
}
impl Base58 for KeyPair {}
impl TryFrom<&[u8]> for KeyPair {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<KeyPair> {
let header_len = Self::MARKER_BYTES.len();
// we must be able to at the very least read the length of secret key which is past the header
// and is 8 bytes long
if bytes.len() < header_len + 8 {
return Err(CoconutError::DeserializationMinLength {
min: header_len + 8,
actual: bytes.len(),
});
}
// safety: we made bound check and we're using constant offest
#[allow(clippy::unwrap_used)]
let secret_key_len =
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
let secret_key_start = header_len + 8;
let secret_key =
SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?;
// we must be able to read the length of verification key
if bytes.len() < secret_key_start + secret_key_len + 8 {
return Err(CoconutError::DeserializationMinLength {
min: secret_key_start + secret_key_len + 8,
actual: bytes.len(),
});
}
// safety: we made bound check
#[allow(clippy::unwrap_used)]
let verification_key_len = u64::from_le_bytes(
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
.try_into()
.unwrap(),
) as usize;
let verification_key_start = secret_key_start + secret_key_len + 8;
let verification_key = VerificationKey::try_from(
&bytes[verification_key_start..verification_key_start + verification_key_len],
)?;
let consumed_bytes = verification_key_start + verification_key_len;
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
#[allow(clippy::unwrap_used)]
Some(u64::from_le_bytes(
bytes[consumed_bytes..consumed_bytes + 8]
.try_into()
.unwrap(),
))
} else {
None
};
Ok(KeyPair {
secret_key,
verification_key,
index,
})
}
}
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
///
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
/// that are independent of each other.
pub fn keygen(params: &Parameters) -> KeyPair {
let attributes = params.gen_hs().len();
let x = params.random_scalar();
let ys = params.n_random_scalars(attributes);
let secret_key = SecretKey { x, ys };
let verification_key = secret_key.verification_key(params);
KeyPair {
secret_key,
verification_key,
index: None,
}
}
/// Generate Coconut keypairs.
///
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
/// such that they support threshold aggregation by `threshold` number of parties.
/// It is expected that this procedure is executed by a Trusted Third Party.
pub fn ttp_keygen(
params: &Parameters,
threshold: u64,
num_authorities: u64,
) -> Result<Vec<KeyPair>> {
if threshold == 0 {
return Err(CoconutError::Setup(
"Tried to generate threshold keys with a 0 threshold value".to_string(),
));
}
if threshold > num_authorities {
return Err(
CoconutError::Setup(
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
));
}
let attributes = params.gen_hs().len();
// generate polynomials
let v = Polynomial::new_random(params, threshold - 1);
let ws = (0..attributes)
.map(|_| Polynomial::new_random(params, threshold - 1))
.collect::<Vec<_>>();
// TODO: potentially if we had some known authority identifier we could use that instead
// of the increasing (1,2,3,...) sequence
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
// generate polynomial shares
let x = polynomial_indices
.iter()
.map(|&id| v.evaluate(&Scalar::from(id)));
let ys = polynomial_indices.iter().map(|&id| {
ws.iter()
.map(|w| w.evaluate(&Scalar::from(id)))
.collect::<Vec<_>>()
});
// finally set the keys
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys });
let keypairs = secret_keys
.zip(polynomial_indices.iter())
.map(|(secret_key, index)| {
let verification_key = secret_key.verification_key(params);
KeyPair {
secret_key,
verification_key,
index: Some(*index),
}
})
.collect();
Ok(keypairs)
}
#[cfg(test)]
mod tests {
use crate::scheme::setup::setup;
use super::*;
#[test]
fn keypair_bytes_roundtrip() {
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = keygen(&params1);
let keypair5 = keygen(&params5);
let bytes1 = keypair1.to_bytes();
let bytes5 = keypair5.to_bytes();
assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1);
assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5);
}
#[test]
fn secret_key_bytes_roundtrip() {
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = keygen(&params1);
let keypair5 = keygen(&params5);
let bytes1 = keypair1.secret_key.to_bytes();
let bytes5 = keypair5.secret_key.to_bytes();
assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key);
assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key);
}
#[test]
fn verification_key_bytes_roundtrip() {
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = &keygen(&params1);
let keypair5 = &keygen(&params5);
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
assert_eq!(
VerificationKey::try_from(bytes1.as_slice()).unwrap(),
keypair1.verification_key
);
assert_eq!(
VerificationKey::try_from(bytes5.as_slice()).unwrap(),
keypair5.verification_key
);
}
}
-672
View File
@@ -1,672 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: implement https://crates.io/crates/signature traits?
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
use group::Curve;
pub use keygen::{SecretKey, VerificationKey};
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::scheme::verification::check_bilinear_pairing;
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g1_projective;
use crate::Attribute;
pub mod aggregation;
pub mod double_use;
pub mod issuance;
pub mod keygen;
pub mod setup;
pub mod verification;
pub type SignerIndex = u64;
// (h, s)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective);
pub type PartialSignature = Signature;
impl TryFrom<&[u8]> for Signature {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Signature> {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"Signature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
// safety: we just checked for the length so the unwraps are fine
#[allow(clippy::expect_used)]
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
#[allow(clippy::expect_used)]
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let sig1 = try_deserialize_g1_projective(
sig1_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()),
)?;
let sig2 = try_deserialize_g1_projective(
sig2_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()),
)?;
Ok(Signature(sig1, sig2))
}
}
impl Signature {
pub(crate) fn sig1(&self) -> &G1Projective {
&self.0
}
pub(crate) fn sig2(&self) -> &G1Projective {
&self.1
}
pub fn randomise_simple(&self, params: &Parameters) -> Signature {
let r = params.random_scalar();
Signature(self.0 * r, self.1 * r)
}
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
let r = params.random_scalar();
let r_prime = params.random_scalar();
let h_prime = self.0 * r_prime;
let s_prime = (self.1 * r_prime) + (h_prime * r);
(Signature(h_prime, s_prime), r)
}
pub fn to_bytes(self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
Signature::try_from(bytes)
}
pub fn verify(
&self,
params: &Parameters,
partial_verification_key: &VerificationKey,
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
commitment_hash: &G1Projective,
) -> Result<()> {
// Verify the commitment hash
if bool::from(self.0.is_identity()) {
return Err(CoconutError::Verification(
"Commitment hash should not be an identity point".to_string(),
));
}
if !(commitment_hash == &self.0) {
return Err(CoconutError::Verification(
"Verification of commitment hash from signature failed".to_string(),
));
}
let alpha = partial_verification_key.alpha;
let signed_attributes = private_attributes
.iter()
.chain(public_attributes.iter())
.zip(partial_verification_key.beta_g2.iter())
.map(|(&attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
// Verify the signature share
if !check_bilinear_pairing(
&self.0.to_affine(),
&G2Prepared::from((alpha + signed_attributes).to_affine()),
&self.1.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CoconutError::Unblind(
"Verification of signature share failed".to_string(),
));
}
Ok(())
}
}
impl Bytable for Signature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Signature::from_bytes(slice)
}
}
impl Base58 for Signature {}
#[derive(Debug, PartialEq, Eq)]
pub struct BlindedSignature(G1Projective, G1Projective);
impl Bytable for BlindedSignature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Self::from_bytes(slice)
}
}
impl Base58 for BlindedSignature {}
impl TryFrom<&[u8]> for BlindedSignature {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"BlindedSignature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
// safety: we just checked for the length so the unwraps are fine
#[allow(clippy::expect_used)]
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
#[allow(clippy::expect_used)]
let sig_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let h = try_deserialize_g1_projective(
h_bytes,
CoconutError::Deserialization("Failed to deserialize compressed h".to_string()),
)?;
let sig = try_deserialize_g1_projective(
sig_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig".to_string()),
)?;
Ok(BlindedSignature(h, sig))
}
}
impl BlindedSignature {
pub fn unblind(
&self,
partial_verification_key: &VerificationKey,
pedersen_commitments_openings: &[Scalar],
) -> Signature {
// parse the signature
let h = &self.0;
let c = &self.1;
let blinding_removers = partial_verification_key
.beta_g1
.iter()
.zip(pedersen_commitments_openings.iter())
.map(|(beta, opening)| beta * opening)
.sum::<G1Projective>();
let unblinded_c = c - blinding_removers;
Signature(*h, unblinded_c)
}
pub fn unblind_and_verify(
&self,
params: &Parameters,
partial_verification_key: &VerificationKey,
private_attributes: &[&Attribute],
public_attributes: &[&Attribute],
commitment_hash: &G1Projective,
pedersen_commitments_openings: &[Scalar],
) -> Result<Signature> {
let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings);
unblinded.verify(
params,
partial_verification_key,
private_attributes,
public_attributes,
commitment_hash,
)?;
Ok(unblinded)
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
BlindedSignature::try_from(bytes)
}
}
// perhaps this should take signature by reference? we'll see how it goes
#[derive(Clone, Copy)]
pub struct SignatureShare {
signature: Signature,
index: SignerIndex,
}
impl From<(Signature, SignerIndex)> for SignatureShare {
fn from(value: (Signature, SignerIndex)) -> Self {
SignatureShare {
signature: value.0,
index: value.1,
}
}
}
impl SignatureShare {
pub fn new(signature: Signature, index: SignerIndex) -> Self {
SignatureShare { signature, index }
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub fn index(&self) -> SignerIndex {
self.index
}
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
// aggregate_signature_shares(shares)
// }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash_to_scalar;
use crate::scheme::aggregation::{
aggregate_signatures_and_verify, aggregate_verification_keys,
};
use crate::scheme::issuance::{blind_sign, compute_hash, prepare_blind_sign, sign};
use crate::scheme::keygen::{keygen, ttp_keygen};
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
use crate::tests::helpers::random_scalars_refs;
#[test]
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
let params = Parameters::new(2).unwrap();
random_scalars_refs!(private_attributes, params, 2);
let (_commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&params);
let sig1 = blind_sign(&params, keypair1.secret_key(), &lambda, &[]).unwrap();
let wrong_commitment_opening = params.random_scalar();
let wrong_commitment = params.gen1() * wrong_commitment_opening;
let fake_commitment_hash = compute_hash(wrong_commitment, &[]);
let wrong_commitments_openings = params.n_random_scalars(private_attributes.len());
assert!(sig1
.unblind_and_verify(
&params,
keypair1.verification_key(),
&private_attributes,
&[],
&fake_commitment_hash,
&wrong_commitments_openings,
)
.is_err());
}
#[test]
fn unblind_returns_error_if_signature_verification_fails() {
let params = Parameters::new(2).unwrap();
let p = [hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
let private_attributes = vec![&p[0], &p[1]];
let p2 = [hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
let private_attributes2 = vec![&p2[0], &p2[1]];
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&params);
let sig1 = blind_sign(&params, keypair1.secret_key(), &lambda, &[]).unwrap();
assert!(sig1
.unblind_and_verify(
&params,
keypair1.verification_key(),
&private_attributes2,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.is_err());
}
#[test]
fn verification_on_two_private_attributes() {
let params = Parameters::new(2).unwrap();
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
let keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let sig1 = blind_sign(&params, keypair1.secret_key(), &lambda, &[])
.unwrap()
.unblind_and_verify(
&params,
keypair1.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let sig2 = blind_sign(&params, keypair2.secret_key(), &lambda, &[])
.unwrap()
.unblind_and_verify(
&params,
keypair2.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&params,
keypair1.verification_key(),
&sig1,
&serial_number,
&binding_number,
)
.unwrap();
let theta2 = prove_bandwidth_credential(
&params,
keypair2.verification_key(),
&sig2,
&serial_number,
&binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
keypair1.verification_key(),
&theta1,
&[],
));
assert!(verify_credential(
&params,
keypair2.verification_key(),
&theta2,
&[],
));
assert!(!verify_credential(
&params,
keypair1.verification_key(),
&theta2,
&[],
));
}
#[test]
fn verification_on_two_public_attributes() {
let params = Parameters::new(2).unwrap();
random_scalars_refs!(attributes, params, 2);
let keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let sig1 = sign(keypair1.secret_key(), &attributes).unwrap();
let sig2 = sign(keypair2.secret_key(), &attributes).unwrap();
assert!(verify(
&params,
keypair1.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
keypair2.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
keypair1.verification_key(),
&attributes,
&sig2,
));
}
#[test]
fn verification_on_two_public_and_two_private_attributes() {
let params = Parameters::new(4).unwrap();
random_scalars_refs!(public_attributes, params, 2);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
let keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let sig1 = blind_sign(&params, keypair1.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind_and_verify(
&params,
keypair1.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let sig2 = blind_sign(&params, keypair2.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind_and_verify(
&params,
keypair2.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&params,
keypair1.verification_key(),
&sig1,
&serial_number,
&binding_number,
)
.unwrap();
let theta2 = prove_bandwidth_credential(
&params,
keypair2.verification_key(),
&sig2,
&serial_number,
&binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
keypair1.verification_key(),
&theta1,
&public_attributes,
));
assert!(verify_credential(
&params,
keypair2.verification_key(),
&theta2,
&public_attributes,
));
assert!(!verify_credential(
&params,
keypair1.verification_key(),
&theta2,
&public_attributes,
));
}
#[test]
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
let params = Parameters::new(4).unwrap();
random_scalars_refs!(public_attributes, params, 2);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
let keypairs = ttp_keygen(&params, 2, 3).unwrap();
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let sigs = keypairs
.iter()
.map(|keypair| {
blind_sign(&params, keypair.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind_and_verify(
&params,
keypair.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap()
})
.collect::<Vec<_>>();
let vks = keypairs
.into_iter()
.map(|keypair| keypair.verification_key().clone())
.collect::<Vec<_>>();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
let aggr_sig = aggregate_signatures_and_verify(
&params,
&aggr_vk,
&attributes,
&sigs[..2],
Some(&[1, 2]),
)
.unwrap();
let theta = prove_bandwidth_credential(
&params,
&aggr_vk,
&aggr_sig,
&serial_number,
&binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&aggr_vk,
&theta,
&public_attributes,
));
// taking different subset of keys and credentials
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
let aggr_sig = aggregate_signatures_and_verify(
&params,
&aggr_vk,
&attributes,
&sigs[1..],
Some(&[2, 3]),
)
.unwrap();
let theta = prove_bandwidth_credential(
&params,
&aggr_vk,
&aggr_sig,
&serial_number,
&binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&aggr_vk,
&theta,
&public_attributes,
));
}
#[test]
fn signature_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let signature = Signature(params.gen1() * r, params.gen1() * s);
let bytes = signature.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
signature.0.to_affine().to_compressed(),
signature.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap())
}
#[test]
fn blinded_signature_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let blinded_sig = BlindedSignature(params.gen1() * r, params.gen1() * s);
let bytes = blinded_sig.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
blinded_sig.0.to_affine().to_compressed(),
blinded_sig.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap())
}
}
-91
View File
@@ -1,91 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar};
use ff::Field;
use group::Curve;
use rand::thread_rng;
use crate::error::{CoconutError, Result};
use crate::utils::hash_g1;
/// System-wide parameters used for the protocol
#[derive(Clone)]
pub struct Parameters {
/// Generator of the G1 group
g1: G1Affine,
/// Additional generators of the G1 group
hs: Vec<G1Affine>,
/// Generator of the G2 group
g2: G2Affine,
/// Precomputed G2 generator used for the miller loop
_g2_prepared_miller: G2Prepared,
}
impl Parameters {
pub fn new(num_attributes: u32) -> Result<Parameters> {
if num_attributes == 0 {
return Err(CoconutError::Setup(
"Tried to setup the scheme for 0 attributes".to_string(),
));
}
let hs = (1..=num_attributes)
.map(|i| hash_g1(format!("h{i}")).to_affine())
.collect();
Ok(Parameters {
g1: G1Affine::generator(),
hs,
g2: G2Affine::generator(),
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
})
}
pub fn gen1(&self) -> &G1Affine {
&self.g1
}
pub fn gen2(&self) -> &G2Affine {
&self.g2
}
pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared {
&self._g2_prepared_miller
}
pub fn gen_hs(&self) -> &[G1Affine] {
&self.hs
}
pub fn random_scalar(&self) -> Scalar {
// lazily-initialized thread-local random number generator, seeded by the system
let mut rng = thread_rng();
Scalar::random(&mut rng)
}
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
(0..n).map(|_| self.random_scalar()).collect()
}
}
pub fn setup(num_attributes: u32) -> Result<Parameters> {
Parameters::new(num_attributes)
}
// for ease of use in tests requiring params
// TODO: not sure if this will have to go away when tests require some specific number of generators
#[cfg(test)]
impl Default for Parameters {
fn default() -> Self {
Parameters {
g1: G1Affine::generator(),
hs: Vec::new(),
g2: G2Affine::generator(),
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
}
}
}
@@ -1,432 +0,0 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::{CoconutError, Result};
use crate::proofs::ProofKappaZeta;
use crate::scheme::setup::Parameters;
use crate::scheme::Signature;
use crate::scheme::VerificationKey;
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g2_projective;
use crate::Attribute;
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
use core::ops::Neg;
use group::{Curve, Group};
pub use crate::scheme::double_use::BlindedSerialNumber;
// TODO NAMING: this whole thing
// Theta
#[derive(Debug, PartialEq, Eq)]
pub struct VerifyCredentialRequest {
// blinded_message (kappa)
pub blinded_message: G2Projective,
// blinded serial number (zeta)
pub blinded_serial_number: BlindedSerialNumber,
// sigma
pub credential: Signature,
// pi_v
pub pi_v: ProofKappaZeta,
}
impl TryFrom<&[u8]> for VerifyCredentialRequest {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
if bytes.len() < 288 {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()),
));
}
// safety: we just checked for the length so the unwraps are fine
#[allow(clippy::unwrap_used)]
let blinded_message_bytes = bytes[..96].try_into().unwrap();
let blinded_message = try_deserialize_g2_projective(
&blinded_message_bytes,
CoconutError::Deserialization(
"failed to deserialize the blinded message (kappa)".to_string(),
),
)?;
let blinded_serial_number_bytes = &bytes[96..192];
let blinded_serial_number =
BlindedSerialNumber::try_from_byte_slice(blinded_serial_number_bytes)?;
let credential = Signature::try_from(&bytes[192..288])?;
let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?;
Ok(VerifyCredentialRequest {
blinded_message,
blinded_serial_number,
credential,
pi_v,
})
}
}
impl VerifyCredentialRequest {
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
self.pi_v.verify(
params,
verification_key,
&self.blinded_message,
&self.blinded_serial_number,
)
}
pub fn has_blinded_serial_number(&self, blinded_serial_number_bs58: &str) -> Result<bool> {
let blinded_serial_number = BlindedSerialNumber::try_from_bs58(blinded_serial_number_bs58)?;
let ret = self.blinded_serial_number.eq(&blinded_serial_number);
Ok(ret)
}
// blinded message (kappa) || blinded serial number (zeta) || credential || pi_v
pub fn to_bytes(&self) -> Vec<u8> {
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed();
let credential_bytes = self.credential.to_bytes();
let proof_bytes = self.pi_v.to_bytes();
let mut bytes = Vec::with_capacity(288 + proof_bytes.len());
bytes.extend_from_slice(&blinded_message_bytes);
bytes.extend_from_slice(&blinded_serial_number_bytes);
bytes.extend_from_slice(&credential_bytes);
bytes.extend_from_slice(&proof_bytes);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
VerifyCredentialRequest::try_from(bytes)
}
pub fn blinded_serial_number(&self) -> BlindedSerialNumber {
self.blinded_serial_number
}
pub fn blinded_serial_number_bs58(&self) -> String {
self.blinded_serial_number.to_bs58()
}
}
impl Bytable for VerifyCredentialRequest {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
VerifyCredentialRequest::try_from(slice)
}
}
impl Base58 for VerifyCredentialRequest {}
pub fn compute_kappa(
params: &Parameters,
verification_key: &VerificationKey,
private_attributes: &[&Attribute],
blinding_factor: Scalar,
) -> G2Projective {
params.gen2() * blinding_factor
+ verification_key.alpha
+ private_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(&priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>()
}
pub fn compute_zeta(params: &Parameters, serial_number: &Attribute) -> G2Projective {
params.gen2() * serial_number
}
pub fn prove_bandwidth_credential(
params: &Parameters,
verification_key: &VerificationKey,
signature: &Signature,
serial_number: &Attribute,
binding_number: &Attribute,
) -> Result<VerifyCredentialRequest> {
if verification_key.beta_g2.len() < 2 {
return Err(
CoconutError::Verification(
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
verification_key.beta_g2.len()
)));
}
// Randomize the signature
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
// blinded_message : kappa in the paper.
// Value kappa is needed since we want to show a signature sigma'.
// In order to verify sigma' we need both the verification key vk and the message m.
// However, we do not want to reveal m to whomever we are showing the signature.
// Thus, we need kappa which allows us to verify sigma'. In particular,
// kappa is computed on m as input, but thanks to the use or random value r,
// it does not reveal any information about m.
let private_attributes = [serial_number, binding_number];
let blinded_message = compute_kappa(
params,
verification_key,
&private_attributes,
sign_blinding_factor,
);
// zeta is a commitment to the serial number (i.e., a public value associated with the serial number)
let blinded_serial_number = compute_zeta(params, serial_number);
let pi_v = ProofKappaZeta::construct(
params,
verification_key,
serial_number,
binding_number,
&sign_blinding_factor,
&blinded_message,
&blinded_serial_number,
);
Ok(VerifyCredentialRequest {
blinded_message,
blinded_serial_number: blinded_serial_number.into(),
credential: signature_prime,
pi_v,
})
}
/// Checks whether e(P, Q) * e(-R, S) == id
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
// checking e(P, Q) * e(-R, S) == id
// is equivalent to checking e(P, Q) == e(R, S)
// but requires only a single final exponentiation rather than two of them
// and therefore, as seen via benchmarks.rs, is almost 50% faster
// (1.47ms vs 2.45ms, tested on R9 5900X)
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
multi_miller.final_exponentiation().is_identity().into()
}
pub fn check_vk_pairing(
params: &Parameters,
dkg_values: &[G2Projective],
vk: &VerificationKey,
) -> bool {
let values_len = dkg_values.len();
if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() {
return false;
}
// safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine
#[allow(clippy::unwrap_used)]
if &vk.alpha != *dkg_values.first().as_ref().unwrap() {
return false;
}
let dkg_betas = &dkg_values[1..];
if dkg_betas
.iter()
.zip(vk.beta_g2.iter())
.any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta)
{
return false;
}
if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| {
!check_bilinear_pairing(
params.gen1(),
&G2Prepared::from(g2.to_affine()),
&g1.to_affine(),
params.prepared_miller_g2(),
)
}) {
return false;
}
true
}
pub fn verify_credential(
params: &Parameters,
verification_key: &VerificationKey,
theta: &VerifyCredentialRequest,
public_attributes: &[&Attribute],
) -> bool {
if public_attributes.len() + theta.pi_v.private_attributes_len()
> verification_key.beta_g2.len()
{
return false;
}
if !theta.verify_proof(params, verification_key) {
return false;
}
let kappa = if public_attributes.is_empty() {
theta.blinded_message
} else {
let signed_public_attributes = public_attributes
.iter()
.zip(
verification_key
.beta_g2
.iter()
.skip(theta.pi_v.private_attributes_len()),
)
.map(|(&pub_attr, beta_i)| beta_i * pub_attr)
.sum::<G2Projective>();
theta.blinded_message + signed_public_attributes
};
check_bilinear_pairing(
&theta.credential.0.to_affine(),
&G2Prepared::from(kappa.to_affine()),
&(theta.credential.1).to_affine(),
params.prepared_miller_g2(),
) && !bool::from(theta.credential.0.is_identity())
}
// Used in tests only
pub fn verify(
params: &Parameters,
verification_key: &VerificationKey,
public_attributes: &[&Attribute],
sig: &Signature,
) -> bool {
let kappa = (verification_key.alpha
+ public_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(&m_i, b_i)| b_i * m_i)
.sum::<G2Projective>())
.to_affine();
check_bilinear_pairing(
&sig.0.to_affine(),
&G2Prepared::from(kappa),
&sig.1.to_affine(),
params.prepared_miller_g2(),
) && !bool::from(sig.0.is_identity())
}
#[cfg(test)]
mod tests {
use crate::scheme::issuance::sign;
use crate::scheme::keygen::keygen;
use crate::scheme::setup::setup;
use super::*;
#[test]
fn vk_pairing() {
let params = setup(2).unwrap();
let keypair = keygen(&params);
let vk = keypair.verification_key();
let mut dkg_values = vec![vk.alpha];
dkg_values.append(&mut vk.beta_g2.clone());
assert!(check_vk_pairing(&params, &dkg_values, vk));
}
#[test]
fn theta_bytes_roundtrip() {
let params = setup(2).unwrap();
let keypair = keygen(&params);
let r = params.random_scalar();
let s = params.random_scalar();
let signature = Signature(params.gen1() * r, params.gen1() * s);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let theta = prove_bandwidth_credential(
&params,
keypair.verification_key(),
&signature,
&serial_number,
&binding_number,
)
.unwrap();
let bytes = theta.to_bytes();
assert_eq!(
VerifyCredentialRequest::try_from(bytes.as_slice()).unwrap(),
theta
);
}
#[test]
fn reject_forged_signature_via_linear_combination() {
// This test checks if the protocol correctly rejects forged signatures created
// by linear combinations of valid signatures. The verification for forged
// signatures should fail.
let params = Parameters::new(4).unwrap();
let scalar_2 = Scalar::one() + Scalar::one();
let scalar_2_inv = Scalar::invert(&scalar_2).unwrap();
//#1
let a = params.random_scalar();
let zero = Scalar::zero();
let a_zero = vec![&a, &zero];
let zero_a = vec![&zero, &a];
let validator_keypair = keygen(&params);
//#2
let sig_a_zero = sign(validator_keypair.secret_key(), &a_zero).unwrap();
let sig_zero_a = sign(validator_keypair.secret_key(), &zero_a).unwrap();
assert!(verify(
&params,
validator_keypair.verification_key(),
&a_zero,
&sig_a_zero
));
assert!(verify(
&params,
validator_keypair.verification_key(),
&zero_a,
&sig_zero_a
));
//#3
let h0 = sig_a_zero.0;
// Removed unnecessary references
let h1 = scalar_2_inv * sig_a_zero.1 + scalar_2_inv * sig_zero_a.1;
let forged_signature = Signature(h0, h1);
let a_half = a * scalar_2_inv;
let new_plaintext = vec![&a_half, &a_half];
// The forged signature should not pass verification
assert!(!verify(
&params,
validator_keypair.verification_key(),
&new_plaintext,
&forged_signature
));
//#4
let scalar_3 = Scalar::one() + Scalar::one() + Scalar::one();
let scalar_4 = Scalar::one() + Scalar::one() + Scalar::one() + Scalar::one();
let scalar_4_inv = Scalar::invert(&scalar_4).unwrap();
let scalar_3_over_4 = scalar_3 * scalar_4_inv;
// Removed unnecessary references
let h1_2 = scalar_4_inv * sig_a_zero.1 + scalar_3_over_4 * sig_zero_a.1;
let forged_signature_2 = Signature(h0, h1_2);
let a_quarter = a * scalar_4_inv;
let a_3_over_4 = a * scalar_3_over_4;
let new_plaintext_2 = vec![&a_quarter, &a_3_over_4];
// The second forged signature should also not pass verification
assert!(!verify(
&params,
validator_keypair.verification_key(),
&new_plaintext_2,
&forged_signature_2
));
}
}
-84
View File
@@ -1,84 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::random_scalars_refs;
use crate::tests::helpers::tests::generate_dkg_keys;
use crate::{
aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential,
CoconutError, VerificationKey,
};
#[test]
fn keygen() -> Result<(), CoconutError> {
let params = setup(5)?;
let node_indices = vec![15u64, 248, 33521];
random_scalars_refs!(public_attributes, params, 2);
// generate_keys
let coconut_keypairs = ttp_keygen(&params, 2, 3)?;
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key().clone())
.collect();
// aggregate verification keys
let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?;
// Generate cryptographic material to verify them
let theta = theta_from_keys_and_attributes(
&params,
&coconut_keypairs,
&node_indices,
&public_attributes,
)?;
// Verify credentials
assert!(verify_credential(
&params,
&verification_key,
&theta,
&public_attributes,
));
Ok(())
}
#[test]
#[ignore] // expensive test
fn dkg() -> Result<(), CoconutError> {
let params = setup(5)?;
let node_indices = vec![15u64, 248, 33521];
random_scalars_refs!(public_attributes, params, 2);
// generate_keys
let coconut_keypairs = generate_dkg_keys(5, &node_indices);
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key().clone())
.collect();
// aggregate verification keys
let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?;
// Generate cryptographic material to verify them
let theta = theta_from_keys_and_attributes(
&params,
&coconut_keypairs,
&node_indices,
&public_attributes,
)?;
// Verify credentials
assert!(verify_credential(
&params,
&verification_key,
&theta,
&public_attributes,
));
Ok(())
}
-186
View File
@@ -1,186 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::*;
use itertools::izip;
use std::fmt::Debug;
// unwraps are fine in the test code
#[allow(clippy::unwrap_used)]
pub fn theta_from_keys_and_attributes(
params: &Parameters,
coconut_keypairs: &Vec<KeyPair>,
indices: &[scheme::SignerIndex],
public_attributes: &[&PublicAttribute],
) -> Result<VerifyCredentialRequest, CoconutError> {
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
// generate commitment
let (commitments_openings, blind_sign_request) =
prepare_blind_sign(params, &private_attributes, public_attributes)?;
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key().clone())
.collect();
// aggregate verification keys
let verification_key = aggregate_verification_keys(&verification_keys, Some(indices))?;
// generate blinded signatures
let mut blinded_signatures = Vec::new();
for keypair in coconut_keypairs {
let blinded_signature = blind_sign(
params,
keypair.secret_key(),
&blind_sign_request,
public_attributes,
)?;
blinded_signatures.push(blinded_signature)
}
// Unblind
let unblinded_signatures: Vec<(scheme::SignerIndex, Signature)> = izip!(
indices.iter(),
blinded_signatures.iter(),
verification_keys.iter()
)
.map(|(idx, s, vk)| {
(
*idx,
s.unblind_and_verify(
params,
vk,
&private_attributes,
public_attributes,
&blind_sign_request.get_commitment_hash(),
&commitments_openings,
)
.unwrap(),
)
})
.collect();
// Aggregate signatures
let signature_shares: Vec<SignatureShare> = unblinded_signatures
.iter()
.map(|(idx, signature)| SignatureShare::new(*signature, *idx))
.collect();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(public_attributes);
// Randomize credentials and generate any cryptographic material to verify them
let signature = aggregate_signature_shares_and_verify(
params,
&verification_key,
&attributes,
&signature_shares,
)?;
// Generate cryptographic material to verify them
let theta = prove_bandwidth_credential(
params,
&verification_key,
&signature,
&serial_number,
&binding_number,
)?;
Ok(theta)
}
// unwraps are fine in the test code
#[allow(clippy::unwrap_used)]
pub fn transpose_matrix<T: Debug>(matrix: Vec<Vec<T>>) -> Vec<Vec<T>> {
if matrix.is_empty() {
return vec![];
}
let len = matrix[0].len();
let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect();
(0..len)
.map(|_| {
iters
.iter_mut()
.map(|it| it.next().unwrap())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
#[macro_export]
macro_rules! random_scalars_refs {
( $x: ident, $params: expr, $n: expr ) => {
let _vec = $params.n_random_scalars($n);
#[allow(clippy::map_identity)]
let $x = _vec.iter().collect::<Vec<_>>();
};
}
pub use random_scalars_refs;
#[cfg(test)]
pub mod tests {
use super::*;
use bls12_381::Scalar;
use nym_dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex};
use rand_chacha::rand_core::SeedableRng;
pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec<Scalar> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = nym_dkg::bte::setup();
// the simplest possible case
let threshold = 2;
let mut receivers = std::collections::BTreeMap::new();
let mut full_keys = Vec::new();
for index in node_indices {
let (dk, pk) = nym_dkg::bte::keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(&mut rng, &params, dealer_index, threshold, &receivers, None).0
})
.collect::<Vec<_>>();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
derived_secrets.push(recovered_secret)
}
derived_secrets
}
pub fn generate_dkg_keys(num_attributes: u32, node_indices: &[NodeIndex]) -> Vec<KeyPair> {
let params = Parameters::new(num_attributes).unwrap();
let mut all_secrets = vec![];
for _ in 0..num_attributes {
let secrets = generate_dkg_secrets(node_indices);
all_secrets.push(secrets);
}
let signers = transpose_matrix(all_secrets);
signers
.into_iter()
.map(|mut secrets| {
let x = secrets.pop().unwrap();
let sk = SecretKey::create_from_raw(x, secrets);
let vk = sk.verification_key(&params);
KeyPair::from_keys(sk, vk)
})
.collect()
}
}
-3
View File
@@ -1,3 +0,0 @@
#[cfg(test)]
mod e2e;
pub mod helpers;
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
use crate::CoconutError;
use bls12_381::{G1Affine, G1Projective, Scalar};
use group::GroupEncoding;
pub trait Bytable
where
Self: Sized,
{
fn to_byte_vec(&self) -> Vec<u8>;
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError>;
}
pub trait Base58
where
Self: Bytable,
{
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
let bs58_decoded = &bs58::decode(x.as_ref()).into_vec()?;
Self::try_from_byte_slice(bs58_decoded)
}
fn to_bs58(&self) -> String {
bs58::encode(self.to_byte_vec()).into_string()
}
}
impl Bytable for Scalar {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
let received = slice.len();
let Ok(arr) = slice.try_into() else {
return Err(CoconutError::UnexpectedArrayLength {
typ: "Scalar".to_string(),
received,
expected: 32,
});
};
let maybe_scalar = Scalar::from_bytes(arr);
if maybe_scalar.is_none().into() {
Err(CoconutError::ScalarDeserializationFailure)
} else {
// safety: this unwrap is fine as we've just checked the element is not none
#[allow(clippy::unwrap_used)]
Ok(maybe_scalar.unwrap())
}
}
}
impl Base58 for Scalar {}
impl Bytable for G1Projective {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().as_ref().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
let received = slice.len();
let arr: Result<[u8; 48], _> = slice.try_into();
let Ok(bytes) = arr else {
return Err(CoconutError::UnexpectedArrayLength {
typ: "G1Projective".to_string(),
received,
expected: 48,
});
};
let maybe_g1 = G1Affine::from_compressed(&bytes);
if maybe_g1.is_none().into() {
Err(CoconutError::G1ProjectiveDeserializationFailure)
} else {
// safety: this unwrap is fine as we've just checked the element is not none
#[allow(clippy::unwrap_used)]
Ok(maybe_g1.unwrap().into())
}
}
}
impl Base58 for G1Projective {}
-382
View File
@@ -1,382 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::iter::Sum;
use core::ops::Mul;
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
use ff::Field;
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::scheme::SignerIndex;
pub struct Polynomial {
coefficients: Vec<Scalar>,
}
impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
pub fn new_random(params: &Parameters, degree: u64) -> Self {
Polynomial {
coefficients: params.n_random_scalars((degree + 1) as usize),
}
}
/// Evaluates the polynomial at point x.
pub fn evaluate(&self, x: &Scalar) -> Scalar {
if self.coefficients.is_empty() {
Scalar::zero()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().into() {
// we checked that coefficients are not empty so unwrap here is fine
#[allow(clippy::unwrap_used)]
*self.coefficients.first().unwrap()
} else {
self.coefficients
.iter()
.enumerate()
// coefficient[n] * x ^ n
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
.sum()
}
}
}
#[inline]
fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
let x = Scalar::zero();
points
.iter()
.enumerate()
.map(|(i, point_i)| {
let mut numerator = Scalar::one();
let mut denominator = Scalar::one();
let xi = Scalar::from(*point_i);
for (j, point_j) in points.iter().enumerate() {
if j != i {
let xj = Scalar::from(*point_j);
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
numerator *= x - xj;
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
denominator *= xi - xj;
}
}
// numerator / denominator
numerator * denominator.invert().unwrap()
})
.collect()
}
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
/// It can be used for Scalars, G1 and G2 points.
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
points: &[SignerIndex],
values: &[T],
) -> Result<T>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
if points.is_empty() || values.is_empty() {
return Err(CoconutError::Interpolation(
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
));
}
if points.len() != values.len() {
return Err(CoconutError::Interpolation(
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
.to_string(),
));
}
let coefficients = generate_lagrangian_coefficients_at_origin(points);
Ok(coefficients
.into_iter()
.zip(values.iter())
.map(|(coeff, val)| val * coeff)
.sum())
}
// A temporary way of hashing particular message into G1.
// Implementation idea was taken from `threshold_crypto`:
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
// Eventually it should get replaced by, most likely, the osswu map
// method once ideally it's implemented inside the pairing crate.
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
pub fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
}
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
let mut output = vec![Scalar::zero()];
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
msg.as_ref(),
SCALAR_HASH_DOMAIN,
&mut output,
);
output[0]
}
pub fn try_deserialize_scalar_vec(
expected_len: u64,
bytes: &[u8],
err: CoconutError,
) -> Result<Vec<Scalar>> {
if bytes.len() != expected_len as usize * 32 {
return Err(err);
}
let mut out = Vec::with_capacity(expected_len as usize);
for i in 0..expected_len as usize {
// we just checked we have exactly the amount of bytes we need and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
None => return Err(err),
Some(scalar) => scalar,
};
out.push(s)
}
Ok(out)
}
pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
}
pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result<G1Projective> {
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
.ok_or(err)
.map(G1Projective::from)
}
pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result<G2Projective> {
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
.ok_or(err)
.map(G2Projective::from)
}
// use core::fmt;
// #[cfg(feature = "serde")]
// use serde::de::Visitor;
// #[cfg(feature = "serde")]
// use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
//
// // #[cfg(feature = "serde")]
// #[serde(remote = "Scalar")]
// pub(crate) struct ScalarDef(pub Scalar);
//
// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
//
// impl Serialize for ScalarDef {
// fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
// where
// S: Serializer,
// {
// use serde::ser::SerializeTuple;
// let mut tup = serializer.serialize_tuple(32)?;
// for byte in self.0.to_bytes().iter() {
// tup.serialize_element(byte)?;
// }
// tup.end()
// }
// }
//
// impl<'de> Deserialize<'de> for ScalarDef {
// fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
// where
// D: Deserializer<'de>,
// {
// struct ScalarVisitor;
//
// impl<'de> Visitor<'de> for ScalarVisitor {
// type Value = ScalarDef;
//
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
// formatter.write_str("a 32-byte canonical bls12_381 scalar")
// }
//
// fn visit_seq<A>(self, mut seq: A) -> core::result::Result<ScalarDef, A::Error>
// where
// A: serde::de::SeqAccess<'de>,
// {
// let mut bytes = [0u8; 32];
// for i in 0..32 {
// bytes[i] = seq
// .next_element()?
// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?;
// }
//
// let res = Scalar::from_bytes(&bytes);
// if res.is_some().into() {
// Ok(ScalarDef(res.unwrap()))
// } else {
// Err(serde::de::Error::custom(
// &"scalar was not canonically encoded",
// ))
// }
// }
// }
//
// deserializer.deserialize_tuple(32, ScalarVisitor)
// }
// }
//
// #[cfg(feature = "serde")]
// pub(crate) struct G1ProjectiveSerdeHelper(Scalar);
//
// #[cfg(feature = "serde")]
// pub(crate) struct G2ProjectiveSerdeHelper(Scalar);
#[cfg(test)]
mod tests {
use rand::RngCore;
use super::*;
#[test]
fn polynomial_evaluation() {
// y = 42 (it should be 42 regardless of x)
let poly = Polynomial {
coefficients: vec![Scalar::from(42)],
};
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = Polynomial {
coefficients: vec![Scalar::from(10), Scalar::from(1)],
};
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let poly = Polynomial {
coefficients: vec![
(-Scalar::from(3)),
Scalar::from(2),
(-Scalar::from(5)),
Scalar::zero(),
Scalar::from(1),
],
};
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
// empty polynomial
let poly = Polynomial {
coefficients: vec![],
};
// should always be 0
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
}
#[test]
fn performing_lagrangian_scalar_interpolation_at_origin() {
// x^2 + 3
// x, f(x):
// 1, 4,
// 2, 7,
// 3, 12,
let points = vec![1, 2, 3];
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
assert_eq!(
Scalar::from(3),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// x^3 + 3x^2 - 5x + 11
// x, f(x):
// 1, 10
// 2, 21
// 3, 50
// 4, 103
let points = vec![1, 2, 3, 4];
let values = vec![
Scalar::from(10),
Scalar::from(21),
Scalar::from(50),
Scalar::from(103),
];
assert_eq!(
Scalar::from(11),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// more points than it is required
// x^2 + x + 10
// x, f(x)
// 1, 12
// 2, 16
// 3, 22
// 4, 30
// 5, 40
let points = vec![1, 2, 3, 4, 5];
let values = vec![
Scalar::from(12),
Scalar::from(16),
Scalar::from(22),
Scalar::from(30),
Scalar::from(40),
];
assert_eq!(
Scalar::from(10),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
}
#[test]
fn hash_g1_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_g1(msg1), hash_g1(msg1));
assert_eq!(hash_g1(msg2), hash_g1(msg2));
assert_ne!(hash_g1(msg1), hash_g1(msg2));
}
#[test]
fn hash_scalar_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
}
}
@@ -2,7 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::AckKey;
use nym_crypto::symmetric::stream_cipher::{self, encrypt, iv_from_slice, random_iv, IvSizeUser};
use nym_crypto::symmetric::stream_cipher::{
self, encrypt, random_iv, try_iv_from_slice, IvSizeUser,
};
use nym_sphinx_params::{AckEncryptionAlgorithm, SerializedFragmentIdentifier, FRAG_ID_LEN};
use rand::{CryptoRng, RngCore};
@@ -25,7 +27,11 @@ pub fn recover_identifier(
iv_id_ciphertext: &[u8],
) -> Option<SerializedFragmentIdentifier> {
let iv_size = AckEncryptionAlgorithm::iv_size();
let iv = iv_from_slice::<AckEncryptionAlgorithm>(&iv_id_ciphertext[..iv_size]);
if iv_id_ciphertext.len() < FRAG_ID_LEN + iv_size {
return None;
}
let iv = try_iv_from_slice::<AckEncryptionAlgorithm>(&iv_id_ciphertext[..iv_size])?;
let id = stream_cipher::decrypt::<AckEncryptionAlgorithm>(
key.inner(),
+6
View File
@@ -161,6 +161,12 @@ impl From<NymNodeRoutingAddress> for SocketAddr {
}
}
impl AsRef<SocketAddr> for NymNodeRoutingAddress {
fn as_ref(&self) -> &SocketAddr {
&self.0
}
}
impl TryInto<NodeAddressBytes> for NymNodeRoutingAddress {
type Error = NymNodeRoutingAddressError;
@@ -19,6 +19,9 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum ReplySurbError {
#[error("did not receive enough data to recover a reply SURB")]
TooShort,
#[error("tried to use reply SURB with an unpadded message")]
UnpaddedMessageError,
@@ -131,7 +134,9 @@ impl ReplySurb {
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ReplySurbError> {
// TODO: introduce bound checks to guard us against out of bound reads
if bytes.len() <= SurbEncryptionKeySize::USIZE {
return Err(ReplySurbError::TooShort);
}
let encryption_key =
SurbEncryptionKey::try_from_bytes(&bytes[..SurbEncryptionKeySize::USIZE])?;
-5
View File
@@ -30,7 +30,6 @@ pub use sphinx_packet::{
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURBMaterial, SURB},
version::Version,
version::UPDATED_LEGACY_VERSION,
Error as SphinxError, ProcessedPacket, ProcessedPacketData,
};
@@ -91,12 +90,8 @@ impl NymPacket {
destination: &Destination,
delays: &[Delay],
) -> Result<NymPacket, NymPacketError> {
// FIXME:
// for now explicitly use the legacy version until sufficient number of nodes
// understand both variants
Ok(NymPacket::Sphinx(
SphinxPacketBuilder::new()
.with_version(UPDATED_LEGACY_VERSION)
.with_payload_size(size)
.build_packet(message, route, destination, delays)?,
))
@@ -182,9 +182,11 @@ impl BlockProcessor {
// the ones concerned with individual messages
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
for msg_module in &mut self.msg_modules {
msg_module
.handle_msg(index, msg, &block_tx, &mut tx)
.await?
if msg.type_url == msg_module.type_url() {
msg_module
.handle_msg(index, msg, &block_tx, &mut tx)
.await?
}
}
}
}
+9
View File
@@ -83,6 +83,15 @@ pub enum ScraperError {
source: cosmrs::ErrorReport,
},
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
MsgParseFailure {
hash: Hash,
index: usize,
type_url: String,
#[source]
source: cosmrs::ErrorReport,
},
#[error("received an invalid chain subscription event of kind {kind} while we were waiting for new block data (query: '{query}')")]
InvalidSubscriptionEvent { query: String, kind: String },

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