diff --git a/Cargo.lock b/Cargo.lock index 089799c87..aad5ca73d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4071,6 +4071,7 @@ dependencies = [ "reth-primitives", "reth-provider", "reth-rlp", + "reth-rpc", "reth-rpc-builder", "reth-staged-sync", "reth-stages", diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index f24e7fb04..c9338ea5f 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -20,7 +20,7 @@ reth-consensus = { path = "../../crates/consensus" } reth-executor = { path = "../../crates/executor" } reth-eth-wire = { path = "../../crates/net/eth-wire" } reth-rpc-builder = { path = "../../crates/rpc/rpc-builder" } -# reth-rpc = {path = "../../crates/rpc/rpc"} +reth-rpc = { path = "../../crates/rpc/rpc" } reth-rlp = { path = "../../crates/rlp" } reth-network = {path = "../../crates/net/network", features = ["serde"] } reth-network-api = {path = "../../crates/net/network-api" } @@ -60,4 +60,4 @@ tempfile = { version = "3.3.0" } backon = "0.2.0" comfy-table = "6.1.4" crossterm = "0.25.0" -tui = "0.19.0" +tui = "0.19.0" \ No newline at end of file diff --git a/bin/reth/src/args/rpc_server_args.rs b/bin/reth/src/args/rpc_server_args.rs index 737e6dd84..be40274f3 100644 --- a/bin/reth/src/args/rpc_server_args.rs +++ b/bin/reth/src/args/rpc_server_args.rs @@ -1,8 +1,10 @@ //! clap [Args](clap::Args) for RPC related arguments. +use crate::dirs::{JwtSecretPath, PlatformPath}; use clap::Args; +use reth_rpc::{JwtError, JwtSecret}; use reth_rpc_builder::RpcModuleConfig; -use std::net::IpAddr; +use std::{net::IpAddr, path::Path}; /// Parameters for configuring the rpc more granularity via CLI #[derive(Debug, Args, PartialEq, Default)] @@ -47,6 +49,35 @@ pub struct RpcServerArgs { /// Filename for IPC socket/pipe within the datadir #[arg(long)] pub ipcpath: Option, + + /// Path to a JWT secret to use for authenticated RPC endpoints + #[arg(long = "authrpc.jwtsecret", value_name = "PATH", global = true, required = false)] + authrpc_jwtsecret: Option>, +} + +impl RpcServerArgs { + /// The execution layer and consensus layer clients SHOULD accept a configuration parameter: + /// jwt-secret, which designates a file containing the hex-encoded 256 bit secret key to be used + /// for verifying/generating JWT tokens. + /// + /// If such a parameter is given, but the file cannot be read, or does not contain a hex-encoded + /// key of 256 bits, the client SHOULD treat this as an error. + /// + /// If such a parameter is not given, the client SHOULD generate such a token, valid for the + /// duration of the execution, and SHOULD store the hex-encoded secret as a jwt.hex file on + /// the filesystem. This file can then be used to provision the counterpart client. + pub(crate) fn jwt_secret(&self) -> Result { + let arg = self.authrpc_jwtsecret.as_ref(); + let path: Option<&Path> = arg.map(|p| p.as_ref()); + match path { + Some(fpath) => JwtSecret::from_file(fpath), + None => { + let default_path = PlatformPath::::default(); + let fpath = default_path.as_ref(); + JwtSecret::try_create(fpath) + } + } + } } #[cfg(test)] diff --git a/bin/reth/src/cli.rs b/bin/reth/src/cli.rs index 3cbcfe9f7..944849dc8 100644 --- a/bin/reth/src/cli.rs +++ b/bin/reth/src/cli.rs @@ -1,4 +1,6 @@ //! CLI definition and entrypoint to executable +use std::str::FromStr; + use crate::{ chain, db, dirs::{LogsDir, PlatformPath}, @@ -10,7 +12,6 @@ use reth_tracing::{ tracing_subscriber::{filter::Directive, registry::LookupSpan}, BoxedLayer, FileWorkerGuard, }; -use std::str::FromStr; /// Parse CLI options, set up logging and run the chosen command. pub async fn run() -> eyre::Result<()> { diff --git a/bin/reth/src/dirs.rs b/bin/reth/src/dirs.rs index bac618cbc..c01ca034f 100644 --- a/bin/reth/src/dirs.rs +++ b/bin/reth/src/dirs.rs @@ -42,6 +42,13 @@ pub fn logs_dir() -> Option { cache_dir().map(|root| root.join("logs")) } +/// Returns the path to the reth jwtsecret directory. +/// +/// Refer to [dirs_next::cache_dir] for cross-platform behavior. +pub fn jwt_secret_dir() -> Option { + data_dir().map(|root| root.join("jwtsecret")) +} + /// Returns the path to the reth database. /// /// Refer to [dirs_next::data_dir] for cross-platform behavior. @@ -55,6 +62,19 @@ impl XdgPath for DbPath { } } +/// Returns the path to the default JWT secret hex file. +/// +/// Refer to [dirs_next::data_dir] for cross-platform behavior. +#[derive(Default, Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct JwtSecretPath; + +impl XdgPath for JwtSecretPath { + fn resolve() -> Option { + jwt_secret_dir().map(|p| p.join("jwt.hex")) + } +} + /// Returns the path to the default reth configuration file. /// /// Refer to [dirs_next::config_dir] for cross-platform behavior. @@ -119,7 +139,7 @@ trait XdgPath { /// /// assert_ne!(default.as_ref(), custom.as_ref()); /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct PlatformPath(PathBuf, std::marker::PhantomData); impl Display for PlatformPath { diff --git a/bin/reth/src/lib.rs b/bin/reth/src/lib.rs index d9a30ef98..7bcd3a499 100644 --- a/bin/reth/src/lib.rs +++ b/bin/reth/src/lib.rs @@ -4,6 +4,7 @@ no_crate_inject, attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) ))] + //! Rust Ethereum (reth) binary executable. pub mod args; diff --git a/bin/reth/src/node/mod.rs b/bin/reth/src/node/mod.rs index 3dae8e83d..d6533ef64 100644 --- a/bin/reth/src/node/mod.rs +++ b/bin/reth/src/node/mod.rs @@ -135,6 +135,10 @@ impl Command { info!(target: "reth::cli", peer_id = %network.peer_id(), local_addr = %network.local_addr(), "Connected to P2P network"); + // TODO: Use the resolved secret to spawn the Engine API server + // Look at `reth_rpc::AuthLayer` for integration hints + let _secret = self.rpc.jwt_secret(); + // TODO(mattsse): cleanup, add cli args let _rpc_server = reth_rpc_builder::launch( ShareableDatabase::new(db.clone()), diff --git a/crates/rpc/rpc/src/layers/jwt_secret.rs b/crates/rpc/rpc/src/layers/jwt_secret.rs index f9340b6cc..dd825c914 100644 --- a/crates/rpc/rpc/src/layers/jwt_secret.rs +++ b/crates/rpc/rpc/src/layers/jwt_secret.rs @@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, + path::Path, time::{Duration, SystemTime, UNIX_EPOCH}, }; use thiserror::Error; +use tracing::info; /// Errors returned by the [`JwtSecret`][crate::layers::JwtSecret] #[derive(Error, Debug)] @@ -27,6 +29,8 @@ pub enum JwtError { MissingOrInvalidAuthorizationHeader, #[error("JWT decoding error {0}")] JwtDecodingError(String), + #[error("An I/O error occurred: {0}")] + IOError(#[from] std::io::Error), } /// Length of the hex-encoded 256 bit secret key. @@ -68,6 +72,32 @@ impl JwtSecret { Ok(JwtSecret(bytes)) } } + + /// Tries to load a [`JwtSecret`] from the specified file path. + /// I/O or secret validation errors might occur during read operations in the form of + /// a [`JwtError`]. + pub fn from_file(fpath: &Path) -> Result { + let hex = std::fs::read_to_string(fpath)?; + let secret = JwtSecret::from_hex(hex)?; + info!("Loaded secret {secret:?} from {fpath:?}"); + Ok(secret) + } + + /// Creates a random [`JwtSecret`] and tries to store it at the specified path. I/O errors might + /// occur during write operations in the form of a [`JwtError`] + pub fn try_create(fpath: &Path) -> Result { + if let Some(dir) = fpath.parent() { + // Create parent directory + std::fs::create_dir_all(dir)? + } + + let secret = JwtSecret::random(); + let bytes = &secret.0; + let hex = hex::encode(bytes); + std::fs::write(fpath, hex)?; + info!("Created ephemeral secret {secret:?} at {fpath:?}"); + Ok(secret) + } } impl std::fmt::Debug for JwtSecret { @@ -157,8 +187,12 @@ impl Claims { mod tests { use super::{Claims, JwtError, JwtSecret}; use crate::layers::jwt_secret::JWT_MAX_IAT_DIFF; + use hex::encode as hex_encode; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; + use std::{ + path::Path, + time::{Duration, SystemTime, UNIX_EPOCH}, + }; #[test] fn from_hex() { @@ -175,7 +209,7 @@ mod tests { let original = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430"; let secret = JwtSecret::from_hex(original).unwrap(); let bytes = &secret.0; - let computed = hex::encode(bytes); + let computed = hex_encode(bytes); assert_eq!(original, computed); } @@ -264,6 +298,72 @@ mod tests { assert!(matches!(result, Err(JwtError::UnsupportedSignatureAlgorithm))); } + #[test] + fn ephemeral_secret_created() { + let fpath: &Path = Path::new("secret0.hex"); + assert!(not_exists(fpath)); + JwtSecret::try_create(fpath).expect("A secret file should be created"); + assert!(exists(fpath)); + delete(fpath); + } + + #[test] + fn valid_secret_provided() { + let fpath = Path::new("secret1.hex"); + assert!(not_exists(fpath)); + + let secret = JwtSecret::random(); + write(fpath, &hex(&secret)); + + match JwtSecret::from_file(fpath) { + Ok(gen_secret) => { + delete(fpath); + assert_eq!(hex(&gen_secret), hex(&secret)); + } + Err(_) => { + delete(fpath); + assert!(false); // Fail test + } + } + } + + #[test] + fn invalid_hex_provided() { + let fpath = Path::new("secret2.hex"); + write(fpath, "invalid hex"); + let result = JwtSecret::from_file(fpath); + assert!(matches!(result, Err(_))); + delete(fpath); + } + + #[test] + fn provided_file_not_exists() { + let fpath = Path::new("secret3.hex"); + let result = JwtSecret::from_file(fpath); + assert!(matches!(result, Err(_))); + assert!(!exists(fpath)); + } + + fn hex(secret: &JwtSecret) -> String { + hex::encode(&secret.0) + } + + fn delete(path: &Path) { + std::fs::remove_file(path).unwrap(); + } + + fn write(path: &Path, s: &str) { + std::fs::write(path, s).unwrap(); + } + + fn not_exists(path: &Path) -> bool { + !exists(path) + } + + fn exists(path: &Path) -> bool { + std::fs::metadata(path).is_ok() + } + fn to_u64(time: SystemTime) -> u64 { time.duration_since(UNIX_EPOCH).unwrap().as_secs() }