feat: JWT secret lifecycle (#1209)

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Andrea Simeoni
2023-02-11 06:50:59 +01:00
committed by GitHub
parent 3d0864bbb9
commit df6ff63806
8 changed files with 165 additions and 7 deletions

1
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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)]

View File

@ -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<()> {

View File

@ -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> {

View File

@ -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;

View File

@ -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()),

View File

@ -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()
} }