diff --git a/Cargo.lock b/Cargo.lock index de96670731..2bd734b3f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5128,6 +5128,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-id-lib", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-name-service-common", @@ -5168,6 +5169,7 @@ dependencies = [ "nym-credentials", "nym-crypto", "nym-gateway-requests", + "nym-id-lib", "nym-network-defaults", "nym-pemstore", "nym-sphinx", @@ -5626,6 +5628,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" @@ -5864,6 +5892,7 @@ dependencies = [ "nym-credentials", "nym-crypto", "nym-exit-policy", + "nym-id-lib", "nym-network-defaults", "nym-ordered-buffer", "nym-sdk", @@ -6151,6 +6180,7 @@ dependencies = [ "nym-credentials", "nym-crypto", "nym-gateway-requests", + "nym-id-lib", "nym-network-defaults", "nym-ordered-buffer", "nym-pemstore", diff --git a/Cargo.toml b/Cargo.toml index e90561c0bb..0f369f0a80 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/clients/native/Cargo.toml b/clients/native/Cargo.toml index b113ecc3e6..9da09cd0d0 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -51,5 +51,6 @@ nym-task = { path = "../../common/task" } nym-topology = { path = "../../common/topology" } nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] } nym-client-websocket-requests = { path = "websocket-requests" } +nym-id-lib = { path = "../../common/nym-id-lib" } [dev-dependencies] diff --git a/clients/native/src/commands/import_credential.rs b/clients/native/src/commands/import_credential.rs index de211aef01..aac7feaa64 100644 --- a/clients/native/src/commands/import_credential.rs +++ b/clients/native/src/commands/import_credential.rs @@ -4,14 +4,10 @@ use crate::commands::try_load_current_config; use crate::error::ClientError; use clap::ArgGroup; -use log::{error, info}; -use nym_credential_storage::models::StorableIssuedCredential; -use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; + +use nym_id_lib::import_credential; use std::fs; use std::path::PathBuf; -use zeroize::Zeroizing; fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { bs58::decode(raw).into_vec() @@ -33,8 +29,8 @@ pub(crate) struct Args { pub(crate) credential_path: Option, // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true, default_value_t = 1)] - pub(crate) version: u8, + #[clap(long, hide = true)] + pub(crate) version: Option, } pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { @@ -52,50 +48,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { fs::read(args.credential_path.unwrap())? } }; - let raw_credential = Zeroizing::new(raw_credential); - // we're unpacking the data in order to make sure it's valid - // and to extract relevant metadata for storage purposes - let credential = match args.version { - 1 => Zeroizing::new( - IssuedBandwidthCredential::unpack_v1(&raw_credential).map_err(|source| { - ClientError::CredentialDeserializationFailure { - storage_revision: 1, - source, - } - })?, - ), - other => panic!("unknown credential serialization version {other}"), - }; - - info!("importing {}", credential.typ()); - match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - info!("with value of {}", voucher_info.value()) - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - info!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - error!("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(ClientError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } - } - } - - let storable = StorableIssuedCredential { - serialization_revision: args.version, - 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?; + import_credential(credentials_store, raw_credential, args.version).await?; Ok(()) } diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 8a21803736..b4f47cf90b 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,6 +1,6 @@ use nym_client_core::error::ClientCoreError; -use nym_credential_storage::error::StorageError; -use time::OffsetDateTime; + +use nym_id_lib::NymIdError; #[derive(thiserror::Error, Debug)] pub enum ClientError { @@ -23,21 +23,6 @@ pub enum ClientError { #[error("Attempted to start the client in invalid socket mode")] InvalidSocketMode, - #[error("failed to store credential: {source}")] - CredentialStorageFailure { - #[from] - source: StorageError, - }, - - #[error( - "failed to deserialize provided credential using revision {storage_revision}: {source}" - )] - CredentialDeserializationFailure { - storage_revision: u8, - #[source] - source: nym_credentials::error::Error, - }, - - #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + #[error(transparent)] + NymIdError(#[from] NymIdError), } diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 37477d9d18..0af4c34770 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -35,6 +35,7 @@ nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" } nym-pemstore = { path = "../../common/pemstore" } nym-topology = { path = "../../common/topology" } nym-socks5-client-core = { path = "../../common/socks5-client-core" } +nym-id-lib = { path = "../../common/nym-id-lib" } [features] default = [] diff --git a/clients/socks5/src/commands/import_credential.rs b/clients/socks5/src/commands/import_credential.rs index 2ce3457015..25285870f8 100644 --- a/clients/socks5/src/commands/import_credential.rs +++ b/clients/socks5/src/commands/import_credential.rs @@ -4,14 +4,10 @@ use crate::commands::try_load_current_config; use crate::error::Socks5ClientError; use clap::ArgGroup; -use log::{error, info}; -use nym_credential_storage::models::StorableIssuedCredential; -use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; + +use nym_id_lib::import_credential; use std::fs; use std::path::PathBuf; -use zeroize::Zeroizing; fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { bs58::decode(raw).into_vec() @@ -33,8 +29,8 @@ pub(crate) struct Args { pub(crate) credential_path: Option, // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true, default_value_t = 1)] - pub(crate) version: u8, + #[clap(long, hide = true)] + pub(crate) version: Option, } pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { @@ -52,50 +48,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { fs::read(args.credential_path.unwrap())? } }; - let raw_credential = Zeroizing::new(raw_credential); - // we're unpacking the data in order to make sure it's valid - // and to extract relevant metadata for storage purposes - let credential = match args.version { - 1 => Zeroizing::new( - IssuedBandwidthCredential::unpack_v1(&raw_credential).map_err(|source| { - Socks5ClientError::CredentialDeserializationFailure { - storage_revision: 1, - source, - } - })?, - ), - other => panic!("unknown credential serialization version {other}"), - }; - - info!("importing {}", credential.typ()); - match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - info!("with value of {}", voucher_info.value()) - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - info!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - error!("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(Socks5ClientError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } - } - } - - let storable = StorableIssuedCredential { - serialization_revision: args.version, - 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?; + import_credential(credentials_store, raw_credential, args.version).await?; Ok(()) } diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index bed06bd385..788d1250f8 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,6 +1,6 @@ use nym_client_core::error::ClientCoreError; -use nym_credential_storage::error::StorageError; -use time::OffsetDateTime; + +use nym_id_lib::NymIdError; #[derive(thiserror::Error, Debug)] pub enum Socks5ClientError { @@ -23,21 +23,6 @@ pub enum Socks5ClientError { #[error(transparent)] ClientCoreError(#[from] ClientCoreError), - #[error("failed to store credential: {source}")] - CredentialStorageFailure { - #[from] - source: StorageError, - }, - - #[error( - "failed to deserialize provided credential using revision {storage_revision}: {source}" - )] - CredentialDeserializationFailure { - storage_revision: u8, - #[source] - source: nym_credentials::error::Error, - }, - - #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + #[error(transparent)] + NymIdError(#[from] NymIdError), } diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index b2ef8bc3ea..dba6b7a9b5 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -55,6 +55,7 @@ nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credential-utils = { path = "../../common/credential-utils" } +nym-id-lib = { path = "../../common/nym-id-lib" } nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" } nym-types = { path = "../../common/types" } diff --git a/common/commands/src/coconut/import_credential.rs b/common/commands/src/coconut/import_credential.rs index 337bf9125b..388961ace4 100644 --- a/common/commands/src/coconut/import_credential.rs +++ b/common/commands/src/coconut/import_credential.rs @@ -5,15 +5,10 @@ use crate::utils::CommonConfigsWrapper; use anyhow::bail; use clap::ArgGroup; use clap::Parser; -use log::{error, info}; use nym_credential_storage::initialise_persistent_storage; -use nym_credential_storage::models::StorableIssuedCredential; -use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; +use nym_id_lib::import_credential; use std::fs; use std::path::PathBuf; -use zeroize::Zeroizing; fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { bs58::decode(raw).into_vec() @@ -35,8 +30,8 @@ pub struct Args { pub(crate) credential_path: Option, // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true, default_value_t = 1)] - pub(crate) version: u8, + #[clap(long, hide = true)] + pub(crate) version: Option, } pub async fn execute(args: Args) -> anyhow::Result<()> { @@ -54,6 +49,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { "using credentials store at '{}'", credentials_store.display() ); + let credentials_store = initialise_persistent_storage(credentials_store).await; let raw_credential = match args.credential_data { Some(data) => data, @@ -62,44 +58,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { fs::read(args.credential_path.unwrap())? } }; - let raw_credential = Zeroizing::new(raw_credential); - // we're unpacking the data in order to make sure it's valid - // and to extract relevant metadata for storage purposes - let credential = match args.version { - 1 => Zeroizing::new(IssuedBandwidthCredential::unpack_v1(&raw_credential)?), - other => panic!("unknown credential serialization version {other}"), - }; - let persistent_storage = initialise_persistent_storage(credentials_store).await; - - info!("importing {}", credential.typ()); - match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - info!("with value of {}", voucher_info.value()) - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - info!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - error!("the free pass has already expired!"); - - // technically we can, but the gateway will just reject it so what's the point - bail!("can't import an expired free pass") - } - } - } - - let storable = StorableIssuedCredential { - serialization_revision: args.version, - 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!"), - }; - - persistent_storage - .insert_issued_credential(storable) - .await?; + import_credential(credentials_store, raw_credential, args.version).await?; 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..362dfd554b --- /dev/null +++ b/nym-id/src/commands/mod.rs @@ -0,0 +1,45 @@ +// 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..755fb6cc8b --- /dev/null +++ b/nym-id/src/commands/setup.rs @@ -0,0 +1,2 @@ +// 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 +} diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 4b9b1c8003..65ca6c2aa3 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -61,7 +61,7 @@ nym-statistics-common = { path = "../../common/statistics" } nym-task = { path = "../../common/task" } nym-types = { path = "../../common/types" } nym-exit-policy = { path = "../../common/exit-policy", features = ["client"] } - +nym-id-lib = { path = "../../common/nym-id-lib" } [dev-dependencies] tempfile = "3.5.0" diff --git a/service-providers/network-requester/src/cli/import_credential.rs b/service-providers/network-requester/src/cli/import_credential.rs index 6d0b8fffb1..1fe0d3de52 100644 --- a/service-providers/network-requester/src/cli/import_credential.rs +++ b/service-providers/network-requester/src/cli/import_credential.rs @@ -4,14 +4,10 @@ use crate::cli::try_load_current_config; use crate::error::NetworkRequesterError; use clap::ArgGroup; -use log::{error, info}; -use nym_credential_storage::models::StorableIssuedCredential; -use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; + +use nym_id_lib::import_credential; use std::fs; use std::path::PathBuf; -use zeroize::Zeroizing; fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { bs58::decode(raw).into_vec() @@ -33,8 +29,8 @@ pub(crate) struct Args { pub(crate) credential_path: Option, // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true, default_value_t = 1)] - pub(crate) version: u8, + #[clap(long, hide = true)] + pub(crate) version: Option, } pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { @@ -52,50 +48,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { fs::read(args.credential_path.unwrap())? } }; - let raw_credential = Zeroizing::new(raw_credential); - // we're unpacking the data in order to make sure it's valid - // and to extract relevant metadata for storage purposes - let credential = match args.version { - 1 => Zeroizing::new( - IssuedBandwidthCredential::unpack_v1(&raw_credential).map_err(|source| { - NetworkRequesterError::CredentialDeserializationFailure { - storage_revision: 1, - source, - } - })?, - ), - other => panic!("unknown credential serialization version {other}"), - }; - - info!("importing {}", credential.typ()); - match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - info!("with value of {}", voucher_info.value()) - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - info!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - error!("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(NetworkRequesterError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } - } - } - - let storable = StorableIssuedCredential { - serialization_revision: args.version, - 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?; + import_credential(credentials_store, raw_credential, args.version).await?; Ok(()) } diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index ce14ed7cf9..0143494696 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only pub use nym_client_core::error::ClientCoreError; -use nym_credential_storage::error::StorageError; + use nym_exit_policy::policy::PolicyError; +use nym_id_lib::NymIdError; use nym_socks5_requests::{RemoteAddress, Socks5RequestError}; use std::net::SocketAddr; -use time::OffsetDateTime; #[derive(thiserror::Error, Debug)] pub enum NetworkRequesterError { @@ -70,21 +70,6 @@ pub enum NetworkRequesterError { #[error("can't setup an exit policy without any upstream urls")] NoUpstreamExitPolicy, - #[error("failed to store credential: {source}")] - CredentialStorageFailure { - #[from] - source: StorageError, - }, - - #[error( - "failed to deserialize provided credential using revision {storage_revision}: {source}" - )] - CredentialDeserializationFailure { - storage_revision: u8, - #[source] - source: nym_credentials::error::Error, - }, - - #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + #[error(transparent)] + NymIdError(#[from] NymIdError), }