myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 9608edf9735169a70a68fbad0a6f48b46a2579b1
parent ccc60e469fee37100764e32eb98265857fc8f88d
Author: triesap <tyson@radroots.org>
Date:   Thu,  2 Apr 2026 00:23:32 +0000

custody: bound external command execution time

- add explicit custody timeout config and env parsing for external command identities
- kill hung helper processes and surface a dedicated timeout error instead of stalling runtime operations
- thread the timeout through runtime cli discovery persistence and operability identity loading paths
- cover config validation and external command timeout mapping in myc lib tests

Diffstat:
M.env.example | 1+
Msrc/app/runtime.rs | 7+++++--
Msrc/cli.rs | 9++++++++-
Msrc/config.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/custody.rs | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/discovery.rs | 9++++++---
Msrc/error.rs | 8++++++++
Msrc/lib.rs | 10+++++-----
Msrc/operability/mod.rs | 8++++++--
Msrc/persistence.rs | 26+++++++++++++++++++++-----
10 files changed, 270 insertions(+), 38 deletions(-)

diff --git a/.env.example b/.env.example @@ -2,6 +2,7 @@ MYC_SERVICE_INSTANCE_NAME=myc MYC_LOGGING_FILTER=info,myc=info MYC_LOGGING_OUTPUT_DIR=/var/log/radroots/services/myc MYC_LOGGING_STDOUT=true +MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=10 MYC_PATHS_STATE_DIR=/var/lib/myc MYC_PATHS_SIGNER_IDENTITY_BACKEND=filesystem diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -3,6 +3,7 @@ use std::future::Future; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::time::Duration; use super::backend::MycSignerBackend; use crate::audit::{ @@ -120,6 +121,7 @@ impl MycRuntime { &config.persistence, config.audit.clone(), MycPolicyContext::from_config(&config.policy)?, + Duration::from_secs(config.custody.external_command_timeout_secs), config.paths.signer_identity_source(), config.paths.user_identity_source(), )?; @@ -1103,13 +1105,14 @@ impl MycSignerContext { persistence: &MycPersistenceConfig, audit_config: MycAuditConfig, policy: MycPolicyContext, + external_command_timeout: Duration, signer_identity_source: MycIdentitySourceSpec, user_identity_source: MycIdentitySourceSpec, ) -> Result<Self, MycError> { let signer_identity_provider = - MycIdentityProvider::from_source("signer", signer_identity_source)?; + MycIdentityProvider::from_source("signer", signer_identity_source, external_command_timeout)?; let user_identity_provider = - MycIdentityProvider::from_source("user", user_identity_source)?; + MycIdentityProvider::from_source("user", user_identity_source, external_command_timeout)?; let signer_identity = signer_identity_provider.load_active_identity()?; let user_identity = user_identity_provider.load_active_identity()?; let signer_store = Self::build_signer_store(persistence, &paths.signer_state_path)?; diff --git a/src/cli.rs b/src/cli.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::time::Duration; use clap::{Args, Parser, Subcommand, ValueEnum}; use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; @@ -623,10 +624,12 @@ fn custody_provider_for_role( MycCustodyRole::Signer => crate::custody::MycIdentityProvider::from_source( "signer", config.paths.signer_identity_source(), + Duration::from_secs(config.custody.external_command_timeout_secs), ), MycCustodyRole::User => crate::custody::MycIdentityProvider::from_source( "user", config.paths.user_identity_source(), + Duration::from_secs(config.custody.external_command_timeout_secs), ), MycCustodyRole::DiscoveryApp => { let Some(source) = config.discovery.app_identity_source() else { @@ -634,7 +637,11 @@ fn custody_provider_for_role( "discovery app identity is not separately configured; it currently reuses the signer identity".to_owned(), )); }; - crate::custody::MycIdentityProvider::from_source("discovery app", source) + crate::custody::MycIdentityProvider::from_source( + "discovery app", + source, + Duration::from_secs(config.custody.external_command_timeout_secs), + ) } } } diff --git a/src/config.rs b/src/config.rs @@ -20,6 +20,7 @@ pub const DEFAULT_ENV_PATH: &str = ".env"; pub struct MycConfig { pub service: MycServiceConfig, pub logging: MycLoggingConfig, + pub custody: MycCustodyConfig, pub paths: MycPathsConfig, pub persistence: MycPersistenceConfig, pub audit: MycAuditConfig, @@ -45,6 +46,12 @@ pub struct MycLoggingConfig { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] +pub struct MycCustodyConfig { + pub external_command_timeout_secs: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] pub struct MycPathsConfig { pub state_dir: PathBuf, pub signer_identity_backend: MycIdentityBackend, @@ -197,6 +204,7 @@ impl Default for MycConfig { Self { service: MycServiceConfig::default(), logging: MycLoggingConfig::default(), + custody: MycCustodyConfig::default(), paths: MycPathsConfig::default(), persistence: MycPersistenceConfig::default(), audit: MycAuditConfig::default(), @@ -226,6 +234,14 @@ impl Default for MycLoggingConfig { } } +impl Default for MycCustodyConfig { + fn default() -> Self { + Self { + external_command_timeout_secs: 10, + } + } +} + impl Default for MycPathsConfig { fn default() -> Self { Self { @@ -501,6 +517,11 @@ impl MycConfig { ); push_env_line( &mut lines, + "MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS", + self.custody.external_command_timeout_secs.to_string(), + ); + push_env_line( + &mut lines, "MYC_PATHS_STATE_DIR", self.paths.state_dir.display().to_string(), ); @@ -828,6 +849,12 @@ impl MycConfig { )); } + if self.custody.external_command_timeout_secs == 0 { + return Err(MycError::InvalidConfig( + "custody.external_command_timeout_secs must be greater than zero".to_owned(), + )); + } + validate_identity_source_config( "paths.signer_identity", &self.paths.signer_identity_source(), @@ -1100,6 +1127,10 @@ fn apply_env_entry( "MYC_LOGGING_STDOUT" => { config.logging.stdout = parse_bool_env(key, value, path, line_number)?; } + "MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS" => { + config.custody.external_command_timeout_secs = + parse_u64_env(key, value, path, line_number)?; + } "MYC_PATHS_STATE_DIR" => config.paths.state_dir = PathBuf::from(value), "MYC_PATHS_SIGNER_IDENTITY_BACKEND" => { config.paths.signer_identity_backend = @@ -2194,6 +2225,17 @@ MYC_UNKNOWN=nope } #[test] + fn validate_rejects_zero_external_command_timeout() { + let mut config = MycConfig::default(); + config.custody.external_command_timeout_secs = 0; + + let err = config.validate().expect_err("invalid custody timeout"); + assert!(err + .to_string() + .contains("custody.external_command_timeout_secs")); + } + + #[test] fn validate_rejects_non_loopback_observability_bind_addr() { let mut config = MycConfig::default(); config.observability.enabled = true; @@ -2450,6 +2492,7 @@ MYC_DISCOVERY_APP_IDENTITY_KEYRING_SERVICE_NAME=org.radroots.myc.test.discovery fn parse_and_validate_external_command_identity_backends() { let config = MycConfig::from_env_str( r#" +MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=21 MYC_PATHS_SIGNER_IDENTITY_BACKEND=external_command MYC_PATHS_SIGNER_IDENTITY_PATH=/usr/local/libexec/myc-signer-helper MYC_PATHS_USER_IDENTITY_BACKEND=external_command @@ -2479,6 +2522,7 @@ MYC_DISCOVERY_APP_IDENTITY_PATH=/usr/local/libexec/myc-discovery-helper config.discovery.app_identity_backend, Some(MycIdentityBackend::ExternalCommand) ); + assert_eq!(config.custody.external_command_timeout_secs, 21); assert_eq!( config .discovery @@ -2505,6 +2549,7 @@ MYC_DISCOVERY_APP_IDENTITY_PATH=/usr/local/libexec/myc-discovery-helper config.logging.output_dir, Some(PathBuf::from("/var/log/radroots/services/myc")) ); + assert_eq!(config.custody.external_command_timeout_secs, 10); assert_eq!( config.transport.delivery_policy, MycTransportDeliveryPolicy::Any @@ -2540,6 +2585,7 @@ MYC_SERVICE_INSTANCE_NAME=myc-dev MYC_LOGGING_FILTER=debug,myc=trace MYC_LOGGING_OUTPUT_DIR=/tmp/myc logs MYC_LOGGING_STDOUT=false +MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=17 MYC_PATHS_STATE_DIR=/tmp/myc state MYC_PATHS_SIGNER_IDENTITY_BACKEND=os_keyring MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/ignored-signer.json diff --git a/src/custody.rs b/src/custody.rs @@ -2,6 +2,7 @@ use std::fs; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::Arc; +use std::time::{Duration, Instant}; use nostr::nips::nip44::Version; use nostr::nips::{nip04, nip44}; @@ -108,6 +109,7 @@ enum MycIdentityProviderBackend { }, ExternalCommand { command_path: PathBuf, + timeout: Duration, executor: Arc<dyn MycExternalCommandExecutor>, }, } @@ -155,12 +157,19 @@ struct MycExternalCommandOutput { stderr: Vec<u8>, } +#[derive(Debug)] +enum MycExternalCommandExecuteError { + Io(std::io::Error), + TimedOut, +} + trait MycExternalCommandExecutor: Send + Sync { fn execute( &self, command_path: &PathBuf, request_json: &[u8], - ) -> Result<MycExternalCommandOutput, std::io::Error>; + timeout: Duration, + ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError>; } #[derive(Debug, Default)] @@ -171,17 +180,35 @@ impl MycExternalCommandExecutor for MycProcessCommandExecutor { &self, command_path: &PathBuf, request_json: &[u8], - ) -> Result<MycExternalCommandOutput, std::io::Error> { + timeout: Duration, + ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> { let mut child = Command::new(command_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .spawn()?; + .spawn() + .map_err(MycExternalCommandExecuteError::Io)?; if let Some(mut stdin) = child.stdin.take() { use std::io::Write; - stdin.write_all(request_json)?; + stdin + .write_all(request_json) + .map_err(MycExternalCommandExecuteError::Io)?; + } + let deadline = Instant::now() + timeout; + loop { + match child.try_wait().map_err(MycExternalCommandExecuteError::Io)? { + Some(_) => break, + None if Instant::now() >= deadline => { + let _ = child.kill(); + let _ = child.wait(); + return Err(MycExternalCommandExecuteError::TimedOut); + } + None => std::thread::sleep(Duration::from_millis(10)), + } } - let output = child.wait_with_output()?; + let output = child + .wait_with_output() + .map_err(MycExternalCommandExecuteError::Io)?; Ok(MycExternalCommandOutput { success: output.status.success(), status: output.status.code(), @@ -316,6 +343,7 @@ impl MycIdentityOperations for MycLoadedIdentityOperations { struct MycExternalCommandIdentityOperations { role: String, command_path: PathBuf, + timeout: Duration, public_identity: RadrootsIdentityPublic, public_key: RadrootsNostrPublicKey, executor: Arc<dyn MycExternalCommandExecutor>, @@ -325,6 +353,7 @@ impl MycExternalCommandIdentityOperations { fn new( role: String, command_path: PathBuf, + timeout: Duration, public_identity: RadrootsIdentityPublic, public_key: RadrootsNostrPublicKey, executor: Arc<dyn MycExternalCommandExecutor>, @@ -332,6 +361,7 @@ impl MycExternalCommandIdentityOperations { Self { role, command_path, + timeout, public_identity, public_key, executor, @@ -345,11 +375,20 @@ impl MycExternalCommandIdentityOperations { let request_json = serde_json::to_vec(request)?; let output = self .executor - .execute(&self.command_path, &request_json) - .map_err(|source| MycError::CustodyExternalCommandIo { - role: self.role.clone(), - path: self.command_path.clone(), - source, + .execute(&self.command_path, &request_json, self.timeout) + .map_err(|error| match error { + MycExternalCommandExecuteError::Io(source) => MycError::CustodyExternalCommandIo { + role: self.role.clone(), + path: self.command_path.clone(), + source, + }, + MycExternalCommandExecuteError::TimedOut => { + MycError::CustodyExternalCommandTimedOut { + role: self.role.clone(), + path: self.command_path.clone(), + timeout_secs: self.timeout.as_secs(), + } + } })?; if !output.success { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned(); @@ -513,6 +552,7 @@ impl MycIdentityProvider { pub fn from_source( role: impl Into<String>, source: MycIdentitySourceSpec, + external_command_timeout: Duration, ) -> Result<Self, MycError> { let role = role.into(); let backend = match source.backend { @@ -565,6 +605,7 @@ impl MycIdentityProvider { })?; MycIdentityProviderBackend::ExternalCommand { command_path, + timeout: external_command_timeout, executor: Arc::new(MycProcessCommandExecutor), } } @@ -673,16 +714,21 @@ impl MycIdentityProvider { match &self.backend { MycIdentityProviderBackend::ExternalCommand { command_path, + timeout, executor, } => { - let (public_identity, public_key) = - self.load_external_command_identity(command_path, executor.as_ref())?; + let (public_identity, public_key) = self.load_external_command_identity( + command_path, + *timeout, + executor.as_ref(), + )?; Ok(MycActiveIdentity::from_operations( public_identity.clone(), public_key, Arc::new(MycExternalCommandIdentityOperations::new( self.role.clone(), command_path.clone(), + *timeout, public_identity, public_key, executor.clone(), @@ -820,9 +866,10 @@ impl MycIdentityProvider { match &self.backend { MycIdentityProviderBackend::ExternalCommand { command_path, + timeout, executor, } => self - .load_external_command_identity(command_path, executor.as_ref()) + .load_external_command_identity(command_path, *timeout, executor.as_ref()) .map(|(identity, _)| identity), _ => self.load_identity().map(|identity| identity.to_public()), } @@ -831,6 +878,7 @@ impl MycIdentityProvider { fn load_external_command_identity( &self, command_path: &PathBuf, + timeout: Duration, executor: &dyn MycExternalCommandExecutor, ) -> Result<(RadrootsIdentityPublic, RadrootsNostrPublicKey), MycError> { let request_json = serde_json::to_vec(&MycExternalCommandRequest { @@ -841,11 +889,20 @@ impl MycIdentityProvider { content: None, })?; let output = executor - .execute(command_path, &request_json) - .map_err(|source| MycError::CustodyExternalCommandIo { - role: self.role.clone(), - path: command_path.clone(), - source, + .execute(command_path, &request_json, timeout) + .map_err(|error| match error { + MycExternalCommandExecuteError::Io(source) => MycError::CustodyExternalCommandIo { + role: self.role.clone(), + path: command_path.clone(), + source, + }, + MycExternalCommandExecuteError::TimedOut => { + MycError::CustodyExternalCommandTimedOut { + role: self.role.clone(), + path: command_path.clone(), + timeout_secs: timeout.as_secs(), + } + } })?; if !output.success { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned(); @@ -1361,7 +1418,8 @@ mod tests { &self, _command_path: &PathBuf, request_json: &[u8], - ) -> Result<MycExternalCommandOutput, std::io::Error> { + _timeout: Duration, + ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> { let request: MycExternalCommandRequest = serde_json::from_slice(request_json).expect("request"); self.requests @@ -1519,6 +1577,7 @@ mod tests { }, backend: MycIdentityProviderBackend::ExternalCommand { command_path, + timeout: Duration::from_secs(10), executor: executor.clone(), }, }, @@ -1536,7 +1595,12 @@ mod tests { ); let provider = - MycIdentityProvider::from_source("signer", fixture_source(&path)).expect("provider"); + MycIdentityProvider::from_source( + "signer", + fixture_source(&path), + Duration::from_secs(10), + ) + .expect("provider"); let identity = provider.load_identity().expect("identity"); assert_eq!( @@ -1773,4 +1837,84 @@ mod tests { assert!(!active.nostr_client().has_signer().await); assert!(!active.nostr_client_owned().has_signer().await); } + + #[derive(Debug, Default)] + struct TimeoutExternalCommandExecutor; + + impl MycExternalCommandExecutor for TimeoutExternalCommandExecutor { + fn execute( + &self, + _command_path: &PathBuf, + _request_json: &[u8], + _timeout: Duration, + ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> { + Err(MycExternalCommandExecuteError::TimedOut) + } + } + + #[test] + fn external_command_provider_maps_describe_timeout() { + let provider = MycIdentityProvider { + role: "signer".to_owned(), + source: MycIdentitySourceSpec { + backend: MycIdentityBackend::ExternalCommand, + path: Some(PathBuf::from("/tmp/signer-helper")), + keyring_account_id: None, + keyring_service_name: None, + profile_path: None, + }, + backend: MycIdentityProviderBackend::ExternalCommand { + command_path: PathBuf::from("/tmp/signer-helper"), + timeout: Duration::from_secs(7), + executor: Arc::new(TimeoutExternalCommandExecutor), + }, + }; + + let err = provider.load_active_identity().err().expect("timeout"); + assert!(matches!( + err, + MycError::CustodyExternalCommandTimedOut { + ref role, + ref path, + timeout_secs: 7, + } if role == "signer" && path == &PathBuf::from("/tmp/signer-helper") + )); + } + + #[test] + fn external_command_provider_maps_operation_timeout() { + let identity = RadrootsIdentity::from_secret_key_str( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + .expect("identity"); + let public_identity = identity.to_public(); + let public_key = identity.public_key(); + let active = MycActiveIdentity::from_operations( + public_identity.clone(), + public_key, + Arc::new(MycExternalCommandIdentityOperations::new( + "signer".to_owned(), + PathBuf::from("/tmp/signer-helper"), + Duration::from_secs(11), + public_identity, + public_key, + Arc::new(TimeoutExternalCommandExecutor), + )), + ); + + let err = active + .sign_event_builder( + RadrootsNostrEventBuilder::text_note("timeout"), + "timeout event", + ) + .expect_err("timeout"); + assert!(matches!( + err, + MycError::CustodyExternalCommandTimedOut { + ref role, + ref path, + timeout_secs: 11, + } if role == "signer" && path == &PathBuf::from("/tmp/signer-helper") + )); + } } diff --git a/src/discovery.rs b/src/discovery.rs @@ -288,9 +288,12 @@ impl MycDiscoveryContext { } let app_identity = match discovery.app_identity_source() { - Some(source) => { - MycIdentityProvider::from_source("discovery app", source)?.load_active_identity()? - } + Some(source) => MycIdentityProvider::from_source( + "discovery app", + source, + Duration::from_secs(runtime.config().custody.external_command_timeout_secs), + )? + .load_active_identity()?, None => runtime.signer_identity().clone(), }; let public_relays = discovery.resolved_public_relays(&runtime.config().transport)?; diff --git a/src/error.rs b/src/error.rs @@ -209,6 +209,14 @@ pub enum MycError { source: std::io::Error, }, #[error( + "external custody command for {role} identity at {path} timed out after {timeout_secs}s" + )] + CustodyExternalCommandTimedOut { + role: String, + path: PathBuf, + timeout_secs: u64, + }, + #[error( "external custody command for {role} identity at {path} failed with status {status}: {stderr}" )] CustodyExternalCommandFailed { diff --git a/src/lib.rs b/src/lib.rs @@ -26,11 +26,11 @@ pub use audit::{ }; pub use audit_sqlite::MycSqliteOperationAuditStore; pub use config::{ - DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycDiscoveryConfig, - MycDiscoveryMetadataConfig, MycIdentityBackend, MycIdentitySourceSpec, MycLoggingConfig, - MycObservabilityConfig, MycPathsConfig, MycPersistenceConfig, MycPolicyConfig, - MycRuntimeAuditBackend, MycServiceConfig, MycSignerStateBackend, MycTransportConfig, - MycTransportDeliveryPolicy, + DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycCustodyConfig, + MycDiscoveryConfig, MycDiscoveryMetadataConfig, MycIdentityBackend, MycIdentitySourceSpec, + MycLoggingConfig, MycObservabilityConfig, MycPathsConfig, MycPersistenceConfig, + MycPolicyConfig, MycRuntimeAuditBackend, MycServiceConfig, MycSignerStateBackend, + MycTransportConfig, MycTransportDeliveryPolicy, }; pub use control::{MycAcceptedConnectionOutput, MycAuthorizedReplayOutput}; pub use custody::{ diff --git a/src/operability/mod.rs b/src/operability/mod.rs @@ -411,8 +411,12 @@ fn collect_custody_status(runtime: &MycRuntime) -> Result<MycCustodyStatusEvalua let discovery_app = if runtime.config().discovery.enabled { match runtime.config().discovery.app_identity_source() { Some(source) => Some( - crate::custody::MycIdentityProvider::from_source("discovery app", source)? - .probe_status(), + crate::custody::MycIdentityProvider::from_source( + "discovery app", + source, + Duration::from_secs(runtime.config().custody.external_command_timeout_secs), + )? + .probe_status(), ), None => Some( runtime diff --git a/src/persistence.rs b/src/persistence.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Component, Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use nostr::PublicKey; use radroots_nostr_signer::prelude::{ @@ -452,13 +452,25 @@ pub fn verify_restored_state( require_existing_restore_file(&delivery_outbox_path, "delivery outbox".to_owned())?; let signer_identity_provider = - MycIdentityProvider::from_source("signer", config.paths.signer_identity_source())?; + MycIdentityProvider::from_source( + "signer", + config.paths.signer_identity_source(), + Duration::from_secs(config.custody.external_command_timeout_secs), + )?; let signer_identity = signer_identity_provider.load_active_identity()?; let user_identity_provider = - MycIdentityProvider::from_source("user", config.paths.user_identity_source())?; + MycIdentityProvider::from_source( + "user", + config.paths.user_identity_source(), + Duration::from_secs(config.custody.external_command_timeout_secs), + )?; let user_identity = user_identity_provider.load_active_identity()?; let discovery_app_identity = match config.discovery.app_identity_source() { - Some(source) => Some(MycIdentityProvider::from_source("discovery app", source)?), + Some(source) => Some(MycIdentityProvider::from_source( + "discovery app", + source, + Duration::from_secs(config.custody.external_command_timeout_secs), + )?), None => None, } .map(|provider| provider.load_active_identity()) @@ -548,7 +560,11 @@ fn import_signer_state_json_to_sqlite( let source_store = RadrootsNostrFileSignerStore::new(&source_path); let source_state = source_store.load()?; let signer_identity_provider = - MycIdentityProvider::from_source("signer", config.paths.signer_identity_source())?; + MycIdentityProvider::from_source( + "signer", + config.paths.signer_identity_source(), + Duration::from_secs(config.custody.external_command_timeout_secs), + )?; let configured_signer_identity = signer_identity_provider.load_identity()?.to_public(); if let Some(imported_signer_identity) = source_state.signer_identity.as_ref() { if imported_signer_identity.id != configured_signer_identity.id {