mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 19:09:54 +00:00
feat: JWT secret lifecycle (#1209)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4071,6 +4071,7 @@ dependencies = [
|
|||||||
"reth-primitives",
|
"reth-primitives",
|
||||||
"reth-provider",
|
"reth-provider",
|
||||||
"reth-rlp",
|
"reth-rlp",
|
||||||
|
"reth-rpc",
|
||||||
"reth-rpc-builder",
|
"reth-rpc-builder",
|
||||||
"reth-staged-sync",
|
"reth-staged-sync",
|
||||||
"reth-stages",
|
"reth-stages",
|
||||||
|
|||||||
@ -20,7 +20,7 @@ reth-consensus = { path = "../../crates/consensus" }
|
|||||||
reth-executor = { path = "../../crates/executor" }
|
reth-executor = { path = "../../crates/executor" }
|
||||||
reth-eth-wire = { path = "../../crates/net/eth-wire" }
|
reth-eth-wire = { path = "../../crates/net/eth-wire" }
|
||||||
reth-rpc-builder = { path = "../../crates/rpc/rpc-builder" }
|
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-rlp = { path = "../../crates/rlp" }
|
||||||
reth-network = {path = "../../crates/net/network", features = ["serde"] }
|
reth-network = {path = "../../crates/net/network", features = ["serde"] }
|
||||||
reth-network-api = {path = "../../crates/net/network-api" }
|
reth-network-api = {path = "../../crates/net/network-api" }
|
||||||
@ -60,4 +60,4 @@ tempfile = { version = "3.3.0" }
|
|||||||
backon = "0.2.0"
|
backon = "0.2.0"
|
||||||
comfy-table = "6.1.4"
|
comfy-table = "6.1.4"
|
||||||
crossterm = "0.25.0"
|
crossterm = "0.25.0"
|
||||||
tui = "0.19.0"
|
tui = "0.19.0"
|
||||||
@ -1,8 +1,10 @@
|
|||||||
//! clap [Args](clap::Args) for RPC related arguments.
|
//! clap [Args](clap::Args) for RPC related arguments.
|
||||||
|
|
||||||
|
use crate::dirs::{JwtSecretPath, PlatformPath};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
use reth_rpc::{JwtError, JwtSecret};
|
||||||
use reth_rpc_builder::RpcModuleConfig;
|
use reth_rpc_builder::RpcModuleConfig;
|
||||||
use std::net::IpAddr;
|
use std::{net::IpAddr, path::Path};
|
||||||
|
|
||||||
/// Parameters for configuring the rpc more granularity via CLI
|
/// Parameters for configuring the rpc more granularity via CLI
|
||||||
#[derive(Debug, Args, PartialEq, Default)]
|
#[derive(Debug, Args, PartialEq, Default)]
|
||||||
@ -47,6 +49,35 @@ pub struct RpcServerArgs {
|
|||||||
/// Filename for IPC socket/pipe within the datadir
|
/// Filename for IPC socket/pipe within the datadir
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub ipcpath: Option<String>,
|
pub ipcpath: Option<String>,
|
||||||
|
|
||||||
|
/// 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<PlatformPath<JwtSecretPath>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JwtSecret, JwtError> {
|
||||||
|
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::<JwtSecretPath>::default();
|
||||||
|
let fpath = default_path.as_ref();
|
||||||
|
JwtSecret::try_create(fpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
//! CLI definition and entrypoint to executable
|
//! CLI definition and entrypoint to executable
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chain, db,
|
chain, db,
|
||||||
dirs::{LogsDir, PlatformPath},
|
dirs::{LogsDir, PlatformPath},
|
||||||
@ -10,7 +12,6 @@ use reth_tracing::{
|
|||||||
tracing_subscriber::{filter::Directive, registry::LookupSpan},
|
tracing_subscriber::{filter::Directive, registry::LookupSpan},
|
||||||
BoxedLayer, FileWorkerGuard,
|
BoxedLayer, FileWorkerGuard,
|
||||||
};
|
};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// Parse CLI options, set up logging and run the chosen command.
|
/// Parse CLI options, set up logging and run the chosen command.
|
||||||
pub async fn run() -> eyre::Result<()> {
|
pub async fn run() -> eyre::Result<()> {
|
||||||
|
|||||||
@ -42,6 +42,13 @@ pub fn logs_dir() -> Option<PathBuf> {
|
|||||||
cache_dir().map(|root| root.join("logs"))
|
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<PathBuf> {
|
||||||
|
data_dir().map(|root| root.join("jwtsecret"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the reth database.
|
/// Returns the path to the reth database.
|
||||||
///
|
///
|
||||||
/// Refer to [dirs_next::data_dir] for cross-platform behavior.
|
/// 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<PathBuf> {
|
||||||
|
jwt_secret_dir().map(|p| p.join("jwt.hex"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the default reth configuration file.
|
/// Returns the path to the default reth configuration file.
|
||||||
///
|
///
|
||||||
/// Refer to [dirs_next::config_dir] for cross-platform behavior.
|
/// Refer to [dirs_next::config_dir] for cross-platform behavior.
|
||||||
@ -119,7 +139,7 @@ trait XdgPath {
|
|||||||
///
|
///
|
||||||
/// assert_ne!(default.as_ref(), custom.as_ref());
|
/// assert_ne!(default.as_ref(), custom.as_ref());
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct PlatformPath<D>(PathBuf, std::marker::PhantomData<D>);
|
pub struct PlatformPath<D>(PathBuf, std::marker::PhantomData<D>);
|
||||||
|
|
||||||
impl<D> Display for PlatformPath<D> {
|
impl<D> Display for PlatformPath<D> {
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
no_crate_inject,
|
no_crate_inject,
|
||||||
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
||||||
))]
|
))]
|
||||||
|
|
||||||
//! Rust Ethereum (reth) binary executable.
|
//! Rust Ethereum (reth) binary executable.
|
||||||
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
|
|||||||
@ -135,6 +135,10 @@ impl Command {
|
|||||||
|
|
||||||
info!(target: "reth::cli", peer_id = %network.peer_id(), local_addr = %network.local_addr(), "Connected to P2P network");
|
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
|
// TODO(mattsse): cleanup, add cli args
|
||||||
let _rpc_server = reth_rpc_builder::launch(
|
let _rpc_server = reth_rpc_builder::launch(
|
||||||
ShareableDatabase::new(db.clone()),
|
ShareableDatabase::new(db.clone()),
|
||||||
|
|||||||
@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::{
|
use std::{
|
||||||
collections::hash_map::DefaultHasher,
|
collections::hash_map::DefaultHasher,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
path::Path,
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
/// Errors returned by the [`JwtSecret`][crate::layers::JwtSecret]
|
/// Errors returned by the [`JwtSecret`][crate::layers::JwtSecret]
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@ -27,6 +29,8 @@ pub enum JwtError {
|
|||||||
MissingOrInvalidAuthorizationHeader,
|
MissingOrInvalidAuthorizationHeader,
|
||||||
#[error("JWT decoding error {0}")]
|
#[error("JWT decoding error {0}")]
|
||||||
JwtDecodingError(String),
|
JwtDecodingError(String),
|
||||||
|
#[error("An I/O error occurred: {0}")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Length of the hex-encoded 256 bit secret key.
|
/// Length of the hex-encoded 256 bit secret key.
|
||||||
@ -68,6 +72,32 @@ impl JwtSecret {
|
|||||||
Ok(JwtSecret(bytes))
|
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<Self, JwtError> {
|
||||||
|
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<Self, JwtError> {
|
||||||
|
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 {
|
impl std::fmt::Debug for JwtSecret {
|
||||||
@ -157,8 +187,12 @@ impl Claims {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{Claims, JwtError, JwtSecret};
|
use super::{Claims, JwtError, JwtSecret};
|
||||||
use crate::layers::jwt_secret::JWT_MAX_IAT_DIFF;
|
use crate::layers::jwt_secret::JWT_MAX_IAT_DIFF;
|
||||||
|
use hex::encode as hex_encode;
|
||||||
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
|
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_hex() {
|
fn from_hex() {
|
||||||
@ -175,7 +209,7 @@ mod tests {
|
|||||||
let original = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430";
|
let original = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430";
|
||||||
let secret = JwtSecret::from_hex(original).unwrap();
|
let secret = JwtSecret::from_hex(original).unwrap();
|
||||||
let bytes = &secret.0;
|
let bytes = &secret.0;
|
||||||
let computed = hex::encode(bytes);
|
let computed = hex_encode(bytes);
|
||||||
assert_eq!(original, computed);
|
assert_eq!(original, computed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +298,72 @@ mod tests {
|
|||||||
assert!(matches!(result, Err(JwtError::UnsupportedSignatureAlgorithm)));
|
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 {
|
fn to_u64(time: SystemTime) -> u64 {
|
||||||
time.duration_since(UNIX_EPOCH).unwrap().as_secs()
|
time.duration_since(UNIX_EPOCH).unwrap().as_secs()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user