diff --git a/Cargo.lock b/Cargo.lock index 7dcb192b24..dd14db56db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5626,6 +5626,32 @@ dependencies = [ "serde", ] +[[package]] +name = "nym-id" +version = "0.1.0" +dependencies = [ + "anyhow", + "bs58 0.5.0", + "clap 4.4.7", + "nym-bin-common", + "nym-credential-storage", + "nym-id-lib", + "tokio", + "tracing", +] + +[[package]] +name = "nym-id-lib" +version = "0.1.0" +dependencies = [ + "nym-credential-storage", + "nym-credentials", + "thiserror", + "time", + "tracing", + "zeroize", +] + [[package]] name = "nym-inclusion-probability" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6e78dea995..040b5132a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,7 +112,7 @@ members = [ "wasm/client", # "wasm/full-nym-wasm", "wasm/mix-fetch", - "wasm/node-tester", + "wasm/node-tester", "nym-id", "common/nym-id-lib", ] default-members = [ diff --git a/common/commands/src/coconut/check_pass.rs b/common/commands/src/coconut/check_pass.rs new file mode 100644 index 0000000000..ef02d80722 --- /dev/null +++ b/common/commands/src/coconut/check_pass.rs @@ -0,0 +1,112 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::{QueryClient, SigningClient}; +use anyhow::{anyhow, bail}; +use clap::ArgGroup; +use clap::Parser; +use futures::StreamExt; +use log::{error, info}; +use nym_coconut_dkg_common::types::EpochId; +use nym_credential_utils::utils::block_until_coconut_is_available; +use nym_credentials::coconut::bandwidth::bandwidth_credential_params; +use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; +use nym_credentials::coconut::utils; +use nym_credentials::{ + obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential, +}; +use nym_credentials_interface::VerificationKey; +use nym_validator_client::coconut::all_coconut_api_clients; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::CosmWasmClient; +use nym_validator_client::signing::AccountData; +use nym_validator_client::CoconutApiClient; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::sync::Arc; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; +use zeroize::Zeroizing; + +#[derive(Debug, Parser)] +pub struct Args { + /// Path to the output directory for generated free passes. + #[clap(long)] + pub(crate) pass: PathBuf, +} + +// async fn get_freepass( +// api_clients: Vec, +// aggregate_vk: &VerificationKey, +// threshold: u64, +// epoch_id: EpochId, +// signing_account: &AccountData, +// expiration_date: OffsetDateTime, +// ) -> anyhow::Result { +// let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date)); +// let signing_data = issuance_pass.prepare_for_signing(); +// +// let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new())); +// +// futures::stream::iter(api_clients) +// .for_each_concurrent(None, |client| async { +// // move the client into the block +// let client = client; +// let api_url = client.api_client.api_url(); +// +// info!("contacting {api_url} for blinded free pass"); +// +// match issuance_pass +// .obtain_partial_freepass_credential( +// &client.api_client, +// signing_account, +// &client.verification_key, +// signing_data.clone(), +// ) +// .await +// { +// Ok(partial_credential) => { +// credential_shares +// .lock() +// .await +// .push((partial_credential, client.node_id).into()); +// } +// Err(err) => { +// error!("failed to obtain partial free pass from {api_url}: {err}") +// } +// } +// }) +// .await; +// +// // SAFETY: the futures have completed, so we MUST have the only arc reference +// #[allow(clippy::unwrap_used)] +// let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner(); +// +// if credential_shares.len() < threshold as usize { +// bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len()); +// } +// +// let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?; +// Ok(issuance_pass.into_issued_credential(signature, epoch_id)) +// } + +pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { + let raw_credential = fs::read(args.pass)?; + let credential = IssuedBandwidthCredential::unpack_v1(&raw_credential)?; + + println!("{:?}", credential.get_plain_public_attributes()); + println!("{}", credential.epoch_id()); + println!("{:?}", credential.variant_data()); + + let api_clients = all_coconut_api_clients(&client, credential.epoch_id()).await?; + + let vk = utils::obtain_aggregate_verification_key(&api_clients)?; + + let foomp = credential.prepare_for_spending(&vk)?; + + let res = foomp.verify(bandwidth_credential_params(), &vk); + println!("res: {res}"); + Ok(()) +} diff --git a/common/credentials/src/coconut/bandwidth/issuance.rs b/common/credentials/src/coconut/bandwidth/issuance.rs index bc56bda0fd..53c89a300b 100644 --- a/common/credentials/src/coconut/bandwidth/issuance.rs +++ b/common/credentials/src/coconut/bandwidth/issuance.rs @@ -315,9 +315,12 @@ impl IssuanceBandwidthCredential { } // TODO: is that actually needed? + // idea: make it consistent with the issued credential and its vX serde pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result { use bincode::Options; - Ok(make_recovery_bincode_serializer().deserialize(bytes)?) + make_recovery_bincode_serializer() + .deserialize(bytes) + .map_err(|source| Error::RecoveryCredentialDeserializationFailure { source }) } } @@ -327,3 +330,18 @@ fn make_recovery_bincode_serializer() -> impl bincode::Options { .with_big_endian() .with_varint_encoding() } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn credential_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/credentials/src/coconut/bandwidth/issued.rs b/common/credentials/src/coconut/bandwidth/issued.rs index 486fd7c7ee..5e2951d1d6 100644 --- a/common/credentials/src/coconut/bandwidth/issued.rs +++ b/common/credentials/src/coconut/bandwidth/issued.rs @@ -116,6 +116,15 @@ impl IssuedBandwidthCredential { } } + pub fn try_unpack(bytes: &[u8], revision: impl Into>) -> Result { + let revision = revision.into().unwrap_or(CURRENT_SERIALIZATION_REVISION); + + match revision { + 1 => Self::unpack_v1(bytes), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } + pub fn epoch_id(&self) -> EpochId { self.epoch_id } @@ -138,7 +147,12 @@ impl IssuedBandwidthCredential { /// Unpack (deserialize) the credential data from the given bytes using v1 serializer. pub fn unpack_v1(bytes: &[u8]) -> Result { use bincode::Options; - Ok(make_storable_bincode_serializer().deserialize(bytes)?) + make_storable_bincode_serializer() + .deserialize(bytes) + .map_err(|source| Error::SerializationFailure { + source, + revision: 1, + }) } pub fn randomise_signature(&mut self) { @@ -191,3 +205,18 @@ fn make_storable_bincode_serializer() -> impl bincode::Options { .with_big_endian() .with_varint_encoding() } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn credential_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index 96d8bf4202..6487d23870 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -5,6 +5,7 @@ use nym_credentials_interface::CoconutError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_validator_client::ValidatorClientError; +use crate::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use thiserror::Error; #[derive(Debug, Error)] @@ -12,8 +13,18 @@ pub enum Error { #[error("IO error")] IOError(#[from] std::io::Error), - #[error("failed to (de)serialize credential structure: {0}")] - SerializationFailure(#[from] bincode::Error), + #[error("failed to deserialize a recovery credential: {source}")] + RecoveryCredentialDeserializationFailure { source: bincode::Error }, + + #[error("failed to (de)serialize provided credential using revision {revision}: {source}")] + SerializationFailure { + #[source] + source: bincode::Error, + revision: u8, + }, + + #[error("unknown credential serializatio revision {revision}. the current (and max supported) version is {CURRENT_SERIALIZATION_REVISION}")] + UnknownSerializationRevision { revision: u8 }, #[error("The detailed description is yet to be determined")] BandwidthCredentialError, diff --git a/common/credentials/src/lib.rs b/common/credentials/src/lib.rs index caa16ee410..c06eeb0a05 100644 --- a/common/credentials/src/lib.rs +++ b/common/credentials/src/lib.rs @@ -9,3 +9,4 @@ pub use coconut::bandwidth::{ IssuedBandwidthCredential, }; pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; +pub use error::Error; diff --git a/common/nym-id-lib/Cargo.toml b/common/nym-id-lib/Cargo.toml new file mode 100644 index 0000000000..3114f0eded --- /dev/null +++ b/common/nym-id-lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nym-id-lib" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror.workspace = true +time.workspace = true +tracing.workspace = true +zeroize.workspace = true + +nym-credential-storage = { path = "../credential-storage" } +nym-credentials = { path = "../credentials" } diff --git a/common/nym-id-lib/src/error.rs b/common/nym-id-lib/src/error.rs new file mode 100644 index 0000000000..2edc03fd0e --- /dev/null +++ b/common/nym-id-lib/src/error.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::error::Error; +use thiserror::Error; +use time::OffsetDateTime; + +#[derive(Debug, Error)] +pub enum NymIdError { + #[error("failed to deserialize provided credential: {source}")] + CredentialDeserializationFailure { source: nym_credentials::Error }, + + #[error("attempted to import an expired credential (it expired on {expiration})")] + ExpiredCredentialImport { expiration: OffsetDateTime }, + + #[error("failed to store credential in the provided store: {source}")] + StorageError { + source: Box, + }, +} diff --git a/common/nym-id-lib/src/import_credential.rs b/common/nym-id-lib/src/import_credential.rs new file mode 100644 index 0000000000..f5058dfe2a --- /dev/null +++ b/common/nym-id-lib/src/import_credential.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::NymIdError; +use nym_credential_storage::models::StorableIssuedCredential; +use nym_credential_storage::storage::Storage; +use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; +use nym_credentials::IssuedBandwidthCredential; +use tracing::{debug, warn}; +use zeroize::Zeroizing; + +pub async fn import_credential( + credentials_store: S, + raw_credential: Vec, + credential_version: impl Into>, +) -> Result<(), NymIdError> +where + S: Storage, + ::StorageError: Send + Sync + 'static, +{ + let raw_credential = Zeroizing::new(raw_credential); + + // note: the type itself implements ZeroizeOnDrop + let credential = IssuedBandwidthCredential::try_unpack(&raw_credential, credential_version) + .map_err(|source| NymIdError::CredentialDeserializationFailure { source })?; + + debug!( + "attempting to import credential of type {}", + credential.typ() + ); + + match credential.variant_data() { + BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { + debug!("with value of {}", voucher_info.value()) + } + BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { + debug!("with expiry at {}", freepass_info.expiry_date()); + if freepass_info.expired() { + warn!("the free pass has already expired!"); + + // technically we can import it, but the gateway will just reject it so what's the point + return Err(NymIdError::ExpiredCredentialImport { + expiration: freepass_info.expiry_date(), + }); + } + } + } + + // SAFETY: + // for the epoch to run over u32::MAX, we'd have to advance it for few centuries every block... + // the alternative is a very particularly malformed serialized data, but at that point blowing up is the right call + // because we can't rely on it anyway + #[allow(clippy::expect_used)] + let storable = StorableIssuedCredential { + serialization_revision: credential.current_serialization_revision(), + credential_data: &raw_credential, + credential_type: credential.typ().to_string(), + epoch_id: credential + .epoch_id() + .try_into() + .expect("our epoch is has run over u32::MAX!"), + }; + + credentials_store + .insert_issued_credential(storable) + .await + .map_err(|source| NymIdError::StorageError { + source: Box::new(source), + })?; + Ok(()) +} diff --git a/common/nym-id-lib/src/lib.rs b/common/nym-id-lib/src/lib.rs new file mode 100644 index 0000000000..e2e3a74adc --- /dev/null +++ b/common/nym-id-lib/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +pub mod error; +pub mod import_credential; + +pub use error::NymIdError; +pub use import_credential::import_credential; diff --git a/nym-id/Cargo.toml b/nym-id/Cargo.toml new file mode 100644 index 0000000000..49da695276 --- /dev/null +++ b/nym-id/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nym-id" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +bs58.workspace = true +clap = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing.workspace = true + +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-credential-storage = { path = "../common/credential-storage" } +nym-id-lib = { path = "../common/nym-id-lib" } diff --git a/nym-id/src/commands/build_info.rs b/nym-id/src/commands/build_info.rs new file mode 100644 index 0000000000..a989a41f10 --- /dev/null +++ b/nym-id/src/commands/build_info.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) { + println!("{}", args.output.format(&bin_info_owned!())) +} diff --git a/nym-id/src/commands/import_credential.rs b/nym-id/src/commands/import_credential.rs new file mode 100644 index 0000000000..9422a34e8d --- /dev/null +++ b/nym-id/src/commands/import_credential.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::ArgGroup; +use nym_id_lib::import_credential; +use std::fs; +use std::path::PathBuf; + +fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { + bs58::decode(raw).into_vec() +} + +#[derive(clap::Args)] +#[clap(group(ArgGroup::new("cred_data").required(true)))] +pub(crate) struct Args { + /// Explicitly provide the encoded credential data (as base58) + #[clap(long, group = "cred_data", value_parser = parse_encoded_credential_data)] + pub(crate) credential_data: Option>, + + /// Specifies the path to file containing binary credential data + #[clap(long, group = "cred_data")] + pub(crate) credential_path: Option, + + /// Specifies path to the credentials storage + #[clap(long)] + pub credentials_store_path: PathBuf, + + // currently hidden as there exists only a single serialization standard + #[clap(long, hide = true)] + pub(crate) version: Option, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + let credentials_store = + nym_credential_storage::initialise_persistent_storage(args.credentials_store_path).await; + + let raw_credential = match args.credential_data { + Some(data) => data, + None => { + // SAFETY: one of those arguments must have been set + #[allow(clippy::unwrap_used)] + fs::read(args.credential_path.unwrap())? + } + }; + + import_credential(credentials_store, raw_credential, args.version).await?; + Ok(()) +} diff --git a/nym-id/src/commands/mod.rs b/nym-id/src/commands/mod.rs new file mode 100644 index 0000000000..e2cf56ad46 --- /dev/null +++ b/nym-id/src/commands/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod build_info; +mod import_credential; +mod setup; + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::ImportCredential(args) => import_credential::execute(args).await?, + Commands::BuildInfo(args) => build_info::execute(args), + } + + Ok(()) + } +} + +#[derive(Subcommand)] +pub(crate) enum Commands { + // TODO: to be determined how it's going to work in nymvpn et al. + // /// + // Setup, + + /// Attempt to import a bandwidth credential into the provided storage. + ImportCredential(import_credential::Args), + + /// Show build information of this binary + BuildInfo(build_info::Args), +} diff --git a/nym-id/src/commands/setup.rs b/nym-id/src/commands/setup.rs new file mode 100644 index 0000000000..20bfe6232a --- /dev/null +++ b/nym-id/src/commands/setup.rs @@ -0,0 +1,3 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + diff --git a/nym-id/src/main.rs b/nym-id/src/main.rs new file mode 100644 index 0000000000..8d8de30903 --- /dev/null +++ b/nym-id/src/main.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +use crate::commands::Cli; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; + +mod commands; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing_logger(); + let cli = Cli::parse(); + + cli.execute().await +}