From 8b49c181d953f7326aef144051544a938c9ae83a Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 30 Sep 2024 18:36:30 -0300 Subject: [PATCH] add a cookie auth system for the rpc endpoint --- Cargo.lock | 1 + zebra-rpc/Cargo.toml | 3 ++ zebra-rpc/src/methods.rs | 12 +++++ zebra-rpc/src/server.rs | 7 ++- zebra-rpc/src/server/cookie.rs | 46 +++++++++++++++++ .../src/server/http_request_compatibility.rs | 49 +++++++++++++++++-- 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 zebra-rpc/src/server/cookie.rs diff --git a/Cargo.lock b/Cargo.lock index 22f5d505038..c2abbfec8f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6164,6 +6164,7 @@ dependencies = [ name = "zebra-rpc" version = "1.0.0-beta.39" dependencies = [ + "base64 0.22.1", "chrono", "futures", "hex", diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index babae9123f1..2141b7a521b 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -68,6 +68,9 @@ jsonrpc-http-server = "18.0.0" serde_json = { version = "1.0.122", features = ["preserve_order"] } indexmap = { version = "2.3.0", features = ["serde"] } +# RPC endpoint basic auth +base64 = "0.22.1" + tokio = { version = "1.39.2", features = [ "time", "rt-multi-thread", diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 8becc5bb79c..2952daa897e 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -314,6 +314,10 @@ pub trait Rpc { /// tags: control #[rpc(name = "stop")] fn stop(&self) -> Result; + + #[rpc(name = "unauthenticated")] + /// A dummy RPC method that just returns a non-authenticated RPC error. + fn unauthenticated(&self) -> Result<()>; } /// RPC method implementations. @@ -1383,6 +1387,14 @@ where data: None, }) } + + fn unauthenticated(&self) -> Result<()> { + Err(Error { + code: ErrorCode::ServerError(401), + message: "unauthenticated method".to_string(), + data: None, + }) + } } /// Returns the best chain tip height of `latest_chain_tip`, diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index b87068ef8f0..831812d28c5 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -33,6 +33,7 @@ use crate::{ #[cfg(feature = "getblocktemplate-rpcs")] use crate::methods::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl}; +mod cookie; pub mod http_request_compatibility; pub mod rpc_call_compatibility; @@ -193,6 +194,9 @@ impl RpcServer { parallel_cpu_threads = available_parallelism().map(usize::from).unwrap_or(1); } + // generate a cookie + let generated_password = cookie::generate().unwrap_or("".to_string()); + // The server is a blocking task, which blocks on executor shutdown. // So we need to start it in a std::thread. // (Otherwise tokio panics on RPC port conflict, which shuts down the RPC server.) @@ -205,7 +209,7 @@ impl RpcServer { .threads(parallel_cpu_threads) // TODO: disable this security check if we see errors from lightwalletd //.allowed_hosts(DomainsValidation::Disabled) - .request_middleware(FixHttpRequestMiddleware) + .request_middleware(FixHttpRequestMiddleware::new(generated_password)) .start_http(&listen_addr) .expect("Unable to start RPC server"); @@ -299,6 +303,7 @@ impl RpcServer { span.in_scope(|| { info!("Stopping RPC server"); close_handle.clone().close(); + cookie::delete(); // delete the auth cookie debug!("Stopped RPC server"); }) }; diff --git a/zebra-rpc/src/server/cookie.rs b/zebra-rpc/src/server/cookie.rs new file mode 100644 index 00000000000..bab777f1d35 --- /dev/null +++ b/zebra-rpc/src/server/cookie.rs @@ -0,0 +1,46 @@ +//! Cookie-based authentication for the RPC server. + +use base64::Engine; +use rand::RngCore; + +use std::{ + fs::{remove_file, File}, + io::{Read, Write}, +}; + +/// The user field in the cookie (arbitrary, only for recognizability in debugging/logging purposes) +pub const COOKIEAUTH_USER: &str = "__cookie__"; +/// Default name for auth cookie file */ +const COOKIEAUTH_FILE: &str = ".cookie"; + +/// Generate a new auth cookie and return the encoded password. +pub fn generate() -> Option { + let mut data = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut data); + let encoded_password = base64::prelude::BASE64_STANDARD.encode(data); + let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password); + + let mut file = File::create(COOKIEAUTH_FILE).ok()?; + file.write_all(cookie_content.as_bytes()).ok()?; + + tracing::info!("RPC auth cookie generated successfully"); + + Some(encoded_password) +} + +/// Get the encoded password from the auth cookie. +pub fn get() -> Option { + let mut file = File::open(COOKIEAUTH_FILE).ok()?; + let mut contents = String::new(); + file.read_to_string(&mut contents).ok()?; + + let parts: Vec<&str> = contents.split(":").collect(); + Some(parts[1].to_string()) +} + +/// Delete the auth cookie. +pub fn delete() -> Option<()> { + remove_file(COOKIEAUTH_FILE).ok()?; + tracing::info!("RPC auth cookie deleted successfully"); + Some(()) +} diff --git a/zebra-rpc/src/server/http_request_compatibility.rs b/zebra-rpc/src/server/http_request_compatibility.rs index fede0e2bef0..79e82829e12 100644 --- a/zebra-rpc/src/server/http_request_compatibility.rs +++ b/zebra-rpc/src/server/http_request_compatibility.rs @@ -8,6 +8,8 @@ use jsonrpc_http_server::{ RequestMiddleware, RequestMiddlewareAction, }; +use crate::server::cookie; + /// HTTP [`RequestMiddleware`] with compatibility workarounds. /// /// This middleware makes the following changes to HTTP requests: @@ -34,8 +36,8 @@ use jsonrpc_http_server::{ /// Any user-specified data in RPC requests is hex or base58check encoded. /// We assume lightwalletd validates data encodings before sending it on to Zebra. /// So any fixes Zebra performs won't change user-specified data. -#[derive(Copy, Clone, Debug)] -pub struct FixHttpRequestMiddleware; +#[derive(Clone, Debug)] +pub struct FixHttpRequestMiddleware(String); impl RequestMiddleware for FixHttpRequestMiddleware { fn on_request(&self, mut request: Request) -> RequestMiddlewareAction { @@ -45,7 +47,7 @@ impl RequestMiddleware for FixHttpRequestMiddleware { FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut()); // Fix the request body - let request = request.map(|body| { + let mut request = request.map(|body| { let body = body.map_ok(|data| { // To simplify data handling, we assume that any search strings won't be split // across multiple `Bytes` data buffers. @@ -70,6 +72,18 @@ impl RequestMiddleware for FixHttpRequestMiddleware { Body::wrap_stream(body) }); + // Check if the request is authenticated + match cookie::get() { + Some(password) => { + if password != self.0 { + request = Self::unauthenticated(request); + } + } + None => { + request = Self::unauthenticated(request); + } + } + tracing::trace!(?request, "modified HTTP request"); RequestMiddlewareAction::Proceed { @@ -141,4 +155,33 @@ impl FixHttpRequestMiddleware { ); } } + + /// Create a new `FixHttpRequestMiddleware`. + pub fn new(password: String) -> Self { + Self(password) + } + + /// Change the method name in the JSON request. + fn change_method_name(data: String) -> String { + let mut json_data: serde_json::Value = serde_json::from_str(&data).expect("Invalid JSON"); + + if let Some(method) = json_data.get_mut("method") { + *method = serde_json::json!("unauthenticated"); + } + + serde_json::to_string(&json_data).expect("Failed to serialize JSON") + } + + /// Modify the request name to be `unauthenticated`. + fn unauthenticated(request: Request) -> Request { + request.map(|body| { + let body = body.map_ok(|data| { + let mut data = String::from_utf8_lossy(data.as_ref()).to_string(); + data = Self::change_method_name(data); + Bytes::from(data) + }); + + Body::wrap_stream(body) + }) + } }