radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 69790f6f6af6dcfbb8a567f7559d9b7e28f16a46
parent 5dec0c01c8184e04b96ff3eec544d90a1df1880d
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 22:00:44 +0000

app: store service identity as encrypted envelope

Diffstat:
MCargo.lock | 22++++++++++++++++++++++
MCargo.toml | 3+++
Asrc/app/identity_storage.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/mod.rs | 1+
Msrc/app/runtime.rs | 33+++++++++++++++++++++------------
5 files changed, 123 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1823,13 +1823,29 @@ dependencies = [ ] [[package]] +name = "radroots-protected-store" +version = "0.1.0-alpha.1" +dependencies = [ + "chacha20poly1305", + "getrandom 0.2.17", + "radroots-secret-vault", + "serde", + "serde_json", + "zeroize", +] + +[[package]] name = "radroots-runtime" version = "0.1.0-alpha.1" dependencies = [ "anyhow", + "chacha20poly1305", "clap", "config", + "getrandom 0.2.17", "radroots-log", + "radroots-protected-store", + "radroots-secret-vault", "serde", "serde_json", "tempfile", @@ -1837,9 +1853,14 @@ dependencies = [ "tokio", "toml", "tracing", + "zeroize", ] [[package]] +name = "radroots-secret-vault" +version = "0.1.0-alpha.1" + +[[package]] name = "radroots-trade" version = "0.1.0-alpha.1" dependencies = [ @@ -1872,6 +1893,7 @@ dependencies = [ "serde_json", "serde_qs", "sha2", + "tempfile", "thiserror 2.0.18", "tokio", "tower", diff --git a/Cargo.toml b/Cargo.toml @@ -48,3 +48,6 @@ tower = { version = "0.5.3", features = ["util"] } tracing = { version = "0.1" } uuid = { version = "1.22.0", features = ["v4"] } url = { version = "2.5.8" } + +[dev-dependencies] +tempfile = { version = "3" } diff --git a/src/app/identity_storage.rs b/src/app/identity_storage.rs @@ -0,0 +1,76 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityFile}; + +const RADROOTSD_IDENTITY_KEY_SLOT: &str = "radrootsd_identity"; + +#[cfg(test)] +pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf { + radroots_runtime::local_wrapping_key_path(path) +} + +pub fn load_service_identity( + path: Option<&Path>, + allow_generate: bool, +) -> Result<RadrootsIdentity> { + let path = resolved_identity_path(path); + if path.exists() { + return load_encrypted_identity(&path); + } + if !allow_generate { + return Err(IdentityError::GenerationNotAllowed(path).into()); + } + + let identity = RadrootsIdentity::generate(); + store_encrypted_identity(&path, &identity)?; + Ok(identity) +} + +pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &RadrootsIdentity) -> Result<()> { + let payload = serde_json::to_vec(&identity.to_file())?; + radroots_runtime::seal_local_secret_file(path, RADROOTSD_IDENTITY_KEY_SLOT, &payload)?; + Ok(()) +} + +pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity> { + let payload = radroots_runtime::open_local_secret_file(path, RADROOTSD_IDENTITY_KEY_SLOT)?; + let file: RadrootsIdentityFile = serde_json::from_slice(&payload)?; + Ok(RadrootsIdentity::try_from(file)?) +} + +fn resolved_identity_path(path: Option<&Path>) -> PathBuf { + path.map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(radroots_runtime::DEFAULT_SERVICE_IDENTITY_PATH)) +} + +#[cfg(test)] +mod tests { + use super::{encrypted_identity_key_path, load_service_identity}; + + #[test] + fn load_service_identity_generates_encrypted_identity_artifacts() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("radrootsd-identity.secret.json"); + + let generated = + load_service_identity(Some(&path), true).expect("generate encrypted identity"); + let loaded = load_service_identity(Some(&path), false).expect("load encrypted identity"); + + assert_eq!(generated.id(), loaded.id()); + assert!(path.is_file()); + assert!(encrypted_identity_key_path(&path).is_file()); + } + + #[test] + fn load_service_identity_fails_when_wrapping_key_is_missing() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("radrootsd-identity.secret.json"); + let _ = load_service_identity(Some(&path), true).expect("generate encrypted identity"); + std::fs::remove_file(encrypted_identity_key_path(&path)).expect("remove wrapping key"); + + let err = load_service_identity(Some(&path), false) + .expect_err("missing wrapping key should fail"); + assert!(err.to_string().contains("identity")); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs @@ -1,5 +1,6 @@ pub mod cli; pub mod config; +mod identity_storage; mod runtime; pub use cli::Args; diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -4,6 +4,7 @@ use radroots_identity::RadrootsIdentity; use std::time::Duration; use tracing::{info, warn}; +use crate::app::identity_storage::load_service_identity; use crate::app::{cli, config}; use crate::core::Radrootsd; use crate::transport::jsonrpc; @@ -266,8 +267,8 @@ pub async fn run() -> Result<()> { info!("Starting radrootsd"); - let identity = RadrootsIdentity::load_or_generate( - args.service.identity.as_ref(), + let identity = load_service_identity( + args.service.identity.as_deref(), args.service.allow_generate_identity, )?; let radrootsd = Radrootsd::new( @@ -335,6 +336,7 @@ mod tests { use radroots_events::kinds::KIND_LISTING; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::RadrootsNostrMetadata; + use std::path::Path; use std::path::PathBuf; use std::sync::{Mutex, MutexGuard}; @@ -364,7 +366,14 @@ mod tests { .duration_since(std::time::UNIX_EPOCH) .expect("time") .as_nanos(); - std::env::temp_dir().join(format!("radrootsd-{suffix}-{nanos}.json")) + std::env::temp_dir().join(format!("radrootsd-{suffix}-{nanos}.secret.json")) + } + + fn cleanup_identity_artifacts(path: &Path) { + let _ = std::fs::remove_file(path); + let _ = std::fs::remove_file(crate::app::identity_storage::encrypted_identity_key_path( + path, + )); } fn args_for_identity(path: PathBuf, allow_generate: bool) -> cli::Args { @@ -429,7 +438,7 @@ mod tests { #[tokio::test] async fn run_returns_error_when_identity_missing() { let _guard = test_guard(); - let args = args_for_identity(PathBuf::from("/tmp/radrootsd-missing.json"), false); + let args = args_for_identity(PathBuf::from("/tmp/radrootsd-missing.secret.json"), false); let settings = settings_with_relays(Vec::new()); *run_load_hook() .lock() @@ -475,7 +484,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(())); assert!(run().await.is_ok()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -500,7 +509,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Err("boom".to_string())); assert!(run().await.is_ok()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -521,7 +530,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunWaitOutcome::Shutdown); assert!(run().await.is_ok()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -536,7 +545,7 @@ mod tests { let err = run().await.expect_err("invalid relay should error"); let msg = format!("{err:#}"); assert!(!msg.is_empty()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -552,7 +561,7 @@ mod tests { let err = run().await.expect_err("invalid rpc addr should error"); let msg = format!("{err:#}"); assert!(msg.contains("invalid")); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -571,7 +580,7 @@ mod tests { let err = run().await.expect_err("rpc start hook should fail"); let msg = format!("{err:#}"); assert!(msg.contains("rpc start failed")); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -589,7 +598,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(handle)); assert!(run().await.is_ok()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[tokio::test] @@ -605,7 +614,7 @@ mod tests { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunWaitOutcome::Shutdown); assert!(run().await.is_ok()); - let _ = std::fs::remove_file(path); + cleanup_identity_artifacts(&path); } #[test]