From c5e9c3fbb91c95753b9b4a6065165533ccb4bde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 9 Feb 2024 09:59:54 +0000 Subject: [PATCH] reintroduced handling of old v1 credentials --- .../client-libs/gateway-client/src/client.rs | 6 +- gateway/gateway-requests/src/models.rs | 155 +++++++++++++++++- gateway/gateway-requests/src/types.rs | 39 ++++- .../connection_handler/authenticated.rs | 73 ++++++--- .../websocket/connection_handler/fresh.rs | 4 +- 5 files changed, 247 insertions(+), 30 deletions(-) diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index b1ebb23fa4..e0cb06215e 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -384,7 +384,7 @@ impl GatewayClient { // note: in +1.2.0 we will have to return a hard error here Ok(()) } - Some(v) if v != PROTOCOL_VERSION => { + Some(v) if v > PROTOCOL_VERSION => { let err = GatewayClientError::IncompatibleProtocol { gateway: Some(v), current: PROTOCOL_VERSION, @@ -394,7 +394,7 @@ impl GatewayClient { } Some(_) => { - info!("the gateway is using exactly the same protocol version as we are. We're good to continue!"); + info!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); Ok(()) } } @@ -522,7 +522,7 @@ impl GatewayClient { let mut rng = OsRng; let iv = IV::new_random(&mut rng); - let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential( + let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2( credential, epoch_id, self.shared_key.as_ref().unwrap(), diff --git a/gateway/gateway-requests/src/models.rs b/gateway/gateway-requests/src/models.rs index 6e06aabc7e..0bef6c4084 100644 --- a/gateway/gateway-requests/src/models.rs +++ b/gateway/gateway-requests/src/models.rs @@ -6,6 +6,106 @@ use nym_credentials::coconut::bandwidth::CredentialSpendingData; use nym_credentials_interface::{CoconutError, VerifyCredentialRequest}; use serde::{Deserialize, Serialize}; +// reimplements old coconut-interface::Credential for backwards compatibility sake +// (so that 'new' gateways could still understand those requests) +#[derive(Debug, PartialEq, Eq)] +pub struct OldV1Credential { + pub n_params: u32, + + pub theta: VerifyCredentialRequest, + + pub voucher_value: u64, + + pub voucher_info: String, + + pub epoch_id: u64, +} + +// attempt to convert the old request type into the new variant +impl TryFrom for CredentialSpendingWithEpoch { + type Error = GatewayRequestsError; + + fn try_from(value: OldV1Credential) -> Result { + if value.n_params <= 2 { + return Err(GatewayRequestsError::InvalidNumberOfEmbededParameters( + value.n_params, + )); + } + let embedded_private_attributes = value.n_params as usize - 2; + let typ = value.voucher_info.parse()?; + let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info]; + + Ok(CredentialSpendingWithEpoch { + data: CredentialSpendingData { + embedded_private_attributes, + verify_credential_request: value.theta, + public_attributes_plain, + typ, + }, + epoch_id: value.epoch_id, + }) + } +} + +impl OldV1Credential { + #[cfg(test)] + pub fn as_bytes(&self) -> Vec { + let n_params_bytes = self.n_params.to_be_bytes(); + let theta_bytes = self.theta.to_bytes(); + let theta_bytes_len = theta_bytes.len(); + let voucher_value_bytes = self.voucher_value.to_be_bytes(); + let epoch_id_bytes = self.epoch_id.to_be_bytes(); + let voucher_info_bytes = self.voucher_info.as_bytes(); + let voucher_info_len = voucher_info_bytes.len(); + + let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len); + bytes.extend_from_slice(&n_params_bytes); + bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes()); + bytes.extend_from_slice(&theta_bytes); + bytes.extend_from_slice(&voucher_value_bytes); + bytes.extend_from_slice(&epoch_id_bytes); + bytes.extend_from_slice(voucher_info_bytes); + + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 28 { + return Err(CoconutError::Deserialization(String::from( + "To few bytes in credential", + ))); + } + let mut four_byte = [0u8; 4]; + let mut eight_byte = [0u8; 8]; + + four_byte.copy_from_slice(&bytes[..4]); + let n_params = u32::from_be_bytes(four_byte); + eight_byte.copy_from_slice(&bytes[4..12]); + let theta_len = u64::from_be_bytes(eight_byte); + if bytes.len() < 28 + theta_len as usize { + return Err(CoconutError::Deserialization(String::from( + "To few bytes in credential", + ))); + } + let theta = VerifyCredentialRequest::from_bytes(&bytes[12..12 + theta_len as usize]) + .map_err(|e| CoconutError::Deserialization(e.to_string()))?; + eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]); + let voucher_value = u64::from_be_bytes(eight_byte); + eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]); + let epoch_id = u64::from_be_bytes(eight_byte); + let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec()) + .map_err(|e| CoconutError::Deserialization(e.to_string()))?; + + Ok(OldV1Credential { + n_params, + theta, + voucher_value, + voucher_info, + epoch_id, + }) + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CredentialSpendingWithEpoch { /// The cryptographic material required for spending the underlying credential. @@ -145,7 +245,60 @@ 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}; + use nym_credentials_interface::{ + blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters, + Signature, VerificationKey, + }; + + #[test] + fn old_v1_coconut_credential_roundtrip() { + let voucher_value = 1000000u64; + let voucher_info = String::from("BandwidthVoucher"); + let serial_number = + Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap(); + let binding_number = + Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap(); + let signature = Signature::try_from_bs58( + "ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\ + 7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\ + ZR9j", + ) + .unwrap(); + let params = Parameters::new(4).unwrap(); + let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\ + woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\ + FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\ + JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\ + UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\ + wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\ + 1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\ + nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\ + ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\ + 6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\ + Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y") + .unwrap(); + let theta = prove_bandwidth_credential( + ¶ms, + &verification_key, + &signature, + &serial_number, + &binding_number, + ) + .unwrap(); + + let credential = OldV1Credential { + n_params: 4, + theta, + voucher_value, + voucher_info, + epoch_id: 42, + }; + + let serialized_credential = credential.as_bytes(); + let deserialized_credential = OldV1Credential::from_bytes(&serialized_credential).unwrap(); + + assert_eq!(credential, deserialized_credential); + } #[test] fn credential_roundtrip() { diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 67ccb148f7..e7e402c37c 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -3,7 +3,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::IV; -use crate::models::CredentialSpendingWithEpoch; +use crate::models::{CredentialSpendingWithEpoch, OldV1Credential}; use crate::registration::handshake::SharedKeys; use crate::{GatewayMacSize, PROTOCOL_VERSION}; use log::error; @@ -116,6 +116,9 @@ pub enum GatewayRequestsError { #[error("failed to deserialize provided credential: malformed verify request: {0}")] CredentialDeserializationFailureMalformedTheta(CoconutError), + + #[error("the provided [v1] credential has invalid number of parameters - {0}")] + InvalidNumberOfEmbededParameters(u32), } #[derive(Serialize, Deserialize, Debug)] @@ -140,6 +143,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + BandwidthCredentialV2 { + enc_credential: Vec, + iv: Vec, + }, ClaimFreeTestnetBandwidth, } @@ -157,7 +164,31 @@ impl ClientControlRequest { } } - pub fn new_enc_coconut_bandwidth_credential( + pub fn new_enc_coconut_bandwidth_credential_v1( + credential: &OldV1Credential, + shared_key: &SharedKeys, + iv: IV, + ) -> Self { + let serialized_credential = credential.as_bytes(); + let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); + + ClientControlRequest::BandwidthCredential { + enc_credential, + iv: iv.to_bytes(), + } + } + + pub fn try_from_enc_coconut_bandwidth_credential_v1( + enc_credential: Vec, + shared_key: &SharedKeys, + iv: IV, + ) -> Result { + let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; + OldV1Credential::from_bytes(&credential_bytes) + .map_err(|_| GatewayRequestsError::MalformedEncryption) + } + + pub fn new_enc_coconut_bandwidth_credential_v2( credential: CredentialSpendingData, epoch_id: u64, shared_key: &SharedKeys, @@ -167,13 +198,13 @@ impl ClientControlRequest { let serialized_credential = cred.to_bytes(); let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - ClientControlRequest::BandwidthCredential { + ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv: iv.to_bytes(), } } - pub fn try_from_enc_coconut_bandwidth_credential( + pub fn try_from_enc_coconut_bandwidth_credential_v2( enc_credential: Vec, shared_key: &SharedKeys, iv: IV, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 3c3ff21fa4..0f4bd0d7d6 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -22,6 +22,7 @@ use futures::{ use log::*; use nym_credentials::coconut::bandwidth::{bandwidth_credential_params, CredentialType}; use nym_credentials_interface::CoconutError; +use nym_gateway_requests::models::CredentialSpendingWithEpoch; use nym_gateway_requests::{ iv::{IVConversionError, IV}, types::{BinaryRequest, ServerResponse}, @@ -82,6 +83,12 @@ pub(crate) enum RequestHandlingError { #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), + + #[error("the provided credential did not contain a valid type attribute")] + InvalidTypeAttribute, + + #[error("the provided credential did not have a bandwidth attribute")] + MissingBandwidthAttribute, } impl RequestHandlingError { @@ -211,25 +218,10 @@ where } } - /// Tries to handle the received bandwidth request by checking correctness of the received data - /// and if successful, increases client's bandwidth by an appropriate amount. - /// - /// # Arguments - /// - /// * `enc_credential`: raw encrypted bandwidth credential to verify. - /// * `iv`: fresh iv used for the credential. - async fn handle_bandwidth( + async fn handle_bandwidth_request( &mut self, - enc_credential: Vec, - iv: Vec, + credential: CredentialSpendingWithEpoch, ) -> Result { - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential( - enc_credential, - &self.client.shared_keys, - iv, - )?; - let aggregated_verification_key = self .inner .coconut_verifier @@ -237,11 +229,11 @@ where .await?; if !credential.data.validate_type_attribute() { - unimplemented!() + return Err(RequestHandlingError::InvalidTypeAttribute); } let Some(bandwidth_attribute) = credential.data.get_bandwidth_attribute() else { - unimplemented!() + return Err(RequestHandlingError::MissingBandwidthAttribute); }; // this will extract token amounts out of bandwidth vouchers and validate expiry of free passes @@ -279,6 +271,43 @@ where Ok(ServerResponse::Bandwidth { available_total }) } + async fn handle_bandwidth_v1( + &mut self, + enc_credential: Vec, + iv: Vec, + ) -> Result { + let iv = IV::try_from_bytes(&iv)?; + let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v1( + enc_credential, + &self.client.shared_keys, + iv, + )?; + + self.handle_bandwidth_request(credential.try_into()?).await + } + + /// Tries to handle the received bandwidth request by checking correctness of the received data + /// and if successful, increases client's bandwidth by an appropriate amount. + /// + /// # Arguments + /// + /// * `enc_credential`: raw encrypted bandwidth credential to verify. + /// * `iv`: fresh iv used for the credential. + async fn handle_bandwidth_v2( + &mut self, + enc_credential: Vec, + iv: Vec, + ) -> Result { + let iv = IV::try_from_bytes(&iv)?; + let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v2( + enc_credential, + &self.client.shared_keys, + iv, + )?; + + self.handle_bandwidth_request(credential).await + } + async fn handle_claim_testnet_bandwidth( &mut self, ) -> Result { @@ -357,7 +386,11 @@ where Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { ClientControlRequest::BandwidthCredential { enc_credential, iv } => self - .handle_bandwidth(enc_credential, iv) + .handle_bandwidth_v1(enc_credential, iv) + .await + .into_ws_message(), + ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv } => self + .handle_bandwidth_v2(enc_credential, iv) .await .into_ws_message(), ClientControlRequest::ClaimFreeTestnetBandwidth => self diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 2d08c85709..ee1395e8a8 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -366,7 +366,7 @@ where // note: in +1.2.0 we will have to return a hard error here Ok(()) } - Some(v) if v != PROTOCOL_VERSION => { + Some(v) if v > PROTOCOL_VERSION => { let err = InitialAuthenticationError::IncompatibleProtocol { client: Some(v), current: PROTOCOL_VERSION, @@ -376,7 +376,7 @@ where } Some(_) => { - info!("the client is using exactly the same protocol version as we are. We're good to continue!"); + info!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!"); Ok(()) } }