From 00e73204ed03f70933882e004b5783e01d41fa68 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 +- .../transition/withdraw_from_identity.rs | 4 ++- packages/rs-sdk/tests/.env.example | 1 + packages/rs-sdk/tests/fetch/config.rs | 35 +++++++++++++++---- 13 files changed, 83 insertions(+), 22 deletions(-) diff --git a/packages/rs-dapi-client/src/dapi_client.rs b/packages/rs-dapi-client/src/dapi_client.rs index 4c29819e20..96436b35ef 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 7c900a7829..642c011fe3 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 91276fe4d1..139bbefe78 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 ); @@ -313,7 +325,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 291feb861b..2c3f5afdb7 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 aa58b63b32..f99df28ec5 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 79d8e87d5a..3e41e942da 100644 --- a/packages/rs-sdk/src/platform/transition/put_contract.rs +++ b/packages/rs-sdk/src/platform/transition/put_contract.rs @@ -57,7 +57,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 7c9fecac3a..e2a8ea6011 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 7ddaef7a68..806b5c4433 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 140a6e3166..7a5034b3ce 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 93da9aaf2b..f0224d01a0 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/withdraw_from_identity.rs b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs index 5e64ed1afb..af431f13aa 100644 --- a/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs +++ b/packages/rs-sdk/src/platform/transition/withdraw_from_identity.rs @@ -46,7 +46,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/.env.example b/packages/rs-sdk/tests/.env.example index 23b48e84df..7e0174dd8b 100644 --- a/packages/rs-sdk/tests/.env.example +++ b/packages/rs-sdk/tests/.env.example @@ -3,6 +3,7 @@ DASH_SDK_PLATFORM_HOST="127.0.0.1" DASH_SDK_PLATFORM_PORT=2443 DASH_SDK_PLATFORM_SSL=false +# DASH_SDK_PLATFORM_CA_CERT_PATH=/some/path/to/ca.pem DASH_SDK_CORE_PORT=20002 DASH_SDK_CORE_USER="someuser" diff --git a/packages/rs-sdk/tests/fetch/config.rs b/packages/rs-sdk/tests/fetch/config.rs index 839f3dec9d..43864a6db2 100644 --- a/packages/rs-sdk/tests/fetch/config.rs +++ b/packages/rs-sdk/tests/fetch/config.rs @@ -3,7 +3,11 @@ //! This module contains [Config] struct that can be used to configure dash-platform-sdk. //! It's mainly used for testing. -use dpp::prelude::Identifier; +use dash_sdk::RequestSettings; +use dpp::{ + dashcore::{hashes::Hash, ProTxHash}, + prelude::Identifier, +}; use rs_dapi_client::AddressList; use serde::Deserialize; use std::{path::PathBuf, str::FromStr}; @@ -44,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. @@ -161,16 +169,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 = { @@ -195,6 +215,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");