Skip to content

Commit

Permalink
feat(node grpc): add grpc authentication to the node (#5928)
Browse files Browse the repository at this point in the history
Description
---
Adds basic auth to the minotari node.
Adds support on the Miner, and MergeMiner Proxy for node basic auth
credentials.

Motivation and Context
---
Make things just that much more secure.

How Has This Been Tested?
---
Locally running a node, wallet, and miner. (MergeMiner untested).

Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify
  • Loading branch information
brianp committed Nov 13, 2023
1 parent 55bbdf2 commit 3d95e8c
Show file tree
Hide file tree
Showing 16 changed files with 215 additions and 54 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions applications/minotari_merge_mining_proxy/Cargo.toml
Expand Up @@ -16,8 +16,9 @@ tari_comms = { path = "../../comms/core" }
tari_core = { path = "../../base_layer/core", default-features = false, features = ["transactions"] }
minotari_app_utilities = { path = "../minotari_app_utilities" }
tari_utilities = { version = "0.6" }
minotari_node_grpc_client = {path="../../clients/rust/base_node_grpc_client" }
minotari_wallet_grpc_client = {path="../../clients/rust/wallet_grpc_client" }
minotari_node_grpc_client = { path = "../../clients/rust/base_node_grpc_client" }
minotari_wallet_grpc_client = { path = "../../clients/rust/wallet_grpc_client" }
minotari_app_grpc = { path = "../minotari_app_grpc" }

anyhow = "1.0.53"
crossterm = { version = "0.25.0" }
Expand Down
Expand Up @@ -25,9 +25,13 @@
use std::{cmp, sync::Arc};

use log::*;
use minotari_node_grpc_client::{grpc, BaseNodeGrpcClient};
use minotari_wallet_grpc_client::WalletGrpcClient;
use minotari_app_grpc::{
authentication::ClientAuthenticationInterceptor,
tari_rpc::{base_node_client::BaseNodeClient, wallet_client::WalletClient},
};
use minotari_node_grpc_client::grpc;
use tari_core::proof_of_work::{monero_rx, monero_rx::FixedByteArray, Difficulty};
use tonic::{codegen::InterceptedService, transport::Channel};

use crate::{
block_template_data::{BlockTemplateData, BlockTemplateDataBuilder},
Expand All @@ -41,14 +45,14 @@ const LOG_TARGET: &str = "minotari_mm_proxy::proxy::block_template_protocol";
/// Structure holding grpc connections.
pub struct BlockTemplateProtocol<'a> {
config: Arc<MergeMiningProxyConfig>,
base_node_client: &'a mut BaseNodeGrpcClient<tonic::transport::Channel>,
wallet_client: &'a mut WalletGrpcClient<tonic::transport::Channel>,
base_node_client: &'a mut BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
wallet_client: &'a mut WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
}

impl<'a> BlockTemplateProtocol<'a> {
pub fn new(
base_node_client: &'a mut BaseNodeGrpcClient<tonic::transport::Channel>,
wallet_client: &'a mut WalletGrpcClient<tonic::transport::Channel>,
base_node_client: &'a mut BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
wallet_client: &'a mut WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
config: Arc<MergeMiningProxyConfig>,
) -> Self {
Self {
Expand Down
3 changes: 3 additions & 0 deletions applications/minotari_merge_mining_proxy/src/config.rs
Expand Up @@ -43,6 +43,8 @@ pub struct MergeMiningProxyConfig {
pub monerod_use_auth: bool,
/// The Minotari base node's GRPC address
pub base_node_grpc_address: Option<Multiaddr>,
/// GRPC authentication for base node
pub base_node_grpc_authentication: GrpcAuthentication,
/// The Minotari wallet's GRPC address
pub console_wallet_grpc_address: Option<Multiaddr>,
/// GRPC authentication for console wallet
Expand Down Expand Up @@ -80,6 +82,7 @@ impl Default for MergeMiningProxyConfig {
monerod_password: String::new(),
monerod_use_auth: false,
base_node_grpc_address: None,
base_node_grpc_authentication: GrpcAuthentication::default(),
console_wallet_grpc_address: None,
console_wallet_grpc_authentication: GrpcAuthentication::default(),
listener_address: "/ip4/127.0.0.1/tcp/18081".parse().unwrap(),
Expand Down
13 changes: 7 additions & 6 deletions applications/minotari_merge_mining_proxy/src/proxy.rs
Expand Up @@ -39,8 +39,8 @@ use bytes::Bytes;
use hyper::{header::HeaderValue, service::Service, Body, Method, Request, Response, StatusCode, Uri};
use json::json;
use jsonrpc::error::StandardError;
use minotari_node_grpc_client::{grpc, BaseNodeGrpcClient};
use minotari_wallet_grpc_client::WalletGrpcClient;
use minotari_node_grpc_client::{grpc, grpc::base_node_client::BaseNodeClient};
use minotari_wallet_grpc_client::{grpc::wallet_client::WalletClient, ClientAuthenticationInterceptor};
use reqwest::{ResponseBuilderExt, Url};
use serde_json as json;
use tari_core::proof_of_work::{
Expand All @@ -50,6 +50,7 @@ use tari_core::proof_of_work::{
randomx_factory::RandomXFactory,
};
use tari_utilities::hex::Hex;
use tonic::{codegen::InterceptedService, transport::Channel};
use tracing::{debug, error, info, instrument, trace, warn};

use crate::{
Expand All @@ -75,8 +76,8 @@ impl MergeMiningProxyService {
pub fn new(
config: MergeMiningProxyConfig,
http_client: reqwest::Client,
base_node_client: BaseNodeGrpcClient<tonic::transport::Channel>,
wallet_client: WalletGrpcClient<tonic::transport::Channel>,
base_node_client: BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
wallet_client: WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
block_templates: BlockTemplateRepository,
randomx_factory: RandomXFactory,
) -> Self {
Expand Down Expand Up @@ -157,8 +158,8 @@ struct InnerService {
config: Arc<MergeMiningProxyConfig>,
block_templates: BlockTemplateRepository,
http_client: reqwest::Client,
base_node_client: BaseNodeGrpcClient<tonic::transport::Channel>,
wallet_client: WalletGrpcClient<tonic::transport::Channel>,
base_node_client: BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
wallet_client: WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>,
initial_sync_achieved: Arc<AtomicBool>,
current_monerod_server: Arc<RwLock<Option<String>>>,
last_assigned_monerod_server: Arc<RwLock<Option<String>>>,
Expand Down
76 changes: 54 additions & 22 deletions applications/minotari_merge_mining_proxy/src/run_merge_miner.rs
Expand Up @@ -20,13 +20,13 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use std::convert::Infallible;
use std::{convert::Infallible, str::FromStr};

use futures::future;
use hyper::{service::make_service_fn, Server};
use log::*;
use minotari_node_grpc_client::BaseNodeGrpcClient;
use minotari_wallet_grpc_client::WalletGrpcClient;
use minotari_node_grpc_client::grpc::base_node_client::BaseNodeClient;
use minotari_wallet_grpc_client::{grpc::wallet_client::WalletClient, ClientAuthenticationInterceptor};
use tari_common::{
configuration::bootstrap::{grpc_default_port, ApplicationType},
load_configuration,
Expand All @@ -35,6 +35,10 @@ use tari_common::{
use tari_comms::utils::multiaddr::multiaddr_to_socketaddr;
use tari_core::proof_of_work::randomx_factory::RandomXFactory;
use tokio::time::Duration;
use tonic::{
codegen::InterceptedService,
transport::{Channel, Endpoint},
};

use crate::{
block_template_data::BlockTemplateRepository,
Expand All @@ -59,25 +63,9 @@ pub async fn start_merge_miner(cli: Cli) -> Result<(), anyhow::Error> {
.build()
.map_err(MmProxyError::ReqwestError)?;

let base_node = multiaddr_to_socketaddr(
config
.base_node_grpc_address
.as_ref()
.expect("No base node address provided"),
)?;
info!(target: LOG_TARGET, "Connecting to base node at {}", base_node);
println!("Connecting to base node at {}", base_node);
let base_node_client = BaseNodeGrpcClient::connect(format!("http://{}", base_node)).await?;
let wallet_addr = multiaddr_to_socketaddr(
config
.console_wallet_grpc_address
.as_ref()
.expect("No waller address provided"),
)?;
info!(target: LOG_TARGET, "Connecting to wallet at {}", wallet_addr);
let wallet_addr = format!("http://{}", wallet_addr);
let wallet_client =
WalletGrpcClient::connect_with_auth(&wallet_addr, &config.console_wallet_grpc_authentication).await?;
let base_node_client = connect_base_node(&config).await?;
let wallet_client = connect_wallet(&config).await?;

let listen_addr = multiaddr_to_socketaddr(&config.listener_address)?;
let randomx_factory = RandomXFactory::new(config.max_randomx_vms);
let randomx_service = MergeMiningProxyService::new(
Expand Down Expand Up @@ -110,6 +98,50 @@ pub async fn start_merge_miner(cli: Cli) -> Result<(), anyhow::Error> {
}
}

async fn connect_wallet(
config: &MergeMiningProxyConfig,
) -> Result<WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>, MmProxyError> {
let wallet_addr = format!(
"http://{}",
multiaddr_to_socketaddr(
&config
.console_wallet_grpc_address
.clone()
.expect("Wallet grpc address not found")
)?
);
info!(target: LOG_TARGET, "👛 Connecting to wallet at {}", wallet_addr);
let channel = Endpoint::from_str(&wallet_addr)?.connect().await?;
let wallet_conn = WalletClient::with_interceptor(
channel,
ClientAuthenticationInterceptor::create(&config.console_wallet_grpc_authentication)?,
);

Ok(wallet_conn)
}

async fn connect_base_node(
config: &MergeMiningProxyConfig,
) -> Result<BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>, MmProxyError> {
let base_node_addr = format!(
"http://{}",
multiaddr_to_socketaddr(
&config
.base_node_grpc_address
.clone()
.expect("Base node grpc address not found")
)?
);
info!(target: LOG_TARGET, "👛 Connecting to base node at {}", base_node_addr);
let channel = Endpoint::from_str(&base_node_addr)?.connect().await?;
let node_conn = BaseNodeClient::with_interceptor(
channel,
ClientAuthenticationInterceptor::create(&config.base_node_grpc_authentication)?,
);

Ok(node_conn)
}

fn setup_grpc_config(config: &mut MergeMiningProxyConfig) {
if config.base_node_grpc_address.is_none() {
config.base_node_grpc_address = Some(
Expand Down
3 changes: 3 additions & 0 deletions applications/minotari_miner/src/config.rs
Expand Up @@ -49,6 +49,8 @@ use tari_comms::multiaddr::Multiaddr;
pub struct MinerConfig {
/// GRPC address of base node
pub base_node_grpc_address: Option<Multiaddr>,
/// GRPC authentication for base node
pub base_node_grpc_authentication: GrpcAuthentication,
/// GRPC address of console wallet
pub wallet_grpc_address: Option<Multiaddr>,
/// GRPC authentication for console wallet
Expand Down Expand Up @@ -97,6 +99,7 @@ impl Default for MinerConfig {
fn default() -> Self {
Self {
base_node_grpc_address: None,
base_node_grpc_authentication: GrpcAuthentication::default(),
wallet_grpc_address: None,
wallet_grpc_authentication: GrpcAuthentication::default(),
num_mining_threads: num_cpus::get(),
Expand Down
49 changes: 35 additions & 14 deletions applications/minotari_miner/src/run_miner.rs
Expand Up @@ -57,6 +57,7 @@ pub const LOG_TARGET: &str = "minotari::miner::main";
pub const LOG_TARGET_FILE: &str = "minotari::logging::miner::main";

type WalletGrpcClient = WalletClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>;
type BaseNodeGrpcClient = BaseNodeClient<InterceptedService<Channel, ClientAuthenticationInterceptor>>;

#[allow(clippy::too_many_lines)]
pub async fn start_miner(cli: Cli) -> Result<(), ExitError> {
Expand Down Expand Up @@ -167,18 +168,18 @@ pub async fn start_miner(cli: Cli) -> Result<(), ExitError> {
}
}

async fn connect(config: &MinerConfig) -> Result<(BaseNodeClient<Channel>, WalletGrpcClient), MinerError> {
let base_node_addr = format!(
"http://{}",
multiaddr_to_socketaddr(
&config
.base_node_grpc_address
.clone()
.expect("no base node grpc address found"),
)?
);
info!(target: LOG_TARGET, "🔗 Connecting to base node at {}", base_node_addr);
let node_conn = BaseNodeClient::connect(base_node_addr).await?;
async fn connect(config: &MinerConfig) -> Result<(BaseNodeGrpcClient, WalletGrpcClient), MinerError> {
let node_conn = match connect_base_node(config).await {
Ok(client) => client,
Err(e) => {
error!(target: LOG_TARGET, "Could not connect to base node");
error!(
target: LOG_TARGET,
"Is its grpc running? try running it with `--enable-grpc` or enable it in config"
);
return Err(e);
},
};

let wallet_conn = match connect_wallet(config).await {
Ok(client) => client,
Expand Down Expand Up @@ -215,8 +216,28 @@ async fn connect_wallet(config: &MinerConfig) -> Result<WalletGrpcClient, MinerE
Ok(wallet_conn)
}

async fn connect_base_node(config: &MinerConfig) -> Result<BaseNodeGrpcClient, MinerError> {
let base_node_addr = format!(
"http://{}",
multiaddr_to_socketaddr(
&config
.base_node_grpc_address
.clone()
.expect("Base node grpc address not found")
)?
);
info!(target: LOG_TARGET, "👛 Connecting to base node at {}", base_node_addr);
let channel = Endpoint::from_str(&base_node_addr)?.connect().await?;
let node_conn = BaseNodeClient::with_interceptor(
channel,
ClientAuthenticationInterceptor::create(&config.base_node_grpc_authentication)?,
);

Ok(node_conn)
}

async fn mining_cycle(
node_conn: &mut BaseNodeClient<Channel>,
node_conn: &mut BaseNodeGrpcClient,
wallet_conn: &mut WalletGrpcClient,
config: &MinerConfig,
cli: &Cli,
Expand Down Expand Up @@ -336,7 +357,7 @@ pub async fn display_report(report: &MiningReport, num_mining_threads: usize) {

/// If config
async fn validate_tip(
node_conn: &mut BaseNodeClient<Channel>,
node_conn: &mut BaseNodeGrpcClient,
height: u64,
mine_until_height: Option<u64>,
) -> Result<(), MinerError> {
Expand Down
2 changes: 2 additions & 0 deletions applications/minotari_node/src/cli.rs
Expand Up @@ -45,6 +45,8 @@ pub struct Cli {
pub watch: Option<String>,
#[clap(long, alias = "profile")]
pub profile_with_tokio_console: bool,
#[clap(long, env = "MINOTARI_NODE_ENABLE_GRPC", alias = "enable-grpc")]
pub grpc_enabled: bool,
}

impl ConfigOverrideProvider for Cli {
Expand Down
@@ -0,0 +1,69 @@
// Copyright 2023, The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use anyhow::{anyhow, Error};
use async_trait::async_trait;
use clap::Parser;
use minotari_app_grpc::authentication::salted_password::create_salted_hashed_password;

use super::{CommandContext, HandleCommand};

/// Hashes the GRPC authentication password from the config and returns an argon2 hash
#[derive(Debug, Parser)]
pub struct Args {}

#[async_trait]
impl HandleCommand<Args> for CommandContext {
async fn handle_command(&mut self, _: Args) -> Result<(), Error> {
self.hash_grpc_password().await
}
}

impl CommandContext {
pub async fn hash_grpc_password(&mut self) -> Result<(), Error> {
match self
.config
.base_node
.grpc_authentication
.username_password()
.ok_or_else(|| anyhow!("GRPC basic auth is not configured"))
{
Ok((username, password)) => {
match create_salted_hashed_password(password.reveal()).map_err(|e| anyhow!(e.to_string())) {
Ok(hashed_password) => {
println!("Your hashed password is:");
println!("{}", *hashed_password);
println!();
println!(
"Use HTTP basic auth with username '{}' and the hashed password to make GRPC requests",
username
);
},
Err(e) => eprintln!("HashGrpcPassword error! {}", e),
}
},
Err(e) => eprintln!("HashGrpcPassword error! {}", e),
}

Ok(())
}
}

0 comments on commit 3d95e8c

Please sign in to comment.