From 37a75ec6e74a1c355efda7883c06de13d625aa07 Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Mon, 8 Jan 2024 13:19:21 -0500 Subject: [PATCH] Adds rpccookie to bitcoind auth methods Currently we are authenticating against bitcoind using user/pass. This adds the option to use a cookie file instead. --- teos/src/bitcoin_cli.rs | 32 +++++++++++++++++---- teos/src/conf_template.toml | 2 ++ teos/src/config.rs | 55 +++++++++++++++++++++++++++++++------ teos/src/main.rs | 16 ++++++++--- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/teos/src/bitcoin_cli.rs b/teos/src/bitcoin_cli.rs index 80b805c2..22b5338d 100644 --- a/teos/src/bitcoin_cli.rs +++ b/teos/src/bitcoin_cli.rs @@ -18,6 +18,7 @@ use bitcoin::base64; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::hex::ToHex; use bitcoin::{Block, Transaction}; +use bitcoincore_rpc::Auth; use lightning::util::ser::Writeable; use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::rpc::RpcClient; @@ -32,9 +33,9 @@ pub struct BitcoindClient<'a> { /// The port to connect to. port: u16, /// The RPC user `bitcoind` is configured with. - rpc_user: &'a str, + rpc_user: String, /// The RPC password for the given user. - rpc_password: &'a str, + rpc_password: String, } impl BlockSource for &BitcoindClient<'_> { @@ -74,12 +75,33 @@ impl<'a> BitcoindClient<'a> { pub async fn new( host: &'a str, port: u16, - rpc_user: &'a str, - rpc_password: &'a str, + auth: Auth, teos_network: &'a str, ) -> std::io::Result> { let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port); - let rpc_credentials = base64::encode(&format!("{rpc_user}:{rpc_password}")); + let (rpc_user, rpc_password) = { + let (user, pass) = auth.get_user_pass().map_err(|e| { + Error::new( + ErrorKind::InvalidInput, + format!("Cannot read cookie file. {}", e), + ) + })?; + if user.is_none() { + Err(Error::new( + ErrorKind::InvalidInput, + "Empty btc_rpc_user parsed from rpc_cookie".to_string(), + )) + } else if pass.is_none() { + Err(Error::new( + ErrorKind::InvalidInput, + "Empty btc_rpc_password parsed from rpc_cookie", + )) + } else { + Ok((user.unwrap(), pass.unwrap())) + } + }?; + + let rpc_credentials = base64::encode(&format!("{}:{}", rpc_user, rpc_password)); let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?; let client = Self { diff --git a/teos/src/conf_template.toml b/teos/src/conf_template.toml index 9540cb4a..6193b355 100644 --- a/teos/src/conf_template.toml +++ b/teos/src/conf_template.toml @@ -12,8 +12,10 @@ rpc_port = 8814 # bitcoind btc_network = "mainnet" btc_rpc_user = "CSW" +## Notice only user+password **OR** cookie is allowed as rpc auth, any other combination would be rejected btc_rpc_password = "NotSatoshi" btc_rpc_connect = "localhost" +btc_rpc_cookie = "~/.bitcoin/.cookie" btc_rpc_port = 8332 # Flags diff --git a/teos/src/config.rs b/teos/src/config.rs index 5436725f..109dd91b 100644 --- a/teos/src/config.rs +++ b/teos/src/config.rs @@ -41,6 +41,14 @@ impl std::fmt::Display for ConfigError { impl std::error::Error for ConfigError {} +#[derive(PartialEq)] +pub enum AuthMethod { + UserPass, + CookieFile, + Multiple, + Invalid, +} + /// Holds all the command line options. #[derive(StructOpt, Debug, Clone)] #[structopt(rename_all = "lowercase")] @@ -66,14 +74,18 @@ pub struct Opt { #[structopt(long)] pub btc_network: Option, - /// bitcoind rpcuser [default: user] + /// bitcoind rpcuser #[structopt(long)] pub btc_rpc_user: Option, - /// bitcoind rpcpassword [default: passwd] + /// bitcoind rpcpassword #[structopt(long)] pub btc_rpc_password: Option, + /// bitcoind rpccookie + #[structopt(long)] + pub btc_rpc_cookie: Option, + /// bitcoind rpcconnect [default: localhost] #[structopt(long)] pub btc_rpc_connect: Option, @@ -136,6 +148,7 @@ pub struct Config { // Bitcoind pub btc_network: String, pub btc_rpc_user: String, + pub btc_rpc_cookie: String, pub btc_rpc_password: String, pub btc_rpc_connect: String, pub btc_rpc_port: u16, @@ -164,6 +177,24 @@ pub struct Config { } impl Config { + /// The only combinations of valid authentication methods are: + /// - User **AND** password + /// - **OR** Cookie file + // + /// Any other combination will be rejected + pub fn get_auth_method(&self) -> AuthMethod { + match ( + self.btc_rpc_user.is_empty(), + self.btc_rpc_password.is_empty(), + self.btc_rpc_cookie.is_empty(), + ) { + (false, false, true) => AuthMethod::UserPass, + (true, true, false) => AuthMethod::CookieFile, + (true, true, true) => AuthMethod::Invalid, + _ => AuthMethod::Multiple, + } + } + /// Patches the configuration options with the command line options. pub fn patch_with_options(&mut self, options: Opt) { if options.api_bind.is_some() { @@ -187,6 +218,9 @@ impl Config { if options.btc_rpc_password.is_some() { self.btc_rpc_password = options.btc_rpc_password.unwrap(); } + if options.btc_rpc_cookie.is_some() { + self.btc_rpc_cookie = options.btc_rpc_cookie.unwrap(); + } if options.btc_rpc_connect.is_some() { self.btc_rpc_connect = options.btc_rpc_connect.unwrap(); } @@ -216,11 +250,14 @@ impl Config { /// This will also assign the default `btc_rpc_port` depending on the network if it has not /// been overwritten at this point. pub fn verify(&mut self) -> Result<(), ConfigError> { - if self.btc_rpc_user == String::new() { - return Err(ConfigError("btc_rpc_user must be set".to_owned())); - } - if self.btc_rpc_password == String::new() { - return Err(ConfigError("btc_rpc_password must be set".to_owned())); + let auth_method = self.get_auth_method(); + if auth_method == AuthMethod::Invalid { + return Err(ConfigError("No valid bitcoind auth provided. Set either both btc_rpc_user/btc_rpc_password or btc_rpc_cookie".to_owned())); + } else if auth_method == AuthMethod::Multiple { + return Err(ConfigError( + "Multiple bitcoind auth provided. Pick a single one (either btc_rpc_user/btc_rpc_password or btc_rpc_cookie)" + .to_owned(), + )); } // Normalize the network option to the ones used by bitcoind. @@ -291,6 +328,7 @@ impl Default for Config { btc_network: "mainnet".into(), btc_rpc_user: String::new(), btc_rpc_password: String::new(), + btc_rpc_cookie: String::new(), btc_rpc_connect: "localhost".into(), btc_rpc_port: 0, @@ -326,6 +364,7 @@ mod tests { btc_network: None, btc_rpc_user: None, btc_rpc_password: None, + btc_rpc_cookie: None, btc_rpc_connect: None, btc_rpc_port: None, data_dir: String::from("~/.teos"), @@ -363,7 +402,7 @@ mod tests { // required to be updated by the user. let mut config = Config::default(); assert!( - matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_rpc_user must be set")) + matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided")) ); } diff --git a/teos/src/main.rs b/teos/src/main.rs index bdd30e06..8cc53c08 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -23,7 +23,7 @@ use teos::api::{http, tor::TorAPI}; use teos::bitcoin_cli::BitcoindClient; use teos::carrier::Carrier; use teos::chain_monitor::ChainMonitor; -use teos::config::{self, Config, Opt}; +use teos::config::{self, AuthMethod, Config, Opt}; use teos::dbm::DBM; use teos::gatekeeper::Gatekeeper; use teos::protos as msgs; @@ -142,12 +142,20 @@ async fn main() { }; log::info!("tower_id: {tower_pk}"); + let btc_rpc_auth = match conf.get_auth_method() { + AuthMethod::CookieFile => { + Auth::CookieFile(config::data_dir_absolute_path(conf.btc_rpc_cookie)) + } + AuthMethod::UserPass => Auth::UserPass(conf.btc_rpc_user, conf.btc_rpc_password), + // Notice an invalid conf would have failed on `Config::verify()` + _ => unreachable!("A verified conf will only have one of these two auth methods"), + }; + // Initialize our bitcoind client let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new( &conf.btc_rpc_connect, conf.btc_rpc_port, - &conf.btc_rpc_user, - &conf.btc_rpc_password, + btc_rpc_auth.clone(), &conf.btc_network, ) .await @@ -176,7 +184,7 @@ async fn main() { let rpc = Arc::new( Client::new( &format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port), - Auth::UserPass(conf.btc_rpc_user.clone(), conf.btc_rpc_password.clone()), + btc_rpc_auth, ) .unwrap(), );