mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +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-provider",
|
||||
"reth-rlp",
|
||||
"reth-rpc",
|
||||
"reth-rpc-builder",
|
||||
"reth-staged-sync",
|
||||
"reth-stages",
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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<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)]
|
||||
|
||||
@ -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<()> {
|
||||
|
||||
@ -42,6 +42,13 @@ pub fn logs_dir() -> Option<PathBuf> {
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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<D>(PathBuf, std::marker::PhantomData<D>);
|
||||
|
||||
impl<D> Display for PlatformPath<D> {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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<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 {
|
||||
@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user