Compare commits

...

48 Commits

Author SHA1 Message Date
Jędrzej Stuczyński a2437ebe3e chore: remove legacy gateway key compatibility (#5531)
* removed aes128ctr key between client and gateway

* removed deprecated code and all references to the legacy aes128ctr key

* clippy

* post rebase fixes
2025-03-04 16:42:29 +00:00
Jędrzej Stuczyński e72103bbe6 deprecated unpaginated /issued-ticketbooks-challenge request (#5555) 2025-03-04 16:42:08 +00:00
Jędrzej Stuczyński f7ebddf84b chore: removed sphinx backwards compatibility (#5515) 2025-03-04 14:59:00 +00:00
Jędrzej Stuczyński c8e825bd1e chore: remove legacy gateway authentication (#5553)
* remove legacy gateway authentication

* fixed test code
2025-03-04 14:56:50 +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
dynco-nym 9de5d7213a Another total_stake SQL fix (#5516) 2025-02-24 18:06:03 +01:00
dynco-nym 94eb362a71 Fix total_stake on SQL update (#5514) 2025-02-24 20:50:42 +05:30
dependabot[bot] 0f615f48f2 build(deps): bump the patch-updates group with 2 updates (#5505) 2025-02-24 13:33:20 +01:00
Bogdan-Ștefan Neacşu d511611641 Connection fd callback before actual connection (#5494) 2025-02-24 14:23:43 +02:00
Jędrzej Stuczyński 17d3ff2d77 feat: use ct_eq for checking bearer token (#5501) 2025-02-24 09:04:34 +00:00
dynco-nym dd3dcfa7fe Treat gateways as Nym Nodes (#5504)
* Generate GW moniker if missing

Beside that:
- clear up gw nomenclature
- adjust counting when legacy nodes are present in nym node APIs
- create utils module

* Store gatewy descriptions

* Clippy & version
2025-02-21 20:32:39 +01:00
dynco-nym 86ea2d23cb Update version in Cargo.toml (#5503) 2025-02-21 16:16:44 +01:00
dynco-nym 42a37442e8 Fix stats bug & remove HM caching (#5495)
* Fix stats bug & remove HM caching

* Use variable for better clarity

* Minor fixes
2025-02-21 16:05:26 +01:00
dynco-nym 6b24f081e1 Add extra args for the probe (#5499) 2025-02-21 12:14:37 +01:00
Jędrzej Stuczyński 6e5d0dac1b feature: allow nym-nodes to understand future version of sphinx packets (#5496)
* use updated sphinx crate

* updated outfox usage of keygen in tests

* use x25519 in outfox

* remove redundant constructor

* adjusted key convertion traits
2025-02-21 11:06:07 +00:00
mfahampshire 5f2740bf66 add vercel config file: turn off autodeploy on master (#5490) 2025-02-19 11:03:04 +00:00
Tommy Verrall ecb15034d3 Merge pull request #5489 from nymtech/fix/contracts-cargo-lock
fix: Cargo.lock for contracts
2025-02-19 11:41:30 +01:00
Fran Arbanas bd49c222a3 fix: Cargo.lock for contracts 2025-02-19 09:06:34 +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
299 changed files with 8011 additions and 11120 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
@@ -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
+90
View File
@@ -4,6 +4,96 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [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
+792 -786
View File
File diff suppressed because it is too large Load Diff
+25 -16
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",
@@ -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,7 +205,7 @@ 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"
@@ -216,14 +215,14 @@ 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"
@@ -247,17 +246,17 @@ 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 +272,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"
@@ -310,19 +309,19 @@ rocket_cors = "0.6.0"
rocket_okapi = "0.8.0"
rs_merkle = "1.4.2"
safer-ffi = "0.1.13"
schemars = "0.8.21"
schemars = "0.8.22"
semver = "1.0.25"
serde = "1.0.217"
serde_bytes = "0.11.15"
serde_bytes = "0.11.16"
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.1.1"
sphinx-packet = "=0.4.0" # make sure to use version 0.4.0 (or higher) that has removed backwards compatibility
sqlx = "0.7.4"
strum = "0.26"
strum_macros = "0.26"
@@ -330,7 +329,7 @@ subtle-encoding = "0.5"
syn = "1"
sysinfo = "0.33.0"
tap = "1.0.1"
tar = "0.4.43"
tar = "0.4.44"
tempfile = "3.15"
thiserror = "2.0"
time = "0.3.37"
@@ -438,3 +437,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
View File
@@ -1,2 +1,3 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
@@ -45,6 +45,7 @@ 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
@@ -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,
@@ -660,6 +665,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,
@@ -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()
},
@@ -0,0 +1,24 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
-- make aes256gcm column non-nullable and drop any gateways that still use the legacy keys
-- (since they'd be unusable after this change)
CREATE TABLE remote_gateway_details_tmp
(
gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58),
derived_aes256_gcm_siv_key BLOB NOT NULL,
gateway_owner_address TEXT,
gateway_listener TEXT NOT NULL
);
INSERT INTO remote_gateway_details_tmp (gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_owner_address,
gateway_listener)
SELECT gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener
FROM remote_gateway_details
WHERE derived_aes256_gcm_siv_key IS NOT NULL;
DROP TABLE remote_gateway_details;
ALTER TABLE remote_gateway_details_tmp
RENAME TO remote_gateway_details;
@@ -156,48 +156,26 @@ impl StorageManager {
pub(crate) async fn set_remote_gateway_details(
&self,
remote: &RawRemoteGatewayDetails,
gateway_id_bs58: String,
derived_aes256_gcm_siv_key: &[u8],
gateway_owner_address: Option<String>,
gateway_listener: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)
VALUES (?, ?, ?, ?, ?)
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)
VALUES (?, ?, ?, ?)
"#,
remote.gateway_id_bs58,
remote.derived_aes128_ctr_blake3_hmac_keys_bs58,
remote.derived_aes256_gcm_siv_key,
remote.gateway_owner_address,
remote.gateway_listener,
gateway_id_bs58,
derived_aes256_gcm_siv_key,
gateway_owner_address,
gateway_listener,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn update_remote_gateway_key(
&self,
gateway_id_bs58: &str,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
derived_aes256_gcm_siv_key: Option<&[u8]>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE remote_gateway_details
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE gateway_id_bs58 = ?
"#,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_id_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_remote_gateway_details(
&self,
gateway_id: &str,
@@ -8,7 +8,6 @@ use crate::{
use async_trait::async_trait;
use manager::StorageManager;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::SharedSymmetricKey;
use std::path::Path;
pub mod error;
@@ -119,9 +118,16 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
match &details.details {
GatewayDetails::Remote(remote_details) => {
let raw_details = remote_details.into();
self.manager
.set_remote_gateway_details(&raw_details)
.set_remote_gateway_details(
remote_details.gateway_id.to_base58_string(),
remote_details.shared_key.as_bytes(),
remote_details
.gateway_owner_address
.as_ref()
.map(|o| o.to_string()),
remote_details.gateway_listener.to_string(),
)
.await?;
}
GatewayDetails::Custom(custom_details) => {
@@ -134,21 +140,6 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: ed25519::PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError> {
self.manager
.update_remote_gateway_key(
&gateway_id.to_base58_string(),
None,
Some(updated_key.as_bytes()),
)
.await?;
Ok(())
}
// ideally all of those should be run under a storage tx to ensure storage consistency,
// but at that point it's fine
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
@@ -2,10 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::{ActiveGateway, GatewayRegistration};
use crate::{BadGateway, GatewayDetails, GatewaysDetailsStore};
use crate::{BadGateway, GatewaysDetailsStore};
use async_trait::async_trait;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_gateway_requests::{SharedGatewayKey, SharedSymmetricKey};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
@@ -96,29 +94,6 @@ impl GatewaysDetailsStore for InMemGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError> {
let mut guard = self.inner.write().await;
#[allow(clippy::unwrap_used)]
if let Some(target) = guard.gateways.get_mut(&gateway_id.to_string()) {
let GatewayDetails::Remote(details) = &mut target.details else {
return Ok(());
};
assert_eq!(Arc::strong_count(&details.shared_key), 1);
// eh. that's nasty, but it's only ever used for ephemeral clients so should be fine for now...
details.shared_key = Arc::new(SharedGatewayKey::Current(
SharedSymmetricKey::try_from_bytes(updated_key.as_bytes()).unwrap(),
))
}
Ok(())
}
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
let mut guard = self.inner.write().await;
if let Some(active) = guard.active_gateway.as_ref() {
@@ -6,7 +6,6 @@
use async_trait::async_trait;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::SharedSymmetricKey;
use std::error::Error;
pub mod backend;
@@ -62,12 +61,6 @@ pub trait GatewaysDetailsStore {
details: &GatewayRegistration,
) -> Result<(), Self::StorageError>;
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: identity::PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError>;
/// Remove given gateway details from the underlying store.
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError>;
}
@@ -4,10 +4,9 @@
use crate::BadGateway;
use cosmrs::AccountId;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
use time::OffsetDateTime;
@@ -65,7 +64,7 @@ impl From<GatewayDetails> for GatewayRegistration {
impl GatewayDetails {
pub fn new_remote(
gateway_id: identity::PublicKey,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
gateway_owner_address: Option<AccountId>,
gateway_listener: Url,
) -> Self {
@@ -88,7 +87,7 @@ impl GatewayDetails {
}
}
pub fn shared_key(&self) -> Option<&SharedGatewayKey> {
pub fn shared_key(&self) -> Option<&SharedSymmetricKey> {
match self {
GatewayDetails::Remote(details) => Some(&details.shared_key),
GatewayDetails::Custom(_) => None,
@@ -168,8 +167,7 @@ pub struct RegisteredGateway {
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct RawRemoteGatewayDetails {
pub gateway_id_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
pub derived_aes256_gcm_siv_key: Vec<u8>,
pub gateway_owner_address: Option<String>,
pub gateway_listener: String,
}
@@ -186,35 +184,11 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
}
})?;
let shared_key =
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => {
return Err(BadGateway::MissingSharedKey {
gateway_id: value.gateway_id_bs58.clone(),
})
}
(Some(aes256gcm_siv), _) => {
let current_key =
SharedSymmetricKey::try_from_bytes(aes256gcm_siv).map_err(|source| {
BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
}
})?;
SharedGatewayKey::Current(current_key)
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
})?;
SharedGatewayKey::Legacy(legacy_key)
}
};
let shared_key = SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
})?;
let gateway_owner_address = value
.gateway_owner_address
@@ -247,29 +221,11 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
}
}
impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
fn from(value: &'a RemoteGatewayDetails) -> Self {
let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) =
match value.shared_key.deref() {
SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())),
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
};
RawRemoteGatewayDetails {
gateway_id_bs58: value.gateway_id.to_base58_string(),
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()),
gateway_listener: value.gateway_listener.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct RemoteGatewayDetails {
pub gateway_id: identity::PublicKey,
pub shared_key: Arc<SharedGatewayKey>,
pub shared_key: Arc<SharedSymmetricKey>,
pub gateway_owner_address: Option<AccountId>,
@@ -394,7 +394,6 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
@@ -403,7 +402,6 @@ where
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
let managed_keys = initialisation_result.client_keys;
let GatewayDetails::Remote(details) = initialisation_result.gateway_registration.details
@@ -458,31 +456,13 @@ where
// we need to:
// - perform handshake (reg or auth)
// - check for key upgrade
// - maybe perform another upgrade handshake
// - check for bandwidth
// - start background tasks
let auth_res = gateway_client
let _auth_res = gateway_client
.perform_initial_authentication()
.await
.map_err(gateway_failure)?;
if auth_res.requires_key_upgrade {
// drop the shared_key arc because we don't need it and we can't hold it for the purposes of upgrade
drop(auth_res);
let updated_key = gateway_client
.upgrade_key_authenticated()
.await
.map_err(gateway_failure)?;
details_store
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
.await.map_err(|err| {
error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
})?
}
gateway_client
.claim_initial_bandwidth()
.await
@@ -501,7 +481,6 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
@@ -510,7 +489,6 @@ where
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
// if we have setup custom gateway sender and persisted details agree with it, return it
if let Some(mut custom_gateway_transceiver) = custom_gateway_transceiver {
@@ -533,7 +511,6 @@ where
config,
initialisation_result,
bandwidth_controller,
details_store,
packet_router,
stats_reporter,
#[cfg(unix)]
@@ -744,8 +721,7 @@ where
)
.await?;
let (reply_storage_backend, credential_store, details_store) =
self.client_store.into_runtime_stores();
let (reply_storage_backend, credential_store, _) = self.client_store.into_runtime_stores();
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
@@ -826,7 +802,6 @@ where
&self.config,
init_res,
bandwidth_controller,
&details_store,
gateway_packet_router,
stats_reporter.clone(),
#[cfg(unix)]
@@ -2,24 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
pub mod v1_1_33 {
use crate::client::base_client::{
non_wasm_helpers::setup_fs_gateways_storage,
storage::helpers::{set_active_gateway, store_gateway_details},
};
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::disk_persistence::CommonClientPaths;
use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33;
use crate::error::ClientCoreError;
use nym_client_core_gateways_storage::{
CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails,
};
use nym_gateway_requests::shared_key::LegacySharedKeys;
use serde::{Deserialize, Serialize};
use sha2::{digest::Digest, Sha256};
use std::ops::Deref;
use std::path::Path;
use std::sync::Arc;
use zeroize::Zeroizing;
mod base64 {
use base64::{engine::general_purpose::STANDARD, Engine as _};
@@ -57,155 +44,18 @@ pub mod v1_1_33 {
details: OldGatewayEndpointConfigV1_1_33,
}
impl PersistedGatewayConfig {
fn verify(&self, shared_key: &LegacySharedKeys) -> bool {
let key_bytes = Zeroizing::new(shared_key.to_bytes());
let mut key_hasher = Sha256::new();
key_hasher.update(&key_bytes);
let key_hash = key_hasher.finalize();
self.key_hash == key_hash.deref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedCustomGatewayDetails {
gateway_id: String,
}
fn load_shared_key<P: AsRef<Path>>(path: P) -> Result<LegacySharedKeys, ClientCoreError> {
// the shared key was a simple pem file
Ok(nym_pemstore::load_key(path)?)
}
fn gateway_details_from_raw(
gateway_id: String,
gateway_owner: String,
gateway_listener: String,
gateway_shared_key: LegacySharedKeys,
) -> Result<GatewayDetails, ClientCoreError> {
Ok(GatewayDetails::Remote(RemoteGatewayDetails {
gateway_id: gateway_id
.parse()
.map_err(|err| ClientCoreError::UpgradeFailure {
message: format!("the stored gateway id was malformed: {err}"),
})?,
shared_key: Arc::new(gateway_shared_key.into()),
gateway_owner_address: Some(gateway_owner.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway owner address was malformed: {err}"),
}
})?),
gateway_listener: gateway_listener.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway listener address was malformed: {err}"),
}
})?,
}))
}
// helper to extract shared key and gateway details into the new GatewayRegistration
fn extract_gateway_registration(
storage_paths: &CommonClientPathsV1_1_33,
) -> Result<GatewayRegistration, ClientCoreError> {
let details_file = std::fs::File::open(&storage_paths.gateway_details).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!(
"failed to open gateway details file at {}: {err}",
storage_paths.gateway_details.display()
),
}
})?;
// in v1.1.33 of the clients, the gateway details struct was saved as json
let details: PersistedGatewayDetails =
serde_json::from_reader(details_file).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!(
"failed to deserialize gateway details from {}: {err}",
storage_paths.gateway_details.display()
),
}
})?;
let details = match details {
PersistedGatewayDetails::Default(config) => {
let gateway_shared_key =
load_shared_key(&storage_paths.keys.gateway_shared_key_file)?;
if !config.verify(&gateway_shared_key) {
return Err(ClientCoreError::UpgradeFailure {
message: "failed to verify consistency of the existing gateway details"
.to_string(),
});
}
gateway_details_from_raw(
config.details.gateway_id,
config.details.gateway_owner,
config.details.gateway_listener,
gateway_shared_key,
)?
}
PersistedGatewayDetails::Custom(custom) => {
GatewayDetails::Custom(CustomGatewayDetails {
gateway_id: custom.gateway_id.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway id was malformed: {err}"),
}
})?,
data: None,
})
}
};
Ok(details.into())
}
// it's responsibility of the caller to ensure this is called **after** new registration has already been saved
fn remove_old_gateway_details(storage_paths: &CommonClientPathsV1_1_33) -> std::io::Result<()> {
std::fs::remove_file(&storage_paths.gateway_details)?;
if storage_paths.keys.gateway_shared_key_file.exists() {
std::fs::remove_file(&storage_paths.keys.gateway_shared_key_file)?;
}
Ok(())
}
pub async fn migrate_gateway_details(
old_storage_paths: &CommonClientPathsV1_1_33,
new_storage_paths: &CommonClientPaths,
preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
_old_storage_paths: &CommonClientPathsV1_1_33,
_new_storage_paths: &CommonClientPaths,
_preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
) -> Result<(), ClientCoreError> {
let gateway_registration = match preloaded_config {
Some(config) => {
let gateway_shared_key =
load_shared_key(&old_storage_paths.keys.gateway_shared_key_file)?;
gateway_details_from_raw(
config.gateway_id,
config.gateway_owner,
config.gateway_listener,
gateway_shared_key,
)?
.into()
}
None => extract_gateway_registration(old_storage_paths)?,
};
// since we're migrating to a brand new store, the store should be empty
// and thus set the 'new' gateway as the active one
let details_store =
setup_fs_gateways_storage(&new_storage_paths.gateway_registrations).await?;
store_gateway_details(&details_store, &gateway_registration).await?;
set_active_gateway(
&details_store,
&gateway_registration.details.gateway_id().to_base58_string(),
)
.await?;
remove_old_gateway_details(old_storage_paths).map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("failed to remove old data: {err}"),
}
})
Err(ClientCoreError::UnsupportedMigration(
"migration of legacy keys has been removed and is no longer supported".into(),
))
}
}
@@ -6,7 +6,7 @@ use nym_crypto::{
asymmetric::{encryption, identity},
hkdf::{DerivationMaterial, InvalidLength},
};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sphinx::acknowledgements::AckKey;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
@@ -106,7 +106,5 @@ fn _assert_keys_zeroize_on_drop() {
_assert_zeroize_on_drop::<identity::KeyPair>();
_assert_zeroize_on_drop::<encryption::KeyPair>();
_assert_zeroize_on_drop::<AckKey>();
_assert_zeroize_on_drop::<LegacySharedKeys>();
_assert_zeroize_on_drop::<SharedSymmetricKey>();
_assert_zeroize_on_drop::<SharedGatewayKey>();
}
@@ -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,
),
);
@@ -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,
+3
View File
@@ -12,6 +12,9 @@ use std::path::PathBuf;
#[derive(thiserror::Error, Debug)]
pub enum ClientCoreError {
#[error("could not perform the state migration: {0}")]
UnsupportedMigration(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
-8
View File
@@ -335,14 +335,6 @@ pub(super) async fn register_with_gateway(
}
})?;
// this should NEVER happen, if it did, it means the function was misused,
// because for any fresh **registration**, the derived key is always up to date
if auth_response.requires_key_upgrade {
return Err(ClientCoreError::UnexpectedKeyUpgrade {
gateway_id: gateway_id.to_base58_string(),
});
}
Ok(RegistrationResult {
shared_keys: auth_response.initial_shared_key,
authenticated_ephemeral_client: gateway_client,
+2 -2
View File
@@ -11,7 +11,7 @@ use nym_client_core_gateways_storage::{
};
use nym_crypto::asymmetric::identity;
use nym_gateway_client::client::InitGatewayClient;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKey;
@@ -96,7 +96,7 @@ impl SelectedGateway {
/// - shared keys derived between ourselves and the node
/// - an authenticated handle of an ephemeral handle created for the purposes of registration
pub struct RegistrationResult {
pub shared_keys: Arc<SharedGatewayKey>,
pub shared_keys: Arc<SharedSymmetricKey>,
pub authenticated_ephemeral_client: InitGatewayClient,
}
@@ -20,9 +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,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, ServerResponse,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -47,7 +46,6 @@ use std::os::raw::c_int as RawFd;
use wasm_utils::websocket::JSWebsocket;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
use zeroize::Zeroizing;
pub mod config;
@@ -82,8 +80,7 @@ impl GatewayConfig {
#[must_use]
#[derive(Debug)]
pub struct AuthenticationResponse {
pub initial_shared_key: Arc<SharedGatewayKey>,
pub requires_key_upgrade: bool,
pub initial_shared_key: Arc<SharedSymmetricKey>,
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
@@ -95,7 +92,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
gateway_address: String,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
shared_key: Option<Arc<SharedGatewayKey>>,
shared_key: Option<Arc<SharedSymmetricKey>>,
connection: SocketState,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
@@ -119,7 +116,7 @@ impl<C, St> GatewayClient<C, St> {
gateway_config: GatewayConfig,
local_identity: Arc<identity::KeyPair>,
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
shared_key: Option<Arc<SharedGatewayKey>>,
shared_key: Option<Arc<SharedSymmetricKey>>,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
@@ -149,7 +146,7 @@ impl<C, St> GatewayClient<C, St> {
self.gateway_identity
}
pub fn shared_key(&self) -> Option<Arc<SharedGatewayKey>> {
pub fn shared_key(&self) -> Option<Arc<SharedSymmetricKey>> {
self.shared_key.clone()
}
@@ -204,15 +201,15 @@ impl<C, St> GatewayClient<C, St> {
"Attemting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(&self.gateway_address).await?;
let (ws_stream, _) = connect_async(
&self.gateway_address,
#[cfg(unix)]
self.connection_fd_callback.clone(),
)
.await?;
self.connection = SocketState::Available(Box::new(ws_stream));
#[cfg(unix)]
if let (Some(callback), Some(fd)) = (self.connection_fd_callback.as_ref(), self.ws_fd()) {
callback.as_ref()(fd);
}
Ok(())
}
@@ -271,7 +268,7 @@ impl<C, St> GatewayClient<C, St> {
message: ClientRequest,
) -> Result<(), GatewayClientError> {
if let Some(shared_key) = self.shared_key() {
let encrypted = message.encrypt(&*shared_key)?;
let encrypted = message.encrypt(&shared_key)?;
Box::pin(self.send_websocket_message(encrypted)).await?;
Ok(())
} else {
@@ -410,49 +407,39 @@ impl<C, St> GatewayClient<C, St> {
}
}
fn check_gateway_protocol(
&self,
gateway_protocol: Option<u8>,
) -> Result<(), GatewayClientError> {
fn check_gateway_protocol(&self, gateway_protocol: u8) -> Result<(), GatewayClientError> {
debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}");
// right now there are no failure cases here, but this might change in the future
match gateway_protocol {
None => {
warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
// note: in +1.2.0 we will have to return a hard error here
Ok(())
}
Some(v) if v > CURRENT_PROTOCOL_VERSION => {
let err = GatewayClientError::IncompatibleProtocol {
gateway: Some(v),
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
// client should reject any gateways that do not indicate they support auth v2 or aes256gcm-siv
if !gateway_protocol.supports_authenticate_v2()
|| !gateway_protocol.supports_aes256_gcm_siv()
{
return Err(GatewayClientError::IncompatibleProtocol {
gateway: gateway_protocol,
current: CURRENT_PROTOCOL_VERSION,
});
}
Some(_) => {
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
}
// we can't handle gateways with higher protocol than ours
if gateway_protocol <= CURRENT_PROTOCOL_VERSION {
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
} else {
let err = GatewayClientError::IncompatibleProtocol {
gateway: gateway_protocol,
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
}
async fn register(
&mut self,
derive_aes256_gcm_siv_key: bool,
) -> Result<(), GatewayClientError> {
async fn register(&mut self) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug_assert!(self.connection.is_available());
log::debug!(
"registering with gateway. using legacy key derivation: {}",
!derive_aes256_gcm_siv_key
);
// it's fine to instantiate it here as it's only used once (during authentication or registration)
// and putting it into the GatewayClient struct would be a hassle
let mut rng = OsRng;
@@ -464,7 +451,6 @@ impl<C, St> GatewayClient<C, St> {
self.local_identity.as_ref(),
self.gateway_identity,
self.cfg.bandwidth.require_tickets,
derive_aes256_gcm_siv_key,
#[cfg(not(target_arch = "wasm32"))]
self.task_client.clone(),
)
@@ -492,99 +478,15 @@ impl<C, St> GatewayClient<C, St> {
}
// populate the negotiated protocol for future uses
self.negotiated_protocol = gateway_protocol;
self.negotiated_protocol = Some(gateway_protocol);
Ok(())
}
pub async fn upgrade_key_authenticated(
async fn send_authenticate_request_and_handle_response(
&mut self,
) -> Result<Zeroizing<SharedSymmetricKey>, GatewayClientError> {
info!("*** STARTING AES128CTR-HMAC KEY UPGRADE INTO AES256GCM-SIV***");
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
if !shared_key.is_legacy() {
return Err(GatewayClientError::KeyAlreadyUpgraded);
}
// make sure we have the only reference, so we could safely swap it
if Arc::strong_count(shared_key) != 1 {
return Err(GatewayClientError::KeyAlreadyInUse);
}
assert!(shared_key.is_legacy());
let legacy_key = shared_key.unwrap_legacy();
let (updated_key, hkdf_salt) = legacy_key.upgrade();
let derived_key_digest = updated_key.digest();
let upgrade_request = ClientRequest::UpgradeKey {
hkdf_salt,
derived_key_digest,
}
.encrypt(legacy_key)?;
info!("sending upgrade request and awaiting the acknowledgement back");
let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? {
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
// attempt to decrypt it using NEW key
let Ok(response) = SensitiveServerResponse::decrypt(&ciphertext, &nonce, &updated_key)
else {
return Err(GatewayClientError::FatalKeyUpgradeFailure);
};
match response {
SensitiveServerResponse::KeyUpgradeAck { .. } => {
info!("received key upgrade acknowledgement")
}
_ => return Err(GatewayClientError::FatalKeyUpgradeFailure),
}
// perform in memory swap and make a copy for updating storage
let zeroizing_updated_key = updated_key.zeroizing_clone();
self.shared_key = Some(Arc::new(updated_key.into()));
Ok(zeroizing_updated_key)
}
async fn 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,
)?;
msg: ClientControlRequest,
) -> Result<(), GatewayClientError> {
match self.send_websocket_message(msg).await? {
ServerResponse::Authenticate {
protocol_version,
@@ -595,7 +497,7 @@ impl<C, St> GatewayClient<C, St> {
self.authenticated = status;
self.bandwidth.update_and_maybe_log(bandwidth_remaining);
self.negotiated_protocol = protocol_version;
self.negotiated_protocol = Some(protocol_version);
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
self.task_client.send_status_msg(Box::new(
@@ -608,6 +510,26 @@ impl<C, St> GatewayClient<C, St> {
}
}
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) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
self.authenticate_v2().await
}
/// Helper method to either call register or authenticate based on self.shared_key value
#[instrument(skip_all,
fields(
@@ -623,18 +545,26 @@ 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,
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
}
};
// if we failed to get this request resolved, it means the gateway is on an old version
// that definitely does not support auth v2 or aes256gcm, so we bail
let gw_protocol = self.get_gateway_protocol().await?;
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");
error!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
}
if !supports_aes_gcm_siv {
error!("this gateway is on an old version that doesn't support authentication v2");
}
if !supports_auth_v2 || !supports_aes_gcm_siv {
// we can't continue
return Err(GatewayClientError::IncompatibleProtocol {
gateway: gw_protocol,
current: CURRENT_PROTOCOL_VERSION,
});
}
if self.authenticated {
@@ -642,7 +572,6 @@ impl<C, St> GatewayClient<C, St> {
return if let Some(shared_key) = &self.shared_key {
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv,
})
} else {
Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey)
@@ -656,17 +585,14 @@ impl<C, St> GatewayClient<C, St> {
// if we are authenticated it means we MUST have an associated shared_key
let shared_key = self.shared_key.as_ref().unwrap();
let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv;
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade,
})
} else {
Err(GatewayClientError::AuthenticationFailure)
}
} else {
self.register(supports_aes_gcm_siv).await?;
self.register().await?;
// if registration didn't return an error, we MUST have an associated shared key
let shared_key = self.shared_key.as_ref().unwrap();
@@ -675,7 +601,6 @@ impl<C, St> GatewayClient<C, St> {
// so no upgrades are required
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: false,
})
}
}
@@ -1,6 +1,11 @@
use crate::error::GatewayClientError;
use nym_http_api_client::HickoryDnsResolver;
#[cfg(unix)]
use std::{
os::fd::{AsRawFd, RawFd},
sync::Arc,
};
use tokio::net::TcpStream;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tungstenite::handshake::client::Response;
@@ -11,7 +16,10 @@ use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn connect_async(
endpoint: &str,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
use tokio::net::TcpSocket;
let resolver = HickoryDnsResolver::default();
let uri =
Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
@@ -37,14 +45,41 @@ pub(crate) async fn connect_async(
}
};
let stream = TcpStream::connect(&sock_addrs[..]).await.map_err(|error| {
GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: error.into(),
let mut stream = Err(GatewayClientError::NoEndpointForConnection {
address: endpoint.to_owned(),
});
for sock_addr in sock_addrs {
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(),
})?;
tokio_tungstenite::client_async_tls(endpoint, stream)
#[cfg(unix)]
if let Some(callback) = connection_fd_callback.as_ref() {
callback.as_ref()(socket.as_raw_fd());
}
match socket.connect(sock_addr).await {
Ok(s) => {
stream = Ok(s);
break;
}
Err(err) => {
stream = Err(GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
});
continue;
}
}
}
tokio_tungstenite::client_async_tls(endpoint, stream?)
.await
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
@@ -43,6 +43,9 @@ pub enum GatewayClientError {
#[error("connection failed: {address}: {source}")]
NetworkConnectionFailed { address: String, source: WsError },
#[error("no socket address for endpoint: {address}")]
NoEndpointForConnection { address: String },
#[error("Invalid URL: {0}")]
InvalidUrl(String),
@@ -111,7 +114,7 @@ pub enum GatewayClientError {
MixnetMsgSenderFailedToSend,
#[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")]
IncompatibleProtocol { gateway: Option<u8>, current: u8 },
IncompatibleProtocol { gateway: u8, current: u8 },
#[error(
"The packet router hasn't been set - are you sure you started up the client correctly?"
+2 -4
View File
@@ -7,9 +7,7 @@ use tracing::{error, warn};
use tungstenite::{protocol::Message, Error as WsError};
pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig};
pub use nym_gateway_requests::shared_key::{
LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey,
};
pub use nym_gateway_requests::shared_key::SharedSymmetricKey;
pub use packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
PacketRouter,
@@ -47,7 +45,7 @@ pub(crate) fn cleanup_socket_messages(
pub(crate) fn try_decrypt_binary_message(
bin_msg: Vec<u8>,
shared_keys: &SharedGatewayKey,
shared_keys: &SharedSymmetricKey,
) -> Option<Vec<u8>> {
match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) {
Ok(bin_response) => match bin_response {
@@ -9,7 +9,7 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message};
use futures::channel::oneshot;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError};
use nym_task::TaskClient;
use si_scale::helpers::bibytes2;
@@ -63,7 +63,7 @@ pub(crate) struct PartiallyDelegatedHandle {
struct PartiallyDelegatedRouter {
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
@@ -73,7 +73,7 @@ struct PartiallyDelegatedRouter {
impl PartiallyDelegatedRouter {
fn new(
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
stream_return_requester: oneshot::Receiver<()>,
@@ -253,7 +253,7 @@ impl PartiallyDelegatedHandle {
pub(crate) fn split_and_listen_for_mixnet_messages(
conn: WsConn,
packet_router: PacketRouter,
shared_key: Arc<SharedGatewayKey>,
shared_key: Arc<SharedSymmetricKey>,
client_bandwidth: ClientBandwidth,
shutdown: TaskClient,
) -> Self {
@@ -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?)
}
}
@@ -15,7 +15,9 @@ use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
@@ -40,6 +42,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;
@@ -1015,6 +1018,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(
@@ -43,6 +43,7 @@ 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";
@@ -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,7 @@ pub use cw3;
pub use cw4;
pub use cw_controllers;
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
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 -2
View File
@@ -37,11 +37,13 @@ nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
rand_chacha = { workspace = true }
[features]
default = ["sphinx"]
default = []
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
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"]
outfox = ["nym-sphinx-types/outfox"]
[lints]
workspace = true
+18 -100
View File
@@ -202,6 +202,18 @@ impl PemStorableKey for PublicKey {
}
}
impl From<x25519_dalek::PublicKey> for PublicKey {
fn from(public_key: x25519_dalek::PublicKey) -> Self {
PublicKey(public_key)
}
}
impl From<PublicKey> for x25519_dalek::PublicKey {
fn from(public_key: PublicKey) -> Self {
public_key.0
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct PrivateKey(x25519_dalek::StaticSecret);
@@ -308,109 +320,15 @@ impl PemStorableKey for PrivateKey {
}
}
// compatibility with sphinx keys:
#[cfg(feature = "sphinx")]
impl From<PublicKey> for nym_sphinx_types::PublicKey {
fn from(key: PublicKey) -> Self {
nym_sphinx_types::PublicKey::from(key.to_bytes())
impl From<x25519_dalek::StaticSecret> for PrivateKey {
fn from(secret: x25519_dalek::StaticSecret) -> Self {
PrivateKey(secret)
}
}
#[cfg(feature = "sphinx")]
impl<'a> From<&'a PublicKey> for nym_sphinx_types::PublicKey {
fn from(key: &'a PublicKey) -> Self {
nym_sphinx_types::PublicKey::from((*key).to_bytes())
}
}
#[cfg(feature = "sphinx")]
impl From<nym_sphinx_types::PublicKey> for PublicKey {
fn from(pub_key: nym_sphinx_types::PublicKey) -> Self {
Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes()))
}
}
#[cfg(feature = "sphinx")]
impl From<PrivateKey> for nym_sphinx_types::PrivateKey {
fn from(key: PrivateKey) -> Self {
nym_sphinx_types::PrivateKey::from(key.to_bytes())
}
}
#[cfg(feature = "sphinx")]
impl<'a> From<&'a PrivateKey> for nym_sphinx_types::PrivateKey {
fn from(key: &'a PrivateKey) -> Self {
nym_sphinx_types::PrivateKey::from(key.to_bytes())
}
}
#[cfg(feature = "sphinx")]
impl From<nym_sphinx_types::PrivateKey> for PrivateKey {
fn from(private_key: nym_sphinx_types::PrivateKey) -> Self {
let private_key_bytes = private_key.to_bytes();
assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE);
Self::from_bytes(&private_key_bytes).unwrap()
}
}
#[cfg(test)]
mod sphinx_key_conversion {
use super::*;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
pub(super) fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
ChaCha20Rng::from_seed(dummy_seed)
}
const NUM_ITERATIONS: usize = 100;
#[test]
fn works_for_forward_conversion() {
let mut rng = test_rng();
for _ in 0..NUM_ITERATIONS {
let keys = KeyPair::new(&mut rng);
let private = &keys.private_key;
let public = &keys.public_key;
let dummy_remote = KeyPair::new(&mut rng);
let dh1 = private.diffie_hellman(&dummy_remote.public_key);
let public_bytes = public.to_bytes();
let sphinx_private: nym_sphinx_types::PrivateKey = private.into();
let recovered_private = PrivateKey::from(sphinx_private);
let dh2 = recovered_private.diffie_hellman(&dummy_remote.public_key);
let sphinx_public: nym_sphinx_types::PublicKey = public.into();
let recovered_public = PublicKey::from(sphinx_public);
assert_eq!(public_bytes, recovered_public.to_bytes());
// even though the byte representation of the private key changed, the resultant DH is the same
// which is what matters
assert_eq!(dh1, dh2);
}
}
#[test]
fn works_for_backward_conversion() {
for _ in 0..NUM_ITERATIONS {
let (sphinx_private, sphinx_public) = nym_sphinx_types::crypto::keygen();
let private_bytes = sphinx_private.to_bytes();
let public_bytes = sphinx_public.as_bytes();
let private: PrivateKey = sphinx_private.into();
let recovered_sphinx_private: nym_sphinx_types::PrivateKey = private.into();
let public: PublicKey = sphinx_public.into();
let recovered_sphinx_public: nym_sphinx_types::PublicKey = public.into();
assert_eq!(private_bytes, recovered_sphinx_private.to_bytes());
assert_eq!(public_bytes, recovered_sphinx_public.as_bytes());
}
impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
fn as_ref(&self) -> &x25519_dalek::StaticSecret {
&self.0
}
}
+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.
+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" }
@@ -1,67 +0,0 @@
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError};
use nym_sphinx::DestinationAddressBytes;
use thiserror::Error;
/// Replacement for what used to be an `AuthToken`.
///
/// Replacement for what used to be an `AuthToken`. We used to be generating an `AuthToken` based on
/// local secret and remote address in order to allow for authentication. Due to changes in registration
/// and the fact we are deriving a shared key, we are encrypting remote's address with the previously
/// derived shared key. If the value is as expected, then authentication is successful.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
// this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag)
pub struct EncryptedAddressBytes(Vec<u8>);
#[derive(Debug, Error)]
pub enum EncryptedAddressConversionError {
#[error("Failed to decode the encrypted address - {0}")]
DecodeError(#[from] bs58::decode::Error),
}
impl EncryptedAddressBytes {
pub fn new(
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> Result<Self, SharedKeyUsageError> {
let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?;
Ok(EncryptedAddressBytes(ciphertext))
}
pub fn verify(
&self,
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> bool {
let Ok(reconstructed) = Self::new(address, key, nonce) else {
return false;
};
self == &reconstructed
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, EncryptedAddressConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
Ok(EncryptedAddressBytes(decoded))
}
pub fn to_base58_string(self) -> String {
bs58::encode(self.0).into_string()
}
}
impl From<EncryptedAddressBytes> for String {
fn from(val: EncryptedAddressBytes) -> Self {
val.to_base58_string()
}
}
@@ -1,4 +0,0 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod encrypted_address;
+18 -12
View File
@@ -2,24 +2,17 @@
// SPDX-License-Identifier: Apache-2.0
pub use nym_crypto::generic_array;
use nym_crypto::OutputSizeUser;
use nym_sphinx::params::GatewayIntegrityHmacAlgorithm;
pub use types::*;
pub mod authentication;
pub mod models;
pub mod registration;
pub mod shared_key;
pub mod types;
pub use shared_key::helpers::SymmetricKey;
pub use shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys};
pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub use shared_key::{SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey};
pub 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 +20,23 @@ 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 u8 {
fn supports_aes256_gcm_siv(&self) -> bool {
*self >= AES_GCM_SIV_PROTOCOL_VERSION
}
fn supports_authenticate_v2(&self) -> bool {
*self >= AUTHENTICATE_V2_PROTOCOL_VERSION
}
}
@@ -3,7 +3,7 @@
use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::SharedSymmetricKey;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
@@ -15,12 +15,12 @@ impl<S, R> State<'_, S, R> {
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
{
// 1. if we're using non-legacy, i.e. aes256gcm-siv derivation, generate initiator salt for kdf
let maybe_hkdf_salt = self.maybe_generate_initiator_salt();
// 1. generate initiator salt for kdf
let hkdf_salt = self.generate_initiator_salt();
// 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a hkdf salt if we're using non-legacy client
// 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a hkdf salt
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
let init_message = self.init_message(maybe_hkdf_salt.clone());
let init_message = self.init_message(hkdf_salt.clone());
self.send_handshake_data(init_message).await?;
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
@@ -31,7 +31,7 @@ impl<S, R> State<'_, S, R> {
// 3. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
self.derive_shared_key(&mid_res.ephemeral_dh, &hkdf_salt);
// 4. verify the received signature using the locally derived keys
self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?;
@@ -49,7 +49,7 @@ impl<S, R> State<'_, S, R> {
pub(crate) async fn perform_client_handshake(
mut self,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<SharedSymmetricKey, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
@@ -5,7 +5,7 @@ use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::SharedSymmetricKey;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use futures::{Sink, Stream};
use tungstenite::Message as WsMessage;
@@ -18,18 +18,14 @@ impl<S, R> State<'_, S, R> {
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
// 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY
// 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and initiator salt
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || INITIATOR_SALT
let init_message = Initialisation::try_from_bytes(&raw_init_message)?;
self.update_remote_identity(init_message.identity);
self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy());
// 2. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(
&init_message.ephemeral_dh,
init_message.initiator_salt.as_deref(),
);
self.derive_shared_key(&init_message.ephemeral_dh, &init_message.initiator_salt);
// 3. send ephemeral x25519 pubkey alongside the encrypted signature
// g^y || AES(k, sig(gate_priv, (g^y || g^x))
@@ -54,7 +50,7 @@ impl<S, R> State<'_, S, R> {
pub(crate) async fn perform_gateway_handshake(
mut self,
raw_init_message: Vec<u8>,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<SharedSymmetricKey, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
@@ -4,7 +4,7 @@
use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::KDF_SALT_LENGTH;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::{nonce_size, tag_size};
use nym_crypto::symmetric::aead::{nonce_size, tag_size, Nonce};
use nym_sphinx::params::GatewayEncryptionAlgorithm;
// it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason,
@@ -21,20 +21,13 @@ pub trait HandshakeMessage {
pub struct Initialisation {
pub identity: ed25519::PublicKey,
pub ephemeral_dh: x25519::PublicKey,
pub initiator_salt: Option<Vec<u8>>,
}
impl Initialisation {
#[cfg(not(target_arch = "wasm32"))]
pub fn is_legacy(&self) -> bool {
self.initiator_salt.is_none()
}
pub initiator_salt: Vec<u8>,
}
#[derive(Debug)]
pub struct MaterialExchange {
pub signature_ciphertext: Vec<u8>,
pub nonce: Option<Vec<u8>>,
pub nonce: Nonce<GatewayEncryptionAlgorithm>,
}
impl MaterialExchange {
@@ -72,17 +65,12 @@ impl HandshakeMessage for Initialisation {
// Eventually the ID_PUBKEY prefix will get removed and recipient will know
// initializer's identity from another source.
fn into_bytes(self) -> Vec<u8> {
let bytes = self
.identity
self.identity
.to_bytes()
.into_iter()
.chain(self.ephemeral_dh.to_bytes());
if let Some(salt) = self.initiator_salt {
bytes.chain(salt).collect()
} else {
bytes.collect()
}
.chain(self.ephemeral_dh.to_bytes())
.chain(self.initiator_salt)
.collect()
}
// this will need to be adjusted when REMOTE_ID_PUBKEY is removed
@@ -90,9 +78,8 @@ impl HandshakeMessage for Initialisation {
where
Self: Sized,
{
let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE;
let current_len = legacy_len + KDF_SALT_LENGTH;
if bytes.len() != legacy_len && bytes.len() != current_len {
let current_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE + KDF_SALT_LENGTH;
if bytes.len() != current_len {
return Err(HandshakeError::MalformedRequest);
}
@@ -101,14 +88,13 @@ impl HandshakeMessage for Initialisation {
// this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// which is impossible
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
let ephemeral_dh = x25519::PublicKey::from_bytes(
&bytes
[ed25519::PUBLIC_KEY_LENGTH..ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE],
)
.unwrap();
let initiator_salt = if bytes.len() == legacy_len {
None
} else {
Some(bytes[legacy_len..].to_vec())
};
let initiator_salt = bytes[ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE..].to_vec();
Ok(Initialisation {
identity,
@@ -121,43 +107,31 @@ impl HandshakeMessage for Initialisation {
impl HandshakeMessage for MaterialExchange {
// AES(k, SIG(PRIV_GATE, G^y || G^x))
fn into_bytes(self) -> Vec<u8> {
if let Some(nonce) = self.nonce {
self.signature_ciphertext
.iter()
.cloned()
.chain(nonce)
.collect()
} else {
self.signature_ciphertext.to_vec()
}
self.signature_ciphertext
.iter()
.cloned()
.chain(self.nonce)
.collect()
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
// we expect to receive either:
// LEGACY: ed25519 signature ciphertext (64 bytes)
// CURRENT: ed25519 signature ciphertext (+ tag) + AES256-GCM-SIV nonce (76 bytes)
let legacy_len = ed25519::SIGNATURE_LENGTH;
let current_len = legacy_len
let current_len = ed25519::SIGNATURE_LENGTH
+ tag_size::<GatewayEncryptionAlgorithm>()
+ nonce_size::<GatewayEncryptionAlgorithm>();
if bytes.len() != legacy_len && bytes.len() != current_len {
if bytes.len() != current_len {
return Err(HandshakeError::MalformedResponse);
}
let (signature_ciphertext, nonce) = if bytes.len() == current_len {
let ciphertext_len =
ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
(
bytes[..ciphertext_len].to_vec(),
Some(bytes[ciphertext_len..].to_vec()),
)
} else {
(bytes.to_vec(), None)
};
let ciphertext_len = ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
let signature_ciphertext = bytes[..ciphertext_len].to_vec();
// SAFETY: we know the bytes have correct length
let nonce = Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(&bytes[ciphertext_len..]);
Ok(MaterialExchange {
signature_ciphertext,
@@ -3,7 +3,7 @@
use self::error::HandshakeError;
use crate::registration::handshake::state::State;
use crate::SharedGatewayKey;
use crate::SharedSymmetricKey;
use futures::future::BoxFuture;
use futures::{Sink, Stream};
use nym_crypto::asymmetric::identity;
@@ -34,11 +34,11 @@ pub const KDF_SALT_LENGTH: usize = 16;
// we do not need to worry about that.
pub struct GatewayHandshake<'a> {
handshake_future: BoxFuture<'a, Result<SharedGatewayKey, HandshakeError>>,
handshake_future: BoxFuture<'a, Result<SharedSymmetricKey, HandshakeError>>,
}
impl Future for GatewayHandshake<'_> {
type Output = Result<SharedGatewayKey, HandshakeError>;
type Output = Result<SharedSymmetricKey, HandshakeError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handshake_future).poll(cx)
@@ -51,7 +51,6 @@ pub fn client_handshake<'a, S, R>(
identity: &'a identity::KeyPair,
gateway_pubkey: identity::PublicKey,
expects_credential_usage: bool,
derive_aes256_gcm_siv_key: bool,
#[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient,
) -> GatewayHandshake<'a>
where
@@ -66,8 +65,7 @@ where
#[cfg(not(target_arch = "wasm32"))]
shutdown,
)
.with_credential_usage(expects_credential_usage)
.with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key);
.with_credential_usage(expects_credential_usage);
GatewayHandshake {
handshake_future: Box::pin(state.perform_client_handshake()),
@@ -5,12 +5,9 @@ use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
use crate::registration::handshake::{WsItem, KDF_SALT_LENGTH};
use crate::shared_key::SharedKeySize;
use crate::{
types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use crate::{types, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::random_nonce;
@@ -52,7 +49,7 @@ pub(crate) struct State<'a, S, R> {
ephemeral_keypair: x25519::KeyPair,
/// The derived shared key using the ephemeral keys of both parties.
derived_shared_keys: Option<SharedGatewayKey>,
derived_shared_keys: Option<SharedSymmetricKey>,
/// The known or received public identity key of the remote.
/// Ideally it would always be known before the handshake was initiated.
@@ -62,9 +59,6 @@ pub(crate) struct State<'a, S, R> {
// in order to establish correct protocol for backwards compatibility reasons
expects_credential_usage: bool,
/// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
derive_aes256_gcm_siv_key: bool,
// channel to receive shutdown signal
#[cfg(not(target_arch = "wasm32"))]
shutdown: TaskClient,
@@ -91,7 +85,6 @@ impl<'a, S, R> State<'a, S, R> {
derived_shared_keys: None,
// later on this should become the default
expects_credential_usage: false,
derive_aes256_gcm_siv_key: false,
#[cfg(not(target_arch = "wasm32"))]
shutdown,
}
@@ -102,38 +95,24 @@ impl<'a, S, R> State<'a, S, R> {
self
}
pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn local_ephemeral_key(&self) -> &encryption::PublicKey {
self.ephemeral_keypair.public_key()
}
pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option<Vec<u8>>
pub(crate) fn generate_initiator_salt(&mut self) -> Vec<u8>
where
R: CryptoRng + RngCore,
{
if self.derive_aes256_gcm_siv_key {
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
Some(salt)
} else {
None
}
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
salt
}
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
// Eventually the ID_PUBKEY prefix will get removed and recipient will know
// initializer's identity from another source.
pub(crate) fn init_message(&self, initiator_salt: Option<Vec<u8>>) -> Initialisation {
pub(crate) fn init_message(&self, initiator_salt: Vec<u8>) -> Initialisation {
Initialisation {
identity: *self.identity.public_key(),
ephemeral_dh: *self.ephemeral_keypair.public_key(),
@@ -151,37 +130,27 @@ impl<'a, S, R> State<'a, S, R> {
pub(crate) fn derive_shared_key(
&mut self,
remote_ephemeral_key: &encryption::PublicKey,
initiator_salt: Option<&[u8]>,
initiator_salt: &[u8],
) {
let dh_result = self
.ephemeral_keypair
.private_key()
.diffie_hellman(remote_ephemeral_key);
let key_size = if self.derive_aes256_gcm_siv_key {
SharedKeySize::to_usize()
} else {
LegacySharedKeySize::to_usize()
};
let key_size = SharedKeySize::to_usize();
// there is no reason for this to fail as our okm is expected to be only 16 bytes
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
initiator_salt,
Some(initiator_salt),
&dh_result,
None,
key_size,
)
.expect("somehow too long okm was provided");
let shared_key = if self.derive_aes256_gcm_siv_key {
let current_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Current(current_key)
} else {
let legacy_key = LegacySharedKeys::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Legacy(legacy_key)
};
let shared_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
self.derived_shared_keys = Some(shared_key)
}
@@ -200,19 +169,15 @@ impl<'a, S, R> State<'a, S, R> {
.collect();
let signature = self.identity.private_key().sign(plaintext);
let nonce = if self.derive_aes256_gcm_siv_key {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
} else {
None
};
let mut rng = thread_rng();
let nonce = random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng);
// SAFETY: this function is only called after the local key has already been derived
let signature_ciphertext = self
.derived_shared_keys
.as_ref()
.expect("shared key was not derived!")
.encrypt_naive(&signature.to_bytes(), nonce.as_deref())?;
.encrypt(&signature.to_bytes(), &nonce)?;
Ok(MaterialExchange {
signature_ciphertext,
@@ -231,15 +196,10 @@ impl<'a, S, R> State<'a, S, R> {
.as_ref()
.expect("shared key was not derived!");
// if the [client] init message contained non-legacy flag, the associated nonce MUST be present
if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() {
return Err(HandshakeError::MissingNonceForCurrentKey);
}
// first decrypt received data
let decrypted_signature = derived_shared_key.decrypt_naive(
let decrypted_signature = derived_shared_key.decrypt(
&remote_response.signature_ciphertext,
remote_response.nonce.as_deref(),
&remote_response.nonce,
)?;
// now verify signature itself
@@ -367,13 +327,7 @@ impl<'a, S, R> State<'a, S, R> {
}
fn request_protocol_version(&self) -> u8 {
if self.derive_aes256_gcm_siv_key {
AES_GCM_SIV_PROTOCOL_VERSION
} else if self.expects_credential_usage {
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION
} else {
INITIAL_PROTOCOL_VERSION
}
AES_GCM_SIV_PROTOCOL_VERSION
}
pub(crate) async fn send_handshake_data<M>(
@@ -398,7 +352,7 @@ impl<'a, S, R> State<'a, S, R> {
/// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed
/// values.
pub(crate) fn finalize_handshake(self) -> SharedGatewayKey {
pub(crate) fn finalize_handshake(self) -> SharedSymmetricKey {
self.derived_shared_keys.unwrap()
}
@@ -1,98 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LegacySharedKeys, SharedGatewayKey, SharedKeyUsageError, SharedSymmetricKey};
use nym_crypto::symmetric::aead::random_nonce;
use nym_crypto::symmetric::stream_cipher::random_iv;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use rand::thread_rng;
pub trait SymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8>;
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
}
impl SymmetricKey for SharedGatewayKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
self.random_nonce_or_iv()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.encrypt(plaintext, nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.decrypt(ciphertext, nonce)
}
}
impl SymmetricKey for SharedSymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.encrypt(plaintext, &nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.decrypt(ciphertext, &nonce)
}
}
impl SymmetricKey for LegacySharedKeys {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
Ok(self.encrypt_and_tag(plaintext, iv))
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
self.decrypt_tagged(ciphertext, iv)
}
}
@@ -1,241 +0,0 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::KDF_SALT_LENGTH;
use crate::shared_key::SharedSymmetricKey;
use crate::shared_key::{SharedKeyConversionError, SharedKeySize, SharedKeyUsageError};
use crate::LegacyGatewayMacSize;
use nym_crypto::generic_array::{
typenum::{Sum, Unsigned, U16},
GenericArray,
};
use nym_crypto::hkdf;
use nym_crypto::hmac::{compute_keyed_hmac, recompute_keyed_hmac_and_verify_tag};
use nym_crypto::symmetric::stream_cipher::{self, CipherKey, KeySizeUser, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{
GatewayIntegrityHmacAlgorithm, GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm,
};
use rand::{thread_rng, RngCore};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
// shared key is as long as the encryption key and the MAC key combined.
pub type LegacySharedKeySize = Sum<EncryptionKeySize, MacKeySize>;
// we're using 16 byte long key in sphinx, so let's use the same one here
type MacKeySize = U16;
type EncryptionKeySize = <LegacyGatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
/// Shared key used when computing MAC for messages exchanged between client and its gateway.
pub type MacKey = GenericArray<u8, MacKeySize>;
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct LegacySharedKeys {
encryption_key: CipherKey<LegacyGatewayEncryptionAlgorithm>,
mac_key: MacKey,
}
impl LegacySharedKeys {
pub fn upgrade(&self) -> (SharedSymmetricKey, Vec<u8>) {
let mut rng = thread_rng();
let mut salt = vec![0u8; KDF_SALT_LENGTH];
rng.fill_bytes(&mut salt);
let legacy_bytes = Zeroizing::new(self.to_bytes());
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(&salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
(key, salt)
}
pub fn upgrade_verify(
&self,
salt: &[u8],
expected_digest: &[u8],
) -> Option<SharedSymmetricKey> {
let legacy_bytes = Zeroizing::new(self.to_bytes());
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
if key.digest() != expected_digest {
// no need to zeroize that key since it's malformed and we won't be using it anyway
None
} else {
Some(key)
}
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != LegacySharedKeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: LegacySharedKeySize::to_usize(),
});
}
let encryption_key =
GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]);
let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]);
Ok(LegacySharedKeys {
encryption_key,
mac_key,
})
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given.
/// It does **NOT** attach any integrity macs on the produced ciphertext
pub fn encrypt_without_tagging(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
match iv {
Some(iv) => stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
data,
),
None => {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
&zero_iv,
data,
)
}
}
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given. Then it computes an integrity mac and concatenates it
/// with the previously produced ciphertext.
pub fn encrypt_and_tag(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
let ciphertext = self.encrypt_without_tagging(data, iv);
let mac = compute_keyed_hmac::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
&ciphertext,
);
mac.into_bytes().into_iter().chain(ciphertext).collect()
}
pub fn decrypt_without_tag(
&self,
ciphertext: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
Ok(stream_cipher::decrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
ciphertext,
))
}
pub fn decrypt_tagged(
&self,
enc_data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let mac_size = LegacyGatewayMacSize::to_usize();
if enc_data.len() < mac_size {
return Err(SharedKeyUsageError::TooShortRequest);
}
let mac_tag = &enc_data[..mac_size];
let message_bytes = &enc_data[mac_size..];
if !recompute_keyed_hmac_and_verify_tag::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
message_bytes,
mac_tag,
) {
return Err(SharedKeyUsageError::InvalidMac);
}
// couldn't have made the first borrow mutable as you can't have an immutable borrow
// together with a mutable one
let mut message_bytes_mut = message_bytes.to_vec();
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
stream_cipher::decrypt_in_place::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
&mut message_bytes_mut,
);
Ok(message_bytes_mut)
}
pub fn encryption_key(&self) -> &CipherKey<LegacyGatewayEncryptionAlgorithm> {
&self.encryption_key
}
pub fn mac_key(&self) -> &MacKey {
&self.mac_key
}
pub fn to_bytes(&self) -> Vec<u8> {
self.encryption_key
.iter()
.copied()
.chain(self.mac_key.iter().copied())
.collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
LegacySharedKeys::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}
impl From<LegacySharedKeys> for String {
fn from(keys: LegacySharedKeys) -> Self {
keys.to_base58_string()
}
}
impl PemStorableKey for LegacySharedKeys {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
// TODO: If common\nymsphinx\params\src\lib::GatewayIntegrityHmacAlgorithm changes
// the pem type needs updating!
"AES-128-CTR + HMAC-BLAKE3 GATEWAY SHARED KEYS"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
+32 -188
View File
@@ -7,213 +7,27 @@ use nym_crypto::generic_array::{typenum::Unsigned, GenericArray};
use nym_crypto::symmetric::aead::{
self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce,
};
use nym_crypto::symmetric::stream_cipher::{iv_size, random_iv, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use nym_sphinx::params::GatewayEncryptionAlgorithm;
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub use legacy::LegacySharedKeys;
pub mod helpers;
pub mod legacy;
pub type SharedKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)]
pub enum SharedGatewayKey {
Current(SharedSymmetricKey),
Legacy(LegacySharedKeys),
}
impl SharedGatewayKey {
pub fn is_legacy(&self) -> bool {
matches!(self, SharedGatewayKey::Legacy(..))
}
pub fn aes128_ctr_hmac_bs58(&self) -> Option<Zeroizing<String>> {
match self {
SharedGatewayKey::Current(_) => None,
SharedGatewayKey::Legacy(key) => Some(Zeroizing::new(key.to_base58_string())),
}
}
pub fn aes256_gcm_siv(&self) -> Option<Zeroizing<Vec<u8>>> {
match self {
SharedGatewayKey::Current(key) => Some(Zeroizing::new(key.to_bytes())),
SharedGatewayKey::Legacy(_) => None,
}
}
pub fn unwrap_legacy(&self) -> &LegacySharedKeys {
match self {
SharedGatewayKey::Current(_) => panic!("expected legacy key"),
SharedGatewayKey::Legacy(key) => key,
}
}
pub fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
if self.is_legacy() {
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
} else {
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
}
pub fn random_nonce_or_zero_iv(&self) -> Option<Vec<u8>> {
if self.is_legacy() {
None
} else {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
}
}
pub fn nonce_size(&self) -> usize {
match self {
SharedGatewayKey::Current(_) => nonce_size::<GatewayEncryptionAlgorithm>(),
SharedGatewayKey::Legacy(_) => iv_size::<LegacyGatewayEncryptionAlgorithm>(),
}
}
}
impl From<LegacySharedKeys> for SharedGatewayKey {
fn from(keys: LegacySharedKeys) -> Self {
SharedGatewayKey::Legacy(keys)
}
}
impl From<SharedSymmetricKey> for SharedGatewayKey {
fn from(keys: SharedSymmetricKey) -> Self {
SharedGatewayKey::Current(keys)
}
}
#[derive(Debug, Error)]
pub enum SharedKeyUsageError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("the provided nonce (or legacy IV) did not have the expected length")]
#[error("the provided nonce did not have the expected length or was malformed")]
MalformedNonce,
#[error("did not provide a valid nonce for aead encryption")]
MissingAeadNonce,
#[error("failed to either encrypt or decrypt provided message")]
AeadFailure(#[from] AeadError),
}
impl SharedGatewayKey {
fn validate_aead_nonce(
raw: Option<&[u8]>,
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
let Some(raw) = raw else {
return Err(SharedKeyUsageError::MissingAeadNonce);
};
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
}
fn validate_cipher_iv(
raw: Option<&[u8]>,
) -> Result<Option<&IV<LegacyGatewayEncryptionAlgorithm>>, SharedKeyUsageError> {
let Some(raw) = raw else { return Ok(None) };
let iv = if raw.is_empty() {
None
} else {
if raw.len() != iv_size::<LegacyGatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Some(IV::<LegacyGatewayEncryptionAlgorithm>::from_slice(raw))
};
Ok(iv)
}
pub fn encrypt(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_and_tag(plaintext, iv))
}
}
}
pub fn decrypt(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_tagged(ciphertext, iv)
}
}
}
// for the legacy keys do not use integrity MAC
pub fn encrypt_naive(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_without_tagging(plaintext, iv))
}
}
}
// for the legacy keys do not use integrity MAC
pub fn decrypt_naive(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_without_tag(ciphertext, iv)
}
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SharedSymmetricKey(AeadKey<GatewayEncryptionAlgorithm>);
@@ -230,6 +44,36 @@ pub enum SharedKeyConversionError {
}
impl SharedSymmetricKey {
pub fn random_nonce(&self) -> Nonce<GatewayEncryptionAlgorithm> {
let mut rng = thread_rng();
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng)
}
pub fn nonce_size(&self) -> usize {
nonce_size::<GatewayEncryptionAlgorithm>()
}
pub fn decode_bs58_nonce<I: AsRef<[u8]>>(
raw: I,
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
// 1. decode bytes from encoding
let decoded = bs58::decode(raw)
.into_vec()
.map_err(|_| SharedKeyUsageError::MalformedNonce)?;
// 2. validate length and convert into the proper type
Self::validate_aead_nonce(&decoded)
}
pub fn validate_aead_nonce(
raw: &[u8],
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != KeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use crate::{GatewayRequestsError, SharedSymmetricKey};
use nym_sphinx::forwarding::packet::MixPacket;
use strum::FromRepr;
use tungstenite::Message;
@@ -46,14 +46,14 @@ impl BinaryRequest {
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
@@ -66,7 +66,7 @@ impl BinaryRequest {
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use crate::{GatewayRequestsError, SharedSymmetricKey};
use strum::FromRepr;
use tungstenite::Message;
@@ -38,14 +38,14 @@ impl BinaryResponse {
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
@@ -58,7 +58,7 @@ impl BinaryResponse {
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
+27 -18
View File
@@ -3,11 +3,11 @@
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;
// specific errors (that should not be nested!!) for clients to match on
@@ -50,9 +50,6 @@ pub enum GatewayRequestsError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("address field was incorrectly encoded: {source}")]
IncorrectlyEncodedAddress {
#[from]
@@ -68,31 +65,43 @@ pub enum GatewayRequestsError {
]
RequestOfInvalidSize(usize),
#[error("received sphinx packet was malformed")]
MalformedSphinxPacket,
#[error("failed to serialise created sphinx packet: {0}")]
SphinxSerialisationFailure(#[from] MixPacketFormattingError),
#[error("the received encrypted data was malformed")]
MalformedEncryption,
#[error("provided packet mode is invalid")]
InvalidPacketMode,
#[error("failed to deserialize provided credential: {0}")]
EcashCredentialDeserializationFailure(#[from] CompactEcashError),
#[error("failed to deserialize provided credential: EOF")]
CredentialDeserializationFailureEOF,
#[error("failed to deserialize provided credential: malformed string: {0}")]
CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error),
#[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("provided request timestamp is in the future")]
RequestTimestampInFuture,
#[error("the client is not registered")]
NotRegistered,
#[error("the provided request is too stale to process")]
StaleRequest,
#[error("the provided request timestamp is smaller or equal to a one previously used")]
RequestReuse,
}
+18 -27
View File
@@ -3,7 +3,7 @@
use crate::{
BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError,
SharedGatewayKey,
SharedSymmetricKey,
};
use std::iter::once;
@@ -22,11 +22,7 @@ pub struct BinaryData<'a> {
impl<'a> BinaryData<'a> {
// serialises possibly encrypted data into bytes to be put on the wire
pub fn into_raw(self, legacy: bool) -> Vec<u8> {
if legacy {
return self.data.to_vec();
}
pub fn into_raw(self) -> Vec<u8> {
let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 }));
if let Some(nonce) = self.maybe_nonce {
i.chain(nonce.iter().copied())
@@ -40,19 +36,8 @@ impl<'a> BinaryData<'a> {
// attempts to perform basic parsing on bytes received from the wire
pub fn from_raw(
raw: &'a [u8],
available_key: &SharedGatewayKey,
available_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
// if we're using legacy key, it's quite simple:
// it's always encrypted with no nonce and the request/response kind is always 1
if available_key.is_legacy() {
return Ok(BinaryData {
kind: 1,
encrypted: true,
maybe_nonce: None,
data: raw,
});
}
if raw.len() < 2 {
return Err(GatewayRequestsError::TooShortRequest);
}
@@ -83,30 +68,33 @@ impl<'a> BinaryData<'a> {
pub fn make_encrypted_blob(
kind: u8,
plaintext: &[u8],
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let maybe_nonce = key.random_nonce_or_zero_iv();
let nonce = key.random_nonce();
let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?;
let ciphertext = key.encrypt(plaintext, &nonce)?;
Ok(BinaryData {
kind,
encrypted: true,
maybe_nonce: maybe_nonce.as_deref(),
maybe_nonce: Some(&nonce),
data: &ciphertext,
}
.into_raw(key.is_legacy()))
.into_raw())
}
// attempts to parse previously recovered bytes into a [`BinaryRequest`]
pub fn into_request(
self,
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<BinaryRequest, GatewayRequestsError> {
let kind = BinaryRequestKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownRequestKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
let raw_nonce = self.maybe_nonce.unwrap_or(&[]);
let nonce = SharedSymmetricKey::validate_aead_nonce(raw_nonce)?;
&*key.decrypt(self.data, &nonce)?
} else {
self.data
};
@@ -117,13 +105,16 @@ impl<'a> BinaryData<'a> {
// attempts to parse previously recovered bytes into a [`BinaryResponse`]
pub fn into_response(
self,
key: &SharedGatewayKey,
key: &SharedSymmetricKey,
) -> Result<BinaryResponse, GatewayRequestsError> {
let kind = BinaryResponseKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownResponseKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
let raw_nonce = self.maybe_nonce.unwrap_or(&[]);
let nonce = SharedSymmetricKey::validate_aead_nonce(raw_nonce)?;
&*key.decrypt(self.data, &nonce)?
} else {
self.data
};
@@ -76,25 +76,7 @@ mod tests {
protocol_version,
data,
} => {
assert_eq!(protocol_version, Some(42));
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
}
let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: None,
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert!(protocol_version.is_none());
assert_eq!(protocol_version, 42);
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
@@ -0,0 +1,141 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{AuthenticationFailure, GatewayRequestsError, SharedSymmetricKey};
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: &SharedSymmetricKey,
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_age: Duration) -> Result<(), AuthenticationFailure> {
let now = OffsetDateTime::now_utc();
if self.content.request_timestamp() + max_request_age < now {
return Err(AuthenticationFailure::StaleRequest);
}
if self.content.request_timestamp() > now {
return Err(AuthenticationFailure::RequestTimestampInFuture);
}
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: &SharedSymmetricKey,
) -> Result<(), AuthenticationFailure> {
let expected = shared_key.encrypt(
self.content
.client_identity
.derive_destination_address()
.as_bytes_ref(),
&SharedSymmetricKey::validate_aead_nonce(&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: &SharedSymmetricKey,
client_identity: ed25519::PublicKey,
) -> Result<AuthenticateRequestContent, GatewayRequestsError> {
let nonce = shared_key.random_nonce();
let destination_address = client_identity.derive_destination_address();
let address_ciphertext = shared_key.encrypt(destination_address.as_bytes_ref(), &nonce)?;
let now = OffsetDateTime::now_utc();
Ok(AuthenticateRequestContent {
protocol_version,
client_identity,
address_ciphertext,
nonce: nonce.to_vec(),
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,34 +2,27 @@
// SPDX-License-Identifier: Apache-2.0
use crate::models::CredentialSpendingRequest;
use crate::{
GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use crate::text_request::authenticate::AuthenticateRequest;
use crate::{GatewayRequestsError, SharedSymmetricKey, AUTHENTICATE_V2_PROTOCOL_VERSION};
use nym_credentials_interface::CredentialSpendingData;
use nym_sphinx::DestinationAddressBytes;
use nym_crypto::asymmetric::ed25519;
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]
pub enum ClientRequest {
UpgradeKey {
hkdf_salt: Vec<u8>,
derived_key_digest: Vec<u8>,
},
ForgetMe {
client: bool,
stats: bool,
},
ForgetMe { client: bool, stats: bool },
}
impl ClientRequest {
pub fn encrypt<S: SymmetricKey>(
pub fn encrypt(
&self,
key: &S,
key: &SharedSymmetricKey,
) -> Result<ClientControlRequest, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
@@ -38,17 +31,21 @@ impl ClientRequest {
// SAFETY: the trait has been derived correctly with no weird variants
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ClientControlRequest::EncryptedRequest { ciphertext, nonce })
let nonce = key.random_nonce();
let ciphertext = key.encrypt(&plaintext, &nonce)?;
Ok(ClientControlRequest::EncryptedRequest {
ciphertext,
nonce: nonce.to_vec(),
})
}
pub fn decrypt<S: SymmetricKey>(
pub fn decrypt(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
let plaintext = key.decrypt(ciphertext, &nonce)?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
@@ -59,32 +56,18 @@ impl ClientRequest {
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum ClientControlRequest {
// TODO: should this also contain a MAC considering that at this point we already
// have the shared key derived?
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
address: String,
enc_address: String,
iv: String,
},
AuthenticateV2(Box<AuthenticateRequest>),
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: u8,
data: Vec<u8>,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
EcashCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
#[serde(alias = "iv")]
nonce: Vec<u8>,
},
ClaimFreeTestnetBandwidth,
EncryptedRequest {
@@ -93,39 +76,44 @@ pub enum ClientControlRequest {
},
SupportedProtocol {},
// if you're adding new variants here, consider putting them inside `ClientRequest` instead
// NO LONGER SUPPORTED:
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
address: String,
enc_address: String,
iv: String,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
}
impl ClientControlRequest {
pub fn new_authenticate(
address: DestinationAddressBytes,
shared_key: &SharedGatewayKey,
uses_credentials: bool,
pub fn new_authenticate_v2(
shared_key: &SharedSymmetricKey,
identity_keys: &ed25519::KeyPair,
) -> Result<Self, GatewayRequestsError> {
// if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV
let protocol_version = if !shared_key.is_legacy() {
Some(AES_GCM_SIV_PROTOCOL_VERSION)
} else if uses_credentials {
Some(CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION)
} else {
// if we're not going to be using credentials, advertise lower protocol version to allow connection
// to wider range of gateways
Some(INITIAL_PROTOCOL_VERSION)
};
// if we're using v2 authentication, we must announce at least that protocol version
// (which also implicitly implies usage of AES256-GCM-SIV
let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION;
let nonce = shared_key.random_nonce_or_iv();
let ciphertext = shared_key.encrypt_naive(address.as_bytes_ref(), Some(&nonce))?;
Ok(ClientControlRequest::Authenticate {
protocol_version,
address: address.as_base58_string(),
enc_address: bs58::encode(&ciphertext).into_string(),
iv: bs58::encode(&nonce).into_string(),
})
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()
}
@@ -144,26 +132,27 @@ impl ClientControlRequest {
pub fn new_enc_ecash_credential(
credential: CredentialSpendingData,
shared_key: &SharedGatewayKey,
shared_key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let nonce = shared_key.random_nonce_or_iv();
let enc_credential = shared_key.encrypt(&serialized_credential, Some(&nonce))?;
let nonce = shared_key.random_nonce();
let enc_credential = shared_key.encrypt(&serialized_credential, &nonce)?;
Ok(ClientControlRequest::EcashCredential {
enc_credential,
iv: nonce,
nonce: nonce.to_vec(),
})
}
pub fn try_from_enc_ecash_credential(
enc_credential: Vec<u8>,
shared_key: &SharedGatewayKey,
iv: Vec<u8>,
shared_key: &SharedSymmetricKey,
nonce: Vec<u8>,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(&nonce)?;
let credential_bytes = shared_key.decrypt(&enc_credential, &nonce)?;
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey};
use crate::{GatewayRequestsError, SharedSymmetricKey, SimpleGatewayRequestsError};
use serde::{Deserialize, Serialize};
use tungstenite::Message;
@@ -15,9 +15,9 @@ pub enum SensitiveServerResponse {
}
impl SensitiveServerResponse {
pub fn encrypt<S: SymmetricKey>(
pub fn encrypt(
&self,
key: &S,
key: &SharedSymmetricKey,
) -> Result<ServerResponse, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
@@ -26,17 +26,21 @@ impl SensitiveServerResponse {
// SAFETY: the trait has been derived correctly with no weird variants
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ServerResponse::EncryptedResponse { ciphertext, nonce })
let nonce = key.random_nonce();
let ciphertext = key.encrypt(&plaintext, &nonce)?;
Ok(ServerResponse::EncryptedResponse {
ciphertext,
nonce: nonce.to_vec(),
})
}
pub fn decrypt<S: SymmetricKey>(
pub fn decrypt(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
key: &SharedSymmetricKey,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
let plaintext = key.decrypt(ciphertext, &nonce)?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
@@ -47,14 +51,12 @@ impl SensitiveServerResponse {
#[non_exhaustive]
pub enum ServerResponse {
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: u8,
status: bool,
bandwidth_remaining: i64,
},
Register {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: u8,
status: bool,
},
EncryptedResponse {
@@ -0,0 +1,22 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
-- make aes256gcm column non-nullable and drop any clients that still use the legacy keys
CREATE TABLE shared_keys_tmp
(
client_id INTEGER NOT NULL PRIMARY KEY REFERENCES clients (id),
client_address_bs58 TEXT NOT NULL UNIQUE,
derived_aes256_gcm_siv_key BLOB NOT NULL
);
INSERT INTO shared_keys_tmp (client_id, client_address_bs58, derived_aes256_gcm_siv_key)
SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key
FROM shared_keys
WHERE derived_aes256_gcm_siv_key IS NOT NULL;
DROP TABLE shared_keys;
ALTER TABLE shared_keys_tmp
RENAME TO shared_keys;
@@ -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;
+17 -4
View File
@@ -8,7 +8,7 @@ use models::{
VerifiedTicket, WireguardPeer,
};
use nym_credentials_interface::ClientTicket;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sphinx::DestinationAddressBytes;
use shared_keys::SharedKeysManager;
use sqlx::{
@@ -152,7 +152,7 @@ impl GatewayStorage {
pub async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedGatewayKey,
shared_keys: &SharedSymmetricKey,
) -> Result<i64, GatewayStorageError> {
let client_address_bs58 = client_address.as_base58_string();
let client_id = match self
@@ -171,8 +171,7 @@ impl GatewayStorage {
.insert_shared_keys(
client_id,
client_address_bs58,
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
shared_keys.aes256_gcm_siv().as_deref(),
shared_keys.to_bytes().as_ref(),
)
.await?;
Ok(client_id)
@@ -200,6 +199,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)
+6 -23
View File
@@ -3,7 +3,7 @@
use crate::error::GatewayStorageError;
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use sqlx::FromRow;
use time::OffsetDateTime;
@@ -14,37 +14,20 @@ 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 derived_aes256_gcm_siv_key: Vec<u8>,
pub last_used_authentication: Option<OffsetDateTime>,
}
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
impl TryFrom<PersistedSharedKeys> for SharedSymmetricKey {
type Error = GatewayStorageError;
fn try_from(value: PersistedSharedKeys) -> Result<Self, Self::Error> {
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => Err(GatewayStorageError::MissingSharedKey {
id: value.client_id,
}),
(Some(aes256gcm_siv), _) => {
let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Current(current_key))
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Legacy(legacy_key))
}
}
SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))
}
}
+24 -14
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 {
@@ -36,31 +37,27 @@ impl SharedKeysManager {
///
/// * `client_id`: The client id for which the shared keys are stored
/// * `client_address_bs58`: base58-encoded address of the client
/// * `derived_aes128_ctr_blake3_hmac_keys_bs58`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store.
/// * `derived_aes256_gcm_siv_key`: shared encryption (AES256GCM_SIV) derived shared keys to store.
pub(crate) async fn insert_shared_keys(
&self,
client_id: i64,
client_address_bs58: String,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>,
derived_aes256_gcm_siv_key: Option<&Vec<u8>>,
derived_aes256_gcm_siv_key: &Vec<u8>,
) -> Result<(), sqlx::Error> {
// https://stackoverflow.com/a/20310838
// we don't want to be using `INSERT OR REPLACE INTO` due to the foreign key on `available_bandwidth` if the entry already exists
sqlx::query!(
r#"
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?);
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?);
UPDATE shared_keys
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE client_address_bs58 = ?
"#,
client_id,
client_address_bs58,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
client_address_bs58,
).execute(&self.connection_pool).await?;
@@ -68,6 +65,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 +90,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.
+89 -11
View File
@@ -13,15 +13,18 @@
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;
@@ -38,6 +41,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 +58,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 +137,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
+1
View File
@@ -20,6 +20,7 @@ mime = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_yaml = { workspace = true }
subtle.workspace = true
tower = { workspace = true }
tracing.workspace = true
utoipa = { workspace = true, optional = true }
@@ -7,6 +7,7 @@ use axum::{extract::Request, response::Response};
use futures::future::BoxFuture;
use std::sync::Arc;
use std::task::{Context, Poll};
use subtle::ConstantTimeEq;
use tower::{Layer, Service};
use tracing::{debug, instrument, trace};
use zeroize::Zeroizing;
@@ -76,7 +77,7 @@ impl<S> RequireAuth<S> {
return Err("`Authorization` header must contain non-empty `Bearer` token");
}
if self.bearer_token.as_str() != bearer_token {
if bool::from(self.bearer_token.as_bytes().ct_ne(bearer_token.as_bytes())) {
return Err("`Authorization` header does not contain the correct `Bearer` token");
}
+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,
}
}
}
-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));
}
}

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