diff --git a/Cargo.lock b/Cargo.lock index f1c4e445..ca279015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3619,6 +3619,7 @@ dependencies = [ "async-trait", "aws-sdk-s3", "backon", + "base64 0.22.1", "base64-simd", "blake2", "byteorder", @@ -3633,6 +3634,7 @@ dependencies = [ "glob", "hex-simd", "highway", + "hmac 0.12.1", "http 1.3.1", "lazy_static", "lock", @@ -3662,7 +3664,7 @@ dependencies = [ "s3s", "serde", "serde_json", - "sha2 0.11.0-pre.5", + "sha2 0.10.9", "shadow-rs", "siphasher 1.0.1", "smallvec", @@ -8254,6 +8256,7 @@ dependencies = [ "axum", "axum-extra", "axum-server", + "base64 0.22.1", "bytes", "chrono", "clap", @@ -8265,6 +8268,7 @@ dependencies = [ "flatbuffers 25.2.10", "futures", "futures-util", + "hmac 0.12.1", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", @@ -8302,6 +8306,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sha2 0.10.9", "shadow-rs", "socket2", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index 1b93abb4..ed4a8ec7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ axum-extra = "0.10.1" axum-server = { version = "0.7.2", features = ["tls-rustls"] } backon = "1.5.1" base64-simd = "0.8.0" +base64 = "0.22.1" blake2 = "0.10.6" bytes = { version = "1.10.1", features = ["serde"] } bytesize = "2.0.1" @@ -99,6 +100,7 @@ glob = "0.3.2" hex = "0.4.3" hex-simd = "0.8.0" highway = { version = "1.3.0" } +hmac = "0.12.1" hyper = "1.6.0" hyper-util = { version = "0.1.14", features = [ "tokio", diff --git a/ecstore/Cargo.toml b/ecstore/Cargo.toml index 5dc950af..e19ef229 100644 --- a/ecstore/Cargo.toml +++ b/ecstore/Cargo.toml @@ -56,7 +56,9 @@ tokio-util = { workspace = true, features = ["io", "compat"] } crc32fast = { workspace = true } siphasher = { workspace = true } base64-simd = { workspace = true } -sha2 = { version = "0.11.0-pre.4" } +base64 = { workspace = true } +hmac = { workspace = true } +sha2 = { workspace = true } hex-simd = { workspace = true } path-clean = { workspace = true } tempfile.workspace = true diff --git a/ecstore/src/disk/remote.rs b/ecstore/src/disk/remote.rs index e3a04195..dae2d3a8 100644 --- a/ecstore/src/disk/remote.rs +++ b/ecstore/src/disk/remote.rs @@ -16,6 +16,10 @@ use protos::{ use rustfs_filemeta::{FileInfo, RawFileInfo}; use rustfs_rio::{HttpReader, HttpWriter}; +use base64::{Engine as _, engine::general_purpose}; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::{ io::AsyncWrite, sync::mpsc::{self, Sender}, @@ -43,6 +47,8 @@ use crate::{ use protos::proto_gen::node_service::RenamePartRequest; +type HmacSha256 = Hmac; + #[derive(Debug)] pub struct RemoteDisk { pub id: Mutex>, @@ -69,6 +75,35 @@ impl RemoteDisk { endpoint: ep.clone(), }) } + + /// Get the shared secret for HMAC signing + fn get_shared_secret() -> String { + std::env::var("RUSTFS_RPC_SECRET").unwrap_or_else(|_| "rustfs-default-secret".to_string()) + } + + /// Generate HMAC-SHA256 signature for the given data + fn generate_signature(secret: &str, url: &str, method: &str, timestamp: u64) -> String { + let data = format!("{}|{}|{}", url, method, timestamp); + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); + mac.update(data.as_bytes()); + let result = mac.finalize(); + general_purpose::STANDARD.encode(result.into_bytes()) + } + + /// Build headers with authentication signature + fn build_auth_headers(&self, url: &str, method: &Method, base_headers: Option) -> HeaderMap { + let mut headers = base_headers.unwrap_or_default(); + + let secret = Self::get_shared_secret(); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + let signature = Self::generate_signature(&secret, url, method.as_str(), timestamp); + + headers.insert("x-rustfs-signature", HeaderValue::from_str(&signature).unwrap()); + headers.insert("x-rustfs-timestamp", HeaderValue::from_str(×tamp.to_string()).unwrap()); + + headers + } } // TODO: all api need to handle errors @@ -579,8 +614,9 @@ impl DiskAPI for RemoteDisk { let opts = serde_json::to_vec(&opts)?; - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + let mut base_headers = HeaderMap::new(); + base_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + let headers = self.build_auth_headers(&url, &Method::GET, Some(base_headers)); let mut reader = HttpReader::new(url, Method::GET, headers, Some(opts)).await?; @@ -603,7 +639,8 @@ impl DiskAPI for RemoteDisk { 0 ); - Ok(Box::new(HttpReader::new(url, Method::GET, HeaderMap::new(), None).await?)) + let headers = self.build_auth_headers(&url, &Method::GET, None); + Ok(Box::new(HttpReader::new(url, Method::GET, headers, None).await?)) } #[tracing::instrument(level = "debug", skip(self))] @@ -626,7 +663,8 @@ impl DiskAPI for RemoteDisk { length ); - Ok(Box::new(HttpReader::new(url, Method::GET, HeaderMap::new(), None).await?)) + let headers = self.build_auth_headers(&url, &Method::GET, None); + Ok(Box::new(HttpReader::new(url, Method::GET, headers, None).await?)) } #[tracing::instrument(level = "debug", skip(self))] @@ -643,7 +681,8 @@ impl DiskAPI for RemoteDisk { 0 ); - Ok(Box::new(HttpWriter::new(url, Method::PUT, HeaderMap::new()).await?)) + let headers = self.build_auth_headers(&url, &Method::PUT, None); + Ok(Box::new(HttpWriter::new(url, Method::PUT, headers).await?)) } #[tracing::instrument(level = "debug", skip(self))] @@ -666,7 +705,8 @@ impl DiskAPI for RemoteDisk { file_size ); - Ok(Box::new(HttpWriter::new(url, Method::PUT, HeaderMap::new()).await?)) + let headers = self.build_auth_headers(&url, &Method::PUT, None); + Ok(Box::new(HttpWriter::new(url, Method::PUT, headers).await?)) } #[tracing::instrument(level = "debug", skip(self))] diff --git a/ecstore/src/set_disk.rs b/ecstore/src/set_disk.rs index b35711ab..47187566 100644 --- a/ecstore/src/set_disk.rs +++ b/ecstore/src/set_disk.rs @@ -68,7 +68,7 @@ use rustfs_utils::{ crypto::{base64_decode, base64_encode, hex}, path::{SLASH_SEPARATOR, encode_dir_object, has_suffix, path_join_buf}, }; -use sha2::{Digest, Sha256}; +use sha2::Sha256; use std::hash::Hash; use std::mem; use std::time::SystemTime; diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 96c6d371..4d760a75 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -95,6 +95,9 @@ urlencoding = { workspace = true } uuid = { workspace = true } rustfs-filemeta.workspace = true rustfs-rio.workspace = true +base64 = { workspace = true } +hmac = { workspace = true } +sha2 = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] libsystemd.workspace = true diff --git a/rustfs/src/admin/router.rs b/rustfs/src/admin/router.rs index bea785cd..7413623b 100644 --- a/rustfs/src/admin/router.rs +++ b/rustfs/src/admin/router.rs @@ -1,3 +1,5 @@ +use base64::{Engine as _, engine::general_purpose}; +use hmac::{Hmac, Mac}; use hyper::HeaderMap; use hyper::Method; use hyper::StatusCode; @@ -12,10 +14,80 @@ use s3s::S3Result; use s3s::header; use s3s::route::S3Route; use s3s::s3_error; +use sha2::Sha256; +use std::time::{SystemTime, UNIX_EPOCH}; use super::ADMIN_PREFIX; use super::RUSTFS_ADMIN_PREFIX; use super::rpc::RPC_PREFIX; +use iam::get_global_action_cred; + +type HmacSha256 = Hmac; + +const SIGNATURE_HEADER: &str = "x-rustfs-signature"; +const TIMESTAMP_HEADER: &str = "x-rustfs-timestamp"; +const SIGNATURE_VALID_DURATION: u64 = 300; // 5 minutes + +/// Get the shared secret for HMAC signing +fn get_shared_secret() -> String { + if let Some(cred) = get_global_action_cred() { + cred.secret_key + } else { + // Fallback to environment variable if global credentials are not available + std::env::var("RUSTFS_RPC_SECRET").unwrap_or_else(|_| "rustfs-default-secret".to_string()) + } +} + +/// Generate HMAC-SHA256 signature for the given data +fn generate_signature(secret: &str, url: &str, method: &str, timestamp: u64) -> String { + let data = format!("{}|{}|{}", url, method, timestamp); + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); + mac.update(data.as_bytes()); + let result = mac.finalize(); + general_purpose::STANDARD.encode(result.into_bytes()) +} + +/// Verify the request signature for RPC requests +fn verify_rpc_signature(req: &S3Request) -> S3Result<()> { + let secret = get_shared_secret(); + + // Get signature from header + let signature = req + .headers + .get(SIGNATURE_HEADER) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| s3_error!(InvalidArgument, "Missing signature header"))?; + + // Get timestamp from header + let timestamp_str = req + .headers + .get(TIMESTAMP_HEADER) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| s3_error!(InvalidArgument, "Missing timestamp header"))?; + + let timestamp: u64 = timestamp_str + .parse() + .map_err(|_| s3_error!(InvalidArgument, "Invalid timestamp format"))?; + + // Check timestamp validity (prevent replay attacks) + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + if current_time.saturating_sub(timestamp) > SIGNATURE_VALID_DURATION { + return Err(s3_error!(InvalidArgument, "Request timestamp expired")); + } + + // Generate expected signature + let url = req.uri.to_string(); + let method = req.method.as_str(); + let expected_signature = generate_signature(&secret, &url, method, timestamp); + + // Compare signatures + if signature != expected_signature { + return Err(s3_error!(AccessDenied, "Invalid signature")); + } + + Ok(()) +} pub struct S3Router { router: Router, @@ -84,10 +156,16 @@ where // check_access before call async fn check_access(&self, req: &mut S3Request) -> S3Result<()> { - // TODO: check access by req.credentials + // Check RPC signature verification if req.uri.path().starts_with(RPC_PREFIX) { + // Skip signature verification for HEAD requests (health checks) + if req.method != Method::HEAD { + verify_rpc_signature(req)?; + } return Ok(()); } + + // For non-RPC admin requests, check credentials match req.credentials { Some(_) => Ok(()), None => Err(s3_error!(AccessDenied, "Signature is required")),