From 443fe05851ba83bb313025e2631799d0a3e3f0d1 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:52:34 +0200 Subject: [PATCH] feat(sdk): allow setting CA cert to use when connecting to servers --- packages/rs-dapi-client/src/dapi_client.rs | 2 ++ .../rs-dapi-client/src/request_settings.rs | 23 ++++++++++++-- packages/rs-dapi-client/src/transport/grpc.rs | 23 +++++++++++--- .../rs-dapi-client/tests/mock_dapi_client.rs | 5 +++- .../platform/transition/purchase_document.rs | 2 +- .../src/platform/transition/put_contract.rs | 2 +- .../src/platform/transition/put_document.rs | 2 +- .../src/platform/transition/put_settings.rs | 2 +- .../platform/transition/transfer_document.rs | 2 +- .../transition/update_price_of_document.rs | 2 +- .../rs-sdk/src/platform/transition/vote.rs | 8 +++-- .../transition/withdraw_from_identity.rs | 4 ++- packages/rs-sdk/tests/fetch/config.rs | 30 +++++++++++++++---- 13 files changed, 83 insertions(+), 24 deletions(-) diff --git a/packages/rs-dapi-client/src/dapi_client.rs b/packages/rs-dapi-client/src/dapi_client.rs index 4c29819e20d..96436b35eff 100644 --- a/packages/rs-dapi-client/src/dapi_client.rs +++ b/packages/rs-dapi-client/src/dapi_client.rs @@ -101,6 +101,7 @@ impl DapiRequestExecutor for DapiClient { // Join settings of different sources to get final version of the settings for this execution: let applied_settings = self .settings + .clone() .override_by(R::SETTINGS_OVERRIDES) .override_by(settings) .finalize(); @@ -122,6 +123,7 @@ impl DapiRequestExecutor for DapiClient { // Setup DAPI request execution routine future. It's a closure that will be called // more once to build new future on each retry. let routine = move || { + let applied_settings = applied_settings.clone(); // Try to get an address to initialize transport on: let address_list = self diff --git a/packages/rs-dapi-client/src/request_settings.rs b/packages/rs-dapi-client/src/request_settings.rs index 7c900a78290..642c011fe3f 100644 --- a/packages/rs-dapi-client/src/request_settings.rs +++ b/packages/rs-dapi-client/src/request_settings.rs @@ -1,6 +1,7 @@ //! DAPI client request settings processing. -use std::time::Duration; +use dapi_grpc::tonic::transport::Certificate; +use std::{fs::read_to_string, path::Path, time::Duration}; /// Default low-level client timeout const DEFAULT_CONNECT_TIMEOUT: Option = None; @@ -15,7 +16,7 @@ const DEFAULT_BAN_FAILED_ADDRESS: bool = true; /// 2. [crate::DapiClient] settings; /// 3. [crate::DapiRequest]-specific settings; /// 4. settings for an exact request execution call. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Default)] pub struct RequestSettings { /// Timeout for establishing a connection. pub connect_timeout: Option, @@ -25,6 +26,8 @@ pub struct RequestSettings { pub retries: Option, /// Ban DAPI address if node not responded or responded with error. pub ban_failed_address: Option, + /// Certificate Authority certificate to use for verifying the server's certificate. + pub ca_certificate: Option, } impl RequestSettings { @@ -36,6 +39,7 @@ impl RequestSettings { timeout: None, retries: None, ban_failed_address: None, + ca_certificate: None, } } @@ -48,6 +52,7 @@ impl RequestSettings { timeout: rhs.timeout.or(self.timeout), retries: rhs.retries.or(self.retries), ban_failed_address: rhs.ban_failed_address.or(self.ban_failed_address), + ca_certificate: rhs.ca_certificate.or(self.ca_certificate), } } @@ -60,12 +65,22 @@ impl RequestSettings { ban_failed_address: self .ban_failed_address .unwrap_or(DEFAULT_BAN_FAILED_ADDRESS), + ca_certificate: self.ca_certificate, } } + + /// Load a certificate from a file and set it as a CA certificate. + pub fn with_ca_certificate(mut self, path: impl AsRef) -> std::io::Result { + let cert_bytes = read_to_string(path)?; + let cert = Certificate::from_pem(cert_bytes); + + self.ca_certificate = Some(cert); + Ok(self) + } } /// DAPI settings ready to use. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct AppliedRequestSettings { /// Timeout for establishing a connection. pub connect_timeout: Option, @@ -75,4 +90,6 @@ pub struct AppliedRequestSettings { pub retries: usize, /// Ban DAPI address if node not responded or responded with error. pub ban_failed_address: bool, + /// Certificate Authority certificate to use for verifying the server's certificate. + pub ca_certificate: Option, } diff --git a/packages/rs-dapi-client/src/transport/grpc.rs b/packages/rs-dapi-client/src/transport/grpc.rs index 9309943818d..9ba452d090f 100644 --- a/packages/rs-dapi-client/src/transport/grpc.rs +++ b/packages/rs-dapi-client/src/transport/grpc.rs @@ -1,17 +1,17 @@ //! Listing of gRPC requests used in DAPI. -use std::time::Duration; - use super::{CanRetry, TransportClient, TransportRequest}; use crate::connection_pool::{ConnectionPool, PoolPrefix}; use crate::{request_settings::AppliedRequestSettings, RequestSettings}; use dapi_grpc::core::v0::core_client::CoreClient; use dapi_grpc::core::v0::{self as core_proto}; use dapi_grpc::platform::v0::{self as platform_proto, platform_client::PlatformClient}; +use dapi_grpc::tonic::transport::ClientTlsConfig; use dapi_grpc::tonic::Streaming; use dapi_grpc::tonic::{transport::Channel, IntoRequest}; use futures::{future::BoxFuture, FutureExt, TryFutureExt}; use http::Uri; +use std::time::Duration; /// Platform Client using gRPC transport. pub type PlatformGrpcClient = PlatformClient; @@ -19,12 +19,22 @@ pub type PlatformGrpcClient = PlatformClient; pub type CoreGrpcClient = CoreClient; fn create_channel(uri: Uri, settings: Option<&AppliedRequestSettings>) -> Channel { + let host = uri.host().expect("Failed to get host from URI").to_string(); + let mut builder = Channel::builder(uri); if let Some(settings) = settings { if let Some(timeout) = settings.connect_timeout { builder = builder.connect_timeout(timeout); } + if let Some(cert) = settings.ca_certificate.as_ref() { + let tls_config = ClientTlsConfig::new() + .domain_name(host) + .ca_certificate(cert.clone()); + builder = builder + .tls_config(tls_config) + .expect("Failed to set TLS config"); + } } builder.connect_lazy() @@ -188,7 +198,9 @@ impl_transport_request_grpc!( RequestSettings { timeout: Some(Duration::from_secs(80)), retries: Some(0), - ..RequestSettings::default() + ca_certificate: None, + ban_failed_address: None, + connect_timeout: None, }, wait_for_state_transition_result ); @@ -365,7 +377,10 @@ impl_transport_request_grpc!( CoreGrpcClient, RequestSettings { timeout: Some(STREAMING_TIMEOUT), - ..RequestSettings::default() + ca_certificate: None, + ban_failed_address: None, + connect_timeout: None, + retries: None, }, subscribe_to_transactions_with_proofs ); diff --git a/packages/rs-dapi-client/tests/mock_dapi_client.rs b/packages/rs-dapi-client/tests/mock_dapi_client.rs index 291feb861b4..2c3f5afdb74 100644 --- a/packages/rs-dapi-client/tests/mock_dapi_client.rs +++ b/packages/rs-dapi-client/tests/mock_dapi_client.rs @@ -23,7 +23,10 @@ async fn test_mock_get_identity_dapi_client() { let settings = RequestSettings::default(); - let result = dapi.execute(request.clone(), settings).await.unwrap(); + let result = dapi + .execute(request.clone(), settings.clone()) + .await + .unwrap(); let result2 = request.execute(&dapi, settings).await.unwrap(); diff --git a/packages/rs-sdk/src/platform/transition/purchase_document.rs b/packages/rs-sdk/src/platform/transition/purchase_document.rs index aa58b63b324..f99df28ec58 100644 --- a/packages/rs-sdk/src/platform/transition/purchase_document.rs +++ b/packages/rs-sdk/src/platform/transition/purchase_document.rs @@ -75,7 +75,7 @@ impl PurchaseDocument for Document { purchaser_id, document_type.data_contract_id(), true, - settings, + settings.clone(), ) .await?; diff --git a/packages/rs-sdk/src/platform/transition/put_contract.rs b/packages/rs-sdk/src/platform/transition/put_contract.rs index fb7e55b5bc0..a4b0f5058dd 100644 --- a/packages/rs-sdk/src/platform/transition/put_contract.rs +++ b/packages/rs-sdk/src/platform/transition/put_contract.rs @@ -59,7 +59,7 @@ impl PutContract for DataContract { settings: Option, ) -> Result { let new_identity_nonce = sdk - .get_identity_nonce(self.owner_id(), true, settings) + .get_identity_nonce(self.owner_id(), true, settings.clone()) .await?; let key_id = identity_public_key.id(); diff --git a/packages/rs-sdk/src/platform/transition/put_document.rs b/packages/rs-sdk/src/platform/transition/put_document.rs index 7c9fecac3a7..e2a8ea60113 100644 --- a/packages/rs-sdk/src/platform/transition/put_document.rs +++ b/packages/rs-sdk/src/platform/transition/put_document.rs @@ -70,7 +70,7 @@ impl PutDocument for Document { self.owner_id(), document_type.data_contract_id(), true, - settings, + settings.clone(), ) .await?; diff --git a/packages/rs-sdk/src/platform/transition/put_settings.rs b/packages/rs-sdk/src/platform/transition/put_settings.rs index 7ddaef7a687..806b5c4433b 100644 --- a/packages/rs-sdk/src/platform/transition/put_settings.rs +++ b/packages/rs-sdk/src/platform/transition/put_settings.rs @@ -2,7 +2,7 @@ use dpp::prelude::UserFeeIncrease; use rs_dapi_client::RequestSettings; /// The options when putting something to platform -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Default)] pub struct PutSettings { pub request_settings: RequestSettings, pub identity_nonce_stale_time_s: Option, diff --git a/packages/rs-sdk/src/platform/transition/transfer_document.rs b/packages/rs-sdk/src/platform/transition/transfer_document.rs index 140a6e31663..7a5034b3ce9 100644 --- a/packages/rs-sdk/src/platform/transition/transfer_document.rs +++ b/packages/rs-sdk/src/platform/transition/transfer_document.rs @@ -71,7 +71,7 @@ impl TransferDocument for Document { self.owner_id(), document_type.data_contract_id(), true, - settings, + settings.clone(), ) .await?; diff --git a/packages/rs-sdk/src/platform/transition/update_price_of_document.rs b/packages/rs-sdk/src/platform/transition/update_price_of_document.rs index 93da9aaf2b7..f0224d01a05 100644 --- a/packages/rs-sdk/src/platform/transition/update_price_of_document.rs +++ b/packages/rs-sdk/src/platform/transition/update_price_of_document.rs @@ -71,7 +71,7 @@ impl UpdatePriceOfDocument for Document { self.owner_id(), document_type.data_contract_id(), true, - settings, + settings.clone(), ) .await?; diff --git a/packages/rs-sdk/src/platform/transition/vote.rs b/packages/rs-sdk/src/platform/transition/vote.rs index 56864a760ca..7fc4be4e876 100644 --- a/packages/rs-sdk/src/platform/transition/vote.rs +++ b/packages/rs-sdk/src/platform/transition/vote.rs @@ -55,7 +55,7 @@ impl PutVote for Vote { let voting_identity_id = get_voting_identity_id(voter_pro_tx_hash, voting_public_key)?; let new_masternode_voting_nonce = sdk - .get_identity_nonce(voting_identity_id, true, settings) + .get_identity_nonce(voting_identity_id, true, settings.clone()) .await?; let settings = settings.unwrap_or_default(); @@ -87,7 +87,7 @@ impl PutVote for Vote { let voting_identity_id = get_voting_identity_id(voter_pro_tx_hash, voting_public_key)?; let new_masternode_voting_nonce = sdk - .get_identity_nonce(voting_identity_id, true, settings) + .get_identity_nonce(voting_identity_id, true, settings.clone()) .await?; let settings = settings.unwrap_or_default(); @@ -106,7 +106,9 @@ impl PutVote for Vote { )?; let request = masternode_vote_transition.broadcast_request_for_state_transition()?; - let response_result = request.execute(sdk, settings.request_settings).await; + let response_result = request + .execute(sdk, settings.request_settings.clone()) + .await; match response_result { Ok(_) => {} diff --git a/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs index 8e9020a5f3d..8df89d97ae9 100644 --- a/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs +++ b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs @@ -48,7 +48,9 @@ impl WithdrawFromIdentity for Identity { signer: S, settings: Option, ) -> Result { - let new_identity_nonce = sdk.get_identity_nonce(self.id(), true, settings).await?; + let new_identity_nonce = sdk + .get_identity_nonce(self.id(), true, settings.clone()) + .await?; let state_transition = IdentityCreditWithdrawalTransition::try_from_identity( self, CoreScript::new(address.script_pubkey()), diff --git a/packages/rs-sdk/tests/fetch/config.rs b/packages/rs-sdk/tests/fetch/config.rs index 27738cb552a..0930ec907c3 100644 --- a/packages/rs-sdk/tests/fetch/config.rs +++ b/packages/rs-sdk/tests/fetch/config.rs @@ -3,6 +3,7 @@ //! This module contains [Config] struct that can be used to configure dash-platform-sdk. //! It's mainly used for testing. +use dash_sdk::RequestSettings; use dpp::{ dashcore::{hashes::Hash, ProTxHash}, prelude::Identifier, @@ -47,6 +48,10 @@ pub struct Config { #[serde(default)] pub platform_ssl: bool, + /// When platform_ssl is true, use the PEM-encoded CA certificate from provided absolute path to verify the server + #[serde(default)] + pub platform_ca_cert_path: Option, + /// Directory where all generated test vectors will be saved. /// /// See [SdkBuilder::with_dump_dir()](crate::SdkBuilder::with_dump_dir()) for more details. @@ -171,16 +176,28 @@ impl Config { panic!("cannot use namespace with root dump dir"); } + let request_settings = self + .platform_ca_cert_path + .as_ref() + .map(|cert| { + RequestSettings::default() + .with_ca_certificate(cert) + .expect("failed to load CA certificate") + }) + .unwrap_or_default(); + // offline testing takes precedence over network testing #[cfg(all(feature = "network-testing", not(feature = "offline-testing")))] let sdk = { // Dump all traffic to disk - let builder = dash_sdk::SdkBuilder::new(self.address_list()).with_core( - &self.platform_host, - self.core_port, - &self.core_user, - &self.core_password, - ); + let builder = dash_sdk::SdkBuilder::new(self.address_list()) + .with_core( + &self.platform_host, + self.core_port, + &self.core_user, + &self.core_password, + ) + .with_settings(request_settings); #[cfg(feature = "generate-test-vectors")] let builder = { @@ -208,6 +225,7 @@ impl Config { #[cfg(feature = "offline-testing")] let sdk = { let mut mock_sdk = dash_sdk::SdkBuilder::new_mock() + .with_settings(request_settings) .build() .expect("initialize api");