diff --git a/Cargo.lock b/Cargo.lock index 6a90787a0f..a91515295e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5388,6 +5388,7 @@ dependencies = [ "bls12_381", "nym-coconut", "serde", + "thiserror", ] [[package]] diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 7b4e787cb5..4adda09b0f 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -45,4 +45,7 @@ pub enum BandwidthControllerError { #[error("Threshold not set yet")] NoThreshold, + + #[error("can't handle recovering storage with revision {stored}. {expected} was expected")] + UnsupportedCredentialStorageRevision { stored: u8, expected: u8 }, } diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 690466daa5..89891dba48 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -2,40 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::{StorableIssuedCredential, StoredIssuedCredential}; +use nym_credential_storage::models::StoredIssuedCredential; +use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential; -use nym_validator_client::nym_api::EpochId; pub fn stored_credential_to_issued_bandwidth( cred: StoredIssuedCredential, ) -> Result { - /* - let bandwidth_credential = self - .storage - .get_next_coconut_credential() - .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?; - 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 = 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) - .map_err(|_| StorageError::InconsistentData)?; + if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION { + return Err( + BandwidthControllerError::UnsupportedCredentialStorageRevision { + stored: cred.serialization_revision, + expected: CURRENT_SERIALIZATION_REVISION, + }, + ); + } - */ - todo!() -} - -pub fn issued_bandwidth_to_stored_credential( - issued: IssuedBandwidthCredential, - epoch_id: EpochId, -) -> StoredIssuedCredential { - todo!() + Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?) } diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index 789552ebff..5393ea37fd 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -13,5 +13,6 @@ license.workspace = true [dependencies] bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } nym-coconut = { path = "../nymcoconut" } diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index f962bf8c9e..36af1ef380 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -4,9 +4,11 @@ use bls12_381::Scalar; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use thiserror::Error; pub use nym_coconut::{ - aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar, + aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential, verify_credential, Attribute, Base58, BlindSignRequest, BlindedSignature, Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey, Signature, SignatureShare, VerificationKey, @@ -20,12 +22,30 @@ pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass"; // fn prove_credential(&self) -> Result<(), ()>; // } -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Debug, Error)] +#[error("{0} is not a valid credential type")] +pub struct UnknownCredentialType(String); + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum CredentialType { Voucher, FreePass, } +impl FromStr for CredentialType { + type Err = UnknownCredentialType; + + fn from_str(s: &str) -> Result { + if s == VOUCHER_INFO_TYPE { + Ok(CredentialType::Voucher) + } else if s == FREE_PASS_INFO_TYPE { + Ok(CredentialType::FreePass) + } else { + Err(UnknownCredentialType(s.to_string())) + } + } +} + impl CredentialType { pub fn validate(&self, type_plain: &str) -> bool { match self { @@ -63,7 +83,7 @@ pub struct CredentialSigningData { pub typ: CredentialType, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CredentialSpendingData { pub embedded_private_attributes: usize, diff --git a/common/credentials/src/coconut/bandwidth/issuance.rs b/common/credentials/src/coconut/bandwidth/issuance.rs index 02eb1a3c6c..1bba208cc5 100644 --- a/common/credentials/src/coconut/bandwidth/issuance.rs +++ b/common/credentials/src/coconut/bandwidth/issuance.rs @@ -127,6 +127,10 @@ impl IssuanceBandwidthCredential { )) } + pub fn new_freepass(expiry_date: Option) -> Self { + Self::new(FreePassIssuanceData::new(expiry_date)) + } + pub fn blind_serial_number_in_g1subgroup(&self) -> G1Projective { bandwidth_credential_params().gen1() * self.serial_number } @@ -137,10 +141,6 @@ impl IssuanceBandwidthCredential { self.blind_serial_number_in_g1subgroup().to_bs58() } - pub fn new_freepass(expiry_date: Option) -> Self { - Self::new(FreePassIssuanceData::new(expiry_date)) - } - pub fn typ(&self) -> CredentialType { self.variant_data.info() } diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index ed258cd557..4ac8245c37 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -14,6 +14,7 @@ pub use scheme::issuance::blind_sign; pub use scheme::issuance::prepare_blind_sign; pub use scheme::issuance::verify_partial_blind_signature; pub use scheme::issuance::BlindSignRequest; +pub use scheme::keygen::keygen; pub use scheme::keygen::ttp_keygen; pub use scheme::keygen::KeyPair; pub use scheme::keygen::SecretKey; diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index 966ebb141c..50d735ef92 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -565,7 +565,6 @@ impl TryFrom<&[u8]> for KeyPair { /// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)). /// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys /// that are independent of each other. -#[cfg(test)] pub fn keygen(params: &Parameters) -> KeyPair { let attributes = params.gen_hs().len(); diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index d63170628e..e771a71de1 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -32,3 +32,4 @@ nym-credentials-interface = { path = "../../common/credentials-interface" } workspace = true default-features = false + diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index 9bb25dc45c..e27d272f23 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -15,7 +15,10 @@ pub mod types; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. -pub const PROTOCOL_VERSION: u8 = 1; +// history: +// 1 - initial release +// 2 - changes to client credentials structure +pub const PROTOCOL_VERSION: u8 = 2; pub type GatewayMac = HmacOutput; diff --git a/gateway/gateway-requests/src/models.rs b/gateway/gateway-requests/src/models.rs index 4e1df1d61e..6e06aabc7e 100644 --- a/gateway/gateway-requests/src/models.rs +++ b/gateway/gateway-requests/src/models.rs @@ -3,10 +3,10 @@ use crate::GatewayRequestsError; use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::CoconutError; +use nym_credentials_interface::{CoconutError, VerifyCredentialRequest}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CredentialSpendingWithEpoch { /// The cryptographic material required for spending the underlying credential. pub data: CredentialSpendingData, @@ -16,6 +16,23 @@ pub struct CredentialSpendingWithEpoch { pub epoch_id: u64, } +// just a helper macro for checking required length and advancing the buffer +macro_rules! ensure_len_and_advance { + ($b:expr, $n:expr) => {{ + if $b.len() < $n { + return Err(GatewayRequestsError::CredentialDeserializationFailureEOF); + } + // create binding to the desired range + let bytes = &$b[..$n]; + + // update the initial binding + + $b = &$b[$n..]; + + bytes + }}; +} + impl CredentialSpendingWithEpoch { pub fn new(data: CredentialSpendingData, epoch_id: u64) -> Self { CredentialSpendingWithEpoch { data, epoch_id } @@ -39,10 +56,138 @@ impl CredentialSpendingWithEpoch { } pub fn to_bytes(&self) -> Vec { - todo!() + // simple length prefixed serialization + // TODO: change it to a standard format instead + let mut bytes = Vec::new(); + + let embedded_private = (self.data.embedded_private_attributes as u32).to_be_bytes(); + let theta = self.data.verify_credential_request.to_bytes(); + let theta_len = (theta.len() as u32).to_be_bytes(); + + let public = (self.data.public_attributes_plain.len() as u32).to_be_bytes(); + let typ = self.data.typ.to_string(); + let typ_bytes = typ.as_bytes(); + let typ_len = (typ_bytes.len() as u32).to_be_bytes(); + + bytes.extend_from_slice(&embedded_private); + bytes.extend_from_slice(&theta_len); + bytes.extend_from_slice(&theta); + bytes.extend_from_slice(&public); + + for pub_element in &self.data.public_attributes_plain { + let bytes_el = pub_element.as_bytes(); + let len = (bytes_el.len() as u32).to_be_bytes(); + + bytes.extend_from_slice(&len); + bytes.extend_from_slice(bytes_el); + } + + bytes.extend_from_slice(&typ_len); + bytes.extend_from_slice(typ_bytes); + bytes.extend_from_slice(&self.epoch_id.to_be_bytes()); + + bytes } pub fn try_from_bytes(raw: &[u8]) -> Result { - todo!() + // initial binding + let mut b = raw; + let embedded_private_bytes = ensure_len_and_advance!(b, 4); + let embedded_private_attributes = + u32::from_be_bytes(embedded_private_bytes.try_into().unwrap()) as usize; + + let theta_len_bytes = ensure_len_and_advance!(b, 4); + let theta_len = u32::from_be_bytes(theta_len_bytes.try_into().unwrap()) as usize; + + let theta_bytes = ensure_len_and_advance!(b, theta_len); + let theta = VerifyCredentialRequest::from_bytes(theta_bytes) + .map_err(GatewayRequestsError::CredentialDeserializationFailureMalformedTheta)?; + + let public_bytes = ensure_len_and_advance!(b, 4); + let public = u32::from_be_bytes(public_bytes.try_into().unwrap()) as usize; + + let mut public_attributes_plain = Vec::with_capacity(public); + for _ in 0..public { + let element_len_bytes = ensure_len_and_advance!(b, 4); + let element_len = u32::from_be_bytes(element_len_bytes.try_into().unwrap()) as usize; + + let element_bytes = ensure_len_and_advance!(b, element_len); + let element = String::from_utf8(element_bytes.to_vec())?; + public_attributes_plain.push(element); + } + + let typ_len_bytes = ensure_len_and_advance!(b, 4); + let typ_len = u32::from_be_bytes(typ_len_bytes.try_into().unwrap()) as usize; + + let typ_bytes = ensure_len_and_advance!(b, typ_len); + let raw_typ = String::from_utf8(typ_bytes.to_vec())?; + let typ = raw_typ.parse()?; + + // tell the linter to chill out in for this last iteration + #[allow(unused_assignments)] + let epoch_id_bytes = ensure_len_and_advance!(b, 8); + let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap()); + + Ok(CredentialSpendingWithEpoch { + data: CredentialSpendingData { + embedded_private_attributes, + verify_credential_request: theta, + public_attributes_plain, + typ, + }, + epoch_id, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_credentials::coconut::bandwidth::bandwidth_credential_params; + use nym_credentials::IssuanceBandwidthCredential; + use nym_credentials_interface::{blind_sign, hash_to_scalar}; + + #[test] + fn credential_roundtrip() { + // make valid request + let params = bandwidth_credential_params(); + let keypair = nym_credentials_interface::keygen(params); + + let issuance = IssuanceBandwidthCredential::new_freepass(None); + let sig_req = issuance.prepare_for_signing(); + let pub_attrs_hashed = sig_req + .public_attributes_plain + .iter() + .map(hash_to_scalar) + .collect::>(); + let pub_attrs = pub_attrs_hashed.iter().collect::>(); + let blind_sig = blind_sign( + params, + keypair.secret_key(), + &sig_req.blind_sign_request, + &pub_attrs, + ) + .unwrap(); + let sig = blind_sig + .unblind( + keypair.verification_key(), + &sig_req.pedersen_commitments_openings, + ) + .unwrap(); + + let issued = issuance.into_issued_credential(sig); + let spending = issued + .prepare_for_spending(keypair.verification_key()) + .unwrap(); + + let with_epoch = CredentialSpendingWithEpoch { + data: spending, + epoch_id: 42, + }; + + let bytes = with_epoch.to_bytes(); + let recovered = CredentialSpendingWithEpoch::try_from_bytes(&bytes).unwrap(); + + assert_eq!(with_epoch, recovered); } } diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 0c5542464e..67ccb148f7 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -1,4 +1,4 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::authentication::encrypted_address::EncryptedAddressBytes; @@ -8,6 +8,7 @@ use crate::registration::handshake::SharedKeys; use crate::{GatewayMacSize, PROTOCOL_VERSION}; use log::error; use nym_credentials::coconut::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{CoconutError, UnknownCredentialType}; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag; use nym_crypto::symmetric::stream_cipher; @@ -18,6 +19,7 @@ use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorit use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; +use std::string::FromUtf8Error; use thiserror::Error; use tungstenite::protocol::Message; @@ -102,6 +104,18 @@ pub enum GatewayRequestsError { #[from] source: MixPacketFormattingError, }, + + #[error("failed to deserialize provided credential: EOF")] + CredentialDeserializationFailureEOF, + + #[error("failed to deserialize provided credential: malformed string: {0}")] + CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), + + #[error("failed to deserialize provided credential: {0}")] + CredentialDeserializationFailureUnknownType(#[from] UnknownCredentialType), + + #[error("failed to deserialize provided credential: malformed verify request: {0}")] + CredentialDeserializationFailureMalformedTheta(CoconutError), } #[derive(Serialize, Deserialize, Debug)] diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index ca362dc721..6480b0a255 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3943,6 +3943,7 @@ dependencies = [ "bls12_381", "nym-coconut", "serde", + "thiserror", ] [[package]] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 8c3109210f..e9cdff7476 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3198,6 +3198,7 @@ dependencies = [ "bls12_381", "nym-coconut", "serde", + "thiserror", ] [[package]]