From bac0f24cf748bb5464e7a209cd4c71469df4bc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 18 Dec 2023 11:51:34 +0000 Subject: [PATCH] Feature/issued credentials api (#4207) * split up coconut module a bit * internal tool for watching dkg state and updating group contract * debug dkg state * display past dealer data * improved EpochState Display impl * display contract errors + advance epoch state * check admin * panic handler * simplify app.rs * split action enum * added new tab with logger information * new dealing display * sort by index * [fixedup] wip: updating epoch issued credentials - OG 92ade10384a6d7b6c6c222d2e29d69d3b3446a4c * storing and signing partial blinded credentials * starting cleanup * fixed coconut tests + clippy * fixed nym-api tests * removed dkg-manager tool it was moved to a different branch * implemented remaining endpoints * unit tests + bug fixes * clippy * added persistent identity keys to nym-api theyre not yet announced - this will be in another PR * cargo fmt * clippy * fixed loading of old configs without storage paths set * added additional logs for blind-sign endpoint * fixed up licenses * lowercasing error variants * changed 'issued_credentials' to a post * added minimal client support * fixed the unit test --- .gitignore | 1 + Cargo.lock | 123 ++-- Cargo.toml | 12 +- common/bandwidth-controller/Cargo.toml | 1 + .../bandwidth-controller/src/acquire/mod.rs | 27 +- .../bandwidth-controller/src/acquire/state.rs | 29 +- common/bandwidth-controller/src/lib.rs | 15 +- common/client-libs/mixnet-client/Cargo.toml | 4 +- .../validator-client/src/nym_api/mod.rs | 61 +- .../validator-client/src/nym_api/routes.rs | 3 + .../coconut_bandwidth_signing_client.rs | 2 +- .../contract_traits/multisig_query_client.rs | 24 +- .../validator-client/src/nyxd/helpers.rs | 13 + .../validator-client/src/nyxd/mod.rs | 5 + common/coconut-interface/src/lib.rs | 22 +- .../commands/src/coconut/issue_credentials.rs | 4 + .../coconut-bandwidth-contract/src/events.rs | 3 + .../coconut-dkg/src/types.rs | 12 +- common/credentials/Cargo.toml | 5 +- common/credentials/src/coconut/bandwidth.rs | 144 +++-- common/credentials/src/coconut/mod.rs | 1 - common/credentials/src/coconut/params.rs | 15 - common/credentials/src/coconut/utils.rs | 61 +- .../crypto/src/asymmetric/encryption/mod.rs | 11 + common/crypto/src/asymmetric/identity/mod.rs | 26 +- common/dkg/Cargo.toml | 6 +- common/mixnode-common/Cargo.toml | 2 +- common/nymcoconut/Cargo.toml | 9 +- common/nymcoconut/benches/benchmarks.rs | 26 +- common/nymcoconut/src/elgamal.rs | 47 +- common/nymcoconut/src/error.rs | 16 + common/nymcoconut/src/lib.rs | 21 +- common/nymcoconut/src/proofs/mod.rs | 64 +- common/nymcoconut/src/scheme/aggregation.rs | 50 +- common/nymcoconut/src/scheme/double_use.rs | 2 + common/nymcoconut/src/scheme/issuance.rs | 84 +-- common/nymcoconut/src/scheme/keygen.rs | 43 +- common/nymcoconut/src/scheme/mod.rs | 240 ++++---- common/nymcoconut/src/scheme/verification.rs | 44 +- common/nymcoconut/src/tests/e2e.rs | 9 +- common/nymcoconut/src/tests/helpers.rs | 29 +- common/nymcoconut/src/traits.rs | 69 ++- common/nymcoconut/src/utils.rs | 3 + common/nymsphinx/framing/Cargo.toml | 2 +- common/socks5/proxy-helpers/Cargo.toml | 4 +- ephemera/Cargo.toml | 2 +- gateway/Cargo.toml | 2 +- mixnode/Cargo.toml | 4 +- nym-api/Cargo.toml | 3 +- ...0231127120000_partial_credentials_info.sql | 28 + nym-api/nym-api-requests/Cargo.toml | 3 + nym-api/nym-api-requests/src/coconut.rs | 158 ----- .../nym-api-requests/src/coconut/helpers.rs | 33 ++ nym-api/nym-api-requests/src/coconut/mod.rs | 10 + .../nym-api-requests/src/coconut/models.rs | 233 ++++++++ nym-api/src/coconut/api_routes/helpers.rs | 28 + nym-api/src/coconut/api_routes/mod.rs | 211 +++++++ nym-api/src/coconut/client.rs | 4 +- nym-api/src/coconut/comm.rs | 74 ++- nym-api/src/coconut/deposit.rs | 389 +++++-------- nym-api/src/coconut/dkg/dealing.rs | 12 +- nym-api/src/coconut/dkg/state.rs | 16 +- nym-api/src/coconut/dkg/verification_key.rs | 38 +- nym-api/src/coconut/error.rs | 72 ++- nym-api/src/coconut/helpers.rs | 18 + nym-api/src/coconut/keypair.rs | 4 + nym-api/src/coconut/mod.rs | 311 ++-------- nym-api/src/coconut/state.rs | 151 +++++ nym-api/src/coconut/storage/manager.rs | 381 ++++++++++++ nym-api/src/coconut/storage/mod.rs | 166 ++++++ nym-api/src/coconut/storage/models.rs | 119 ++++ .../src/coconut/tests/issued_credentials.rs | 276 +++++++++ .../src/coconut/{tests.rs => tests/mod.rs} | 549 +++++++++++------- nym-api/src/main.rs | 2 + nym-api/src/support/cli/mod.rs | 11 +- nym-api/src/support/config/mod.rs | 23 +- .../src/support/config/old_config_v1_1_21.rs | 1 + .../src/support/config/old_config_v1_1_27.rs | 1 + nym-api/src/support/config/persistence.rs | 70 +++ nym-api/src/support/config/template.rs | 6 + nym-api/src/support/http/mod.rs | 7 +- nym-api/src/support/nyxd/mod.rs | 12 +- nym-api/src/support/storage/manager.rs | 59 +- nym-api/src/support/storage/mod.rs | 21 - nym-connect/desktop/Cargo.lock | 134 ++--- sdk/rust/nym-sdk/Cargo.toml | 2 +- 86 files changed, 3360 insertions(+), 1678 deletions(-) create mode 100644 common/client-libs/validator-client/src/nyxd/helpers.rs delete mode 100644 common/credentials/src/coconut/params.rs create mode 100644 nym-api/migrations/20231127120000_partial_credentials_info.sql delete mode 100644 nym-api/nym-api-requests/src/coconut.rs create mode 100644 nym-api/nym-api-requests/src/coconut/helpers.rs create mode 100644 nym-api/nym-api-requests/src/coconut/mod.rs create mode 100644 nym-api/nym-api-requests/src/coconut/models.rs create mode 100644 nym-api/src/coconut/api_routes/helpers.rs create mode 100644 nym-api/src/coconut/api_routes/mod.rs create mode 100644 nym-api/src/coconut/state.rs create mode 100644 nym-api/src/coconut/storage/manager.rs create mode 100644 nym-api/src/coconut/storage/mod.rs create mode 100644 nym-api/src/coconut/storage/models.rs create mode 100644 nym-api/src/coconut/tests/issued_credentials.rs rename nym-api/src/coconut/{tests.rs => tests/mod.rs} (75%) diff --git a/.gitignore b/.gitignore index 9d4cada8a2..4dfbe52586 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target .env .env.dev +envs/devnet.env /.vscode/settings.json validator/.vscode sample-configs/validator-config.toml diff --git a/Cargo.lock b/Cargo.lock index 26ecc4a3b9..153ba1b793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -987,27 +987,13 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bls12_381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" +version = "0.8.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" dependencies = [ "digest 0.9.0", - "ff 0.10.1", - "group 0.10.0", - "pairing 0.20.0", - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "bls12_381" -version = "0.6.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" -dependencies = [ - "digest 0.9.0", - "ff 0.11.1", - "group 0.11.0", - "pairing 0.21.0", + "ff 0.13.0", + "group 0.13.0", + "pairing", "rand_core 0.6.4", "subtle 2.4.1", "zeroize", @@ -3030,26 +3016,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" -[[package]] -name = "ff" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" -dependencies = [ - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "ff" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" -dependencies = [ - "rand_core 0.6.4", - "subtle 2.4.1", -] - [[package]] name = "ff" version = "0.12.1" @@ -3066,6 +3032,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -3519,30 +3486,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "byteorder", - "ff 0.10.1", - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "group" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" -dependencies = [ - "byteorder", - "ff 0.11.1", - "rand_core 0.6.4", - "subtle 2.4.1", -] - [[package]] name = "group" version = "0.12.1" @@ -4286,6 +4229,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -5937,6 +5889,7 @@ dependencies = [ "futures-util", "getset", "humantime-serde", + "itertools 0.12.0", "lazy_static", "log", "nym-api-requests", @@ -6001,10 +5954,12 @@ dependencies = [ "cosmwasm-std", "getset", "nym-coconut-interface", + "nym-crypto", "nym-mixnet-contract-common", "nym-node-requests", "schemars", "serde", + "tendermint", "ts-rs", ] @@ -6022,6 +5977,7 @@ dependencies = [ "rand 0.7.3", "thiserror", "url", + "zeroize", ] [[package]] @@ -6257,14 +6213,14 @@ dependencies = [ name = "nym-coconut" version = "0.5.0" dependencies = [ - "bls12_381 0.6.0", + "bls12_381", "bs58 0.4.0", "criterion", "digest 0.9.0", "doc-comment", - "ff 0.11.1", + "ff 0.13.0", "getrandom 0.2.10", - "group 0.11.0", + "group 0.13.0", "itertools 0.10.5", "nym-dkg", "nym-pemstore", @@ -6274,6 +6230,7 @@ dependencies = [ "serde_derive", "sha2 0.9.9", "thiserror", + "zeroize", ] [[package]] @@ -6364,7 +6321,7 @@ dependencies = [ name = "nym-credentials" version = "0.1.0" dependencies = [ - "bls12_381 0.5.0", + "bls12_381", "cosmrs", "log", "nym-api-requests", @@ -6373,6 +6330,7 @@ dependencies = [ "nym-validator-client", "rand 0.7.3", "thiserror", + "zeroize", ] [[package]] @@ -6406,11 +6364,11 @@ name = "nym-dkg" version = "0.1.0" dependencies = [ "bitvec", - "bls12_381 0.6.0", + "bls12_381", "bs58 0.4.0", "criterion", - "ff 0.11.1", - "group 0.11.0", + "ff 0.13.0", + "group 0.13.0", "lazy_static", "nym-contracts-common", "nym-pemstore", @@ -7882,20 +7840,11 @@ dependencies = [ [[package]] name = "pairing" -version = "0.20.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" dependencies = [ - "group 0.10.0", -] - -[[package]] -name = "pairing" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" -dependencies = [ - "group 0.11.0", + "group 0.13.0", ] [[package]] @@ -9790,9 +9739,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -11014,16 +10963,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.12.3", + "hashbrown 0.14.1", "pin-project-lite 0.2.13", "slab", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 4d786023d4..52b8a5f382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,7 +162,8 @@ serde_json = "1.0.91" tap = "1.0.1" time = "0.3.30" thiserror = "1.0.48" -tokio = "1.24.1" +tokio = "1.33.0" +tokio-util = "0.7.10" tokio-tungstenite = "0.20.1" tracing = "0.1.37" tungstenite = { version = "0.20.1", default-features = false } @@ -172,6 +173,14 @@ utoipa-swagger-ui = "3.1.5" url = "2.4" zeroize = "1.6.0" +# coconut/DKG related +# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork +# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="feature/gt-serialization-0.8.0" } +group = "0.13.0" +ff = "0.13.0" + + # cosmwasm-related cosmwasm-derive = "=1.3.0" cosmwasm-schema = "=1.3.0" @@ -192,6 +201,7 @@ cw-controllers = { version = "=1.1.0" } bip32 = "0.5.1" cosmrs = "=0.15.0" tendermint-rpc = "0.34" # same version as used by cosmrs +tendermint = "0.34" # same version as used by cosmrs prost = "0.12" # wasm-related dependencies diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 7b8e4a51cb..79b5759e94 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -10,6 +10,7 @@ bip39 = { workspace = true } rand = "0.7.3" thiserror = { workspace = true } url = { workspace = true } +zeroize = { workspace = true } nym-coconut-interface = { path = "../coconut-interface" } nym-credential-storage = { path = "../credential-storage" } diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index c03ae8e90a..116634cf41 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_coconut_interface::{Base58, Parameters}; +use nym_coconut_interface::Base58; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES}; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; use nym_credentials::coconut::utils::obtain_aggregate_signature; use nym_crypto::asymmetric::{encryption, identity}; use nym_network_defaults::VOUCHER_INFO; @@ -12,10 +12,8 @@ use nym_validator_client::coconut::all_coconut_api_clients; use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use nym_validator_client::nyxd::Coin; -use nym_validator_client::nyxd::Hash; use rand::rngs::OsRng; -use state::{KeyPair, State}; -use std::str::FromStr; +use state::State; pub mod state; @@ -24,30 +22,29 @@ where C: CoconutBandwidthSigningClient + Sync, { let mut rng = OsRng; - let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng)); - let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng)); - let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap(); + let signing_key = identity::PrivateKey::new(&mut rng); + let encryption_key = encryption::PrivateKey::new(&mut rng); + let params = BandwidthVoucher::default_parameters(); let voucher_value = amount.amount.to_string(); let tx_hash = client .deposit( amount, String::from(VOUCHER_INFO), - signing_keypair.public_key.clone(), - encryption_keypair.public_key.clone(), + signing_key.public_key().to_base58_string(), + encryption_key.public_key().to_base58_string(), None, ) .await? - .transaction_hash - .to_string(); + .transaction_hash; let voucher = BandwidthVoucher::new( ¶ms, voucher_value, VOUCHER_INFO.to_string(), - Hash::from_str(&tx_hash).map_err(|_| BandwidthControllerError::InvalidTxHash)?, - identity::PrivateKey::from_base58_string(&signing_keypair.private_key)?, - encryption::PrivateKey::from_base58_string(&encryption_keypair.private_key)?, + tx_hash, + signing_key, + encryption_key, ); let state = State { voucher, params }; diff --git a/common/bandwidth-controller/src/acquire/state.rs b/common/bandwidth-controller/src/acquire/state.rs index de77992962..7c6cc31c05 100644 --- a/common/bandwidth-controller/src/acquire/state.rs +++ b/common/bandwidth-controller/src/acquire/state.rs @@ -2,32 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_coconut_interface::Parameters; -use nym_credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES}; - -use nym_crypto::asymmetric::{encryption, identity}; - -pub(crate) struct KeyPair { - pub public_key: String, - pub private_key: String, -} - -impl From for KeyPair { - fn from(kp: identity::KeyPair) -> Self { - Self { - public_key: kp.public_key().to_base58_string(), - private_key: kp.private_key().to_base58_string(), - } - } -} - -impl From for KeyPair { - fn from(kp: encryption::KeyPair) -> Self { - Self { - public_key: kp.public_key().to_base58_string(), - private_key: kp.private_key().to_base58_string(), - } - } -} +use nym_credentials::coconut::bandwidth::BandwidthVoucher; pub struct State { pub voucher: BandwidthVoucher, @@ -38,7 +13,7 @@ impl State { pub fn new(voucher: BandwidthVoucher) -> Self { State { voucher, - params: Parameters::new(TOTAL_ATTRIBUTES).unwrap(), + params: BandwidthVoucher::default_parameters(), } } } diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index d9a714b263..b7d61e2935 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -7,6 +7,7 @@ use nym_credential_storage::storage::Storage; use nym_validator_client::coconut::all_coconut_api_clients; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use std::str::FromStr; +use zeroize::Zeroizing; use { nym_coconut_interface::Base58, nym_credentials::coconut::{ @@ -46,10 +47,12 @@ impl BandwidthController { let voucher_value = u64::from_str(&bandwidth_credential.voucher_value) .map_err(|_| StorageError::InconsistentData)?; let voucher_info = bandwidth_credential.voucher_info.clone(); - let serial_number = - nym_coconut_interface::Attribute::try_from_bs58(bandwidth_credential.serial_number)?; - let binding_number = - nym_coconut_interface::Attribute::try_from_bs58(bandwidth_credential.binding_number)?; + let serial_number = Zeroizing::new(nym_coconut_interface::Attribute::try_from_bs58( + bandwidth_credential.serial_number, + )?); + let binding_number = Zeroizing::new(nym_coconut_interface::Attribute::try_from_bs58( + bandwidth_credential.binding_number, + )?); let signature = nym_coconut_interface::Signature::try_from_bs58(bandwidth_credential.signature)?; let epoch_id = u64::from_str(&bandwidth_credential.epoch_id) @@ -64,8 +67,8 @@ impl BandwidthController { prepare_for_spending( voucher_value, voucher_info, - serial_number, - binding_number, + &serial_number, + &binding_number, epoch_id, &signature, &verification_key, diff --git a/common/client-libs/mixnet-client/Cargo.toml b/common/client-libs/mixnet-client/Cargo.toml index 9eede069da..8496a6fae9 100644 --- a/common/client-libs/mixnet-client/Cargo.toml +++ b/common/client-libs/mixnet-client/Cargo.toml @@ -9,8 +9,8 @@ edition = "2021" [dependencies] futures = { workspace = true } log = { workspace = true } -tokio = { version = "1.24.1", features = ["time", "net", "rt"] } -tokio-util = { version = "0.7.4", features = ["codec"] } +tokio = { workspace = true, features = ["time", "net", "rt"] } +tokio-util = { workspace = true, features = ["codec"] } # internal nym-sphinx = { path = "../../nymsphinx" } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 21ac049840..d4902bd1a1 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -5,8 +5,12 @@ use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; use http_api_client::{ApiClient, NO_PARAMS}; +use nym_api_requests::coconut::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +}; use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, VerifyCredentialBody, + VerifyCredentialResponse, }; use nym_api_requests::models::{ ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, @@ -15,6 +19,7 @@ use nym_api_requests::models::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; +use nym_coconut_dkg_common::types::EpochId; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use nym_name_service_common::response::NamesListResponse; @@ -399,6 +404,60 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn epoch_credentials( + &self, + dkg_epoch: EpochId, + ) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::COCONUT_ROUTES, + routes::BANDWIDTH, + routes::COCONUT_EPOCH_CREDENTIALS, + &dkg_epoch.to_string(), + ], + NO_PARAMS, + ) + .await + } + + async fn issued_credential( + &self, + credential_id: i64, + ) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::COCONUT_ROUTES, + routes::BANDWIDTH, + routes::COCONUT_ISSUED_CREDENTIAL, + &credential_id.to_string(), + ], + NO_PARAMS, + ) + .await + } + + async fn issued_credentials( + &self, + credential_ids: Vec, + ) -> Result { + self.post_json( + &[ + routes::API_VERSION, + routes::COCONUT_ROUTES, + routes::BANDWIDTH, + routes::COCONUT_ISSUED_CREDENTIALS, + ], + NO_PARAMS, + &CredentialsRequestBody { + credential_ids, + pagination: None, + }, + ) + .await + } + async fn get_service_providers(&self) -> Result { log::trace!("Getting service providers"); self.get_json(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS) diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index df063a114d..d670033049 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -17,6 +17,9 @@ pub const BANDWIDTH: &str = "bandwidth"; pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; +pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials"; +pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential"; +pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials"; pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs index 030d78b29a..2402dc078f 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs @@ -32,7 +32,7 @@ pub trait CoconutBandwidthSigningClient { fee: Option, ) -> Result { let req = CoconutBandwidthExecuteMsg::DepositFunds { - data: DepositData::new(info.to_string(), verification_key, encryption_key), + data: DepositData::new(info, verification_key, encryption_key), }; self.execute_coconut_bandwidth_contract( fee, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs index 48fcadc050..57c6ce2bf7 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs @@ -6,8 +6,8 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cw3::{ - ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterListResponse, - VoterResponse, + ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail, + VoterListResponse, VoterResponse, }; use cw_utils::ThresholdResponse; use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg; @@ -114,6 +114,26 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient { Ok(proposals) } + + async fn get_all_voters(&self) -> Result, NyxdError> { + let mut voters = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self.list_voters(start_after.take(), None).await?; + + let last_voter = paged_response.voters.last().map(|prop| prop.addr.clone()); + voters.append(&mut paged_response.voters); + + if let Some(start_after_res) = last_voter { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(voters) + } } #[async_trait] diff --git a/common/client-libs/validator-client/src/nyxd/helpers.rs b/common/client-libs/validator-client/src/nyxd/helpers.rs new file mode 100644 index 0000000000..85999eac6f --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/helpers.rs @@ -0,0 +1,13 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::TxResponse; + +pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option { + let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?; + let attribute = event + .attributes + .iter() + .find(|attr| attr.key == attribute_key)?; + Some(attribute.value.clone()) +} diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index f28df6567f..12f941d2aa 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -47,6 +47,10 @@ pub use cosmrs::Coin as CosmosCoin; pub use cosmrs::Gas; pub use cosmrs::{bip32, AccountId, Denom}; pub use cosmwasm_std::Coin as CosmWasmCoin; +pub use cw2; +pub use cw3; +pub use cw4; +pub use cw_controllers; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; pub use tendermint_rpc::{ endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse}, @@ -67,6 +71,7 @@ pub mod contract_traits; pub mod cosmwasm_client; pub mod error; pub mod fee; +pub mod helpers; #[derive(Debug, Clone)] pub struct Config { diff --git a/common/coconut-interface/src/lib.rs b/common/coconut-interface/src/lib.rs index 2322bc3978..65aab8fb7f 100644 --- a/common/coconut-interface/src/lib.rs +++ b/common/coconut-interface/src/lib.rs @@ -21,10 +21,14 @@ pub use nym_coconut::{ pub struct Credential { #[getset(get = "pub")] n_params: u32, + #[getset(get = "pub")] theta: Theta, + voucher_value: u64, + voucher_info: String, + #[getset(get = "pub")] epoch_id: u64, } @@ -64,14 +68,12 @@ impl Credential { pub fn verify(&self, verification_key: &VerificationKey) -> bool { let params = Parameters::new(self.n_params).unwrap(); - let public_attributes = [ - self.voucher_value.to_string().as_bytes(), - self.voucher_info.as_bytes(), - ] - .iter() - .map(hash_to_scalar) - .collect::>(); - nym_coconut::verify_credential(¶ms, verification_key, &self.theta, &public_attributes) + + let hashed_value = hash_to_scalar(self.voucher_value.to_string()); + let hashed_info = hash_to_scalar(&self.voucher_info); + let public_attributes = &[&hashed_value, &hashed_info]; + + nym_coconut::verify_credential(¶ms, verification_key, &self.theta, public_attributes) } pub fn as_bytes(&self) -> Vec { @@ -180,8 +182,8 @@ mod tests { ¶ms, &verification_key, &signature, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); let credential = Credential::new(4, theta, voucher_value, voucher_info, 42); diff --git a/common/commands/src/coconut/issue_credentials.rs b/common/commands/src/coconut/issue_credentials.rs index cd8a9b5f36..9533af614e 100644 --- a/common/commands/src/coconut/issue_credentials.rs +++ b/common/commands/src/coconut/issue_credentials.rs @@ -26,6 +26,10 @@ pub struct Args { } pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { + if args.amount == 0 { + bail!("did not specify credential amount") + } + let loaded = CommonConfigsWrapper::try_load(args.client_config)?; if let Ok(id) = loaded.try_get_id() { diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs index 5204f6a97e..450ecd2543 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/events.rs @@ -4,6 +4,9 @@ // event types pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds"; +// a 'wasm-' prefix is added to all cosmwasm events +pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds"; + // attributes that are used in multiple places pub const DEPOSIT_VALUE: &str = "deposit-value"; pub const DEPOSIT_INFO: &str = "deposit-info"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index b01f1101ab..29e7c52d67 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -174,17 +174,19 @@ impl Display for EpochState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { EpochState::PublicKeySubmission { resharing } => { - write!(f, "PublicKeySubmission with resharing {resharing}") + write!(f, "PublicKeySubmission (resharing: {resharing})") + } + EpochState::DealingExchange { resharing } => { + write!(f, "DealingExchange (resharing: {resharing})") } - EpochState::DealingExchange { resharing } => write!(f, "DealingExchange {resharing}"), EpochState::VerificationKeySubmission { resharing } => { - write!(f, "VerificationKeySubmission with resharing {resharing}") + write!(f, "VerificationKeySubmission (resharing: {resharing})") } EpochState::VerificationKeyValidation { resharing } => { - write!(f, "VerificationKeyValidation with resharing {resharing}") + write!(f, "VerificationKeyValidation (resharing: {resharing})") } EpochState::VerificationKeyFinalization { resharing } => { - write!(f, "VerificationKeyFinalization with resharing {resharing}") + write!(f, "VerificationKeyFinalization (resharing: {resharing})") } EpochState::InProgress => write!(f, "InProgress"), } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index aff9cf01ab..a50898f518 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -6,14 +6,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } +bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] } cosmrs = { workspace = true } thiserror = { workspace = true } log = { workspace = true } +zeroize = { workspace = true } # I guess temporarily until we get serde support in coconut up and running nym-coconut-interface = { path = "../coconut-interface" } -nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "hashing"] } +nym-crypto = { path = "../crypto", features = ["rand", "asymmetric"] } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } diff --git a/common/credentials/src/coconut/bandwidth.rs b/common/credentials/src/coconut/bandwidth.rs index 7e458456df..9646860a74 100644 --- a/common/credentials/src/coconut/bandwidth.rs +++ b/common/credentials/src/coconut/bandwidth.rs @@ -13,38 +13,60 @@ use nym_coconut_interface::{ PrivateAttribute, PublicAttribute, Signature, VerificationKey, }; use nym_crypto::asymmetric::{encryption, identity}; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::utils::prepare_credential_for_spending; use crate::error::Error; -pub const PUBLIC_ATTRIBUTES: u32 = 2; -pub const PRIVATE_ATTRIBUTES: u32 = 2; -pub const TOTAL_ATTRIBUTES: u32 = PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES; - +#[derive(Zeroize, ZeroizeOnDrop)] pub struct BandwidthVoucher { - // a random secret value generated by the client used for double-spending detection + // private attributes + /// a random secret value generated by the client used for double-spending detection serial_number: PrivateAttribute, - // a random secret value generated by the client used to bind multiple credentials together + + /// a random secret value generated by the client used to bind multiple credentials together binding_number: PrivateAttribute, - // the value (e.g., bandwidth) encoded in this voucher - voucher_value: PublicAttribute, - // the plain text value (e.g., bandwidth) encoded in this voucher + + // public atttributes: + /// the plain text value (e.g., bandwidth) encoded in this voucher + // TODO: in another PR change the value from `"1000"` to `"1000unym"` voucher_value_plain: String, - // a field with public information, e.g., type of voucher, interval etc. - voucher_info: PublicAttribute, - // the plain text information + + /// the plain text information voucher_info_plain: String, - // the hash of the deposit transaction + + /// the precomputed value (e.g., bandwidth) encoded in this voucher + _voucher_value_prehashed: PublicAttribute, + + /// the precomputed field with public information, e.g., type of voucher, interval etc. + _voucher_info_prehashed: PublicAttribute, + + /// the hash of the deposit transaction + #[zeroize(skip)] tx_hash: Hash, - // base58 encoded private key ensuring the depositer requested these attributes + + /// base58 encoded private key ensuring the depositer requested these attributes signing_key: identity::PrivateKey, - // base58 encoded private key ensuring only this client receives the signature share - encryption_key: encryption::PrivateKey, + + /// base58 encoded private key ensuring only this client receives the signature share + unused_ed25519: encryption::PrivateKey, + pedersen_commitments_openings: Vec, + + #[zeroize(skip)] blind_sign_request: BlindSignRequest, } impl BandwidthVoucher { + pub const PUBLIC_ATTRIBUTES: u32 = 2; + pub const PRIVATE_ATTRIBUTES: u32 = 2; + pub const ENCODED_ATTRIBUTES: u32 = 4; + + pub fn default_parameters() -> Parameters { + // safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero + Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap() + } + pub fn new( params: &Parameters, voucher_value: String, @@ -57,24 +79,26 @@ impl BandwidthVoucher { let binding_number = params.random_scalar(); let voucher_value_plain = voucher_value.clone(); let voucher_info_plain = voucher_info.clone(); - let voucher_value = hash_to_scalar(voucher_value.as_bytes()); - let voucher_info = hash_to_scalar(voucher_info.as_bytes()); + + let _voucher_value_prehashed = hash_to_scalar(voucher_value); + let _voucher_info_prehashed = hash_to_scalar(voucher_info); + let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign( params, - &[serial_number, binding_number], - &[voucher_value, voucher_info], + &[&serial_number, &binding_number], + &[&_voucher_value_prehashed, &_voucher_info_prehashed], ) .unwrap(); BandwidthVoucher { serial_number, binding_number, - voucher_value, + _voucher_value_prehashed, voucher_value_plain, - voucher_info, + _voucher_info_prehashed, voucher_info_plain, tx_hash, signing_key, - encryption_key, + unused_ed25519: encryption_key, pedersen_commitments_openings, blind_sign_request, } @@ -87,7 +111,7 @@ impl BandwidthVoucher { let voucher_info_plain_b = self.voucher_info_plain.as_bytes(); let tx_hash_b = self.tx_hash.as_bytes(); let signing_key_b = self.signing_key.to_bytes(); - let encryption_key_b = self.encryption_key.to_bytes(); + let encryption_key_b = self.unused_ed25519.to_bytes(); let blind_sign_request_b = self.blind_sign_request.to_bytes(); let mut ret = Vec::new(); @@ -171,13 +195,13 @@ impl BandwidthVoucher { bytes[var_length_pointer..var_length_pointer + voucher_value_plain_no].to_vec(), ) .or_else(utf_err)?; - let voucher_value = hash_to_scalar(&voucher_value_plain); + let _voucher_value_prehashed = hash_to_scalar(&voucher_value_plain); var_length_pointer += voucher_value_plain_no; let voucher_info_plain = String::from_utf8( bytes[var_length_pointer..var_length_pointer + voucher_info_plain_no].to_vec(), ) .or_else(utf_err)?; - let voucher_info = hash_to_scalar(&voucher_info_plain); + let _voucher_info_prehashed = hash_to_scalar(&voucher_info_plain); var_length_pointer += voucher_info_plain_no; let blind_sign_request = BlindSignRequest::from_bytes( &bytes[var_length_pointer..var_length_pointer + blind_sign_request_no], @@ -196,36 +220,43 @@ impl BandwidthVoucher { Ok(Self { serial_number, binding_number, - voucher_value, + _voucher_value_prehashed, voucher_value_plain, - voucher_info, + _voucher_info_prehashed, voucher_info_plain, tx_hash, signing_key, - encryption_key, + unused_ed25519: encryption_key, pedersen_commitments_openings, blind_sign_request, }) } /// Check if the plain values correspond to the PublicAttributes - pub fn verify_against_plain(values: &[PublicAttribute], plain_values: &[String]) -> bool { + pub fn verify_against_plain(values: &[&PublicAttribute], plain_values: &[String]) -> bool { values.len() == 2 && plain_values.len() == 2 - && values[0] == hash_to_scalar(&plain_values[0]) - && values[1] == hash_to_scalar(&plain_values[1]) + && values[0] == &hash_to_scalar(&plain_values[0]) + && values[1] == &hash_to_scalar(&plain_values[1]) } - pub fn tx_hash(&self) -> &Hash { - &self.tx_hash + pub fn tx_hash(&self) -> Hash { + self.tx_hash } - pub fn get_public_attributes(&self) -> Vec { - vec![self.voucher_value, self.voucher_info] + pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> { + vec![ + &self._voucher_value_prehashed, + &self._voucher_info_prehashed, + ] + } + + pub fn identity_key(&self) -> &identity::PrivateKey { + &self.signing_key } pub fn encryption_key(&self) -> &encryption::PrivateKey { - &self.encryption_key + &self.unused_ed25519 } pub fn pedersen_commitments_openings(&self) -> &Vec { @@ -247,27 +278,32 @@ impl BandwidthVoucher { ] } - pub fn get_private_attributes(&self) -> Vec { - vec![self.serial_number, self.binding_number] + pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> { + vec![&self.serial_number, &self.binding_number] } - pub fn sign(&self, request: &BlindSignRequest) -> identity::Signature { + pub fn signable_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec { let mut message = request.to_bytes(); - message.extend_from_slice(self.tx_hash.to_string().as_bytes()); - self.signing_key.sign(&message) + message.extend_from_slice(tx_hash.as_bytes()); + message + } + + pub fn sign(&self) -> identity::Signature { + let message = Self::signable_plaintext(&self.blind_sign_request, self.tx_hash); + self.signing_key.sign(message) } } pub fn prepare_for_spending( voucher_value: u64, voucher_info: String, - serial_number: PrivateAttribute, - binding_number: PrivateAttribute, + serial_number: &PrivateAttribute, + binding_number: &PrivateAttribute, epoch_id: u64, signature: &Signature, verification_key: &VerificationKey, ) -> Result { - let params = Parameters::new(TOTAL_ATTRIBUTES)?; + let params = Parameters::new(BandwidthVoucher::ENCODED_ATTRIBUTES)?; prepare_credential_for_spending( ¶ms, @@ -316,24 +352,30 @@ mod test { let deserialized_voucher = BandwidthVoucher::try_from_bytes(&bytes).unwrap(); assert_eq!(voucher.serial_number, deserialized_voucher.serial_number); assert_eq!(voucher.binding_number, deserialized_voucher.binding_number); - assert_eq!(voucher.voucher_value, deserialized_voucher.voucher_value); assert_eq!( voucher.voucher_value_plain, deserialized_voucher.voucher_value_plain ); - assert_eq!(voucher.voucher_info, deserialized_voucher.voucher_info); assert_eq!( voucher.voucher_info_plain, deserialized_voucher.voucher_info_plain ); + assert_eq!( + voucher._voucher_value_prehashed, + deserialized_voucher._voucher_value_prehashed + ); + assert_eq!( + voucher._voucher_info_prehashed, + deserialized_voucher._voucher_info_prehashed + ); assert_eq!(voucher.tx_hash, deserialized_voucher.tx_hash); assert_eq!( voucher.signing_key.to_string(), deserialized_voucher.signing_key.to_string() ); assert_eq!( - voucher.encryption_key.to_string(), - deserialized_voucher.encryption_key.to_string() + voucher.unused_ed25519.to_string(), + deserialized_voucher.unused_ed25519.to_string() ); assert_eq!( voucher.pedersen_commitments_openings, @@ -371,11 +413,11 @@ mod test { ] )); assert!(!BandwidthVoucher::verify_against_plain( - &[voucher.get_public_attributes()[0], Attribute::one()], + &[voucher.get_public_attributes()[0], &Attribute::one()], &voucher.get_public_attributes_plain() )); assert!(!BandwidthVoucher::verify_against_plain( - &[Attribute::one(), voucher.get_public_attributes()[1]], + &[&Attribute::one(), voucher.get_public_attributes()[1]], &voucher.get_public_attributes_plain() )); assert!(BandwidthVoucher::verify_against_plain( diff --git a/common/credentials/src/coconut/mod.rs b/common/credentials/src/coconut/mod.rs index c1d900689a..bf480ad58a 100644 --- a/common/credentials/src/coconut/mod.rs +++ b/common/credentials/src/coconut/mod.rs @@ -2,5 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod bandwidth; -pub mod params; pub mod utils; diff --git a/common/credentials/src/coconut/params.rs b/common/credentials/src/coconut/params.rs deleted file mode 100644 index 906ef7cae0..0000000000 --- a/common/credentials/src/coconut/params.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_crypto::aes::Aes128; -use nym_crypto::blake3; -use nym_crypto::ctr; - -type Aes128Ctr = ctr::Ctr64LE; - -/// Hashing algorithm used during hkdf for ephemeral shared key generation per blinded signature -/// response encryption. -pub type NymApiCredentialHkdfAlgorithm = blake3::Hasher; - -/// Encryption algorithm used for end-to-end encryption of blinded signature response -pub type NymApiCredentialEncryptionAlgorithm = Aes128Ctr; diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs index b7fda42f73..367f62e2c3 100644 --- a/common/credentials/src/coconut/utils.rs +++ b/common/credentials/src/coconut/utils.rs @@ -1,18 +1,14 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; -use crate::coconut::params::{NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm}; +use crate::coconut::bandwidth::BandwidthVoucher; use crate::error::Error; use log::{debug, warn}; use nym_api_requests::coconut::BlindSignRequestBody; use nym_coconut_interface::{ aggregate_signature_shares, aggregate_verification_keys, prove_bandwidth_credential, Attribute, - BlindedSignature, Credential, Parameters, Signature, SignatureShare, VerificationKey, + Credential, Parameters, Signature, SignatureShare, VerificationKey, }; -use nym_crypto::asymmetric::encryption::PublicKey; -use nym_crypto::shared_key::recompute_shared_key; -use nym_crypto::symmetric::stream_cipher; use nym_validator_client::client::CoconutApiClient; pub async fn obtain_aggregate_verification_key( @@ -36,47 +32,34 @@ pub async fn obtain_aggregate_verification_key( async fn obtain_partial_credential( params: &Parameters, - attributes: &BandwidthVoucher, + voucher: &BandwidthVoucher, client: &nym_validator_client::client::NymApiClient, validator_vk: &VerificationKey, ) -> Result { - let public_attributes = attributes.get_public_attributes(); - let public_attributes_plain = attributes.get_public_attributes_plain(); - let private_attributes = attributes.get_private_attributes(); - let blind_sign_request = attributes.blind_sign_request(); + let public_attributes_plain = voucher.get_public_attributes_plain(); + let blind_sign_request = voucher.blind_sign_request(); + let request_signature = voucher.sign(); let blind_sign_request_body = BlindSignRequestBody::new( - blind_sign_request, - attributes.tx_hash().to_string(), - attributes.sign(blind_sign_request).to_base58_string(), - &public_attributes, + blind_sign_request.clone(), + voucher.tx_hash(), + request_signature, public_attributes_plain, - (public_attributes.len() + private_attributes.len()) as u32, ); let response = client.blind_sign(&blind_sign_request_body).await?; - let encrypted_signature = response.encrypted_signature; - let remote_key = PublicKey::from_bytes(&response.remote_key)?; - let encryption_key = recompute_shared_key::< - NymApiCredentialEncryptionAlgorithm, - NymApiCredentialHkdfAlgorithm, - >(&remote_key, attributes.encryption_key()); - let zero_iv = stream_cipher::zero_iv::(); - let blinded_signature_bytes = stream_cipher::decrypt::( - &encryption_key, - &zero_iv, - &encrypted_signature, - ); + let blinded_signature = response.blinded_signature; - let blinded_signature = BlindedSignature::from_bytes(&blinded_signature_bytes)?; + let public_attributes = voucher.get_public_attributes(); + let private_attributes = voucher.get_private_attributes(); - let unblinded_signature = blinded_signature.unblind( + let unblinded_signature = blinded_signature.unblind_and_verify( params, validator_vk, &private_attributes, &public_attributes, &blind_sign_request.get_commitment_hash(), - attributes.pedersen_commitments_openings(), + voucher.pedersen_commitments_openings(), )?; Ok(unblinded_signature) @@ -84,16 +67,13 @@ async fn obtain_partial_credential( pub async fn obtain_aggregate_signature( params: &Parameters, - attributes: &BandwidthVoucher, + voucher: &BandwidthVoucher, coconut_api_clients: &[CoconutApiClient], threshold: u64, ) -> Result { if coconut_api_clients.is_empty() { return Err(Error::NoValidatorsAvailable); } - let public_attributes = attributes.get_public_attributes(); - let private_attributes = attributes.get_private_attributes(); - let mut shares = Vec::with_capacity(coconut_api_clients.len()); let validators_partial_vks: Vec<_> = coconut_api_clients .iter() @@ -114,7 +94,7 @@ pub async fn obtain_aggregate_signature( match obtain_partial_credential( params, - attributes, + voucher, &coconut_api_client.api_client, &coconut_api_client.verification_key, ) @@ -136,6 +116,9 @@ pub async fn obtain_aggregate_signature( return Err(Error::NotEnoughShares); } + let public_attributes = voucher.get_public_attributes(); + let private_attributes = voucher.get_private_attributes(); + let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); attributes.extend_from_slice(&private_attributes); attributes.extend_from_slice(&public_attributes); @@ -150,8 +133,8 @@ pub fn prepare_credential_for_spending( params: &Parameters, voucher_value: u64, voucher_info: String, - serial_number: Attribute, - binding_number: Attribute, + serial_number: &Attribute, + binding_number: &Attribute, epoch_id: u64, signature: &Signature, verification_key: &VerificationKey, @@ -165,7 +148,7 @@ pub fn prepare_credential_for_spending( )?; Ok(Credential::new( - PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES, + BandwidthVoucher::ENCODED_ATTRIBUTES, theta, voucher_value, voucher_info, diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index 6c45d41d60..bc97f67df4 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -201,6 +201,17 @@ impl<'a> From<&'a PrivateKey> for PublicKey { } impl PrivateKey { + #[cfg(feature = "rand")] + pub fn new(rng: &mut R) -> Self { + let x25519_secret = x25519_dalek::StaticSecret::new(rng); + + PrivateKey(x25519_secret) + } + + pub fn public_key(&self) -> PublicKey { + self.into() + } + pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] { self.0.to_bytes() } diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index a56abdee32..ffbcac88ac 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -5,13 +5,14 @@ pub use ed25519_dalek::ed25519::signature::Signature as SignatureTrait; pub use ed25519_dalek::SignatureError; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -#[cfg(feature = "sphinx")] -use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; +#[cfg(feature = "sphinx")] +use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; + #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; #[cfg(feature = "serde")] @@ -224,6 +225,17 @@ impl<'a> From<&'a PrivateKey> for PublicKey { } impl PrivateKey { + #[cfg(feature = "rand")] + pub fn new(rng: &mut R) -> Self { + let ed25519_secret = ed25519_dalek::SecretKey::generate(rng); + + PrivateKey(ed25519_secret) + } + + pub fn public_key(&self) -> PublicKey { + self.into() + } + pub fn to_bytes(&self) -> [u8; SECRET_KEY_LENGTH] { self.0.to_bytes() } @@ -295,7 +307,7 @@ impl PemStorableKey for PrivateKey { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Signature(ed25519_dalek::Signature); impl Signature { @@ -319,6 +331,14 @@ impl Signature { } } +impl FromStr for Signature { + type Err = Ed25519RecoveryError; + + fn from_str(s: &str) -> Result { + Signature::from_base58_string(s) + } +} + #[cfg(feature = "serde")] impl Serialize for Signature { fn serialize(&self, serializer: S) -> Result diff --git a/common/dkg/Cargo.toml b/common/dkg/Cargo.toml index 26808724bf..b85805218e 100644 --- a/common/dkg/Cargo.toml +++ b/common/dkg/Cargo.toml @@ -11,7 +11,7 @@ bitvec = "1.0.0" # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork # as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] } +bls12_381 = { workspace = true, default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common", optional = true } bs58 = "0.4" @@ -29,11 +29,11 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } nym-pemstore = { path = "../pemstore" } [dependencies.group] -version = "0.11" +workspace = true default-features = false [dependencies.ff] -version = "0.11" +workspace = true default-features = false [dev-dependencies] diff --git a/common/mixnode-common/Cargo.toml b/common/mixnode-common/Cargo.toml index b2fcf6e2a3..b48707eddf 100644 --- a/common/mixnode-common/Cargo.toml +++ b/common/mixnode-common/Cargo.toml @@ -20,7 +20,7 @@ tokio = { version = "1.24.1", features = [ "net", "io-util", ] } -tokio-util = { version = "0.7.4", features = ["codec"] } +tokio-util = { workspace = true, features = ["codec"] } url = { workspace = true } thiserror = { workspace = true } diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml index 7aed2bb595..e5a98a92b0 100644 --- a/common/nymcoconut/Cargo.toml +++ b/common/nymcoconut/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["pairings", "alloc", "experimental"] } +bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] } itertools = "0.10" digest = "0.9" rand = "0.8" @@ -16,16 +16,17 @@ serde = { workspace = true } serde_derive = "1.0" bs58 = "0.4.0" sha2 = "0.9" +zeroize = { workspace = true, optional = true } nym-dkg = { path = "../dkg" } nym-pemstore = { path = "../pemstore" } [dependencies.ff] -version = "0.11" +workspace = true default-features = false [dependencies.group] -version = "0.11" +workspace = true default-features = false [dev-dependencies] @@ -38,7 +39,9 @@ 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"] } diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs index 6627b437e1..6bf582a244 100644 --- a/common/nymcoconut/benches/benchmarks.rs +++ b/common/nymcoconut/benches/benchmarks.rs @@ -7,7 +7,7 @@ use ff::Field; use group::{Curve, Group}; use nym_coconut::{ aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign, - prove_bandwidth_credential, setup, ttp_keygen, verify_credential, + prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen, verify_credential, verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, Signature, SignatureShare, VerificationKey, }; @@ -66,8 +66,8 @@ fn unblind_and_aggregate( params: &Parameters, blinded_signatures: &[BlindedSignature], partial_verification_keys: &[VerificationKey], - private_attributes: &[Attribute], - public_attributes: &[Attribute], + private_attributes: &[&Attribute], + public_attributes: &[&Attribute], commitment_hash: &G1Projective, pedersen_commitments_openings: &[Scalar], verification_key: &VerificationKey, @@ -78,7 +78,7 @@ fn unblind_and_aggregate( .zip(partial_verification_keys.iter()) .map(|(signature, partial_verification_key)| { signature - .unblind( + .unblind_and_verify( params, partial_verification_key, private_attributes, @@ -171,10 +171,10 @@ fn bench_coconut(c: &mut Criterion) { let params = setup(case.num_public_attrs + case.num_private_attrs).unwrap(); - let public_attributes = params.n_random_scalars(case.num_public_attrs as usize); + 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]; + let private_attributes = vec![&serial_number, &binding_number]; // The prepare blind sign is performed by the user let (pedersen_commitments_openings, blind_sign_request) = @@ -213,7 +213,7 @@ fn bench_coconut(c: &mut Criterion) { b.iter(|| { blind_sign( ¶ms, - &keypair.secret_key(), + keypair.secret_key(), &blind_sign_request, &public_attributes, ) @@ -228,7 +228,7 @@ fn bench_coconut(c: &mut Criterion) { for keypair in coconut_keypairs.iter() { let blinded_signature = blind_sign( ¶ms, - &keypair.secret_key(), + keypair.secret_key(), &blind_sign_request, &public_attributes, ) @@ -238,7 +238,7 @@ fn bench_coconut(c: &mut Criterion) { let verification_keys: Vec = coconut_keypairs .iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect(); // verify a random partial blind signature @@ -310,8 +310,8 @@ fn bench_coconut(c: &mut Criterion) { ¶ms, &aggr_verification_key, &aggregated_signature, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); @@ -329,8 +329,8 @@ fn bench_coconut(c: &mut Criterion) { ¶ms, &aggr_verification_key, &aggregated_signature, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap() }) diff --git a/common/nymcoconut/src/elgamal.rs b/common/nymcoconut/src/elgamal.rs index bf3895861e..3a1d473b2e 100644 --- a/common/nymcoconut/src/elgamal.rs +++ b/common/nymcoconut/src/elgamal.rs @@ -34,7 +34,10 @@ impl TryFrom<&[u8]> for Ciphertext { ))); } + // 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( @@ -112,7 +115,16 @@ impl Bytable for PrivateKey { } fn try_from_byte_slice(slice: &[u8]) -> Result { - PrivateKey::from_bytes(slice.try_into().unwrap()) + 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) } } @@ -145,21 +157,36 @@ impl PublicKey { } pub fn to_bytes(&self) -> [u8; 48] { - self.to_byte_vec().try_into().unwrap() + self.0.to_affine().to_compressed() } pub fn from_bytes(bytes: &[u8; 48]) -> Result { - Ok(PublicKey::try_from(bytes.to_vec().as_slice()).unwrap()) + 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 { - self.0.to_affine().to_compressed().into() + self.to_bytes().into() } fn try_from_byte_slice(slice: &[u8]) -> Result { - Ok(PublicKey::from_bytes(slice.try_into().unwrap()).unwrap()) + 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) } } @@ -167,13 +194,7 @@ impl TryFrom<&[u8]> for PublicKey { type Error = CoconutError; fn try_from(slice: &[u8]) -> Result { - try_deserialize_g1_projective( - slice.try_into().unwrap(), - CoconutError::Deserialization( - "Failed to deserialize compressed ElGamal public key".to_string(), - ), - ) - .map(PublicKey) + PublicKey::try_from_byte_slice(slice) } } @@ -225,7 +246,7 @@ pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair { pub fn compute_attribute_encryption( params: &Parameters, - private_attributes: &[Attribute], + private_attributes: &[&Attribute], pub_key: &PublicKey, commitment_hash: G1Projective, ) -> (Vec, Vec) { diff --git a/common/nymcoconut/src/error.rs b/common/nymcoconut/src/error.rs index 7faf11c86e..d502c76c95 100644 --- a/common/nymcoconut/src/error.rs +++ b/common/nymcoconut/src/error.rs @@ -50,4 +50,20 @@ pub enum CoconutError { 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, } diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 821b1b3b29..4f0db30a15 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -1,11 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::convert::TryInto; +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] -use bls12_381::Scalar; - -pub use crate::traits::Bytable; pub use elgamal::elgamal_keygen; pub use elgamal::ElGamalKeyPair; pub use elgamal::PublicKey; @@ -30,6 +28,7 @@ pub use scheme::BlindedSignature; pub use scheme::Signature; pub use scheme::SignatureShare; pub use traits::Base58; +pub use traits::Bytable; pub use utils::hash_to_scalar; pub mod elgamal; @@ -41,18 +40,6 @@ pub mod tests; mod traits; mod utils; -pub type Attribute = Scalar; +pub type Attribute = bls12_381::Scalar; pub type PrivateAttribute = Attribute; pub type PublicAttribute = Attribute; - -impl Bytable for Attribute { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap()) - } -} - -impl Base58 for Attribute {} diff --git a/common/nymcoconut/src/proofs/mod.rs b/common/nymcoconut/src/proofs/mod.rs index 42f923466d..a7a5a97f79 100644 --- a/common/nymcoconut/src/proofs/mod.rs +++ b/common/nymcoconut/src/proofs/mod.rs @@ -91,8 +91,8 @@ impl ProofCmCs { commitment_opening: &Scalar, commitments: &[G1Projective], pedersen_commitments_openings: &[Scalar], - private_attributes: &[Attribute], - public_attributes: &[Attribute], + 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 @@ -162,11 +162,8 @@ impl ProofCmCs { &challenge, &pedersen_commitments_openings.iter().collect::>(), ); - let response_attributes = produce_responses( - &witness_attributes, - &challenge, - &private_attributes.iter().collect::>(), - ); + let response_attributes = + produce_responses(&witness_attributes, &challenge, private_attributes); ProofCmCs { challenge, @@ -181,7 +178,7 @@ impl ProofCmCs { params: &Parameters, commitment: &G1Projective, commitments: &[G1Projective], - public_attributes: &[Attribute], + public_attributes: &[&Attribute], ) -> bool { if self.response_attributes.len() != commitments.len() { return false; @@ -203,7 +200,7 @@ impl ProofCmCs { - public_attributes .iter() .zip(params.gen_hs().iter().skip(self.response_attributes.len())) - .map(|(pub_attr, hs)| hs * pub_attr) + .map(|(&pub_attr, hs)| hs * pub_attr) .sum::()) * self.challenge + g1 * self.response_opening @@ -280,8 +277,12 @@ impl ProofCmCs { } 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; @@ -297,6 +298,8 @@ impl ProofCmCs { ), )?; + // 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 { @@ -313,6 +316,8 @@ impl ProofCmCs { 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, @@ -462,7 +467,7 @@ impl ProofKappaZeta { pub(crate) fn from_bytes(bytes: &[u8]) -> Result { // at the very minimum there must be a single attribute being proven - if bytes.len() < 32 * 4 || (bytes.len()) % 32 != 0 { + if bytes.len() != 128 { return Err(CoconutError::DeserializationInvalidLength { actual: bytes.len(), modulus_target: bytes.len(), @@ -472,24 +477,32 @@ impl ProofKappaZeta { }); } + // 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, @@ -512,14 +525,13 @@ impl ProofKappaZeta { #[cfg(test)] mod tests { - use group::Group; - use rand::thread_rng; - + use super::*; use crate::scheme::keygen::keygen; use crate::scheme::setup::setup; use crate::scheme::verification::{compute_kappa, compute_zeta}; - - use super::*; + use crate::tests::helpers::random_scalars_refs; + use group::Group; + use rand::thread_rng; #[test] fn proof_cm_cs_bytes_roundtrip() { @@ -530,7 +542,7 @@ mod tests { let r = params.random_scalar(); let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)]; let rs = params.n_random_scalars(1); - let private_attributes = params.n_random_scalars(1); + random_scalars_refs!(private_attributes, params, 1); // 0 public 1 private let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]); @@ -546,7 +558,7 @@ mod tests { G1Projective::random(&mut rng), ]; let rs = params.n_random_scalars(2); - let private_attributes = params.n_random_scalars(2); + random_scalars_refs!(private_attributes, params, 2); // 0 public 2 privates let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]); @@ -562,20 +574,20 @@ mod tests { let keypair = keygen(¶ms); // 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 serial_number = ¶ms.random_scalar(); + let binding_number = ¶ms.random_scalar(); let private_attributes = vec![serial_number, binding_number]; let r = params.random_scalar(); - let kappa = compute_kappa(¶ms, &keypair.verification_key(), &private_attributes, r); + let kappa = compute_kappa(¶ms, keypair.verification_key(), &private_attributes, r); let zeta = compute_zeta(¶ms, serial_number); // 0 public 2 private let pi_v = ProofKappaZeta::construct( ¶ms, - &keypair.verification_key(), - &serial_number, - &binding_number, + keypair.verification_key(), + serial_number, + binding_number, &r, &kappa, &zeta, @@ -592,9 +604,9 @@ mod tests { let pi_v = ProofKappaZeta::construct( ¶ms, - &keypair.verification_key(), - &serial_number, - &binding_number, + keypair.verification_key(), + serial_number, + binding_number, &r, &kappa, &zeta, diff --git a/common/nymcoconut/src/scheme/aggregation.rs b/common/nymcoconut/src/scheme/aggregation.rs index e41beb8d36..3ff5f93bc2 100644 --- a/common/nymcoconut/src/scheme/aggregation.rs +++ b/common/nymcoconut/src/scheme/aggregation.rs @@ -83,7 +83,7 @@ pub fn aggregate_verification_keys( pub fn aggregate_signatures( params: &Parameters, verification_key: &VerificationKey, - attributes: &[Attribute], + attributes: &[&Attribute], signatures: &[PartialSignature], indices: Option<&[SignerIndex]>, ) -> Result { @@ -100,7 +100,7 @@ pub fn aggregate_signatures( let tmp = attributes .iter() .zip(verification_key.beta_g2.iter()) - .map(|(attr, beta_i)| beta_i * attr) + .map(|(&attr, beta_i)| beta_i * attr) .sum::(); if !check_bilinear_pairing( @@ -119,7 +119,7 @@ pub fn aggregate_signatures( pub fn aggregate_signature_shares( params: &Parameters, verification_key: &VerificationKey, - attributes: &[Attribute], + attributes: &[&Attribute], shares: &[SignatureShare], ) -> Result { let (signatures, indices): (Vec<_>, Vec<_>) = shares @@ -138,13 +138,13 @@ pub fn aggregate_signature_shares( #[cfg(test)] mod tests { - use bls12_381::G1Projective; - use group::Group; - use crate::scheme::issuance::sign; use crate::scheme::keygen::ttp_keygen; use crate::scheme::setup::Parameters; use crate::scheme::verification::verify; + use crate::tests::helpers::random_scalars_refs; + use bls12_381::G1Projective; + use group::Group; use super::*; @@ -155,7 +155,7 @@ mod tests { let vks = keypairs .into_iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect::>(); let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap(); @@ -212,13 +212,18 @@ mod tests { #[test] fn signature_aggregation_works_for_any_subset_of_signatures() { let mut params = Parameters::new(2).unwrap(); - let attributes = params.n_random_scalars(2); + random_scalars_refs!(attributes, params, 2); let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); let (sks, vks): (Vec<_>, Vec<_>) = keypairs .into_iter() - .map(|keypair| (keypair.secret_key(), keypair.verification_key())) + .map(|keypair| { + ( + keypair.secret_key().clone(), + keypair.verification_key().clone(), + ) + }) .unzip(); let sigs = sks @@ -312,12 +317,17 @@ mod tests { fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() { let signatures: Vec = vec![]; let params = Parameters::new(2).unwrap(); - let attributes = params.n_random_scalars(2); + random_scalars_refs!(attributes, params, 2); let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); let (_, vks): (Vec<_>, Vec<_>) = keypairs .into_iter() - .map(|keypair| (keypair.secret_key(), keypair.verification_key())) + .map(|keypair| { + ( + keypair.secret_key().clone(), + keypair.verification_key().clone(), + ) + }) .unzip(); let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); @@ -330,11 +340,16 @@ mod tests { fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() { let signatures = vec![random_signature()]; let params = Parameters::new(2).unwrap(); - let attributes = params.n_random_scalars(2); + random_scalars_refs!(attributes, params, 2); let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); let (_, vks): (Vec<_>, Vec<_>) = keypairs .into_iter() - .map(|keypair| (keypair.secret_key(), keypair.verification_key())) + .map(|keypair| { + ( + keypair.secret_key().clone(), + keypair.verification_key().clone(), + ) + }) .unzip(); let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); @@ -356,11 +371,16 @@ mod tests { fn signature_aggregation_doesnt_work_for_non_unique_indices() { let signatures = vec![random_signature(), random_signature()]; let params = Parameters::new(2).unwrap(); - let attributes = params.n_random_scalars(2); + random_scalars_refs!(attributes, params, 2); let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); let (_, vks): (Vec<_>, Vec<_>) = keypairs .into_iter() - .map(|keypair| (keypair.secret_key(), keypair.verification_key())) + .map(|keypair| { + ( + keypair.secret_key().clone(), + keypair.verification_key().clone(), + ) + }) .unzip(); let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); diff --git a/common/nymcoconut/src/scheme/double_use.rs b/common/nymcoconut/src/scheme/double_use.rs index f952938a72..ce3cf58d67 100644 --- a/common/nymcoconut/src/scheme/double_use.rs +++ b/common/nymcoconut/src/scheme/double_use.rs @@ -25,6 +25,8 @@ impl TryFrom<&[u8]> for BlindedSerialNumber { )); } + // 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( diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index 1109dfe890..11d0dcb35b 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -54,6 +54,8 @@ impl TryFrom<&[u8]> for BlindSignRequest { 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, @@ -63,6 +65,8 @@ impl TryFrom<&[u8]> for BlindSignRequest { )?; 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, @@ -72,6 +76,8 @@ impl TryFrom<&[u8]> for BlindSignRequest { )?; 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 { @@ -86,6 +92,14 @@ impl TryFrom<&[u8]> for BlindSignRequest { 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, @@ -137,7 +151,7 @@ impl Bytable for BlindSignRequest { impl Base58 for BlindSignRequest {} impl BlindSignRequest { - fn verify_proof(&self, params: &Parameters, public_attributes: &[Attribute]) -> bool { + fn verify_proof(&self, params: &Parameters, public_attributes: &[&Attribute]) -> bool { self.pi_s.verify( params, &self.commitment, @@ -150,8 +164,8 @@ impl BlindSignRequest { self.commitment_hash } - pub fn get_private_attributes_pedersen_commitments(&self) -> Vec { - self.private_attributes_commitments.clone() + pub fn get_private_attributes_pedersen_commitments(&self) -> &[G1Projective] { + &self.private_attributes_commitments } pub fn to_bytes(&self) -> Vec { @@ -161,12 +175,16 @@ impl BlindSignRequest { pub fn from_bytes(bytes: &[u8]) -> Result { 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], + private_attributes: &[&Attribute], + public_attributes: &[&Attribute], hs: &[G1Affine], ) -> (Scalar, G1Projective) { let commitment_opening = params.random_scalar(); @@ -187,7 +205,7 @@ pub fn compute_attributes_commitment( pub fn compute_pedersen_commitments_for_private_attributes( params: &Parameters, - private_attributes: &[Attribute], + private_attributes: &[&Attribute], h: &G1Projective, ) -> (Vec, Vec) { // Generate openings for Pedersen commitment for each private attribute @@ -197,13 +215,13 @@ pub fn compute_pedersen_commitments_for_private_attributes( let pedersen_commitments = commitments_openings .iter() .zip(private_attributes.iter()) - .map(|(o_j, m_j)| params.gen1() * o_j + h * m_j) + .map(|(o_j, &m_j)| params.gen1() * o_j + h * m_j) .collect::>(); (commitments_openings, pedersen_commitments) } -pub fn compute_hash(commitment: G1Projective, public_attributes: &[Attribute]) -> G1Projective { +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 { @@ -215,8 +233,8 @@ pub fn compute_hash(commitment: G1Projective, public_attributes: &[Attribute]) - /// Builds cryptographic material required for blind sign. pub fn prepare_blind_sign( params: &Parameters, - private_attributes: &[Attribute], - public_attributes: &[Attribute], + private_attributes: &[&Attribute], + public_attributes: &[&Attribute], ) -> Result<(Vec, BlindSignRequest)> { if private_attributes.is_empty() { return Err(CoconutError::Issuance( @@ -271,7 +289,7 @@ pub fn blind_sign( params: &Parameters, signing_secret_key: &SecretKey, blind_sign_request: &BlindSignRequest, - public_attributes: &[Attribute], + public_attributes: &[&Attribute], ) -> Result { let num_private = blind_sign_request.private_attributes_commitments.len(); let hs = params.gen_hs(); @@ -304,7 +322,7 @@ pub fn blind_sign( let signed_public = h * public_attributes .iter() .zip(signing_secret_key.ys.iter().skip(num_private)) - .map(|(attr, yi)| attr * yi) + .map(|(&attr, yi)| attr * yi) .sum::(); // h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) @@ -345,7 +363,7 @@ pub fn blind_sign( pub fn verify_partial_blind_signature( params: &Parameters, blind_sign_request: &BlindSignRequest, - public_attributes: &[Attribute], + public_attributes: &[&Attribute], blind_sig: &BlindedSignature, partial_verification_key: &VerificationKey, ) -> bool { @@ -383,7 +401,7 @@ pub fn verify_partial_blind_signature( } // 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( + for (&pub_attr, beta_g2) in public_attributes.iter().zip( partial_verification_key .beta_g2 .iter() @@ -415,7 +433,7 @@ pub fn verify_partial_blind_signature( pub fn sign( params: &mut Parameters, secret_key: &SecretKey, - public_attributes: &[Attribute], + public_attributes: &[&Attribute], ) -> Result { if public_attributes.len() > secret_key.ys.len() { return Err(CoconutError::IssuanceMaxAttributes { @@ -429,7 +447,7 @@ pub fn sign( // (the python implementation hashes string representation of all attributes onto the curve, // but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary // transformation. If I'm wrong, please correct me.) - let attributes_sum = public_attributes.iter().sum::(); + let attributes_sum = public_attributes.iter().copied().sum::(); let h = hash_g1((params.gen1() * attributes_sum).to_bytes()); // x + m0 * y0 + m1 * y1 + ... mn * yn @@ -437,7 +455,7 @@ pub fn sign( + public_attributes .iter() .zip(secret_key.ys.iter()) - .map(|(m_i, y_i)| m_i * y_i) + .map(|(&m_i, y_i)| m_i * y_i) .sum::(); let sig2 = h * exponent; @@ -448,13 +466,14 @@ pub fn sign( 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(); - let private_attributes = params.n_random_scalars(1); - let public_attributes = params.n_random_scalars(0); + random_scalars_refs!(private_attributes, params, 1); + random_scalars_refs!(public_attributes, params, 0); let (_commitments_openings, lambda) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); @@ -467,8 +486,8 @@ mod tests { // 2 public and 2 private attributes let params = Parameters::new(4).unwrap(); - let private_attributes = params.n_random_scalars(2); - let public_attributes = params.n_random_scalars(2); + random_scalars_refs!(private_attributes, params, 2); + random_scalars_refs!(public_attributes, params, 2); let (_commitments_openings, lambda) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); @@ -483,8 +502,8 @@ mod tests { #[test] fn successful_verify_partial_blind_signature() { let params = Parameters::new(4).unwrap(); - let private_attributes = params.n_random_scalars(2); - let public_attributes = params.n_random_scalars(2); + random_scalars_refs!(private_attributes, params, 2); + random_scalars_refs!(public_attributes, params, 2); let (_commitments_openings, request) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); @@ -492,7 +511,7 @@ mod tests { let validator_keypair = keygen(¶ms); let blind_sig = blind_sign( ¶ms, - &validator_keypair.secret_key(), + validator_keypair.secret_key(), &request, &public_attributes, ) @@ -503,36 +522,35 @@ mod tests { &request, &public_attributes, &blind_sig, - &validator_keypair.verification_key() + validator_keypair.verification_key() )); } #[test] fn successful_verify_partial_blind_signature_no_public_attributes() { let params = Parameters::new(4).unwrap(); - let private_attributes = params.n_random_scalars(2); + random_scalars_refs!(private_attributes, params, 2); let (_commitments_openings, request) = prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); let validator_keypair = keygen(¶ms); - let blind_sig = - blind_sign(¶ms, &validator_keypair.secret_key(), &request, &[]).unwrap(); + let blind_sig = blind_sign(¶ms, validator_keypair.secret_key(), &request, &[]).unwrap(); assert!(verify_partial_blind_signature( ¶ms, &request, &[], &blind_sig, - &validator_keypair.verification_key() + validator_keypair.verification_key() )); } #[test] fn fail_verify_partial_blind_signature_with_wrong_key() { let params = Parameters::new(4).unwrap(); - let private_attributes = params.n_random_scalars(2); - let public_attributes = params.n_random_scalars(2); + random_scalars_refs!(private_attributes, params, 2); + random_scalars_refs!(public_attributes, params, 2); let (_commitments_openings, request) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); @@ -541,7 +559,7 @@ mod tests { let validator2_keypair = keygen(¶ms); let blind_sig = blind_sign( ¶ms, - &validator_keypair.secret_key(), + validator_keypair.secret_key(), &request, &public_attributes, ) @@ -553,7 +571,7 @@ mod tests { &request, &public_attributes, &blind_sig, - &validator2_keypair.verification_key() + validator2_keypair.verification_key() ),); } } diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index 352d704193..d033d8f0be 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -23,8 +23,12 @@ use crate::utils::{ }; use crate::Base58; -#[derive(Debug, Clone)] -#[cfg_attr(test, derive(PartialEq, Eq))] +#[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, @@ -62,7 +66,9 @@ impl TryFrom<&[u8]> for SecretKey { } // 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; @@ -97,6 +103,10 @@ impl SecretKey { (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(); @@ -141,6 +151,10 @@ impl Base58 for SecretKey {} // TODO: perhaps change points to affine representation // to make verification slightly more efficient? #[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr( + feature = "key-zeroize", + derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop) +)] pub struct VerificationKey { // TODO add gen2 as per the paper or imply it from the fact library is using bls381? pub(crate) alpha: G2Projective, @@ -180,7 +194,9 @@ impl TryFrom<&[u8]> for VerificationKey { } // 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); @@ -204,6 +220,8 @@ impl TryFrom<&[u8]> for VerificationKey { 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, @@ -220,6 +238,8 @@ impl TryFrom<&[u8]> for VerificationKey { 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, @@ -392,7 +412,11 @@ impl Bytable for VerificationKey { impl Base58 for VerificationKey {} #[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] +#[cfg_attr(test, derive(PartialEq, Eq, Clone))] +#[cfg_attr( + feature = "key-zeroize", + derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop) +)] pub struct KeyPair { secret_key: SecretKey, verification_key: VerificationKey, @@ -429,12 +453,12 @@ impl KeyPair { } } - pub fn secret_key(&self) -> SecretKey { - self.secret_key.clone() + pub fn secret_key(&self) -> &SecretKey { + &self.secret_key } - pub fn verification_key(&self) -> VerificationKey { - self.verification_key.clone() + pub fn verification_key(&self) -> &VerificationKey { + &self.verification_key } pub fn to_bytes(&self) -> Vec { @@ -488,6 +512,8 @@ impl TryFrom<&[u8]> for KeyPair { }); } + // 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; @@ -503,6 +529,8 @@ impl TryFrom<&[u8]> for KeyPair { }); } + // 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() @@ -515,6 +543,7 @@ impl TryFrom<&[u8]> for KeyPair { )?; 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() diff --git a/common/nymcoconut/src/scheme/mod.rs b/common/nymcoconut/src/scheme/mod.rs index 9e642716ca..68dd0d2b9a 100644 --- a/common/nymcoconut/src/scheme/mod.rs +++ b/common/nymcoconut/src/scheme/mod.rs @@ -44,7 +44,10 @@ impl TryFrom<&[u8]> for Signature { ))); } + // 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( @@ -88,6 +91,45 @@ impl Signature { pub fn from_bytes(bytes: &[u8]) -> Result { 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 !(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::(); + + // 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 { @@ -102,8 +144,7 @@ impl Bytable for Signature { impl Base58 for Signature {} -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Debug, PartialEq, Eq)] pub struct BlindedSignature(G1Projective, G1Projective); impl Bytable for BlindedSignature { @@ -129,7 +170,10 @@ impl TryFrom<&[u8]> for BlindedSignature { ))); } + // 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( @@ -148,24 +192,12 @@ impl TryFrom<&[u8]> for BlindedSignature { impl BlindedSignature { pub fn unblind( &self, - params: &Parameters, partial_verification_key: &VerificationKey, - private_attributes: &[Attribute], - public_attributes: &[Attribute], - commitment_hash: &G1Projective, pedersen_commitments_openings: &[Scalar], ) -> Result { // parse the signature let h = &self.0; let c = &self.1; - - // Verify the commitment hash - if !(commitment_hash == h) { - return Err(CoconutError::Unblind( - "Verification of commitment hash from signature failed".to_string(), - )); - } - let blinding_removers = partial_verification_key .beta_g1 .iter() @@ -175,30 +207,29 @@ impl BlindedSignature { let unblinded_c = c - blinding_removers; - 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::(); - - // Verify the signature share - if !check_bilinear_pairing( - &h.to_affine(), - &G2Prepared::from((alpha + signed_attributes).to_affine()), - &unblinded_c.to_affine(), - params.prepared_miller_g2(), - ) { - return Err(CoconutError::Unblind( - "Verification of signature share failed".to_string(), - )); - } - Ok(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 { + 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()); @@ -237,25 +268,25 @@ impl SignatureShare { #[cfg(test)] mod tests { + use super::*; use crate::hash_to_scalar; use crate::scheme::aggregation::{aggregate_signatures, 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 super::*; + 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(); - let private_attributes = params.n_random_scalars(2_usize); + random_scalars_refs!(private_attributes, params, 2); let (_commitments_openings, lambda) = prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); let keypair1 = keygen(¶ms); - let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[]).unwrap(); + let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap(); let wrong_commitment_opening = params.random_scalar(); let wrong_commitment = params.gen1() * wrong_commitment_opening; @@ -263,9 +294,9 @@ mod tests { let wrong_commitments_openings = params.n_random_scalars(private_attributes.len()); assert!(sig1 - .unblind( + .unblind_and_verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &private_attributes, &[], &fake_commitment_hash, @@ -277,20 +308,23 @@ mod tests { #[test] fn unblind_returns_error_if_signature_verification_fails() { let params = Parameters::new(2).unwrap(); - let private_attributes = vec![hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")]; - let private_attributes2 = vec![hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")]; + 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(¶ms, &private_attributes, &[]).unwrap(); let keypair1 = keygen(¶ms); - let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[]).unwrap(); + let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap(); assert!(sig1 - .unblind( + .unblind_and_verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &private_attributes2, &[], &lambda.get_commitment_hash(), @@ -304,7 +338,7 @@ mod tests { 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 private_attributes = vec![&serial_number, &binding_number]; let keypair1 = keygen(¶ms); let keypair2 = keygen(¶ms); @@ -312,11 +346,11 @@ mod tests { let (commitments_openings, lambda) = prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); - let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[]) + let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]) .unwrap() - .unblind( + .unblind_and_verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &private_attributes, &[], &lambda.get_commitment_hash(), @@ -324,11 +358,11 @@ mod tests { ) .unwrap(); - let sig2 = blind_sign(¶ms, &keypair2.secret_key(), &lambda, &[]) + let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &[]) .unwrap() - .unblind( + .unblind_and_verify( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &private_attributes, &[], &lambda.get_commitment_hash(), @@ -338,39 +372,39 @@ mod tests { let theta1 = prove_bandwidth_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &sig1, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); let theta2 = prove_bandwidth_credential( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &sig2, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); assert!(verify_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &theta1, &[], )); assert!(verify_credential( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &theta2, &[], )); assert!(!verify_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &theta2, &[], )); @@ -379,30 +413,30 @@ mod tests { #[test] fn verification_on_two_public_attributes() { let mut params = Parameters::new(2).unwrap(); - let attributes = params.n_random_scalars(2); + random_scalars_refs!(attributes, params, 2); let keypair1 = keygen(¶ms); let keypair2 = keygen(¶ms); - let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap(); - let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap(); + let sig1 = sign(&mut params, keypair1.secret_key(), &attributes).unwrap(); + let sig2 = sign(&mut params, keypair2.secret_key(), &attributes).unwrap(); assert!(verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &attributes, &sig1, )); assert!(!verify( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &attributes, &sig1, )); assert!(!verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &attributes, &sig2, )); @@ -411,10 +445,11 @@ mod tests { #[test] fn verification_on_two_public_and_two_private_attributes() { let params = Parameters::new(4).unwrap(); - let public_attributes = params.n_random_scalars(2); + 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 private_attributes = vec![&serial_number, &binding_number]; let keypair1 = keygen(¶ms); let keypair2 = keygen(¶ms); @@ -422,11 +457,11 @@ mod tests { let (commitments_openings, lambda) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &public_attributes) + let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &public_attributes) .unwrap() - .unblind( + .unblind_and_verify( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &private_attributes, &public_attributes, &lambda.get_commitment_hash(), @@ -434,11 +469,11 @@ mod tests { ) .unwrap(); - let sig2 = blind_sign(¶ms, &keypair2.secret_key(), &lambda, &public_attributes) + let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &public_attributes) .unwrap() - .unblind( + .unblind_and_verify( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &private_attributes, &public_attributes, &lambda.get_commitment_hash(), @@ -448,39 +483,39 @@ mod tests { let theta1 = prove_bandwidth_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &sig1, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); let theta2 = prove_bandwidth_credential( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &sig2, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); assert!(verify_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &theta1, &public_attributes, )); assert!(verify_credential( ¶ms, - &keypair2.verification_key(), + keypair2.verification_key(), &theta2, &public_attributes, )); assert!(!verify_credential( ¶ms, - &keypair1.verification_key(), + keypair1.verification_key(), &theta2, &public_attributes, )); @@ -489,10 +524,11 @@ mod tests { #[test] fn verification_on_two_public_and_two_private_attributes_from_two_signers() { let params = Parameters::new(4).unwrap(); - let public_attributes = params.n_random_scalars(2); + 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 private_attributes = vec![&serial_number, &binding_number]; let keypairs = ttp_keygen(¶ms, 2, 3).unwrap(); @@ -502,11 +538,11 @@ mod tests { let sigs = keypairs .iter() .map(|keypair| { - blind_sign(¶ms, &keypair.secret_key(), &lambda, &public_attributes) + blind_sign(¶ms, keypair.secret_key(), &lambda, &public_attributes) .unwrap() - .unblind( + .unblind_and_verify( ¶ms, - &keypair.verification_key(), + keypair.verification_key(), &private_attributes, &public_attributes, &lambda.get_commitment_hash(), @@ -518,7 +554,7 @@ mod tests { let vks = keypairs .into_iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect::>(); let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); @@ -530,9 +566,14 @@ mod tests { aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2])) .unwrap(); - let theta = - prove_bandwidth_credential(¶ms, &aggr_vk, &aggr_sig, serial_number, binding_number) - .unwrap(); + let theta = prove_bandwidth_credential( + ¶ms, + &aggr_vk, + &aggr_sig, + &serial_number, + &binding_number, + ) + .unwrap(); assert!(verify_credential( ¶ms, @@ -547,9 +588,14 @@ mod tests { aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3])) .unwrap(); - let theta = - prove_bandwidth_credential(¶ms, &aggr_vk, &aggr_sig, serial_number, binding_number) - .unwrap(); + let theta = prove_bandwidth_credential( + ¶ms, + &aggr_vk, + &aggr_sig, + &serial_number, + &binding_number, + ) + .unwrap(); assert!(verify_credential( ¶ms, diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs index 96ef28e9ca..0addda985a 100644 --- a/common/nymcoconut/src/scheme/verification.rs +++ b/common/nymcoconut/src/scheme/verification.rs @@ -43,6 +43,8 @@ impl TryFrom<&[u8]> for Theta { )); } + // 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, @@ -51,6 +53,8 @@ impl TryFrom<&[u8]> for Theta { ), )?; + // safety: we just checked for the length so the unwraps are fine + #[allow(clippy::unwrap_used)] let blinded_serial_number_bytes = bytes[96..192].try_into().unwrap(); let blinded_serial_number = try_deserialize_g2_projective( &blinded_serial_number_bytes, @@ -130,7 +134,7 @@ impl Base58 for Theta {} pub fn compute_kappa( params: &Parameters, verification_key: &VerificationKey, - private_attributes: &[Attribute], + private_attributes: &[&Attribute], blinding_factor: Scalar, ) -> G2Projective { params.gen2() * blinding_factor @@ -138,11 +142,11 @@ pub fn compute_kappa( + private_attributes .iter() .zip(verification_key.beta_g2.iter()) - .map(|(priv_attr, beta_i)| beta_i * priv_attr) + .map(|(&priv_attr, beta_i)| beta_i * priv_attr) .sum::() } -pub fn compute_zeta(params: &Parameters, serial_number: Attribute) -> G2Projective { +pub fn compute_zeta(params: &Parameters, serial_number: &Attribute) -> G2Projective { params.gen2() * serial_number } @@ -150,8 +154,8 @@ pub fn prove_bandwidth_credential( params: &Parameters, verification_key: &VerificationKey, signature: &Signature, - serial_number: Attribute, - binding_number: Attribute, + serial_number: &Attribute, + binding_number: &Attribute, ) -> Result { if verification_key.beta_g2.len() < 2 { return Err( @@ -171,7 +175,7 @@ pub fn prove_bandwidth_credential( // 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 = vec![serial_number, binding_number]; + let private_attributes = [serial_number, binding_number]; let blinded_message = compute_kappa( params, verification_key, @@ -185,8 +189,8 @@ pub fn prove_bandwidth_credential( let pi_v = ProofKappaZeta::construct( params, verification_key, - &serial_number, - &binding_number, + serial_number, + binding_number, &sign_blinding_factor, &blinded_message, &blinded_serial_number, @@ -221,7 +225,10 @@ pub fn check_vk_pairing( if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { return false; } - if vk.alpha != *dkg_values.last().unwrap() { + + // 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.last().as_ref().unwrap() { return false; } if dkg_values @@ -249,7 +256,7 @@ pub fn verify_credential( params: &Parameters, verification_key: &VerificationKey, theta: &Theta, - public_attributes: &[Attribute], + public_attributes: &[&Attribute], ) -> bool { if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta_g2.len() @@ -272,7 +279,7 @@ pub fn verify_credential( .iter() .skip(theta.pi_v.private_attributes_len()), ) - .map(|(pub_attr, beta_i)| beta_i * pub_attr) + .map(|(&pub_attr, beta_i)| beta_i * pub_attr) .sum::(); theta.blinded_message + signed_public_attributes @@ -291,14 +298,14 @@ pub fn verify_credential( pub fn verify( params: &Parameters, verification_key: &VerificationKey, - public_attributes: &[Attribute], + 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) + .map(|(&m_i, b_i)| b_i * m_i) .sum::()) .to_affine(); @@ -320,10 +327,11 @@ mod tests { #[test] fn vk_pairing() { let params = setup(2).unwrap(); - let vk = keygen(¶ms).verification_key(); + let keypair = keygen(¶ms); + let vk = keypair.verification_key(); let mut dkg_values = vk.beta_g2.clone(); dkg_values.push(vk.alpha); - assert!(check_vk_pairing(¶ms, &dkg_values, &vk)); + assert!(check_vk_pairing(¶ms, &dkg_values, vk)); } #[test] @@ -340,10 +348,10 @@ mod tests { let theta = prove_bandwidth_credential( ¶ms, - &keypair.verification_key(), + keypair.verification_key(), &signature, - serial_number, - binding_number, + &serial_number, + &binding_number, ) .unwrap(); diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs index db950d0c3f..b9cc397845 100644 --- a/common/nymcoconut/src/tests/e2e.rs +++ b/common/nymcoconut/src/tests/e2e.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // 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, @@ -12,14 +13,14 @@ fn keygen() -> Result<(), CoconutError> { let params = setup(5)?; let node_indices = vec![15u64, 248, 33521]; - let public_attributes = params.n_random_scalars(2); + random_scalars_refs!(public_attributes, params, 2); // generate_keys let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?; let verification_keys: Vec = coconut_keypairs .iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect(); // aggregate verification keys @@ -50,14 +51,14 @@ fn dkg() -> Result<(), CoconutError> { let params = setup(5)?; let node_indices = vec![15u64, 248, 33521]; - let public_attributes = params.n_random_scalars(2); + random_scalars_refs!(public_attributes, params, 2); // generate_keys let coconut_keypairs = generate_dkg_keys(5, &node_indices); let verification_keys: Vec = coconut_keypairs .iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect(); // aggregate verification keys diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs index a432cecd60..61c4531903 100644 --- a/common/nymcoconut/src/tests/helpers.rs +++ b/common/nymcoconut/src/tests/helpers.rs @@ -5,15 +5,17 @@ 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, indices: &[scheme::SignerIndex], - public_attributes: &Vec, + public_attributes: &[&PublicAttribute], ) -> Result { let serial_number = params.random_scalar(); let binding_number = params.random_scalar(); - let private_attributes = vec![serial_number, binding_number]; + let private_attributes = vec![&serial_number, &binding_number]; // generate commitment let (commitments_openings, blind_sign_request) = @@ -21,7 +23,7 @@ pub fn theta_from_keys_and_attributes( let verification_keys: Vec = coconut_keypairs .iter() - .map(|keypair| keypair.verification_key()) + .map(|keypair| keypair.verification_key().clone()) .collect(); // aggregate verification keys @@ -33,7 +35,7 @@ pub fn theta_from_keys_and_attributes( for keypair in coconut_keypairs { let blinded_signature = blind_sign( params, - &keypair.secret_key(), + keypair.secret_key(), &blind_sign_request, public_attributes, )?; @@ -49,7 +51,7 @@ pub fn theta_from_keys_and_attributes( .map(|(idx, s, vk)| { ( *idx, - s.unblind( + s.unblind_and_verify( params, vk, &private_attributes, @@ -81,13 +83,15 @@ pub fn theta_from_keys_and_attributes( params, &verification_key, &signature, - serial_number, - binding_number, + &serial_number, + &binding_number, )?; Ok(theta) } +// unwraps are fine in the test code +#[allow(clippy::unwrap_used)] pub fn transpose_matrix(matrix: Vec>) -> Vec> { if matrix.is_empty() { return vec![]; @@ -166,3 +170,14 @@ pub mod tests { .collect() } } + +#[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::>(); + }; +} + +pub use random_scalars_refs; diff --git a/common/nymcoconut/src/traits.rs b/common/nymcoconut/src/traits.rs index a800388f88..d521b133ac 100644 --- a/common/nymcoconut/src/traits.rs +++ b/common/nymcoconut/src/traits.rs @@ -1,4 +1,13 @@ +// Copyright 2021-2023 - Nym Technologies SA +// 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; +use std::convert::TryInto; pub trait Bytable where @@ -14,9 +23,67 @@ where Self: Bytable, { fn try_from_bs58>(x: S) -> Result { - Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap()) + 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 { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + 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 { + self.to_bytes().as_ref().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + 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 {} diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 9fec3b334c..c2fcd4da3f 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -34,6 +34,7 @@ impl Polynomial { // 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 @@ -148,6 +149,8 @@ pub(crate) fn try_deserialize_scalar_vec( 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::>::into(Scalar::from_bytes(&s_bytes)) { None => return Err(err), diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml index bf2c030e2e..b62de91f53 100644 --- a/common/nymsphinx/framing/Cargo.toml +++ b/common/nymsphinx/framing/Cargo.toml @@ -9,7 +9,7 @@ repository = { workspace = true } [dependencies] bytes = "1.0" -tokio-util = { version = "0.7.4", features = ["codec"] } +tokio-util = { workspace = true, features = ["codec"] } thiserror = { workspace = true } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } diff --git a/common/socks5/proxy-helpers/Cargo.toml b/common/socks5/proxy-helpers/Cargo.toml index 6ebaaa03c9..3043d88efd 100644 --- a/common/socks5/proxy-helpers/Cargo.toml +++ b/common/socks5/proxy-helpers/Cargo.toml @@ -8,8 +8,8 @@ edition = "2021" [dependencies] bytes = "1.0" -tokio = { version = "1.24.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] } -tokio-util = { version = "0.7.4", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use +tokio = { workspace = true, features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] } +tokio-util = { workspace = true, features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use # their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition. # In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine. futures = { workspace = true } diff --git a/ephemera/Cargo.toml b/ephemera/Cargo.toml index f0af6af3a9..e32fec2746 100644 --- a/ephemera/Cargo.toml +++ b/ephemera/Cargo.toml @@ -47,7 +47,7 @@ serde_json = "1.0.91" thiserror = { workspace = true } tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] } tokio-tungstenite = { workspace = true } -tokio-util = { version = "0.7.4", features = ["full"] } +tokio-util = { workspace = true, features = ["full"] } toml = "0.7.0" unsigned-varint = "0.7.1" utoipa = { workspace = true, features = ["actix_extras"] } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 285864be20..b2c36ee33c 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -53,7 +53,7 @@ tokio = { workspace = true, features = [ ] } tokio-stream = { version = "0.1.11", features = ["fs"] } tokio-tungstenite = { version = "0.20.1" } -tokio-util = { version = "0.7.4", features = ["codec"] } +tokio-util = { workspace = true, features = ["codec"] } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true } diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index bdcb0b3a9b..1c7997ea92 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -33,8 +33,8 @@ rand = "0.7.3" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sysinfo = "0.27.7" -tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } -tokio-util = { version = "0.7.3", features = ["codec"] } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } +tokio-util = { workspace = true, features = ["codec"] } toml = "0.5.8" url = { workspace = true, features = ["serde"] } cfg-if = "1.0.0" diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 243e152d52..cc3b679ef8 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -24,6 +24,7 @@ clap = { workspace = true, features = ["cargo", "derive"] } console-subscriber = { version = "0.1.1", optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" dirs = "4.0" futures = { workspace = true } +itertools = "0.12.0" humantime-serde = "1.0" lazy_static = "1.4.0" log = { workspace = true } @@ -97,7 +98,7 @@ nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-co nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nym-service-provider-directory-common = { path = "../common/cosmwasm-smart-contracts/service-provider-directory" } nym-name-service-common = { path = "../common/cosmwasm-smart-contracts/name-service" } -nym-coconut = { path = "../common/nymcoconut" } +nym-coconut = { path = "../common/nymcoconut", features = ["key-zeroize"] } nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } diff --git a/nym-api/migrations/20231127120000_partial_credentials_info.sql b/nym-api/migrations/20231127120000_partial_credentials_info.sql new file mode 100644 index 0000000000..1c01a3f30d --- /dev/null +++ b/nym-api/migrations/20231127120000_partial_credentials_info.sql @@ -0,0 +1,28 @@ +/* + * Copyright 2023 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +DROP TABLE signed_deposit; + +-- represents information about credentials issued in that epoch so that the other parties could make their appropriate queries +-- note: before this information can be returned to a client (of the API), it needs to be signed first +CREATE TABLE epoch_credentials +( + epoch_id INTEGER NOT NULL PRIMARY KEY UNIQUE, + start_id INTEGER NOT NULL, + total_issued INTEGER NOT NULL +); + +-- particular credential issued in this epoch +CREATE TABLE issued_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + tx_hash VARCHAR NOT NULL UNIQUE, + bs58_partial_credential VARCHAR NOT NULL, + bs58_signature VARCHAR NOT NULL, + joined_private_commitments VARCHAR NOT NULL, + joined_public_attributes VARCHAR NOT NULL +); \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index d9869bd7ae..edbd00dad3 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -13,8 +13,11 @@ getset = "0.1.1" schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } +tendermint = { workspace = true } nym-coconut-interface = { path = "../../common/coconut-interface" } +nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"]} + nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } diff --git a/nym-api/nym-api-requests/src/coconut.rs b/nym-api/nym-api-requests/src/coconut.rs deleted file mode 100644 index 6f79bc2f21..0000000000 --- a/nym-api/nym-api-requests/src/coconut.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmrs::AccountId; -use getset::{CopyGetters, Getters}; -use serde::{Deserialize, Serialize}; - -use nym_coconut_interface::{ - error::CoconutInterfaceError, Attribute, Base58, BlindSignRequest, Credential, VerificationKey, -}; - -#[derive(Serialize, Deserialize, Getters, CopyGetters)] -pub struct VerifyCredentialBody { - #[getset(get = "pub")] - credential: Credential, - #[getset(get = "pub")] - proposal_id: u64, - #[getset(get = "pub")] - gateway_cosmos_addr: AccountId, -} - -impl VerifyCredentialBody { - pub fn new( - credential: Credential, - proposal_id: u64, - gateway_cosmos_addr: AccountId, - ) -> VerifyCredentialBody { - VerifyCredentialBody { - credential, - proposal_id, - gateway_cosmos_addr, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VerifyCredentialResponse { - pub verification_result: bool, -} - -impl VerifyCredentialResponse { - pub fn new(verification_result: bool) -> Self { - VerifyCredentialResponse { - verification_result, - } - } -} - -// All strings are base58 encoded representations of structs -#[derive(Clone, Serialize, Deserialize, Debug, Getters, CopyGetters)] -pub struct BlindSignRequestBody { - #[getset(get = "pub")] - blind_sign_request: BlindSignRequest, - #[getset(get = "pub")] - tx_hash: String, - #[getset(get = "pub")] - signature: String, - public_attributes: Vec, - #[getset(get = "pub")] - public_attributes_plain: Vec, - #[getset(get = "pub")] - total_params: u32, -} - -impl BlindSignRequestBody { - pub fn new( - blind_sign_request: &BlindSignRequest, - tx_hash: String, - signature: String, - public_attributes: &[Attribute], - public_attributes_plain: Vec, - total_params: u32, - ) -> BlindSignRequestBody { - BlindSignRequestBody { - blind_sign_request: blind_sign_request.clone(), - tx_hash, - signature, - public_attributes: public_attributes - .iter() - .map(|attr| attr.to_bs58()) - .collect(), - public_attributes_plain, - total_params, - } - } - - pub fn public_attributes(&self) -> Vec { - self.public_attributes - .iter() - .map(|x| Attribute::try_from_bs58(x).unwrap()) - .collect() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BlindedSignatureResponse { - pub remote_key: [u8; 32], - pub encrypted_signature: Vec, -} - -impl BlindedSignatureResponse { - pub fn new(encrypted_signature: Vec, remote_key: [u8; 32]) -> BlindedSignatureResponse { - BlindedSignatureResponse { - encrypted_signature, - remote_key, - } - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string>(val: I) -> Result { - let bytes = bs58::decode(val).into_vec()?; - Self::from_bytes(&bytes) - } - - pub fn to_bytes(&self) -> Vec { - let mut bytes = self.remote_key.to_vec(); - bytes.extend_from_slice(&self.encrypted_signature); - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < 32 { - return Err(CoconutInterfaceError::InvalidByteLength(bytes.len(), 32)); - } - let mut remote_key = [0u8; 32]; - remote_key.copy_from_slice(&bytes[..32]); - let encrypted_signature = bytes[32..].to_vec(); - Ok(BlindedSignatureResponse { - remote_key, - encrypted_signature, - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct VerificationKeyResponse { - pub key: VerificationKey, -} - -impl VerificationKeyResponse { - pub fn new(key: VerificationKey) -> VerificationKeyResponse { - VerificationKeyResponse { key } - } -} - -#[derive(Serialize, Deserialize)] -pub struct CosmosAddressResponse { - pub addr: AccountId, -} - -impl CosmosAddressResponse { - pub fn new(addr: AccountId) -> CosmosAddressResponse { - CosmosAddressResponse { addr } - } -} diff --git a/nym-api/nym-api-requests/src/coconut/helpers.rs b/nym-api/nym-api-requests/src/coconut/helpers.rs new file mode 100644 index 0000000000..58aec3eca0 --- /dev/null +++ b/nym-api/nym-api-requests/src/coconut/helpers.rs @@ -0,0 +1,33 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_coconut_interface::BlindedSignature; +use tendermint::hash::Hash; + +// recomputes plaintext on the credential nym-api has used for signing +// +// note: this method doesn't have to be reversible so just naively concatenate everything +pub fn issued_credential_plaintext( + epoch_id: u32, + tx_hash: Hash, + blinded_partial_credential: &BlindedSignature, + bs58_encoded_private_attributes_commitments: &[String], + public_attributes: &[String], +) -> Vec { + epoch_id + .to_be_bytes() + .into_iter() + .chain(tx_hash.as_bytes().iter().copied()) + .chain(blinded_partial_credential.to_bytes()) + .chain( + bs58_encoded_private_attributes_commitments + .iter() + .flat_map(|attr| attr.as_bytes().iter().copied()), + ) + .chain( + public_attributes + .iter() + .flat_map(|attr| attr.as_bytes().iter().copied()), + ) + .collect() +} diff --git a/nym-api/nym-api-requests/src/coconut/mod.rs b/nym-api/nym-api-requests/src/coconut/mod.rs new file mode 100644 index 0000000000..fb5740928c --- /dev/null +++ b/nym-api/nym-api-requests/src/coconut/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +pub mod models; + +pub use models::{ + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, + VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, +}; diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs new file mode 100644 index 0000000000..79db872288 --- /dev/null +++ b/nym-api/nym-api-requests/src/coconut/models.rs @@ -0,0 +1,233 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::helpers::issued_credential_plaintext; +use cosmrs::AccountId; +use nym_coconut_interface::{ + error::CoconutInterfaceError, hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, + Bytable, Credential, VerificationKey, +}; +use nym_crypto::asymmetric::identity; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use tendermint::hash::Hash; + +#[derive(Serialize, Deserialize)] +pub struct VerifyCredentialBody { + pub credential: Credential, + + pub proposal_id: u64, + + pub gateway_cosmos_addr: AccountId, +} + +impl VerifyCredentialBody { + pub fn new( + credential: Credential, + proposal_id: u64, + gateway_cosmos_addr: AccountId, + ) -> VerifyCredentialBody { + VerifyCredentialBody { + credential, + proposal_id, + gateway_cosmos_addr, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerifyCredentialResponse { + pub verification_result: bool, +} + +impl VerifyCredentialResponse { + pub fn new(verification_result: bool) -> Self { + VerifyCredentialResponse { + verification_result, + } + } +} + +// All strings are base58 encoded representations of structs +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct BlindSignRequestBody { + pub inner_sign_request: BlindSignRequest, + + /// Hash of the deposit transaction + pub tx_hash: Hash, + + /// Signature on the inner sign request and the tx hash + pub signature: identity::Signature, + + // public_attributes: Vec, + pub public_attributes_plain: Vec, +} + +impl BlindSignRequestBody { + pub fn new( + inner_sign_request: BlindSignRequest, + tx_hash: Hash, + signature: identity::Signature, + public_attributes_plain: Vec, + ) -> BlindSignRequestBody { + BlindSignRequestBody { + inner_sign_request, + tx_hash, + signature, + public_attributes_plain, + } + } + + pub fn attributes(&self) -> u32 { + (self.public_attributes_plain.len() + self.inner_sign_request.num_private_attributes()) + as u32 + } + + pub fn public_attributes_hashed(&self) -> Vec { + self.public_attributes_plain + .iter() + .map(hash_to_scalar) + .collect() + } + + pub fn encode_commitments(&self) -> Vec { + use nym_coconut_interface::Base58; + + self.inner_sign_request + .get_private_attributes_pedersen_commitments() + .iter() + .map(|c| c.to_bs58()) + .collect() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BlindedSignatureResponse { + pub blinded_signature: BlindedSignature, +} + +impl BlindedSignatureResponse { + pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { + BlindedSignatureResponse { blinded_signature } + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.blinded_signature.to_byte_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::from_bytes(bytes)?, + }) + } +} + +#[derive(Serialize, Deserialize)] +pub struct VerificationKeyResponse { + pub key: VerificationKey, +} + +impl VerificationKeyResponse { + pub fn new(key: VerificationKey) -> VerificationKeyResponse { + VerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CosmosAddressResponse { + pub addr: AccountId, +} + +impl CosmosAddressResponse { + pub fn new(addr: AccountId) -> CosmosAddressResponse { + CosmosAddressResponse { addr } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Pagination { + /// last_key is the last value returned in the previous query. + /// it's used to indicate the start of the next (this) page. + /// the value itself is not included in the response. + pub last_key: Option, + + /// limit is the total number of results to be returned in the result page. + /// If left empty it will default to a value to be set by each app. + pub limit: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CredentialsRequestBody { + /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. + pub credential_ids: Vec, + + /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. + pub pagination: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct EpochCredentialsResponse { + pub epoch_id: u64, + pub first_epoch_credential_id: Option, + pub total_issued: u32, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialsResponse { + // note: BTreeMap returns ordered results so it's fine to use it with pagination + pub credentials: BTreeMap, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialResponse { + pub credential: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialInner { + pub credential: IssuedCredential, + + pub signature: identity::Signature, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub tx_hash: Hash, + + // NOTE: if we find creation of this guy takes too long, + // change `BlindedSignature` to `BlindedSignatureBytes` + // so that nym-api wouldn't need to parse the value out of its storage + pub blinded_partial_credential: BlindedSignature, + pub bs58_encoded_private_attributes_commitments: Vec, + pub public_attributes: Vec, +} + +impl IssuedCredential { + // this method doesn't have to be reversible so just naively concatenate everything + pub fn signable_plaintext(&self) -> Vec { + issued_credential_plaintext( + self.epoch_id, + self.tx_hash, + &self.blinded_partial_credential, + &self.bs58_encoded_private_attributes_commitments, + &self.public_attributes, + ) + } +} diff --git a/nym-api/src/coconut/api_routes/helpers.rs b/nym-api/src/coconut/api_routes/helpers.rs new file mode 100644 index 0000000000..6589499767 --- /dev/null +++ b/nym-api/src/coconut/api_routes/helpers.rs @@ -0,0 +1,28 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::error::Result; +use crate::coconut::storage::models::IssuedCredential; +use nym_api_requests::coconut::models::IssuedCredentialInner; +use nym_api_requests::coconut::models::IssuedCredentialsResponse; +use std::collections::BTreeMap; + +pub(crate) fn build_credentials_response( + raw: Vec, +) -> Result { + let mut credentials = BTreeMap::new(); + + for raw_credential in raw { + let id = raw_credential.id; + let api_issued = IssuedCredentialInner::try_from(raw_credential)?; + let old = credentials.insert(id, api_issued); + if old.is_some() { + // why do we panic here rather than return an error? because it's a critical failure because + // since the raw values came directly from the database with the PRIMARY KEY constraint + // it should be IMPOSSIBLE to have duplicate values here + panic!("somehow retrieved multiple credentials with id {id} from the database!") + } + } + + Ok(IssuedCredentialsResponse { credentials }) +} diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs new file mode 100644 index 0000000000..b783f9c0a2 --- /dev/null +++ b/nym-api/src/coconut/api_routes/mod.rs @@ -0,0 +1,211 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::api_routes::helpers::build_credentials_response; +use crate::coconut::error::{CoconutError, Result}; +use crate::coconut::helpers::{accepted_vote_err, blind_sign}; +use crate::coconut::state::State; +use crate::coconut::storage::CoconutStorageExt; +use nym_api_requests::coconut::models::{ + CredentialsRequestBody, EpochCredentialsResponse, IssuedCredentialResponse, + IssuedCredentialsResponse, +}; +use nym_api_requests::coconut::{ + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, +}; +use nym_coconut_bandwidth_contract_common::spend_credential::{ + funds_from_cosmos_msgs, SpendCredentialStatus, +}; +use nym_coconut_dkg_common::types::EpochId; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; +use nym_validator_client::nyxd::{Coin, Fee}; +use rocket::serde::json::Json; +use rocket::State as RocketState; + +mod helpers; + +#[post("/blind-sign", data = "")] +// Until we have serialization and deserialization traits we'll be using a crutch +pub async fn post_blind_sign( + blind_sign_request_body: Json, + state: &RocketState, +) -> Result> { + debug!("Received blind sign request"); + trace!("body: {:?}", blind_sign_request_body); + + // early check: does the request have the expected number of public attributes? + debug!("performing basic request validation"); + if blind_sign_request_body.public_attributes_plain.len() + != BandwidthVoucher::PUBLIC_ATTRIBUTES as usize + { + return Err(CoconutError::InconsistentPublicAttributes); + } + + // check if we already issued a credential for this tx hash + debug!( + "checking if we have already issued credential for this tx_hash (hash: {})", + blind_sign_request_body.tx_hash + ); + if let Some(blinded_signature) = state + .already_issued(blind_sign_request_body.tx_hash) + .await? + { + return Ok(Json(BlindedSignatureResponse { blinded_signature })); + } + + // check if we have the signing key available + debug!("checking if we actually have coconut keys derived..."); + let keypair_guard = state.coconut_keypair.get().await; + let Some(signing_key) = keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + + // get the transaction details of the claimed deposit + debug!("getting transaction details from the chain"); + let tx = state + .get_transaction(blind_sign_request_body.tx_hash) + .await?; + + // check validity of the request + debug!("fully validating received request"); + state.validate_request(&blind_sign_request_body, tx).await?; + + // produce the partial signature + debug!("producing the partial credential"); + let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.secret_key())?; + + // store the information locally + debug!("storing the issued credential in the database"); + state + .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .await?; + + // finally return the credential to the client + Ok(Json(BlindedSignatureResponse { blinded_signature })) +} + +#[post("/verify-bandwidth-credential", data = "")] +pub async fn verify_bandwidth_credential( + verify_credential_body: Json, + state: &RocketState, +) -> Result> { + let proposal_id = verify_credential_body.proposal_id; + let proposal = state.client.get_proposal(proposal_id).await?; + // Proposal description is the blinded serial number + if !verify_credential_body + .credential + .has_blinded_serial_number(&proposal.description)? + { + return Err(CoconutError::IncorrectProposal { + reason: String::from("incorrect blinded serial number in description"), + }); + } + let proposed_release_funds = + funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { + reason: String::from("action is not to release funds"), + })?; + // Credential has not been spent before, and is on its way of being spent + let credential_status = state + .client + .get_spent_credential(verify_credential_body.credential.blinded_serial_number()) + .await? + .spend_credential + .ok_or(CoconutError::InvalidCredentialStatus { + status: String::from("Inexistent"), + })? + .status(); + if credential_status != SpendCredentialStatus::InProgress { + return Err(CoconutError::InvalidCredentialStatus { + status: format!("{:?}", credential_status), + }); + } + let verification_key = state + .verification_key(*verify_credential_body.credential.epoch_id()) + .await?; + let mut vote_yes = verify_credential_body.credential.verify(&verification_key); + + vote_yes &= Coin::from(proposed_release_funds) + == Coin::new( + verify_credential_body.credential.voucher_value() as u128, + state.mix_denom.clone(), + ); + + // Vote yes or no on the proposal based on the verification result + let ret = state + .client + .vote_proposal( + proposal_id, + vote_yes, + Some(Fee::new_payer_granter_auto( + None, + None, + Some(verify_credential_body.gateway_cosmos_addr.clone()), + )), + ) + .await; + accepted_vote_err(ret)?; + + Ok(Json(VerifyCredentialResponse::new(vote_yes))) +} + +#[get("/epoch-credentials/")] +pub async fn epoch_credentials( + epoch: EpochId, + state: &RocketState, +) -> Result> { + let issued = state.storage.get_epoch_credentials(epoch).await?; + + let response = if let Some(issued) = issued { + issued.into() + } else { + EpochCredentialsResponse { + epoch_id: epoch, + first_epoch_credential_id: None, + total_issued: 0, + } + }; + + Ok(Json(response)) +} + +#[get("/issued-credential/")] +pub async fn issued_credential( + id: i64, + state: &RocketState, +) -> Result> { + let issued = state.storage.get_issued_credential(id).await?; + + let credential = if let Some(issued) = issued { + Some(issued.try_into()?) + } else { + None + }; + + Ok(Json(IssuedCredentialResponse { credential })) +} + +#[post("/issued-credentials", data = "")] +pub async fn issued_credentials( + params: Json, + state: &RocketState, +) -> Result> { + let params = params.into_inner(); + + if params.pagination.is_some() && !params.credential_ids.is_empty() { + return Err(CoconutError::InvalidQueryArguments); + } + + let credentials = if let Some(pagination) = params.pagination { + state + .storage + .get_issued_credentials_paged(pagination) + .await? + } else { + state + .storage + .get_issued_credentials(params.credential_ids) + .await? + }; + + build_credentials_response(credentials).map(Json) +} diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index adb132e1b1..8ff1f5ed79 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -13,12 +13,12 @@ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyS use nym_contracts_common::dealings::ContractSafeBytes; use nym_dkg::Threshold; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::{AccountId, Fee, TxResponse}; +use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; #[async_trait] pub trait Client { async fn address(&self) -> AccountId; - async fn get_tx(&self, tx_hash: &str) -> Result; + async fn get_tx(&self, tx_hash: Hash) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; async fn list_proposals(&self) -> Result>; async fn get_spent_credential( diff --git a/nym-api/src/coconut/comm.rs b/nym-api/src/coconut/comm.rs index 9358af95be..a18d10588e 100644 --- a/nym-api/src/coconut/comm.rs +++ b/nym-api/src/coconut/comm.rs @@ -7,29 +7,101 @@ use nym_coconut_dkg_common::types::EpochId; use nym_coconut_interface::VerificationKey; use nym_credentials::coconut::utils::obtain_aggregate_verification_key; use nym_validator_client::coconut::all_coconut_api_clients; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::cmp::min; +use std::collections::HashMap; use std::ops::Deref; +use time::OffsetDateTime; +use tokio::sync::RwLock; #[async_trait] pub trait APICommunicationChannel { + async fn current_epoch(&self) -> Result; async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result; } +struct CachedEpoch { + valid_until: OffsetDateTime, + current_epoch_id: EpochId, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch_id: 0, + } + } +} + +impl CachedEpoch { + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + async fn update(&mut self, client: &DirectSigningHttpRpcNyxdClient) -> Result<()> { + let epoch = client.get_current_epoch().await?; + + let now = OffsetDateTime::now_utc(); + let state_end = + OffsetDateTime::from_unix_timestamp(epoch.finish_timestamp.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + + // make it valid until the next epoch transition or next 5min, whichever is smaller + self.valid_until = now + min(until_epoch_state_end, 5 * time::Duration::MINUTE); + self.current_epoch_id = epoch.epoch_id; + + Ok(()) + } +} + pub(crate) struct QueryCommunicationChannel { nyxd_client: nyxd::Client, + + epoch_keys: RwLock>, + cached_epoch: RwLock, } impl QueryCommunicationChannel { pub fn new(nyxd_client: nyxd::Client) -> Self { - QueryCommunicationChannel { nyxd_client } + QueryCommunicationChannel { + nyxd_client, + epoch_keys: Default::default(), + cached_epoch: Default::default(), + } } } #[async_trait] impl APICommunicationChannel for QueryCommunicationChannel { + async fn current_epoch(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(guard.current_epoch_id); + } + + // update cache + drop(guard); + let mut guard = self.cached_epoch.write().await; + let client = self.nyxd_client.0.read().await; + guard.update(&client).await?; + + return Ok(guard.current_epoch_id); + } + async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result { + if let Some(vk) = self.epoch_keys.read().await.get(&epoch_id) { + return Ok(vk.clone()); + } + + let mut guard = self.epoch_keys.write().await; let client = self.nyxd_client.0.read().await; let coconut_api_clients = all_coconut_api_clients(client.deref(), epoch_id).await?; let vk = obtain_aggregate_verification_key(&coconut_api_clients).await?; + + guard.insert(epoch_id, vk.clone()); + Ok(vk) } } diff --git a/nym-api/src/coconut/deposit.rs b/nym-api/src/coconut/deposit.rs index d7db423d60..22f59fc969 100644 --- a/nym-api/src/coconut/deposit.rs +++ b/nym-api/src/coconut/deposit.rs @@ -1,189 +1,112 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::coconut::error::{CoconutError, Result}; use nym_api_requests::coconut::BlindSignRequestBody; use nym_coconut_bandwidth_contract_common::events::{ - DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, - DEPOSIT_VALUE, + COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, + DEPOSIT_INFO, DEPOSIT_VALUE, }; use nym_credentials::coconut::bandwidth::BandwidthVoucher; -use nym_crypto::asymmetric::encryption; -use nym_crypto::asymmetric::identity::{self, Signature}; +use nym_crypto::asymmetric::identity; +use nym_validator_client::nyxd::helpers::find_tx_attribute; use nym_validator_client::nyxd::TxResponse; -use super::error::{CoconutError, Result}; - -pub async fn extract_encryption_key( - blind_sign_request_body: &BlindSignRequestBody, - tx: TxResponse, -) -> Result { - let blind_sign_request = blind_sign_request_body.blind_sign_request(); - let public_attributes = blind_sign_request_body.public_attributes(); - let public_attributes_plain = blind_sign_request_body.public_attributes_plain(); - - if !BandwidthVoucher::verify_against_plain(&public_attributes, public_attributes_plain) { +pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> { + if request.public_attributes_plain.len() != BandwidthVoucher::PUBLIC_ATTRIBUTES as usize { return Err(CoconutError::InconsistentPublicAttributes); } - let tx_hash_str = blind_sign_request_body.tx_hash(); - let mut message = blind_sign_request.to_bytes(); - message.extend_from_slice(tx_hash_str.as_bytes()); + // extract actual public attributes + associated x25519 public key + let deposit_value = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) + .ok_or(CoconutError::DepositValueNotFound)?; - let signature = Signature::from_base58_string(blind_sign_request_body.signature())?; + let deposit_info = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) + .ok_or(CoconutError::DepositInfoNotFound)?; - let attributes: &Vec<_> = tx - .tx_result - .events - .iter() - .find(|event| event.kind == format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE)) - .ok_or(CoconutError::DepositEventNotFound)? - .attributes - .as_ref(); + let x25519_raw = find_tx_attribute( + &tx, + COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, + DEPOSIT_IDENTITY_KEY, + ) + .ok_or(CoconutError::DepositVerifKeyNotFound)?; - let deposit_value: &str = attributes - .iter() - .find(|tag| tag.key == DEPOSIT_VALUE) - .ok_or(CoconutError::DepositValueNotFound)? - .value - .as_ref(); - let deposit_value_plain = public_attributes_plain.first().cloned().unwrap_or_default(); - if deposit_value != deposit_value_plain { - return Err(CoconutError::DifferentPublicAttributes( - deposit_value.to_string(), - deposit_value_plain, - )); + // we're not using it anymore, but for legacy reasons it must be present + let _ed25519_raw = find_tx_attribute( + &tx, + COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, + DEPOSIT_ENCRYPTION_KEY, + ) + .ok_or(CoconutError::DepositEncrKeyNotFound)?; + + // check public attributes against request data + // (thinking about it attaching that data might be redundant since we have the source of truth on the chain) + // safety: we won't read data out of bounds since we just checked we have BandwidthVoucher::PUBLIC_ATTRIBUTES values in the vec + if deposit_value != request.public_attributes_plain[0] { + return Err(CoconutError::InconsistentDepositValue { + request: request.public_attributes_plain[0].clone(), + on_chain: deposit_value, + }); } - let deposit_info: &str = attributes - .iter() - .find(|tag| tag.key == DEPOSIT_INFO) - .ok_or(CoconutError::DepositInfoNotFound)? - .value - .as_ref(); - let deposit_info_plain = public_attributes_plain.get(1).cloned().unwrap_or_default(); - if deposit_info != deposit_info_plain { - return Err(CoconutError::DifferentPublicAttributes( - deposit_info.to_string(), - deposit_info_plain, - )); + if deposit_info != request.public_attributes_plain[1] { + return Err(CoconutError::InconsistentDepositInfo { + request: request.public_attributes_plain[1].clone(), + on_chain: deposit_info, + }); } - let verification_key = identity::PublicKey::from_base58_string( - &attributes - .iter() - .find(|tag| tag.key == DEPOSIT_IDENTITY_KEY) - .ok_or(CoconutError::DepositVerifKeyNotFound)? - .value, - )?; + // verify signature + let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?; + let plaintext = + BandwidthVoucher::signable_plaintext(&request.inner_sign_request, request.tx_hash); + x25519.verify(plaintext, &request.signature)?; - let encryption_key = encryption::PublicKey::from_base58_string( - &attributes - .iter() - .find(|tag| tag.key == DEPOSIT_ENCRYPTION_KEY) - .ok_or(CoconutError::DepositEncrKeyNotFound)? - .value, - )?; - - verification_key.verify(&message, &signature)?; - - Ok(encryption_key) + Ok(()) } #[cfg(test)] mod test { use super::*; - use crate::coconut::tests::tx_entry_fixture; - use nym_coconut::{prepare_blind_sign, BlindSignRequest, Parameters}; + use crate::coconut::tests::{tx_entry_fixture, voucher_request_fixture}; + use cosmwasm_std::coin; + use nym_api_requests::coconut::BlindSignRequestBody; + use nym_coconut::BlindSignRequest; + use nym_coconut_bandwidth_contract_common::events::DEPOSITED_FUNDS_EVENT_TYPE; use nym_config::defaults::VOUCHER_INFO; - use nym_validator_client::nyxd::Hash; use nym_validator_client::nyxd::{Event, EventAttribute}; - use rand_07::rngs::OsRng; - use std::str::FromStr; #[tokio::test] - async fn extract_encryption_key_test() { - let tx_hash = - Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E") - .unwrap(); - let mut tx_entry = tx_entry_fixture(&tx_hash.to_string()); - let params = Parameters::new(4).unwrap(); - let mut rng = OsRng; - let voucher = BandwidthVoucher::new( - ¶ms, - "1234".to_string(), - VOUCHER_INFO.to_string(), - tx_hash, - identity::PrivateKey::from_base58_string( - identity::KeyPair::new(&mut rng) - .private_key() - .to_base58_string(), - ) - .unwrap(), - encryption::PrivateKey::from_bytes( - &encryption::KeyPair::new(&mut rng).private_key().to_bytes(), - ) - .unwrap(), - ); - let (_, blind_sign_req) = prepare_blind_sign( - ¶ms, - &voucher.get_private_attributes(), - &voucher.get_public_attributes(), - ) - .unwrap(); - let signature = "2DHbEZ6pzToGpsAXJrqJi7Wj1pAXeT18283q2YEEyNH5gTymwRozWBdja6SMAVt1dyYmUnM4ZNhsJ4wxZyGh4Z6J".to_string(); + async fn validate_deposit_tx_test() { + let (voucher, correct_request) = voucher_request_fixture(coin(1234, "unym"), None); - let req = BlindSignRequestBody::new( - &blind_sign_req, - tx_hash.to_string(), - signature.clone(), - &voucher.get_public_attributes(), - vec![ - String::from("First wrong plain"), - String::from("Second wrong plain"), - ], - 4, - ); - let err = extract_encryption_key(&req, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentPublicAttributes.to_string() - ); - - let req = BlindSignRequestBody::new( - &blind_sign_req, - tx_hash.to_string(), - String::from("Invalid signature"), - &voucher.get_public_attributes(), - voucher.get_public_attributes_plain(), - 4, - ); - let err = extract_encryption_key(&req, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!( - err, - CoconutError::Ed25519ParseError( - nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedSignatureString { .. } - ) - )); - - let correct_request = BlindSignRequestBody::new( - &blind_sign_req, - tx_hash.to_string(), - signature.clone(), - &voucher.get_public_attributes(), - voucher.get_public_attributes_plain(), - 4, - ); + let mut tx_entry = tx_entry_fixture(correct_request.tx_hash); + let good_deposit_attribute = EventAttribute { + key: DEPOSIT_VALUE.to_string(), + value: correct_request.public_attributes_plain[0].clone(), + index: false, + }; + let good_info_attribute = EventAttribute { + key: DEPOSIT_INFO.to_string(), + value: correct_request.public_attributes_plain[1].clone(), + index: false, + }; + let good_identity_key_attribute = EventAttribute { + key: DEPOSIT_IDENTITY_KEY.to_string(), + value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), + index: false, + }; + let good_encryption_key_attribute = EventAttribute { + key: DEPOSIT_ENCRYPTION_KEY.to_string(), + value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), + index: false, + }; tx_entry.tx_result.events.push(Event { kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), attributes: vec![], }); - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( @@ -191,26 +114,34 @@ mod test { CoconutError::DepositValueNotFound.to_string(), ); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "10".parse().unwrap(), - index: false, - }]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ + EventAttribute { + key: DEPOSIT_VALUE.parse().unwrap(), + value: "10".parse().unwrap(), + index: false, + }, + good_info_attribute.clone(), + good_identity_key_attribute.clone(), + good_encryption_key_attribute.clone(), + ]; + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( err.to_string(), - CoconutError::DifferentPublicAttributes("10".to_string(), "1234".to_string()) - .to_string(), + CoconutError::InconsistentDepositValue { + request: "1234".to_string(), + on_chain: "10".to_string() + } + .to_string() ); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ + good_deposit_attribute.clone(), + good_identity_key_attribute.clone(), + good_encryption_key_attribute.clone(), + ]; + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( @@ -219,42 +150,33 @@ mod test { ); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), EventAttribute { key: DEPOSIT_INFO.parse().unwrap(), value: "bandwidth deposit info".parse().unwrap(), index: false, }, + good_identity_key_attribute.clone(), + good_encryption_key_attribute.clone(), ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( err.to_string(), - CoconutError::DifferentPublicAttributes( - "bandwidth deposit info".to_string(), - VOUCHER_INFO.to_string(), - ) + CoconutError::InconsistentDepositInfo { + on_chain: "bandwidth deposit info".to_string(), + request: VOUCHER_INFO.to_string(), + } .to_string(), ); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), + good_encryption_key_attribute.clone(), ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( @@ -263,23 +185,16 @@ mod test { ); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), EventAttribute { key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), value: "verification key".parse().unwrap(), index: false, }, + good_encryption_key_attribute.clone(), ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); @@ -291,16 +206,8 @@ mod test { )); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), EventAttribute { key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He" @@ -309,7 +216,7 @@ mod test { index: false, }, ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( @@ -318,16 +225,8 @@ mod test { ); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), EventAttribute { key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" @@ -341,29 +240,16 @@ mod test { index: false, }, ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); - assert!(matches!( - err, - CoconutError::X25519ParseError( - nym_crypto::asymmetric::encryption::KeyRecoveryError::MalformedPublicKeyString { .. } - ) - )); + assert!(matches!(err, CoconutError::SignatureVerificationError(..))); let expected_encryption_key = "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6"; tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), EventAttribute { key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" @@ -377,7 +263,7 @@ mod test { index: false, }, ]; - let err = extract_encryption_key(&correct_request, tx_entry.clone()) + let err = validate_deposit_tx(&correct_request, tx_entry.clone()) .await .unwrap_err(); assert_eq!( @@ -414,28 +300,21 @@ mod test { 66, 137, 17, 32, ]) .unwrap(); + let correct_request = BlindSignRequestBody::new( - &blind_sign_req, - "7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B".to_string(), - "gSFgpma5GAVMcsmZwKieqGNHNd3dPzcfa8eT2Qn2LoBccSeyiJdphREbNrkuh5XWxMe2hUsranaYzLro48L9Qhd".to_string(), - &voucher.get_public_attributes(), + blind_sign_req, + "7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B" + .parse() + .unwrap(), + "3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(), voucher.get_public_attributes_plain(), - 4, ); tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, + good_deposit_attribute.clone(), + good_info_attribute.clone(), EventAttribute { key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "64auwDkWan7R8yH1Mwe9dS4qXgrDBCUNDg3Q4KFnd2P5" + value: "3xoM5GmUSq7YW4YNQrax1fEFLw1GbZozxe6UUoJcrqLG" .parse() .unwrap(), index: false, @@ -448,9 +327,7 @@ mod test { index: false, }, ]; - let encryption_key = extract_encryption_key(&correct_request, tx_entry.clone()) - .await - .unwrap(); - assert_eq!(encryption_key.to_base58_string(), expected_encryption_key); + let res = validate_deposit_tx(&correct_request, tx_entry.clone()).await; + assert!(res.is_ok()) } } diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 5a05915b0a..266c6db66e 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -11,6 +11,7 @@ use nym_dkg::bte::setup; use nym_dkg::Dealing; use rand::RngCore; use std::collections::VecDeque; +use zeroize::Zeroize; pub(crate) async fn dealing_exchange( dkg_client: &DkgClient, @@ -39,16 +40,19 @@ pub(crate) async fn dealing_exchange( .keys() .position(|node_index| *node_index == dealer_index); - let prior_resharing_secrets = if let Some(sk) = state.coconut_secret_key().await { + let prior_resharing_secrets = if let Some(mut keypair) = state.take_coconut_keypair().await { // Double check that we are in resharing mode if resharing { - let (x, mut scalars) = sk.into_raw(); - if scalars.len() + 1 != TOTAL_DEALINGS { + let sk = keypair.secret_key(); + if sk.size() + 1 != TOTAL_DEALINGS { return Err(CoconutError::CorruptedCoconutKeyPair); } + + let (x, mut scalars) = sk.into_raw(); + // We can now erase the keypair from memory debug!("Removing coconut keypair from memory"); - state.set_coconut_keypair(None).await; + keypair.zeroize(); scalars.push(x); scalars } else { diff --git a/nym-api/src/coconut/dkg/state.rs b/nym-api/src/coconut/dkg/state.rs index 59aaec6416..b22cd81f50 100644 --- a/nym-api/src/coconut/dkg/state.rs +++ b/nym-api/src/coconut/dkg/state.rs @@ -6,7 +6,6 @@ use crate::coconut::error::CoconutError; use crate::coconut::keypair::KeyPair as CoconutKeyPair; use cosmwasm_std::Addr; use log::debug; -use nym_coconut::SecretKey; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EpochState; use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; @@ -274,12 +273,15 @@ impl State { self.coconut_keypair.get().await.is_some() } - pub async fn coconut_secret_key(&self) -> Option { - self.coconut_keypair - .get() - .await - .as_ref() - .map(|kp| kp.secret_key()) + pub async fn take_coconut_keypair(&self) -> Option { + self.coconut_keypair.take().await + } + + #[cfg(test)] + pub async fn coconut_keypair( + &self, + ) -> tokio::sync::RwLockReadGuard<'_, Option> { + self.coconut_keypair.get().await } pub fn node_index(&self) -> Option { diff --git a/nym-api/src/coconut/dkg/verification_key.rs b/nym-api/src/coconut/dkg/verification_key.rs index 4a59b5fa65..78e490af73 100644 --- a/nym-api/src/coconut/dkg/verification_key.rs +++ b/nym-api/src/coconut/dkg/verification_key.rs @@ -6,16 +6,16 @@ use crate::coconut::dkg::complaints::ComplaintReason; use crate::coconut::dkg::state::{ConsistentState, State}; use crate::coconut::error::CoconutError; use crate::coconut::helpers::accepted_vote_err; +use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS; use cosmwasm_std::Addr; use cw3::{ProposalResponse, Status}; use log::debug; use nym_coconut::tests::helpers::transpose_matrix; -use nym_coconut::{check_vk_pairing, Base58, KeyPair, Parameters, SecretKey, VerificationKey}; +use nym_coconut::{check_vk_pairing, Base58, KeyPair, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_coconut_interface::KeyPair as CoconutKeyPair; -use nym_credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; use nym_dkg::bte::{decrypt_share, setup}; use nym_dkg::error::DkgError; use nym_dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; @@ -140,7 +140,6 @@ fn derive_partial_keypair( } state.set_recovered_vks(recovered_vks); - let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; let x = scalars.pop().ok_or(CoconutError::DkgError( DkgError::NotEnoughDealingsAvailable { available: 0, @@ -148,7 +147,7 @@ fn derive_partial_keypair( }, ))?; let sk = SecretKey::create_from_raw(x, scalars); - let vk = sk.verification_key(¶ms); + let vk = sk.verification_key(&BANDWIDTH_CREDENTIAL_PARAMS); Ok(CoconutKeyPair::from_keys(sk, vk)) } @@ -234,7 +233,7 @@ pub(crate) async fn verification_key_validation( .map(|recovered_vk| recovered_vk.recovered_partials.clone()) .collect(); let recovered_partials = transpose_matrix(recovered_partials); - let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; + let params = &BANDWIDTH_CREDENTIAL_PARAMS; for contract_share in vk_shares { if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() { match VerificationKey::try_from_bs58(contract_share.share) { @@ -243,7 +242,7 @@ pub(crate) async fn verification_key_validation( .iter() .position(|node_index| contract_share.node_index == *node_index) { - let ret = if !check_vk_pairing(¶ms, &recovered_partials[idx], &vk) { + let ret = if !check_vk_pairing(params, &recovered_partials[idx], &vk) { debug!( "Voting NO to proposal {} because of failed VK pairing", proposal_id @@ -836,16 +835,17 @@ pub(crate) mod tests { for (_, state) in clients_and_states.iter_mut() { state.set_was_in_progress(); } - let params = Parameters::new(4).unwrap(); let mut vks = vec![]; let mut indices = vec![]; for (_, state) in clients_and_states.iter() { let vk = state - .coconut_secret_key() + .coconut_keypair() .await + .as_ref() .unwrap() - .verification_key(¶ms); + .verification_key() + .clone(); let index = state.node_index().unwrap(); vks.push(vk); indices.push(index); @@ -933,10 +933,12 @@ pub(crate) mod tests { let mut indices = vec![]; for (_, state) in clients_and_states.iter() { let vk = state - .coconut_secret_key() + .coconut_keypair() .await + .as_ref() .unwrap() - .verification_key(¶ms); + .verification_key() + .clone(); let index = state.node_index().unwrap(); vks.push(vk); indices.push(index); @@ -1047,16 +1049,16 @@ pub(crate) mod tests { } // DKG in reshare mode - let params = Parameters::new(4).unwrap(); - let mut vks = vec![]; let mut indices = vec![]; for (_, state) in clients_and_states.iter() { let vk = state - .coconut_secret_key() + .coconut_keypair() .await + .as_ref() .unwrap() - .verification_key(¶ms); + .verification_key() + .clone(); let index = state.node_index().unwrap(); vks.push(vk); indices.push(index); @@ -1124,10 +1126,12 @@ pub(crate) mod tests { let mut indices = vec![]; for (_, state) in clients_and_states.iter() { let vk = state - .coconut_secret_key() + .coconut_keypair() .await + .as_ref() .unwrap() - .verification_key(¶ms); + .verification_key() + .clone(); let index = state.node_index().unwrap(); vks.push(vk); indices.push(index); diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 14f7b2f933..290f448fb1 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -13,7 +13,7 @@ use nym_crypto::asymmetric::{ }; use nym_dkg::error::DkgError; use nym_validator_client::coconut::CoconutApiError; -use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::error::{NyxdError, TendermintError}; use crate::node_status_api::models::NymApiStorageError; @@ -30,82 +30,100 @@ pub enum CoconutError { #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), - #[error("Could not parse Ed25519 data - {0}")] + #[error("could not parse Ed25519 data: {0}")] Ed25519ParseError(#[from] Ed25519RecoveryError), - #[error("Could not parse X25519 data - {0}")] + #[error("could not parse X25519 data: {0}")] X25519ParseError(#[from] KeyRecoveryError), - #[error("Could not parse tx hash in request body")] - TxHashParseError, + #[error("could not parse tx hash in request body: {source}")] + TxHashParseError { + #[source] + source: TendermintError, + }, - #[error("Nyxd error - {0}")] + #[error("could not get transaction details for '{tx_hash}': {source}")] + TxRetrievalFailure { + tx_hash: String, + #[source] + source: NyxdError, + }, + + #[error("nyxd error: {0}")] NyxdError(#[from] NyxdError), - #[error("Validator client error - {0}")] + #[error("validator client error: {0}")] ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), - #[error("Coconut internal error - {0}")] + #[error("coconut internal error: {0}")] CoconutInternalError(#[from] nym_coconut::CoconutError), - #[error("Could not find a deposit event in the transaction provided")] + #[error("could not find a deposit event in the transaction provided")] DepositEventNotFound, - #[error("Could not find the deposit value in the event")] + #[error("could not find the deposit value in the event")] DepositValueNotFound, - #[error("Could not find the deposit info in the event")] + #[error("could not find the deposit info in the event")] DepositInfoNotFound, - #[error("Could not find the verification key in the event")] + #[error("could not find the verification key in the event")] DepositVerifKeyNotFound, - #[error("Could not find the encryption key in the event")] + #[error("could not find the encryption key in the event")] DepositEncrKeyNotFound, - #[error("Signature didn't verify correctly")] + #[error("signature didn't verify correctly")] SignatureVerificationError(#[from] SignatureError), - #[error("Inconsistent public attributes")] + #[error("inconsistent public attributes")] InconsistentPublicAttributes, - #[error( - "Public attributes in request differ from the ones in deposit - Expected {0}, got {1}" - )] + #[error("the provided deposit value is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] + InconsistentDepositValue { request: String, on_chain: String }, + + #[error("the provided deposit info is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] + InconsistentDepositInfo { request: String, on_chain: String }, + + #[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")] DifferentPublicAttributes(String, String), - #[error("Error in coconut interface - {0}")] + #[error("error in coconut interface: {0}")] CoconutInterfaceError(#[from] nym_coconut_interface::error::CoconutInterfaceError), - #[error("Storage error - {0}")] + #[error("storage error: {0}")] StorageError(#[from] NymApiStorageError), - #[error("Credentials error - {0}")] + #[error("credentials error: {0}")] CredentialsError(#[from] nym_credentials::error::Error), - #[error("Incorrect credential proposal description: {reason}")] + #[error("incorrect credential proposal description: {reason}")] IncorrectProposal { reason: String }, - #[error("Invalid status of credential: {status}")] + #[error("invalid status of credential: {status}")] InvalidCredentialStatus { status: String }, #[error("DKG error: {0}")] DkgError(#[from] DkgError), - #[error("Failed to recover assigned node index: {reason}")] + #[error("failed to recover assigned node index: {reason}")] NodeIndexRecoveryError { reason: String }, - #[error("Unrecoverable state: {reason}")] + #[error("unrecoverable state: {reason}")] UnrecoverableState { reason: String }, #[error("DKG has not finished yet in order to derive the coconut key")] KeyPairNotDerivedYet, - #[error("The coconut keypair is corrupted")] + #[error("the coconut keypair is corrupted")] CorruptedCoconutKeyPair, - #[error("There was a problem with the proposal id: {reason}")] + #[error("there was a problem with the proposal id: {reason}")] ProposalIdError { reason: String }, + + // I guess we should make this one a bit more detailed + #[error("the provided query arguments were invalid")] + InvalidQueryArguments, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs index 50afa84420..a7e25d7b46 100644 --- a/nym-api/src/coconut/helpers.rs +++ b/nym-api/src/coconut/helpers.rs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::error::CoconutError; +use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS; +use nym_api_requests::coconut::BlindSignRequestBody; +use nym_coconut::{BlindedSignature, SecretKey}; use nym_validator_client::nyxd::error::NyxdError::AbciError; // If the result is already established, the vote might be redundant and @@ -17,3 +20,18 @@ pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), Coc } Ok(()) } + +pub(crate) fn blind_sign( + request: &BlindSignRequestBody, + signing_key: &SecretKey, +) -> Result { + let public_attributes = request.public_attributes_hashed(); + let attributes_ref = public_attributes.iter().collect::>(); + + Ok(nym_coconut_interface::blind_sign( + &BANDWIDTH_CREDENTIAL_PARAMS, + signing_key, + &request.inner_sign_request, + &attributes_ref, + )?) +} diff --git a/nym-api/src/coconut/keypair.rs b/nym-api/src/coconut/keypair.rs index ef27856c83..57b7872971 100644 --- a/nym-api/src/coconut/keypair.rs +++ b/nym-api/src/coconut/keypair.rs @@ -16,6 +16,10 @@ impl KeyPair { } } + pub async fn take(&self) -> Option { + self.inner.write().await.take() + } + pub async fn get(&self) -> RwLockReadGuard<'_, Option> { self.inner.read().await } diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs index 7059e95160..51c4199018 100644 --- a/nym-api/src/coconut/mod.rs +++ b/nym-api/src/coconut/mod.rs @@ -3,39 +3,15 @@ use self::comm::APICommunicationChannel; use crate::coconut::client::Client as LocalClient; -use crate::coconut::deposit::extract_encryption_key; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::helpers::accepted_vote_err; +use crate::coconut::state::State; use crate::support::storage::NymApiStorage; -use getset::{CopyGetters, Getters}; use keypair::KeyPair; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - funds_from_cosmos_msgs, SpendCredentialStatus, -}; -use nym_coconut_dkg_common::types::EpochId; -use nym_coconut_interface::KeyPair as CoconutKeyPair; -use nym_coconut_interface::{ - Attribute, BlindSignRequest, BlindedSignature, Parameters, VerificationKey, -}; use nym_config::defaults::NYM_API_VERSION; -use nym_credentials::coconut::params::{ - NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm, -}; -use nym_crypto::asymmetric::encryption; -use nym_crypto::shared_key::new_ephemeral_shared_key; -use nym_crypto::symmetric::stream_cipher; +use nym_crypto::asymmetric::identity; use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES}; -use nym_validator_client::nyxd::{Coin, Fee}; -use rand_07::rngs::OsRng; use rocket::fairing::AdHoc; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use std::sync::Arc; -use tokio::sync::Mutex; +pub(crate) mod api_routes; pub(crate) mod client; pub(crate) mod comm; mod deposit; @@ -43,263 +19,42 @@ pub(crate) mod dkg; pub(crate) mod error; pub(crate) mod helpers; pub(crate) mod keypair; +pub(crate) mod state; +pub(crate) mod storage; #[cfg(test)] pub(crate) mod tests; -pub struct State { - client: Arc, +pub fn stage( + client: C, mix_denom: String, + identity_keypair: identity::KeyPair, key_pair: KeyPair, - comm_channel: Arc, + comm_channel: D, storage: NymApiStorage, - rng: Arc>, -} - -impl State { - pub(crate) fn new( - client: C, - mix_denom: String, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, - ) -> Self - where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, - { - let client = Arc::new(client); - let comm_channel = Arc::new(comm_channel); - let rng = Arc::new(Mutex::new(OsRng)); - Self { - client, - mix_denom, - key_pair, - comm_channel, - storage, - rng, - } - } - - pub async fn signed_before(&self, tx_hash: &str) -> Result> { - let ret = self.storage.get_blinded_signature_response(tx_hash).await?; - if let Some(blinded_signature_reponse) = ret { - Ok(Some(BlindedSignatureResponse::from_base58_string( - blinded_signature_reponse, - )?)) - } else { - Ok(None) - } - } - - pub async fn encrypt_and_store( - &self, - tx_hash: &str, - remote_key: &encryption::PublicKey, - signature: &BlindedSignature, - ) -> Result { - let (keypair, shared_key) = { - let mut rng = *self.rng.lock().await; - new_ephemeral_shared_key::< - NymApiCredentialEncryptionAlgorithm, - NymApiCredentialHkdfAlgorithm, - _, - >(&mut rng, remote_key) - }; - - let chunk_data = signature.to_bytes(); - - let zero_iv = stream_cipher::zero_iv::(); - let encrypted_data = stream_cipher::encrypt::( - &shared_key, - &zero_iv, - &chunk_data, - ); - - let response = - BlindedSignatureResponse::new(encrypted_data, keypair.public_key().to_bytes()); - - // Atomically insert data, only if there is no signature stored in the meantime - // This prevents race conditions on storing two signatures for the same deposit transaction - if self - .storage - .insert_blinded_signature_response(tx_hash, &response.to_base58_string()) - .await - .is_err() - { - Ok(self - .signed_before(tx_hash) - .await? - .expect("The signature was expected to be there")) - } else { - Ok(response) - } - } - - pub async fn verification_key(&self, epoch_id: EpochId) -> Result { - self.comm_channel - .aggregated_verification_key(epoch_id) - .await - } -} - -#[derive(Getters, CopyGetters, Debug)] -pub(crate) struct InternalSignRequest { - // Total number of parameters to generate for - #[getset(get_copy)] - total_params: u32, - #[getset(get)] - public_attributes: Vec, - #[getset(get)] - blind_sign_request: BlindSignRequest, -} - -impl InternalSignRequest { - pub fn new( - total_params: u32, - public_attributes: Vec, - blind_sign_request: BlindSignRequest, - ) -> InternalSignRequest { - InternalSignRequest { - total_params, - public_attributes, - blind_sign_request, - } - } - - pub fn stage( - client: C, - mix_denom: String, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, - ) -> AdHoc - where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, - { - let state = State::new(client, mix_denom, key_pair, comm_channel, storage); - AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { - rocket.manage(state).mount( - // this format! is so ugly... - format!("/{}/{}/{}", NYM_API_VERSION, COCONUT_ROUTES, BANDWIDTH), - routes![post_blind_sign, verify_bandwidth_credential], - ) - }) - } -} - -fn blind_sign(request: InternalSignRequest, key_pair: &CoconutKeyPair) -> Result { - let params = Parameters::new(request.total_params())?; - Ok(nym_coconut_interface::blind_sign( - ¶ms, - &key_pair.secret_key(), - request.blind_sign_request(), - request.public_attributes(), - )?) -} - -#[post("/blind-sign", data = "")] -// Until we have serialization and deserialization traits we'll be using a crutch -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("{:?}", blind_sign_request_body); - if let Some(response) = state - .signed_before(blind_sign_request_body.tx_hash()) - .await? - { - return Ok(Json(response)); - } - let tx = state - .client - .get_tx(blind_sign_request_body.tx_hash()) - .await?; - let encryption_key = extract_encryption_key(&blind_sign_request_body, tx).await?; - let internal_request = InternalSignRequest::new( - *blind_sign_request_body.total_params(), - blind_sign_request_body.public_attributes(), - blind_sign_request_body.blind_sign_request().clone(), +) -> AdHoc +where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, +{ + let state = State::new( + client, + mix_denom, + identity_keypair, + key_pair, + comm_channel, + storage, ); - let blinded_signature = if let Some(keypair) = state.key_pair.get().await.as_ref() { - blind_sign(internal_request, keypair)? - } else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - let response = state - .encrypt_and_store( - blind_sign_request_body.tx_hash(), - &encryption_key, - &blinded_signature, + AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { + rocket.manage(state).mount( + // this format! is so ugly... + format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"), + routes![ + api_routes::post_blind_sign, + api_routes::verify_bandwidth_credential, + api_routes::epoch_credentials, + api_routes::issued_credential, + api_routes::issued_credentials, + ], ) - .await?; - - Ok(Json(response)) -} - -#[post("/verify-bandwidth-credential", data = "")] -pub async fn verify_bandwidth_credential( - verify_credential_body: Json, - state: &RocketState, -) -> Result> { - let proposal_id = *verify_credential_body.proposal_id(); - let proposal = state.client.get_proposal(proposal_id).await?; - // Proposal description is the blinded serial number - if !verify_credential_body - .credential() - .has_blinded_serial_number(&proposal.description)? - { - return Err(CoconutError::IncorrectProposal { - reason: String::from("incorrect blinded serial number in description"), - }); - } - let proposed_release_funds = - funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { - reason: String::from("action is not to release funds"), - })?; - // Credential has not been spent before, and is on its way of being spent - let credential_status = state - .client - .get_spent_credential(verify_credential_body.credential().blinded_serial_number()) - .await? - .spend_credential - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("Inexistent"), - })? - .status(); - if credential_status != SpendCredentialStatus::InProgress { - return Err(CoconutError::InvalidCredentialStatus { - status: format!("{:?}", credential_status), - }); - } - let verification_key = state - .verification_key(*verify_credential_body.credential().epoch_id()) - .await?; - let mut vote_yes = verify_credential_body - .credential() - .verify(&verification_key); - - vote_yes &= Coin::from(proposed_release_funds) - == Coin::new( - verify_credential_body.credential().voucher_value() as u128, - state.mix_denom.clone(), - ); - - // Vote yes or no on the proposal based on the verification result - let ret = state - .client - .vote_proposal( - proposal_id, - vote_yes, - Some(Fee::new_payer_granter_auto( - None, - None, - Some(verify_credential_body.gateway_cosmos_addr().to_owned()), - )), - ) - .await; - accepted_vote_err(ret)?; - - Ok(Json(VerifyCredentialResponse::new(vote_yes))) + }) } diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs new file mode 100644 index 0000000000..0cffc9e752 --- /dev/null +++ b/nym-api/src/coconut/state.rs @@ -0,0 +1,151 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::client::Client as LocalClient; +use crate::coconut::comm::APICommunicationChannel; +use crate::coconut::deposit::validate_deposit_tx; +use crate::coconut::error::Result; +use crate::coconut::keypair::KeyPair; +use crate::coconut::storage::CoconutStorageExt; +use crate::support::storage::NymApiStorage; +use lazy_static::lazy_static; +use nym_api_requests::coconut::helpers::issued_credential_plaintext; +use nym_api_requests::coconut::BlindSignRequestBody; +use nym_coconut::Parameters; +use nym_coconut_dkg_common::types::EpochId; +use nym_coconut_interface::{BlindedSignature, VerificationKey}; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; +use nym_crypto::asymmetric::identity; +use nym_validator_client::nyxd::{Hash, TxResponse}; +use std::sync::Arc; + +// keep it as a global static due to relatively high cost of computing the curve points; +// plus we expect all clients to use the same set of parameters +// +// future note: once we allow for credentials with variable number of attributes, just create Parameters(max_allowed_attributes) +// and take as many hs elements as required (since they will match for all variants) +lazy_static! { + pub(crate) static ref BANDWIDTH_CREDENTIAL_PARAMS: Parameters = + BandwidthVoucher::default_parameters(); +} + +pub struct State { + pub(crate) client: Arc, + pub(crate) mix_denom: String, + pub(crate) coconut_keypair: KeyPair, + pub(crate) identity_keypair: identity::KeyPair, + pub(crate) comm_channel: Arc, + pub(crate) storage: NymApiStorage, +} + +impl State { + pub(crate) fn new( + client: C, + mix_denom: String, + identity_keypair: identity::KeyPair, + key_pair: KeyPair, + comm_channel: D, + storage: NymApiStorage, + ) -> Self + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + let client = Arc::new(client); + let comm_channel = Arc::new(comm_channel); + + Self { + client, + mix_denom, + coconut_keypair: key_pair, + identity_keypair, + comm_channel, + storage, + } + } + + /// Check if this nym-api has already issued a credential for the provided deposit hash. + /// If so, return it. + pub async fn already_issued(&self, tx_hash: Hash) -> Result> { + self.storage + .get_issued_bandwidth_credential_by_hash(&tx_hash.to_string()) + .await? + .map(|cred| cred.try_into()) + .transpose() + } + + pub async fn get_transaction(&self, tx_hash: Hash) -> Result { + self.client.get_tx(tx_hash).await + } + + pub async fn validate_request( + &self, + request: &BlindSignRequestBody, + tx: TxResponse, + ) -> Result<()> { + validate_deposit_tx(request, tx).await + } + + pub(crate) async fn sign_and_store_credential( + &self, + current_epoch: EpochId, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result { + let encoded_commitments = request_body.encode_commitments(); + + let plaintext = issued_credential_plaintext( + current_epoch as u32, + request_body.tx_hash, + blinded_signature, + &encoded_commitments, + &request_body.public_attributes_plain, + ); + + let signature = self.identity_keypair.private_key().sign(plaintext); + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .storage + .store_issued_credential( + current_epoch as u32, + request_body.tx_hash, + blinded_signature, + signature, + encoded_commitments, + request_body.public_attributes_plain, + ) + .await?; + + Ok(credential_id) + } + + pub async fn store_issued_credential( + &self, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result<()> { + let current_epoch = self.comm_channel.current_epoch().await?; + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .sign_and_store_credential(current_epoch, request_body, blinded_signature) + .await?; + self.storage + .update_epoch_credentials_entry(current_epoch, credential_id) + .await?; + debug!("the stored credential has id {credential_id}"); + + Ok(()) + } + + pub async fn verification_key(&self, epoch_id: EpochId) -> Result { + self.comm_channel + .aggregated_verification_key(epoch_id) + .await + } +} diff --git a/nym-api/src/coconut/storage/manager.rs b/nym-api/src/coconut/storage/manager.rs new file mode 100644 index 0000000000..e40b2a4a96 --- /dev/null +++ b/nym-api/src/coconut/storage/manager.rs @@ -0,0 +1,381 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::storage::models::{EpochCredentials, IssuedCredential}; +use crate::support::storage::manager::StorageManager; +use nym_coconut_dkg_common::types::EpochId; + +#[async_trait] +pub trait CoconutStorageManagerExt { + /// Creates new encrypted blinded signature response entry for a given deposit tx hash. + /// + /// # Arguments + /// + /// * `tx_hash`: hash of the deposit transaction. + /// * `blinded_signature_response`: the encrypted blinded signature response. + #[deprecated] + async fn insert_blinded_signature_response( + &self, + _tx_hash: &str, + _blinded_signature_response: &str, + ) -> Result<(), sqlx::Error> { + Ok(()) + } + + /// Tries to obtain encrypted blinded signature response for a given transaction hash. + /// + /// # Arguments + /// + /// * `tx_hash`: transaction hash of the deposit. + #[deprecated] + async fn get_blinded_signature_response( + &self, + _tx_hash: &str, + ) -> Result, sqlx::Error> { + Ok(None) + } + + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error>; + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; + + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_hash( + &self, + tx_hash: &str, + ) -> Result, sqlx::Error>; + + /// Store the provided issued credential information and return its (database) id. + /// + /// # Arguments + /// + /// * `credential`: partial credential, alongside any data required for verification. + async fn store_issued_credential( + &self, + epoch_id: u32, + tx_hash: String, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + joined_public_attributes: String, + ) -> Result; + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error>; +} + +#[async_trait] +impl CoconutStorageManagerExt for StorageManager { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + -1, + 0 + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + // the logic in this function can be summarised with: + // 1. get the current EpochCredentials for this epoch + // 2. if it exists -> increment `total_issued` + // 3. it has invalid (i.e. -1) `start_id` set it to the provided value + // 4. if it didn't exist, create new entry + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + // make the atomic transaction in case other tasks are attempting to use the pool + let mut tx = self.connection_pool.begin().await?; + + if let Some(existing) = sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&mut tx) + .await? + { + // the entry has existed before -> update it + if existing.total_issued == 0 { + // no credentials has been issued -> we have to set the `start_id` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = 1, start_id = ? + WHERE epoch_id = ? + "#, + credential_id, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } else { + // we have issued credentials in this epoch before -> just increment `total_issued` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = total_issued + 1 + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } + } else { + // the entry has never been created -> probably some race condition; create it instead + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + credential_id, + 1 + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes + FROM issued_credential + WHERE id = ? + "#, + credential_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_hash( + &self, + tx_hash: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes + FROM issued_credential + WHERE tx_hash = ? + "#, + tx_hash + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Store the provided issued credential information and return its (database) id. + /// + /// # Arguments + /// + /// * `credential`: partial credential, alongside any data required for verification. + async fn store_issued_credential( + &self, + epoch_id: u32, + tx_hash: String, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + joined_public_attributes: String, + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO issued_credential + (epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes) + VALUES + (?, ?, ?, ?, ?, ?) + "#, + epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes + ).execute(&self.connection_pool).await?.last_insert_rowid(); + + Ok(row_id) + } + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error> { + // that sucks : ( + // https://stackoverflow.com/a/70032524 + let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); + let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); + let mut query = sqlx::query_as(&query_str); + for id in credential_ids { + query = query.bind(id) + } + + query.fetch_all(&self.connection_pool).await + } + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes + FROM issued_credential + WHERE id > ? + ORDER BY id + LIMIT ? + "#, + start_after, + limit + ) + .fetch_all(&self.connection_pool) + .await + } +} diff --git a/nym-api/src/coconut/storage/mod.rs b/nym-api/src/coconut/storage/mod.rs new file mode 100644 index 0000000000..5633ba468d --- /dev/null +++ b/nym-api/src/coconut/storage/mod.rs @@ -0,0 +1,166 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::storage::manager::CoconutStorageManagerExt; +use crate::coconut::storage::models::{join_attributes, EpochCredentials, IssuedCredential}; +use crate::node_status_api::models::NymApiStorageError; +use crate::support::storage::NymApiStorage; +use nym_api_requests::coconut::models::Pagination; +use nym_coconut::{Base58, BlindedSignature}; +use nym_coconut_dkg_common::types::EpochId; +use nym_crypto::asymmetric::identity; +use nym_validator_client::nyxd::Hash; + +pub(crate) mod manager; +pub(crate) mod models; + +const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; + +#[async_trait] +pub trait CoconutStorageExt { + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError>; + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError>; + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError>; + + async fn get_issued_bandwidth_credential_by_hash( + &self, + tx_hash: &str, + ) -> Result, NymApiStorageError>; + + async fn store_issued_credential( + &self, + epoch_id: u32, + tx_hash: Hash, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + public_attributes: Vec, + ) -> Result; + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError>; + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError>; +} + +#[async_trait] +impl CoconutStorageExt for NymApiStorage { + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_epoch_credentials(epoch_id).await?) + } + + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .create_epoch_credentials_entry(epoch_id) + .await?) + } + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_epoch_credentials_entry(epoch_id, credential_id) + .await?) + } + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credential(credential_id).await?) + } + + async fn get_issued_bandwidth_credential_by_hash( + &self, + tx_hash: &str, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_issued_bandwidth_credential_by_hash(tx_hash) + .await?) + } + + async fn store_issued_credential( + &self, + epoch_id: u32, + tx_hash: Hash, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + public_attributes: Vec, + ) -> Result { + Ok(self + .manager + .store_issued_credential( + epoch_id, + tx_hash.to_string(), + partial_credential.to_bs58(), + signature.to_base58_string(), + join_attributes(private_commitments), + join_attributes(public_attributes), + ) + .await?) + } + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credentials(credential_ids).await?) + } + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError> { + // rows start at 1 + let start_after = pagination.last_key.unwrap_or(0); + let limit = match pagination.limit { + Some(v) => { + if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { + DEFAULT_CREDENTIALS_PAGE_LIMIT + } else { + v + } + } + None => DEFAULT_CREDENTIALS_PAGE_LIMIT, + }; + + Ok(self + .manager + .get_issued_credentials_paged(start_after, limit) + .await?) + } +} diff --git a/nym-api/src/coconut/storage/models.rs b/nym-api/src/coconut/storage/models.rs new file mode 100644 index 0000000000..fe1afbac22 --- /dev/null +++ b/nym-api/src/coconut/storage/models.rs @@ -0,0 +1,119 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::error::CoconutError; +use nym_api_requests::coconut::models::{ + EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, + IssuedCredentialInner as ApiIssuedCredentialInner, +}; +use nym_api_requests::coconut::BlindedSignatureResponse; +use nym_coconut::{Base58, BlindedSignature}; +use sqlx::FromRow; +use std::fmt::Display; + +pub struct EpochCredentials { + pub epoch_id: u32, + pub start_id: i64, + pub total_issued: u32, +} + +impl From for EpochCredentialsResponse { + fn from(value: EpochCredentials) -> Self { + let first_epoch_credential_id = if value.start_id == -1 { + None + } else { + Some(value.start_id) + }; + + EpochCredentialsResponse { + epoch_id: value.epoch_id as u64, + first_epoch_credential_id, + total_issued: value.total_issued, + } + } +} + +#[derive(FromRow)] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub tx_hash: String, + + /// base58-encoded issued credential + pub bs58_partial_credential: String, + + /// base58-encoded signature on the issued credential (and the attributes) + pub bs58_signature: String, + + // i.e. "'attr1','attr2',..." + pub joined_private_commitments: String, + + // i.e. "'attr1','attr2',..." + pub joined_public_attributes: String, +} + +impl TryFrom for ApiIssuedCredentialInner { + type Error = CoconutError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(ApiIssuedCredentialInner { + credential: ApiIssuedCredential { + id: value.id, + epoch_id: value.epoch_id, + tx_hash: value + .tx_hash + .parse() + .map_err(|source| CoconutError::TxHashParseError { source })?, + blinded_partial_credential: BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?, + bs58_encoded_private_attributes_commitments: split_attributes( + &value.joined_private_commitments, + ), + public_attributes: split_attributes(&value.joined_public_attributes), + }, + signature: value.bs58_signature.parse()?, + }) + } +} + +impl TryFrom for BlindedSignatureResponse { + type Error = CoconutError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, + }) + } +} + +impl TryFrom for BlindedSignature { + type Error = CoconutError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?) + } +} + +impl IssuedCredential { + // safety: this should only ever be called on sanitized data from the database, + // thus the unwraps are fine (if somebody manually entered their db file and modified it, it's on them) + // pub fn private_attribute_commitments(&self) -> Vec + // pub fn public_attributes(&self) +} + +pub fn join_attributes(attrs: I) -> String +where + I: IntoIterator, + M: Display, +{ + // I could have used `attrs.into_iter().join(",")`, + // but my IDE didn't like it (compiler was fine) + itertools::Itertools::join(&mut attrs.into_iter(), ",") +} + +pub fn split_attributes(attrs: &str) -> Vec { + attrs.split(',').map(|s| s.to_string()).collect() +} diff --git a/nym-api/src/coconut/tests/issued_credentials.rs b/nym-api/src/coconut/tests/issued_credentials.rs new file mode 100644 index 0000000000..d753476235 --- /dev/null +++ b/nym-api/src/coconut/tests/issued_credentials.rs @@ -0,0 +1,276 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::tests::{voucher_request_fixture, TestFixture}; +use cosmwasm_std::coin; +use nym_api_requests::coconut::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, +}; +use nym_api_requests::coconut::CredentialsRequestBody; +use nym_coconut::Base58; +use nym_validator_client::nym_api::routes::{API_VERSION, BANDWIDTH, COCONUT_ROUTES}; +use rocket::http::Status; +use std::collections::BTreeMap; + +#[tokio::test] +async fn epoch_credentials() { + let route_epoch1 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/1"); + let route_epoch2 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/2"); + let route_epoch42 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/42"); + + let test_fixture = TestFixture::new().await; + + // initially we expect 0 issued + let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + + assert_eq!(parsed_response.epoch_id, 1); + assert_eq!(parsed_response.total_issued, 0); + assert_eq!(parsed_response.first_epoch_credential_id, None); + + // get credential + test_fixture.issue_dummy_credential().await; + + // now there should be one + let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + + assert_eq!(parsed_response.epoch_id, 1); + assert_eq!(parsed_response.total_issued, 1); + assert_eq!(parsed_response.first_epoch_credential_id, Some(1)); + + // and another + test_fixture.issue_dummy_credential().await; + + let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + + // note that first epoch credential didn't change + assert_eq!(parsed_response.epoch_id, 1); + assert_eq!(parsed_response.total_issued, 2); + assert_eq!(parsed_response.first_epoch_credential_id, Some(1)); + + test_fixture.set_epoch(2); + + let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + + // note the epoch change + assert_eq!(parsed_response.epoch_id, 2); + assert_eq!(parsed_response.total_issued, 0); + assert_eq!(parsed_response.first_epoch_credential_id, None); + + test_fixture.issue_dummy_credential().await; + + let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + + // note the epoch change + assert_eq!(parsed_response.epoch_id, 2); + assert_eq!(parsed_response.total_issued, 1); + assert_eq!(parsed_response.first_epoch_credential_id, Some(3)); + + // random epoch in the future + let response = test_fixture.rocket.get(&route_epoch42).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: EpochCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(parsed_response.epoch_id, 42); + assert_eq!(parsed_response.total_issued, 0); + assert_eq!(parsed_response.first_epoch_credential_id, None); +} + +#[tokio::test] +async fn issued_credential() { + fn route(id: i64) -> String { + format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}") + } + + // let test_fixture = TestFixture::new() + let hash1 = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E".to_string(); + let hash2 = "9F4DF28B36189B4410BC23D97FD757FC74B919122E80534CC2CA6F3D646F6518".to_string(); + + let (voucher1, req1) = voucher_request_fixture(coin(1234, "unym"), Some(hash1.clone())); + let (voucher2, req2) = voucher_request_fixture(coin(1234, "unym"), Some(hash2.clone())); + + let test_fixture = TestFixture::new().await; + test_fixture.add_deposit_tx(&voucher1); + test_fixture.add_deposit_tx(&voucher2); + + // random credential that was never issued + let response = test_fixture.rocket.get(route(42)).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: IssuedCredentialResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert!(parsed_response.credential.is_none()); + + let cred1 = test_fixture.issue_credential(req1).await; + + test_fixture.set_epoch(3); + let cred2 = test_fixture.issue_credential(req2).await; + + let response = test_fixture.rocket.get(route(1)).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: IssuedCredentialResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let issued1 = parsed_response.credential.unwrap(); + + let response = test_fixture.rocket.get(route(2)).dispatch().await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: IssuedCredentialResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let issued2 = parsed_response.credential.unwrap(); + + // TODO: currently we have no signature checks + assert_eq!(1, issued1.credential.id); + assert_eq!(1, issued1.credential.epoch_id); + assert_eq!(voucher1.tx_hash(), issued1.credential.tx_hash); + assert_eq!( + cred1.to_bytes(), + issued1.credential.blinded_partial_credential.to_bytes() + ); + let cms: Vec<_> = voucher1 + .blind_sign_request() + .get_private_attributes_pedersen_commitments() + .iter() + .map(|c| c.to_bs58()) + .collect(); + assert_eq!( + cms, + issued1 + .credential + .bs58_encoded_private_attributes_commitments + ); + assert_eq!( + voucher1.get_public_attributes_plain(), + issued1.credential.public_attributes + ); + + assert_eq!(2, issued2.credential.id); + assert_eq!(3, issued2.credential.epoch_id); + assert_eq!(voucher2.tx_hash(), issued2.credential.tx_hash); + assert_eq!( + cred2.to_bytes(), + issued2.credential.blinded_partial_credential.to_bytes() + ); + let cms: Vec<_> = voucher2 + .blind_sign_request() + .get_private_attributes_pedersen_commitments() + .iter() + .map(|c| c.to_bs58()) + .collect(); + assert_eq!( + cms, + issued2 + .credential + .bs58_encoded_private_attributes_commitments + ); + assert_eq!( + voucher2.get_public_attributes_plain(), + issued2.credential.public_attributes + ); +} + +#[tokio::test] +async fn issued_credentials() { + let route = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credentials"); + + let test_fixture = TestFixture::new().await; + + // issue some credentials + for _ in 0..20 { + test_fixture.issue_dummy_credential().await; + } + + let issued1 = test_fixture.issued_unchecked(1).await; + let issued2 = test_fixture.issued_unchecked(2).await; + let issued3 = test_fixture.issued_unchecked(3).await; + let issued4 = test_fixture.issued_unchecked(4).await; + let issued5 = test_fixture.issued_unchecked(5).await; + let issued13 = test_fixture.issued_unchecked(13).await; + + let response = test_fixture + .rocket + .post(&route) + .json(&CredentialsRequestBody { + credential_ids: vec![5], + pagination: None, + }) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: IssuedCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(parsed_response.credentials[&5], issued5); + assert!(parsed_response.credentials.get(&13).is_none()); + + let response = test_fixture + .rocket + .post(&route) + .json(&CredentialsRequestBody { + credential_ids: vec![5, 13], + pagination: None, + }) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let parsed_response: IssuedCredentialsResponse = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(parsed_response.credentials[&5], issued5); + assert_eq!(parsed_response.credentials[&13], issued13); + + let response_paginated = test_fixture + .rocket + .post(&route) + .json(&CredentialsRequestBody { + credential_ids: vec![], + pagination: Some(Pagination { + last_key: None, + limit: Some(2), + }), + }) + .dispatch() + .await; + assert_eq!(response_paginated.status(), Status::Ok); + let parsed_response: IssuedCredentialsResponse = + serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + + let mut expected = BTreeMap::new(); + expected.insert(1, issued1); + expected.insert(2, issued2); + assert_eq!(expected, parsed_response.credentials); + + let response_paginated = test_fixture + .rocket + .post(&route) + .json(&CredentialsRequestBody { + credential_ids: vec![], + pagination: Some(Pagination { + last_key: Some(2), + limit: Some(3), + }), + }) + .dispatch() + .await; + assert_eq!(response_paginated.status(), Status::Ok); + let parsed_response: IssuedCredentialsResponse = + serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + + let mut expected = BTreeMap::new(); + expected.insert(3, issued3); + expected.insert(4, issued4); + expected.insert(5, issued5); + assert_eq!(expected, parsed_response.credentials); +} diff --git a/nym-api/src/coconut/tests.rs b/nym-api/src/coconut/tests/mod.rs similarity index 75% rename from nym-api/src/coconut/tests.rs rename to nym-api/src/coconut/tests/mod.rs index 54af19dcae..446a85eab6 100644 --- a/nym-api/src/coconut/tests.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -1,14 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use super::InternalSignRequest; use crate::coconut::error::{CoconutError, Result}; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, WasmMsg}; +use crate::coconut::storage::CoconutStorageExt; +use crate::coconut::{self, State}; +use crate::support::storage::NymApiStorage; +use async_trait::async_trait; +use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Decimal, WasmMsg}; +use cw3::ProposalResponse; +use cw4::MemberResponse; +use nym_api_requests::coconut::models::{IssuedCredentialInner, IssuedCredentialResponse}; use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use nym_coconut::tests::helpers::theta_from_keys_and_attributes; -use nym_coconut::{prepare_blind_sign, ttp_keygen, Base58, BlindedSignature, Parameters}; +use nym_coconut::{ttp_keygen, Base58, BlindedSignature, Parameters}; use nym_coconut_bandwidth_contract_common::events::{ DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, DEPOSIT_VALUE, @@ -16,27 +22,6 @@ use nym_coconut_bandwidth_contract_common::events::{ use nym_coconut_bandwidth_contract_common::spend_credential::{ SpendCredential, SpendCredentialResponse, }; -use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey}; -use nym_config::defaults::VOUCHER_INFO; -use nym_credentials::coconut::bandwidth::BandwidthVoucher; -use nym_credentials::coconut::params::{ - NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm, -}; -use nym_crypto::shared_key::recompute_shared_key; -use nym_crypto::symmetric::stream_cipher; -use nym_validator_client::nym_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, -}; -use nym_validator_client::nyxd::Coin; -use nym_validator_client::nyxd::{ - AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse, -}; - -use crate::coconut::State; -use crate::support::storage::NymApiStorage; -use async_trait::async_trait; -use cw3::ProposalResponse; -use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{ ContractDealing, DealerDetails, DealerDetailsResponse, DealerType, }; @@ -45,26 +30,39 @@ use nym_coconut_dkg_common::types::{ EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, TOTAL_DEALINGS, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey}; +use nym_config::defaults::VOUCHER_INFO; use nym_contracts_common::dealings::ContractSafeBytes; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; use nym_crypto::asymmetric::{encryption, identity}; use nym_dkg::Threshold; +use nym_validator_client::nym_api::routes::{ + API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, +}; use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; +use nym_validator_client::nyxd::Coin; +use nym_validator_client::nyxd::{ + AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse, +}; use rand_07::rngs::OsRng; -use rand_07::Rng; +use rand_07::{Rng, RngCore}; use rocket::http::Status; use rocket::local::asynchronous::Client; use std::collections::HashMap; use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, RwLock}; +mod issued_credentials; + const TEST_COIN_DENOM: &str = "unym"; const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; #[derive(Clone, Debug)] pub(crate) struct DummyClient { validator_address: AccountId, - tx_db: Arc>>, + tx_db: Arc>>, proposal_db: Arc>>, spent_credential_db: Arc>>, @@ -94,7 +92,7 @@ impl DummyClient { } } - pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { + pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { self.tx_db = Arc::clone(tx_db); self } @@ -172,13 +170,8 @@ impl super::client::Client for DummyClient { self.validator_address.clone() } - async fn get_tx(&self, tx_hash: &str) -> Result { - self.tx_db - .read() - .unwrap() - .get(tx_hash) - .cloned() - .ok_or(CoconutError::TxHashParseError) + async fn get_tx(&self, tx_hash: Hash) -> Result { + Ok(self.tx_db.read().unwrap().get(&tx_hash).cloned().unwrap()) } async fn get_proposal(&self, proposal_id: u64) -> Result { @@ -467,27 +460,38 @@ impl super::client::Client for DummyClient { #[derive(Clone, Debug)] pub struct DummyCommunicationChannel { + current_epoch: Arc, aggregated_verification_key: VerificationKey, } impl DummyCommunicationChannel { pub fn new(aggregated_verification_key: VerificationKey) -> Self { DummyCommunicationChannel { + current_epoch: Arc::new(AtomicU64::new(1)), aggregated_verification_key, } } + + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { + self.current_epoch = current_epoch; + self + } } #[async_trait] impl super::comm::APICommunicationChannel for DummyCommunicationChannel { + async fn current_epoch(&self) -> Result { + Ok(self.current_epoch.load(Ordering::Relaxed)) + } + async fn aggregated_verification_key(&self, _epoch_id: EpochId) -> Result { Ok(self.aggregated_verification_key.clone()) } } -pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse { +pub fn tx_entry_fixture(hash: Hash) -> TxResponse { TxResponse { - hash: Hash::from_str(tx_hash).unwrap(), + hash, height: Default::default(), index: 0, tx_result: ExecTxResult { @@ -505,88 +509,242 @@ pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse { } } -#[tokio::test] -async fn signed_before() { - let tx_hash = - Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap(); - let tx_entry = tx_entry_fixture(&tx_hash.to_string()); - let signature = String::from( - "2DHbEZ6pzToGpsAXJrqJi7Wj1pAXeT18283q2YEEyNH5gTymwRozWBdja6SMAVt1dyYmUnM4ZNhsJ4wxZyGh4Z6J", - ); +pub fn deposit_tx_fixture(voucher: &BandwidthVoucher) -> TxResponse { + TxResponse { + hash: voucher.tx_hash(), + height: Default::default(), + index: 0, + tx_result: ExecTxResult { + code: Default::default(), + data: Default::default(), + log: "".to_string(), + info: "".to_string(), + gas_wanted: 0, + gas_used: 0, + events: vec![Event { + kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), + attributes: vec![ + EventAttribute { + key: DEPOSIT_VALUE.to_string(), + value: voucher.get_voucher_value(), + index: false, + }, + EventAttribute { + key: DEPOSIT_INFO.to_string(), + value: VOUCHER_INFO.to_string(), + index: false, + }, + EventAttribute { + key: DEPOSIT_IDENTITY_KEY.to_string(), + value: voucher.identity_key().public_key().to_base58_string(), + index: false, + }, + EventAttribute { + key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), + value: voucher.encryption_key().public_key().to_base58_string(), + index: false, + }, + ], + }], + codespace: "".to_string(), + }, + tx: vec![], + proof: None, + } +} +pub fn blinded_signature_fixture() -> BlindedSignature { + let gen1_bytes = [ + 151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140, + 79, 151, 116, 185, 5, 161, 78, 58, 63, 23, 27, 172, 88, 108, 85, 232, 63, 249, 122, 26, + 239, 251, 58, 240, 10, 219, 34, 198, 187, + ]; + + let dummy_bytes = gen1_bytes + .iter() + .chain(gen1_bytes.iter()) + .copied() + .collect::>(); + + BlindedSignature::from_bytes(&dummy_bytes).unwrap() +} + +pub fn voucher_request_fixture>( + amount: C, + tx_hash: Option, +) -> (BandwidthVoucher, BlindSignRequestBody) { let params = Parameters::new(4).unwrap(); let mut rng = OsRng; + let tx_hash = if let Some(provided) = &tx_hash { + provided.parse().unwrap() + } else { + Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap() + }; + + let identity_keypair = identity::KeyPair::new(&mut rng); + let encryption_keypair = encryption::KeyPair::new(&mut rng); + let id_priv = + identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); + let enc_priv = + encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap(); + let voucher = BandwidthVoucher::new( ¶ms, - "1234".to_string(), + amount.into().amount.to_string(), VOUCHER_INFO.to_string(), tx_hash, - identity::PrivateKey::from_base58_string( - identity::KeyPair::new(&mut rng) - .private_key() - .to_base58_string(), - ) - .unwrap(), - encryption::PrivateKey::from_bytes( - &encryption::KeyPair::new(&mut rng).private_key().to_bytes(), - ) - .unwrap(), + id_priv, + enc_priv, ); - let (_, blind_sign_req) = prepare_blind_sign( - ¶ms, - &voucher.get_private_attributes(), - &voucher.get_public_attributes(), - ) - .unwrap(); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); - let mut db_dir = std::env::temp_dir(); - db_dir.push(&key_pair.verification_key().to_bs58()[..8]); - let storage = NymApiStorage::init(db_dir).await.unwrap(); - let tx_db = Arc::new(RwLock::new(HashMap::new())); - tx_db - .write() - .unwrap() - .insert(tx_hash.to_string(), tx_entry.clone()); - let nyxd_client = - DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()) - .with_tx_db(&tx_db); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair.set(Some(key_pair)).await; - - let rocket = rocket::build().attach(InternalSignRequest::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - staged_key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let request_body = BlindSignRequestBody::new( - &blind_sign_req, - tx_hash.to_string(), - signature.clone(), - &voucher.get_public_attributes(), + let request = BlindSignRequestBody::new( + voucher.blind_sign_request().clone(), + tx_hash, + voucher.sign(), voucher.get_public_attributes_plain(), - 4, ); - let encrypted_signature = vec![1, 2, 3, 4]; - let remote_key = [42; 32]; - let expected_response = BlindedSignatureResponse::new(encrypted_signature, remote_key); - storage - .insert_blinded_signature_response( - &tx_hash.to_string(), - &expected_response.to_base58_string(), - ) + (voucher, request) +} + +fn dummy_signature() -> identity::Signature { + "3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp" + .parse() + .unwrap() +} + +async fn nym_api_storage_fixture(identity: &identity::KeyPair) -> NymApiStorage { + let mut db_dir = std::env::temp_dir(); + db_dir.push(identity.public_key().to_base58_string()); + NymApiStorage::init(db_dir).await.unwrap() +} + +struct TestFixture { + rocket: Client, + storage: NymApiStorage, + tx_db: Arc>>, + epoch: Arc, +} + +impl TestFixture { + async fn new() -> Self { + let mut rng = OsRng; + let params = Parameters::new(4).unwrap(); + let coconut_keypair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let identity = identity::KeyPair::new(&mut rng); + let epoch = Arc::new(AtomicU64::new(1)); + let comm_channel = + DummyCommunicationChannel::new(coconut_keypair.verification_key().clone()) + .with_epoch(epoch.clone()); + let storage = nym_api_storage_fixture(&identity).await; + + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(Some(coconut_keypair)).await; + + let tx_db = Arc::new(RwLock::new(HashMap::new())); + let nyxd_client = + DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()) + .with_tx_db(&tx_db); + + let rocket = rocket::build().attach(coconut::stage( + nyxd_client, + TEST_COIN_DENOM.to_string(), + identity, + staged_key_pair, + comm_channel, + storage.clone(), + )); + + TestFixture { + rocket: Client::tracked(rocket) + .await + .expect("valid rocket instance"), + storage, + tx_db, + epoch, + } + } + + fn set_epoch(&self, epoch: u64) { + self.epoch.store(epoch, Ordering::Relaxed) + } + + fn add_tx(&self, hash: Hash, tx: TxResponse) { + self.tx_db.write().unwrap().insert(hash, tx); + } + + fn add_deposit_tx(&self, voucher: &BandwidthVoucher) { + let mut guard = self.tx_db.write().unwrap(); + let fixture = deposit_tx_fixture(voucher); + guard.insert(voucher.tx_hash(), fixture); + } + + async fn issue_dummy_credential(&self) { + let mut rng = OsRng; + let mut tx_hash = [0u8; 32]; + rng.fill_bytes(&mut tx_hash); + let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap(); + + let (voucher, req) = voucher_request_fixture(coin(1234, "unym"), Some(tx_hash.to_string())); + self.add_deposit_tx(&voucher); + + self.issue_credential(req).await; + } + + async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { + let response = self + .rocket + .post(format!( + "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}", + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + } + + async fn issued_credential(&self, id: i64) -> Option { + let response = self + .rocket + .get(format!( + "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}" + )) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + } + + async fn issued_unchecked(&self, id: i64) -> IssuedCredentialInner { + self.issued_credential(id) + .await + .unwrap() + .credential + .unwrap() + } +} + +#[tokio::test] +async fn already_issued() { + let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None); + let tx_hash = request_body.tx_hash; + let tx_entry = tx_entry_fixture(tx_hash); + + let test_fixture = TestFixture::new().await; + test_fixture.add_tx(tx_hash, tx_entry); + + let sig = blinded_signature_fixture(); + let commitments = request_body.encode_commitments(); + let public = request_body.public_attributes_plain.clone(); + test_fixture + .storage + .store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public) .await .unwrap(); - let response = client + let response = test_fixture + .rocket .post(format!( "/{}/{}/{}/{}", API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN @@ -595,6 +753,7 @@ async fn signed_before() { .dispatch() .await; assert_eq!(response.status(), Status::Ok); + let expected_response = BlindedSignatureResponse::new(sig); // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 // let blinded_signature_response = response @@ -612,6 +771,9 @@ async fn signed_before() { #[tokio::test] async fn state_functions() { + let mut rng = OsRng; + let identity = identity::KeyPair::new(&mut rng); + let nyxd_client = DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()); let params = Parameters::new(4).unwrap(); @@ -619,38 +781,49 @@ async fn state_functions() { let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = NymApiStorage::init(db_dir).await.unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(Some(key_pair)).await; let state = State::new( nyxd_client, TEST_COIN_DENOM.to_string(), + identity, staged_key_pair, comm_channel, storage.clone(), ); - let tx_hash = String::from("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E"); - assert!(state.signed_before(&tx_hash).await.unwrap().is_none()); + let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E" + .parse() + .unwrap(); + assert!(state.already_issued(tx_hash).await.unwrap().is_none()); - let encrypted_signature = vec![1, 2, 3, 4]; - let remote_key = [42; 32]; - let expected_response = BlindedSignatureResponse::new(encrypted_signature, remote_key); + let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None); + let commitments = request_body.encode_commitments(); + let public = request_body.public_attributes_plain.clone(); + let sig = blinded_signature_fixture(); storage - .insert_blinded_signature_response(&tx_hash, &expected_response.to_base58_string()) + .store_issued_credential( + 42, + tx_hash, + &sig, + dummy_signature(), + commitments.clone(), + public.clone(), + ) .await .unwrap(); + assert_eq!( state - .signed_before(&tx_hash) + .already_issued(tx_hash) .await .unwrap() .unwrap() .to_bytes(), - expected_response.to_bytes() + blinded_signature_fixture().to_bytes() ); - let encryption_keypair = nym_crypto::asymmetric::encryption::KeyPair::new(&mut OsRng); let blinded_signature = BlindedSignature::from_bytes(&[ 183, 217, 166, 113, 40, 123, 74, 25, 72, 31, 136, 19, 125, 95, 217, 228, 96, 113, 25, 240, 12, 102, 125, 11, 174, 20, 216, 82, 192, 71, 27, 194, 48, 20, 17, 95, 243, 179, 82, 21, 57, @@ -659,68 +832,46 @@ async fn state_functions() { 222, 119, 93, 146, 116, 229, 0, 152, 51, 232, 2, 102, 204, 147, 202, 254, 243, ]) .unwrap(); + // Check that the new payload is not stored if there was already something signed for tx_hash - assert_eq!( - state - .encrypt_and_store( - &tx_hash, - encryption_keypair.public_key(), - &blinded_signature, - ) - .await - .unwrap() - .to_bytes(), - expected_response.to_bytes() - ); + let storage_err = storage + .store_issued_credential( + 42, + tx_hash, + &blinded_signature, + dummy_signature(), + commitments.clone(), + public.clone(), + ) + .await; + assert!(storage_err.is_err()); // And use a new hash to store a new signature - let tx_hash = String::from("97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF"); - let response = state - .encrypt_and_store( - &tx_hash, - encryption_keypair.public_key(), + let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF" + .parse() + .unwrap(); + + storage + .store_issued_credential( + 42, + tx_hash, &blinded_signature, + dummy_signature(), + commitments.clone(), + public.clone(), ) .await .unwrap(); - let remote_key = - nym_crypto::asymmetric::encryption::PublicKey::from_bytes(&response.remote_key).unwrap(); - - let encryption_key = recompute_shared_key::< - NymApiCredentialEncryptionAlgorithm, - NymApiCredentialHkdfAlgorithm, - >(&remote_key, encryption_keypair.private_key()); - let zero_iv = stream_cipher::zero_iv::(); - let blinded_signature_bytes = stream_cipher::decrypt::( - &encryption_key, - &zero_iv, - &response.encrypted_signature, - ); - - let received_blinded_signature = - BlindedSignature::from_bytes(&blinded_signature_bytes).unwrap(); - assert_eq!( - blinded_signature.to_bytes(), - received_blinded_signature.to_bytes() - ); // Check that the same value for tx_hash is returned - - let other_signature = BlindedSignature::from_bytes(&[ - 183, 217, 166, 113, 40, 123, 74, 25, 72, 31, 136, 19, 125, 95, 217, 228, 96, 113, 25, 240, - 12, 102, 125, 11, 174, 20, 216, 82, 192, 71, 27, 194, 48, 20, 17, 95, 243, 179, 82, 21, 57, - 143, 101, 19, 22, 186, 147, 13, 131, 236, 38, 138, 192, 235, 169, 142, 176, 118, 153, 238, - 141, 91, 94, 139, 168, 214, 17, 250, 96, 206, 139, 89, 139, 87, 31, 8, 106, 171, 8, 140, - 201, 158, 18, 152, 24, 98, 153, 170, 141, 35, 190, 200, 19, 148, 71, 197, - ]) - .unwrap(); assert_eq!( state - .encrypt_and_store(&tx_hash, encryption_keypair.public_key(), &other_signature,) + .already_issued(tx_hash) .await .unwrap() + .unwrap() .to_bytes(), - response.to_bytes() + blinded_signature.to_bytes() ); } @@ -731,6 +882,8 @@ async fn blind_sign_correct() { let params = Parameters::new(4).unwrap(); let mut rng = OsRng; + let nym_api_identity = identity::KeyPair::new(&mut rng); + let identity_keypair = identity::KeyPair::new(&mut rng); let encryption_keypair = encryption::KeyPair::new(&mut rng); let voucher = BandwidthVoucher::new( @@ -749,55 +902,20 @@ async fn blind_sign_correct() { let storage = NymApiStorage::init(db_dir).await.unwrap(); let tx_db = Arc::new(RwLock::new(HashMap::new())); - let mut tx_entry = tx_entry_fixture(&tx_hash.to_string()); - tx_entry.tx_result.events.push(Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![], - }); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "1234".parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: VOUCHER_INFO.parse().unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: identity_keypair - .public_key() - .to_base58_string() - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: encryption_keypair - .public_key() - .to_base58_string() - .parse() - .unwrap(), - index: false, - }, - ]; - tx_db - .write() - .unwrap() - .insert(tx_hash.to_string(), tx_entry.clone()); + let tx_entry = deposit_tx_fixture(&voucher); + + tx_db.write().unwrap().insert(tx_hash, tx_entry.clone()); let nyxd_client = DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()) .with_tx_db(&tx_db); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(Some(key_pair)).await; - let rocket = rocket::build().attach(InternalSignRequest::stage( + let rocket = rocket::build().attach(coconut::stage( nyxd_client, TEST_COIN_DENOM.to_string(), + nym_api_identity, staged_key_pair, comm_channel, storage.clone(), @@ -806,15 +924,13 @@ async fn blind_sign_correct() { .await .expect("valid rocket instance"); + let request_signature = voucher.sign(); + let request_body = BlindSignRequestBody::new( - voucher.blind_sign_request(), - tx_hash.to_string(), - voucher - .sign(voucher.blind_sign_request()) - .to_base58_string(), - &voucher.get_public_attributes(), + voucher.blind_sign_request().clone(), + tx_hash, + request_signature, voucher.get_public_attributes_plain(), - 4, ); let response = client @@ -825,6 +941,7 @@ async fn blind_sign_correct() { .json(&request_body) .dispatch() .await; + assert_eq!(response.status(), Status::Ok); // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 // assert!(response.into_json::().is_some()); @@ -847,26 +964,32 @@ async fn verification_of_bandwidth_credential() { let mut key_pairs = ttp_keygen(¶ms, 1, 1).unwrap(); let voucher_value = 1234u64; let voucher_info = "voucher info"; - let public_attributes = vec![ + let public_attributes = [ hash_to_scalar(voucher_value.to_string()), hash_to_scalar(voucher_info), ]; + let public_attributes_ref = vec![&public_attributes[0], &public_attributes[1]]; let indices: Vec = key_pairs .iter() .enumerate() .map(|(idx, _)| (idx + 1) as u64) .collect(); let theta = - theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes).unwrap(); + theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes_ref) + .unwrap(); let key_pair = key_pairs.remove(0); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage1 = NymApiStorage::init(db_dir).await.unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(Some(key_pair)).await; - let rocket = rocket::build().attach(InternalSignRequest::stage( + let mut rng = OsRng; + let identity = identity::KeyPair::new(&mut rng); + + let rocket = rocket::build().attach(coconut::stage( nyxd_client.clone(), TEST_COIN_DENOM.to_string(), + identity, staged_key_pair, comm_channel.clone(), storage1.clone(), diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 20982ef087..e63e22167f 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -80,12 +80,14 @@ async fn start_nym_api_tasks( let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); let coconut_keypair = coconut::keypair::KeyPair::new(); + let identity_keypair = config.base.storage_paths.load_identity()?; // let's build our rocket! let rocket = http::setup_rocket( &config, network_details, nyxd_client.clone(), + identity_keypair, coconut_keypair.clone(), ) .await?; diff --git a/nym-api/src/support/cli/mod.rs b/nym-api/src/support/cli/mod.rs index c339d52668..2427b5e7c8 100644 --- a/nym-api/src/support/cli/mod.rs +++ b/nym-api/src/support/cli/mod.rs @@ -126,7 +126,16 @@ pub(crate) fn build_config(args: run::Args) -> Result { } }; - let config = override_config(config, args); + let mut config = override_config(config, args); + // since we have no proper `init`, we have to do id check here: + let made_new_keys = config + .base + .storage_paths + .generate_identity_if_missing(&config.base.id)?; + + if made_new_keys { + config.save_to_default_location()? + } Ok(config) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 6431ef611b..d4052be515 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::config::persistence::{ - CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, + CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, }; use crate::support::config::template::CONFIG_TEMPLATE; use nym_config::defaults::{mainnet, NymNetworkDetails}; @@ -21,6 +21,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) mod helpers; pub(crate) mod old_config_v1_1_21; pub(crate) mod old_config_v1_1_27; + mod persistence; mod template; @@ -272,22 +273,29 @@ impl Config { #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize, ZeroizeOnDrop)] pub struct Base { /// ID specifies the human readable ID of this particular nym-api. - id: String, + pub id: String, #[zeroize(skip)] - local_validator: Url, + pub local_validator: Url, /// Address of the validator contract managing the network #[zeroize(skip)] - mixnet_contract_address: nyxd::AccountId, + pub mixnet_contract_address: nyxd::AccountId, /// Address of the vesting contract holding locked tokens #[zeroize(skip)] - vesting_contract_address: nyxd::AccountId, + pub vesting_contract_address: nyxd::AccountId, /// Mnemonic used for rewarding and/or multisig operations // TODO: similarly to the note in gateway, this should get moved to a separate file mnemonic: bip39::Mnemonic, + + /// Storage paths to the common nym-api files + #[zeroize(skip)] + // ideally we wouldn't be using default here, but I really really don't want to be writing + // another big config migration + #[serde(default)] + pub storage_paths: NymApiPaths, } impl Base { @@ -296,8 +304,11 @@ impl Base { .parse() .expect("default local validator is malformed!"); + let id = id.into(); + Base { - id: id.into(), + storage_paths: NymApiPaths::new_default(&id), + id, local_validator: default_validator, mixnet_contract_address: mainnet::MIXNET_CONTRACT_ADDRESS.parse().unwrap(), vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS.parse().unwrap(), diff --git a/nym-api/src/support/config/old_config_v1_1_21.rs b/nym-api/src/support/config/old_config_v1_1_21.rs index 3eee079a53..39058c3844 100644 --- a/nym-api/src/support/config/old_config_v1_1_21.rs +++ b/nym-api/src/support/config/old_config_v1_1_21.rs @@ -90,6 +90,7 @@ impl From for Config { mixnet_contract_address: value.base.mixnet_contract_address, vesting_contract_address: value.base.vesting_contract_address, mnemonic: value.base.mnemonic, + storage_paths: Default::default(), }, network_monitor: NetworkMonitor { enabled: value.network_monitor.enabled, diff --git a/nym-api/src/support/config/old_config_v1_1_27.rs b/nym-api/src/support/config/old_config_v1_1_27.rs index 3243147bc4..550c14c1bf 100644 --- a/nym-api/src/support/config/old_config_v1_1_27.rs +++ b/nym-api/src/support/config/old_config_v1_1_27.rs @@ -60,6 +60,7 @@ impl From for Config { mixnet_contract_address: value.base.mixnet_contract_address, vesting_contract_address: value.base.vesting_contract_address, mnemonic: value.base.mnemonic, + storage_paths: Default::default(), }, network_monitor: NetworkMonitor { enabled: value.network_monitor.enabled, diff --git a/nym-api/src/support/config/persistence.rs b/nym-api/src/support/config/persistence.rs index 7d4e58190a..24a16a4bae 100644 --- a/nym-api/src/support/config/persistence.rs +++ b/nym-api/src/support/config/persistence.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::config::default_data_directory; +use anyhow::{anyhow, Context}; +use nym_config::serde_helpers::de_maybe_stringified; +use nym_crypto::asymmetric::identity; +use rand_07::rngs::OsRng; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -15,6 +19,9 @@ pub const DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME: &str = "dkg_public_key_wit pub const DEFAULT_COCONUT_VERIFICATION_KEY_FILENAME: &str = "coconut_verification_key.pem"; pub const DEFAULT_COCONUT_SECRET_KEY_FILENAME: &str = "coconut_secret_key.pem"; +pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; +pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem"; + // #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] // pub struct NymApiPathfinder { // pub network_monitor: NetworkMonitorPathfinder, @@ -99,3 +106,66 @@ impl CoconutSignerPaths { } } } + +#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct NymApiPaths { + /// Path to file containing private identity key of the nym-api. + #[serde(deserialize_with = "de_maybe_stringified")] + pub private_identity_key_file: Option, + + /// Path to file containing public identity key of the nym-api. + #[serde(deserialize_with = "de_maybe_stringified")] + pub public_identity_key_file: Option, +} + +impl NymApiPaths { + pub fn new_default>(id: P) -> Self { + let data_dir = default_data_directory(id); + + NymApiPaths { + private_identity_key_file: Some(data_dir.join(DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME)), + public_identity_key_file: Some(data_dir.join(DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME)), + } + } + + pub fn generate_identity_if_missing>(&mut self, id: P) -> anyhow::Result { + if self.private_identity_key_file.is_none() { + log::warn!("identity key paths are not set - going to generate a fresh pair!"); + + let data_dir = default_data_directory(id); + self.private_identity_key_file = + Some(data_dir.join(DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME)); + self.public_identity_key_file = + Some(data_dir.join(DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME)); + + let keypaths = nym_pemstore::KeyPairPath::new( + self.private_identity_key_file.as_ref().unwrap(), + self.public_identity_key_file.as_ref().unwrap(), + ); + + let mut rng = OsRng; + let keypair = identity::KeyPair::new(&mut rng); + + nym_pemstore::store_keypair(&keypair, &keypaths) + .context("failed to store identity keys of the nym api")?; + Ok(true) + } else { + Ok(false) + } + } + + pub fn load_identity(&self) -> anyhow::Result { + // if somebody has set their private key but removed public key, the panic is totally on them. + let keypaths = nym_pemstore::KeyPairPath::new( + self.private_identity_key_file + .as_ref() + .ok_or(anyhow!("private key path is not specified"))?, + self.public_identity_key_file + .as_ref() + .ok_or(anyhow!("public key path is not specified"))?, + ); + + nym_pemstore::load_keypair(&keypaths).context("failed to load identity keys of the nym api") + } +} diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 88b0fb85d7..21d8487762 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -24,6 +24,12 @@ vesting_contract_address = '{{ base.vesting_contract_address }}' # Mnemonic used for rewarding and validator interaction mnemonic = '{{ base.mnemonic }}' +[base.storage_paths] +# Path to file containing private identity key of the nym-api. +keys.private_identity_key_file = '{{ base.storage_paths.private_identity_key_file }}' + +# Path to file containing public identity key of the nym-api. +keys.public_identity_key_file = '{{ base.storage_paths.public_identity_key_file }}' ##### network monitor config options ##### diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index be46cb73d6..88ded2ca45 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::coconut::{self, comm::QueryCommunicationChannel, InternalSignRequest}; +use crate::coconut::{self, comm::QueryCommunicationChannel}; use crate::network::models::NetworkDetails; use crate::network::network_routes; use crate::node_describe_cache::DescribedNodes; @@ -13,6 +13,7 @@ use crate::support::config::Config; use crate::support::{nyxd, storage}; use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; use anyhow::Result; +use nym_crypto::asymmetric::identity; use rocket::http::Method; use rocket::{Ignite, Rocket}; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; @@ -25,6 +26,7 @@ pub(crate) async fn setup_rocket( config: &Config, network_details: NetworkDetails, _nyxd_client: nyxd::Client, + identity_keypair: identity::KeyPair, coconut_keypair: coconut::keypair::KeyPair, ) -> anyhow::Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); @@ -66,9 +68,10 @@ pub(crate) async fn setup_rocket( let rocket = if config.coconut_signer.enabled { let comm_channel = QueryCommunicationChannel::new(_nyxd_client.clone()); - rocket.attach(InternalSignRequest::stage( + rocket.attach(coconut::stage( _nyxd_client.clone(), mix_denom, + identity_keypair, coconut_keypair, comm_channel, storage.clone().unwrap(), diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 4c08df3eb9..3c04803226 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -283,11 +283,13 @@ impl crate::coconut::client::Client for Client { self.client_address().await } - async fn get_tx(&self, tx_hash: &str) -> crate::coconut::error::Result { - let tx_hash: Hash = tx_hash - .parse() - .map_err(|_| CoconutError::TxHashParseError)?; - Ok(self.0.read().await.get_tx(tx_hash).await?) + async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result { + self.0.read().await.get_tx(tx_hash).await.map_err(|source| { + CoconutError::TxRetrievalFailure { + tx_hash: tx_hash.to_string(), + source, + } + }) } async fn get_proposal( diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 2bb3fa5c99..4e3c23c1e9 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -487,15 +487,15 @@ impl StorageManager { // insert the actual status sqlx::query!( - r#" - INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - mixnode_id, - mixnode_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; + r#" + INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); + "#, + mixnode_id, + mixnode_result.reliability, + timestamp + ) + .execute(&mut tx) + .await?; } // finally commit the transaction @@ -972,45 +972,4 @@ impl StorageManager { Ok(active_day_statuses) } - - /// Creates new encrypted blinded signature response entry for a given deposit tx hash. - /// - /// # Arguments - /// - /// * `tx_hash`: hash of the deposit transaction. - /// * `blinded_signature_response`: the encrypted blinded signature response. - pub(crate) async fn insert_blinded_signature_response( - &self, - tx_hash: &str, - blinded_signature_response: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "INSERT INTO signed_deposit(tx_hash, blinded_signature_response) VALUES (?, ?)", - tx_hash, - blinded_signature_response - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Tries to obtain encrypted blinded signature response for a given transaction hash. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit. - pub(crate) async fn get_blinded_signature_response( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error> { - let blinded_signature_response = sqlx::query!( - "SELECT blinded_signature_response FROM signed_deposit WHERE tx_hash = ?", - tx_hash - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.blinded_signature_response); - - Ok(blinded_signature_response) - } } diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index 4ae899aac1..3cd1e350cd 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -716,25 +716,4 @@ impl NymApiStorage { .await .map_err(|err| err.into()) } - - pub(crate) async fn get_blinded_signature_response( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError> { - self.manager - .get_blinded_signature_response(tx_hash) - .await - .map_err(|err| err.into()) - } - - pub(crate) async fn insert_blinded_signature_response( - &self, - tx_hash: &str, - blinded_signature_response: &str, - ) -> Result<(), NymApiStorageError> { - self.manager - .insert_blinded_signature_response(tx_hash, blinded_signature_response) - .await - .map_err(|err| err.into()) - } } diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 90510b9aa9..a031a0ad4a 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -569,27 +569,13 @@ dependencies = [ [[package]] name = "bls12_381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" +version = "0.8.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" dependencies = [ "digest 0.9.0", - "ff 0.10.1", - "group 0.10.0", - "pairing 0.20.0", - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "bls12_381" -version = "0.6.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" -dependencies = [ - "digest 0.9.0", - "ff 0.11.1", - "group 0.11.0", - "pairing 0.21.0", + "ff 0.13.0", + "group 0.13.0", + "pairing", "rand_core 0.6.4", "subtle 2.4.1", "zeroize", @@ -1352,6 +1338,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", + "serde", "subtle 2.4.1", "zeroize", ] @@ -1779,6 +1766,7 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ + "serde", "signature 1.6.4", ] @@ -1815,6 +1803,7 @@ dependencies = [ "ed25519 1.5.3", "rand 0.7.3", "serde", + "serde_bytes", "sha2 0.9.9", "zeroize", ] @@ -2038,26 +2027,6 @@ dependencies = [ "log", ] -[[package]] -name = "ff" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" -dependencies = [ - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "ff" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" -dependencies = [ - "rand_core 0.6.4", - "subtle 2.4.1", -] - [[package]] name = "ff" version = "0.12.1" @@ -2074,6 +2043,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -2663,30 +2633,6 @@ dependencies = [ "system-deps 6.1.1", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "byteorder", - "ff 0.10.1", - "rand_core 0.6.4", - "subtle 2.4.1", -] - -[[package]] -name = "group" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" -dependencies = [ - "byteorder", - "ff 0.11.1", - "rand_core 0.6.4", - "subtle 2.4.1", -] - [[package]] name = "group" version = "0.12.1" @@ -3411,9 +3357,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgit2-sys" @@ -3656,9 +3602,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -3860,10 +3806,12 @@ dependencies = [ "cosmwasm-std", "getset", "nym-coconut-interface", + "nym-crypto", "nym-mixnet-contract-common", "nym-node-requests", "schemars", "serde", + "tendermint", ] [[package]] @@ -3880,6 +3828,7 @@ dependencies = [ "rand 0.7.3", "thiserror", "url", + "zeroize", ] [[package]] @@ -3950,12 +3899,12 @@ dependencies = [ name = "nym-coconut" version = "0.5.0" dependencies = [ - "bls12_381 0.6.0", + "bls12_381", "bs58 0.4.0", "digest 0.9.0", - "ff 0.11.1", + "ff 0.13.0", "getrandom 0.2.10", - "group 0.11.0", + "group 0.13.0", "itertools", "nym-dkg", "nym-pemstore", @@ -4088,7 +4037,7 @@ dependencies = [ name = "nym-credentials" version = "0.1.0" dependencies = [ - "bls12_381 0.5.0", + "bls12_381", "cosmrs", "log", "nym-api-requests", @@ -4096,6 +4045,7 @@ dependencies = [ "nym-crypto", "nym-validator-client", "thiserror", + "zeroize", ] [[package]] @@ -4115,6 +4065,8 @@ dependencies = [ "nym-pemstore", "nym-sphinx-types", "rand 0.7.3", + "serde", + "serde_bytes", "subtle-encoding", "thiserror", "x25519-dalek 1.1.1", @@ -4126,10 +4078,10 @@ name = "nym-dkg" version = "0.1.0" dependencies = [ "bitvec", - "bls12_381 0.6.0", + "bls12_381", "bs58 0.4.0", - "ff 0.11.1", - "group 0.11.0", + "ff 0.13.0", + "group 0.13.0", "lazy_static", "nym-pemstore", "rand 0.8.5", @@ -4871,20 +4823,11 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pairing" -version = "0.20.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" dependencies = [ - "group 0.10.0", -] - -[[package]] -name = "pairing" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" -dependencies = [ - "group 0.11.0", + "group 0.13.0", ] [[package]] @@ -6494,9 +6437,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -7448,9 +7391,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.31.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -7459,16 +7402,16 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -7557,9 +7500,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -8625,6 +8568,7 @@ checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek 3.2.0", "rand_core 0.5.1", + "serde", "zeroize", ] diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index fb5cbf4ac8..63e60e281f 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -49,7 +49,7 @@ nym-bin-common = { path = "../../../common/bin-common" } # extra dependencies for libp2p examples libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]} tokio-stream = "0.1.12" -tokio-util = { version = "0.7", features = ["codec"] } +tokio-util = { workspace = true, features = ["codec"] } parking_lot = "0.12" hex = "0.4"