myc

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

commit 16f972d0b3e5443ae74775dbdf9fe899074b3646
parent ef737fbb510a5ec20476f202462fcda15932a6e3
Author: triesap <tyson@radroots.org>
Date:   Thu, 26 Mar 2026 23:59:07 +0000

custody: add active identity operations

Diffstat:
Msrc/app/runtime.rs | 41++++++++++++++++++++++++-----------------
Msrc/control.rs | 10++++++++--
Msrc/custody.rs | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/discovery.rs | 34++++++++++++++--------------------
Msrc/lib.rs | 2+-
Msrc/operability/mod.rs | 17+++++++----------
Msrc/transport.rs | 30++++++++++++++----------------
Msrc/transport/nip46.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mtests/nip46_e2e.rs | 73+++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtests/operability_cli.rs | 11+++++++----
Mtests/operability_e2e.rs | 16++++++++++------
11 files changed, 315 insertions(+), 142 deletions(-)

diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -13,7 +13,7 @@ use crate::config::{ MycAuditConfig, MycConfig, MycIdentitySourceSpec, MycPersistenceConfig, MycRuntimeAuditBackend, MycSignerStateBackend, MycTransportDeliveryPolicy, }; -use crate::custody::MycIdentityProvider; +use crate::custody::{MycActiveIdentity, MycIdentityProvider}; use crate::discovery::MycDiscoveryContext; use crate::error::MycError; use crate::operability::server::run_observability_server; @@ -25,7 +25,7 @@ use crate::policy::MycPolicyContext; use crate::transport::{ MycNip46Service, MycNostrTransport, MycPublishOutcome, MycTransportSnapshot, }; -use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; +use radroots_identity::RadrootsIdentityPublic; use radroots_nostr_signer::prelude::{ RadrootsNostrFileSignerStore, RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerManager, @@ -73,8 +73,8 @@ pub struct MycStartupSnapshot { pub struct MycSignerContext { signer_identity_provider: MycIdentityProvider, user_identity_provider: MycIdentityProvider, - signer_identity: RadrootsIdentity, - user_identity: RadrootsIdentity, + signer_identity: MycActiveIdentity, + user_identity: MycActiveIdentity, signer_store: Arc<dyn RadrootsNostrSignerStore>, operation_audit_store: Arc<dyn MycOperationAuditStore>, policy: MycPolicyContext, @@ -124,7 +124,7 @@ impl MycRuntime { &self.config } - pub fn signer_identity(&self) -> &RadrootsIdentity { + pub fn signer_identity(&self) -> &MycActiveIdentity { self.signer.signer_identity() } @@ -132,7 +132,7 @@ impl MycRuntime { self.signer.signer_public_identity() } - pub fn user_identity(&self) -> &RadrootsIdentity { + pub fn user_identity(&self) -> &MycActiveIdentity { self.signer.user_identity() } @@ -698,7 +698,7 @@ impl MycRuntime { fn recovery_publisher_identity( &self, record: &MycDeliveryOutboxRecord, - ) -> Result<RadrootsIdentity, MycError> { + ) -> Result<MycActiveIdentity, MycError> { if record.kind != MycDeliveryOutboxKind::DiscoveryHandlerPublish { return Ok(self.signer_identity().clone()); } @@ -966,7 +966,7 @@ impl MycRuntimePaths { } impl MycSignerContext { - pub fn signer_identity(&self) -> &RadrootsIdentity { + pub fn signer_identity(&self) -> &MycActiveIdentity { &self.signer_identity } @@ -982,7 +982,7 @@ impl MycSignerContext { self.signer_identity.to_public() } - pub fn user_identity(&self) -> &RadrootsIdentity { + pub fn user_identity(&self) -> &MycActiveIdentity { &self.user_identity } @@ -1048,8 +1048,8 @@ impl MycSignerContext { MycIdentityProvider::from_source("signer", signer_identity_source)?; let user_identity_provider = MycIdentityProvider::from_source("user", user_identity_source)?; - let signer_identity = signer_identity_provider.load_identity()?; - let user_identity = user_identity_provider.load_identity()?; + 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)?; let operation_audit_store = Self::build_operation_audit_store(persistence, &paths.audit_dir, audit_config)?; @@ -1558,8 +1558,12 @@ mod tests { .mark_publish_workflow_published(&workflow.workflow_id) .expect("mark workflow published"); - let event = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "recovery") - .sign_with_keys(runtime.signer_identity().keys()) + let event = runtime + .signer_identity() + .sign_event_builder( + RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "recovery"), + "recovery test", + ) .expect("sign event"); let outbox_record = MycDeliveryOutboxRecord::new( MycDeliveryOutboxKind::ListenerResponsePublish, @@ -1666,10 +1670,13 @@ mod tests { let workflow = manager .begin_connect_secret_publish_finalization(&connection.connection_id) .expect("begin workflow"); - let event = - RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "queued-recovery") - .sign_with_keys(runtime.signer_identity().keys()) - .expect("sign event"); + let event = runtime + .signer_identity() + .sign_event_builder( + RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "queued-recovery"), + "queued recovery test", + ) + .expect("sign event"); let outbox_record = MycDeliveryOutboxRecord::new( MycDeliveryOutboxKind::ListenerResponsePublish, event, diff --git a/src/control.rs b/src/control.rs @@ -158,7 +158,10 @@ pub async fn accept_client_uri( )?; let response_relays = merge_relays(&client_uri.relays, &preferred_relays); let workflow = manager.begin_connect_secret_publish_finalization(&connection.connection_id)?; - let event = match event.sign_with_keys(runtime.signer_identity().keys()) { + let event = match runtime + .signer_identity() + .sign_event_builder(event, "connect accept response") + { Ok(event) => event, Err(error) => { return Err(cancel_connect_accept_workflow_on_error( @@ -413,7 +416,10 @@ async fn replay_authorized_request( let connection = manager.get_connection(connection_id)?.ok_or_else(|| { MycError::InvalidOperation(format!("connection `{connection_id}` was not found")) })?; - let event = match event.sign_with_keys(runtime.signer_identity().keys()) { + let event = match runtime + .signer_identity() + .sign_event_builder(event, "authorized auth replay response") + { Ok(event) => event, Err(error) => { return Err(cancel_auth_replay_workflow_on_error( diff --git a/src/custody.rs b/src/custody.rs @@ -1,7 +1,12 @@ use std::path::PathBuf; use std::sync::Arc; +use nostr::nips::nip44::Version; +use nostr::nips::{nip04, nip44}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; +use radroots_nostr::prelude::{ + RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrPublicKey, +}; use radroots_nostr_accounts::prelude::{ RadrootsNostrSecretVault, RadrootsNostrSecretVaultOsKeyring, }; @@ -10,6 +15,11 @@ use serde::Serialize; use crate::config::{MycIdentityBackend, MycIdentitySourceSpec}; use crate::error::MycError; +#[derive(Clone)] +pub struct MycActiveIdentity { + identity: Arc<RadrootsIdentity>, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct MycIdentityStatusOutput { pub backend: MycIdentityBackend, @@ -146,8 +156,12 @@ impl MycIdentityProvider { } } - pub fn resolved_status(&self, identity: &RadrootsIdentity) -> MycIdentityStatusOutput { - self.status_with_result(Ok(identity)) + pub fn load_active_identity(&self) -> Result<MycActiveIdentity, MycError> { + self.load_identity().map(MycActiveIdentity::new) + } + + pub fn resolved_status(&self, identity: &MycActiveIdentity) -> MycIdentityStatusOutput { + self.status_with_result(Ok(identity.as_identity())) } pub fn probe_status(&self) -> MycIdentityStatusOutput { @@ -210,6 +224,123 @@ impl MycIdentityProvider { } } +impl MycActiveIdentity { + pub fn new(identity: RadrootsIdentity) -> Self { + Self { + identity: Arc::new(identity), + } + } + + pub fn id(&self) -> RadrootsIdentityId { + self.identity.id() + } + + pub fn public_key(&self) -> RadrootsNostrPublicKey { + self.identity.public_key() + } + + pub fn public_key_hex(&self) -> String { + self.identity.public_key_hex() + } + + pub fn secret_key_hex(&self) -> String { + self.identity.secret_key_hex() + } + + pub fn to_public(&self) -> radroots_identity::RadrootsIdentityPublic { + self.identity.to_public() + } + + pub fn nostr_client(&self) -> RadrootsNostrClient { + RadrootsNostrClient::from_identity(self.as_identity()) + } + + pub fn nostr_client_owned(&self) -> RadrootsNostrClient { + RadrootsNostrClient::from_identity_owned((*self.identity).clone()) + } + + pub fn sign_event_builder( + &self, + builder: RadrootsNostrEventBuilder, + operation: &str, + ) -> Result<RadrootsNostrEvent, MycError> { + builder + .sign_with_keys(self.identity.keys()) + .map_err(|error| { + MycError::InvalidOperation(format!("failed to sign {operation} event: {error}")) + }) + } + + pub fn sign_unsigned_event( + &self, + unsigned_event: nostr::UnsignedEvent, + operation: &str, + ) -> Result<nostr::Event, MycError> { + unsigned_event + .sign_with_keys(self.identity.keys()) + .map_err(|error| { + MycError::InvalidOperation(format!("failed to sign {operation}: {error}")) + }) + } + + pub fn nip04_encrypt( + &self, + public_key: &RadrootsNostrPublicKey, + plaintext: impl Into<String>, + ) -> Result<String, MycError> { + nip04::encrypt( + self.identity.keys().secret_key(), + public_key, + plaintext.into(), + ) + .map_err(|error| MycError::Nip46Encrypt(error.to_string())) + } + + pub fn nip04_decrypt( + &self, + public_key: &RadrootsNostrPublicKey, + ciphertext: impl AsRef<str>, + ) -> Result<String, MycError> { + nip04::decrypt( + self.identity.keys().secret_key(), + public_key, + ciphertext.as_ref(), + ) + .map_err(|error| MycError::Nip46Decrypt(error.to_string())) + } + + pub fn nip44_encrypt( + &self, + public_key: &RadrootsNostrPublicKey, + plaintext: impl Into<String>, + ) -> Result<String, MycError> { + nip44::encrypt( + self.identity.keys().secret_key(), + public_key, + plaintext.into(), + Version::V2, + ) + .map_err(|error| MycError::Nip46Encrypt(error.to_string())) + } + + pub fn nip44_decrypt( + &self, + public_key: &RadrootsNostrPublicKey, + ciphertext: impl AsRef<str>, + ) -> Result<String, MycError> { + nip44::decrypt( + self.identity.keys().secret_key(), + public_key, + ciphertext.as_ref(), + ) + .map_err(|error| MycError::Nip46Decrypt(error.to_string())) + } + + pub(crate) fn as_identity(&self) -> &RadrootsIdentity { + self.identity.as_ref() + } +} + impl MycIdentityStatusOutput { pub fn with_inherited_from(mut self, inherited_from: impl Into<String>) -> Self { self.inherited_from = Some(inherited_from.into()); diff --git a/src/discovery.rs b/src/discovery.rs @@ -3,12 +3,11 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ - RadrootsNostrApplicationHandlerSpec, RadrootsNostrClient, RadrootsNostrError, - RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrMetadata, - RadrootsNostrRelayUrl, radroots_nostr_build_application_handler_event, - radroots_nostr_filter_tag, radroots_nostr_metadata_has_fields, radroots_nostr_tag_first_value, + RadrootsNostrApplicationHandlerSpec, RadrootsNostrError, RadrootsNostrEvent, + RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrMetadata, RadrootsNostrRelayUrl, + radroots_nostr_build_application_handler_event, radroots_nostr_filter_tag, + radroots_nostr_metadata_has_fields, radroots_nostr_tag_first_value, }; use radroots_nostr_connect::prelude::{RadrootsNostrConnectBunkerUri, RadrootsNostrConnectUri}; use radroots_nostr_signer::prelude::RadrootsNostrSignerRequestId; @@ -18,7 +17,7 @@ use tokio::task::JoinSet; use crate::app::MycRuntime; use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord}; use crate::config::MycDiscoveryMetadataConfig; -use crate::custody::MycIdentityProvider; +use crate::custody::{MycActiveIdentity, MycIdentityProvider}; use crate::error::MycError; use crate::outbox::{MycDeliveryOutboxKind, MycDeliveryOutboxRecord}; use crate::transport::{MycNostrTransport, MycPublishOutcome, MycRelayPublishResult}; @@ -32,8 +31,8 @@ const DISCOVERY_RELAY_FETCH_CONCURRENCY_LIMIT: usize = 8; #[derive(Clone)] pub struct MycDiscoveryContext { - app_identity: RadrootsIdentity, - signer_identity: RadrootsIdentity, + app_identity: MycActiveIdentity, + signer_identity: MycActiveIdentity, domain: String, handler_identifier: String, public_relays: Vec<RadrootsNostrRelayUrl>, @@ -290,7 +289,7 @@ impl MycDiscoveryContext { let app_identity = match discovery.app_identity_source() { Some(source) => { - MycIdentityProvider::from_source("discovery app", source)?.load_identity()? + MycIdentityProvider::from_source("discovery app", source)?.load_active_identity()? } None => runtime.signer_identity().clone(), }; @@ -322,11 +321,11 @@ impl MycDiscoveryContext { }) } - pub fn app_identity(&self) -> &RadrootsIdentity { + pub fn app_identity(&self) -> &MycActiveIdentity { &self.app_identity } - pub fn signer_identity(&self) -> &RadrootsIdentity { + pub fn signer_identity(&self) -> &MycActiveIdentity { &self.signer_identity } @@ -452,13 +451,8 @@ impl MycDiscoveryContext { pub fn build_signed_handler_event(&self) -> Result<RadrootsNostrEvent, MycError> { let builder = radroots_nostr_build_application_handler_event(&self.build_handler_spec())?; - builder - .sign_with_keys(self.app_identity.keys()) - .map_err(|error| { - MycError::InvalidOperation(format!( - "failed to sign NIP-89 application handler event: {error}" - )) - }) + self.app_identity + .sign_event_builder(builder, "NIP-89 application handler") } pub fn write_bundle( @@ -1436,7 +1430,7 @@ async fn fetch_live_nip89_events_for_relay( context: &MycDiscoveryContext, relay: &RadrootsNostrRelayUrl, ) -> Result<Vec<MycSourcedLiveNip89Event>, MycError> { - let client = RadrootsNostrClient::from_identity(context.app_identity()); + let client = context.app_identity().nostr_client(); let _ = client.add_relay(relay.as_str()).await?; client .try_connect_relay( @@ -2088,7 +2082,7 @@ impl MycDiscoveryBundleOutput { fn render_nostrconnect_url( template: &str, - signer_identity: &RadrootsIdentity, + signer_identity: &MycActiveIdentity, public_relays: &[RadrootsNostrRelayUrl], ) -> Result<String, MycError> { let bunker_uri = RadrootsNostrConnectUri::Bunker(RadrootsNostrConnectBunkerUri { diff --git a/src/lib.rs b/src/lib.rs @@ -31,7 +31,7 @@ pub use config::{ MycTransportDeliveryPolicy, }; pub use control::{MycAcceptedConnectionOutput, MycAuthorizedReplayOutput}; -pub use custody::{MycIdentityProvider, MycIdentityStatusOutput}; +pub use custody::{MycActiveIdentity, MycIdentityProvider, MycIdentityStatusOutput}; pub use discovery::{ MycDiscoveryBundleManifest, MycDiscoveryBundleOutput, MycDiscoveryContext, MycDiscoveryDiffOutput, MycDiscoveryLiveStatus, MycDiscoveryRelayFetchStatus, diff --git a/src/operability/mod.rs b/src/operability/mod.rs @@ -4,10 +4,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Duration; -use radroots_identity::RadrootsIdentity; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrRelayStatus, RadrootsNostrRelayUrl, -}; +use radroots_nostr::prelude::{RadrootsNostrRelayStatus, RadrootsNostrRelayUrl}; use radroots_nostr_signer::prelude::{ RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerPublishWorkflowState, RadrootsNostrSignerRequestDecision, @@ -19,7 +16,7 @@ use tokio::task::JoinSet; use crate::app::MycRuntime; use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome}; use crate::config::{MycRuntimeAuditBackend, MycSignerStateBackend, MycTransportDeliveryPolicy}; -use crate::custody::MycIdentityStatusOutput; +use crate::custody::{MycActiveIdentity, MycIdentityStatusOutput}; use crate::discovery::MycDiscoveryContext; use crate::error::MycError; use crate::outbox::{MycDeliveryOutboxRecord, MycDeliveryOutboxStatus, now_unix_secs}; @@ -1176,7 +1173,7 @@ fn required_available_relays( } async fn probe_relays( - identity: &RadrootsIdentity, + identity: &MycActiveIdentity, relays: &[RadrootsNostrRelayUrl], connect_timeout_secs: u64, ) -> Result<Vec<MycRelayProbe>, MycError> { @@ -1198,7 +1195,7 @@ async fn probe_relays( let Some((relay_index, relay)) = pending.next() else { break; }; - let identity = (*identity).clone(); + let identity = identity.clone(); join_set.spawn(async move { let probe = probe_relay(identity, relay.clone(), connect_timeout_secs).await; (relay_index, probe) @@ -1218,7 +1215,7 @@ async fn probe_relays( let Some((relay_index, relay)) = pending.next() else { break; }; - let identity = (*identity).clone(); + let identity = identity.clone(); join_set.spawn(async move { let probe = probe_relay(identity, relay.clone(), connect_timeout_secs).await; (relay_index, probe) @@ -1235,12 +1232,12 @@ async fn probe_relays( } async fn probe_relay( - identity: RadrootsIdentity, + identity: MycActiveIdentity, relay: RadrootsNostrRelayUrl, connect_timeout_secs: u64, ) -> Result<MycRelayProbe, MycError> { let relay_url = relay.to_string(); - let client = RadrootsNostrClient::from_identity_owned(identity); + let client = identity.nostr_client_owned(); client .add_relay(relay.as_str()) .await diff --git a/src/transport.rs b/src/transport.rs @@ -3,7 +3,6 @@ pub mod nip46; use std::collections::{BTreeMap, BTreeSet}; use std::time::Duration; -use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrOutput, RadrootsNostrRelayUrl, @@ -12,6 +11,7 @@ use serde::Serialize; use tokio::time::sleep; use crate::config::{MycTransportConfig, MycTransportDeliveryPolicy}; +use crate::custody::MycActiveIdentity; use crate::error::MycError; pub use nip46::{MycNip46Handler, MycNip46Service}; @@ -72,14 +72,14 @@ struct MycPublishSettings { impl MycNostrTransport { pub fn bootstrap( config: &MycTransportConfig, - signer_identity: &RadrootsIdentity, + signer_identity: &MycActiveIdentity, ) -> Result<Option<Self>, MycError> { if !config.enabled { return Ok(None); } Ok(Some(Self { - client: RadrootsNostrClient::from_identity(signer_identity), + client: signer_identity.nostr_client(), relays: config.parse_relays()?, connect_timeout_secs: config.connect_timeout_secs, delivery_policy: config.delivery_policy, @@ -118,7 +118,7 @@ impl MycNostrTransport { } pub async fn publish_once( - signer_identity: &RadrootsIdentity, + signer_identity: &MycActiveIdentity, relays: &[RadrootsNostrRelayUrl], config: &MycTransportConfig, operation: &str, @@ -130,16 +130,12 @@ impl MycNostrTransport { )); } - let event = event - .sign_with_keys(signer_identity.keys()) - .map_err(|error| { - MycError::InvalidOperation(format!("failed to sign publish event: {error}")) - })?; + let event = signer_identity.sign_event_builder(event, "publish")?; Self::publish_event_once(signer_identity, relays, config, operation, &event).await } pub async fn publish_event_once( - signer_identity: &RadrootsIdentity, + signer_identity: &MycActiveIdentity, relays: &[RadrootsNostrRelayUrl], config: &MycTransportConfig, operation: &str, @@ -153,7 +149,7 @@ impl MycNostrTransport { let settings = MycPublishSettings::from_config(config); publish_with_policy(relays, &settings, operation, || async { - let client = RadrootsNostrClient::from_identity(signer_identity); + let client = signer_identity.nostr_client(); for relay in relays { client .add_relay(relay.as_str()) @@ -517,21 +513,23 @@ mod tests { use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; - use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrEventId, RadrootsNostrOutput, RadrootsNostrRelayUrl, }; use tokio::time::Instant; use crate::config::{MycTransportConfig, MycTransportDeliveryPolicy}; + use crate::custody::MycActiveIdentity; use super::{MycNostrTransport, MycPublishSettings, MycTransportSnapshot, publish_with_policy}; - fn signer_identity() -> RadrootsIdentity { - RadrootsIdentity::from_secret_key_str( - "1111111111111111111111111111111111111111111111111111111111111111", + fn signer_identity() -> MycActiveIdentity { + MycActiveIdentity::new( + radroots_identity::RadrootsIdentity::from_secret_key_str( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + .expect("identity"), ) - .expect("identity") } #[test] diff --git a/src/transport/nip46.rs b/src/transport/nip46.rs @@ -1,8 +1,5 @@ use std::future::Future; -use nostr::nips::nip04; -use nostr::nips::nip44; -use nostr::nips::nip44::Version; use radroots_nostr::prelude::{ RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl, @@ -75,12 +72,10 @@ impl MycNip46Handler { &self, event: &RadrootsNostrEvent, ) -> Result<RadrootsNostrConnectRequestMessage, MycError> { - let decrypted = nip44::decrypt( - self.signer.signer_identity().keys().secret_key(), - &event.pubkey, - &event.content, - ) - .map_err(|err| MycError::Nip46Decrypt(err.to_string()))?; + let decrypted = self + .signer + .signer_identity() + .nip44_decrypt(&event.pubkey, &event.content)?; serde_json::from_str(&decrypted) .map_err(radroots_nostr_connect::prelude::RadrootsNostrConnectError::from) .map_err(Into::into) @@ -95,13 +90,10 @@ impl MycNip46Handler { let envelope = response.into_envelope(request_id.into())?; let payload = serde_json::to_string(&envelope) .map_err(|err| MycError::Nip46Encrypt(err.to_string()))?; - let ciphertext = nip44::encrypt( - self.signer.signer_identity().keys().secret_key(), - &client_public_key, - payload, - Version::V2, - ) - .map_err(|err| MycError::Nip46Encrypt(err.to_string()))?; + let ciphertext = self + .signer + .signer_identity() + .nip44_encrypt(&client_public_key, payload)?; Ok(RadrootsNostrEventBuilder::new( radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND), @@ -541,7 +533,11 @@ impl MycNip46Handler { }); } - match unsigned_event.sign_with_keys(self.signer.user_identity().keys()) { + match self + .signer + .user_identity() + .sign_unsigned_event(unsigned_event, "managed user sign_event") + { Ok(event) => Ok(RadrootsNostrConnectResponse::SignedEvent(event)), Err(error) => Ok(RadrootsNostrConnectResponse::Error { result: None, @@ -554,12 +550,15 @@ impl MycNip46Handler { &self, request: RadrootsNostrConnectRequest, ) -> Result<RadrootsNostrConnectResponse, MycError> { - let user_secret_key = self.signer.user_identity().keys().secret_key(); Ok(match request { RadrootsNostrConnectRequest::Nip04Encrypt { public_key, plaintext, - } => match nip04::encrypt(user_secret_key, &public_key, plaintext) { + } => match self + .signer + .user_identity() + .nip04_encrypt(&public_key, plaintext) + { Ok(ciphertext) => RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext), Err(error) => RadrootsNostrConnectResponse::Error { result: None, @@ -569,7 +568,11 @@ impl MycNip46Handler { RadrootsNostrConnectRequest::Nip04Decrypt { public_key, ciphertext, - } => match nip04::decrypt(user_secret_key, &public_key, ciphertext) { + } => match self + .signer + .user_identity() + .nip04_decrypt(&public_key, ciphertext) + { Ok(plaintext) => RadrootsNostrConnectResponse::Nip04Decrypt(plaintext), Err(error) => RadrootsNostrConnectResponse::Error { result: None, @@ -579,7 +582,11 @@ impl MycNip46Handler { RadrootsNostrConnectRequest::Nip44Encrypt { public_key, plaintext, - } => match nip44::encrypt(user_secret_key, &public_key, plaintext, Version::V2) { + } => match self + .signer + .user_identity() + .nip44_encrypt(&public_key, plaintext) + { Ok(ciphertext) => RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext), Err(error) => RadrootsNostrConnectResponse::Error { result: None, @@ -589,7 +596,11 @@ impl MycNip46Handler { RadrootsNostrConnectRequest::Nip44Decrypt { public_key, ciphertext, - } => match nip44::decrypt(user_secret_key, &public_key, ciphertext) { + } => match self + .signer + .user_identity() + .nip44_decrypt(&public_key, ciphertext) + { Ok(plaintext) => RadrootsNostrConnectResponse::Nip44Decrypt(plaintext), Err(error) => RadrootsNostrConnectResponse::Error { result: None, @@ -692,18 +703,22 @@ impl MycNip46Service { let response_event = self.handler .build_response_event(event.pubkey, request_id.as_str(), response)?; - let response_event = - match response_event.sign_with_keys(self.handler.signer.signer_identity().keys()) { - Ok(event) => event, - Err(error) => { - self.record_listener_publish_local_rejection( - connection_id.as_ref(), - request_id.as_str(), - format!("failed to sign NIP-46 response event: {error}"), - ); - continue; - } - }; + let response_event = match self + .handler + .signer + .signer_identity() + .sign_event_builder(response_event, "NIP-46 response") + { + Ok(event) => event, + Err(error) => { + self.record_listener_publish_local_rejection( + connection_id.as_ref(), + request_id.as_str(), + format!("failed to sign NIP-46 response event: {error}"), + ); + continue; + } + }; let mut workflow_id = None; if let Some(connect_connection_id) = consume_connect_secret_for.as_ref() { @@ -1296,8 +1311,9 @@ mod tests { let response_builder = handler .build_response_event(event.pubkey, "req-1", RadrootsNostrConnectResponse::Pong) .expect("response builder"); - let response_event = response_builder - .sign_with_keys(runtime.signer_identity().keys()) + let response_event = runtime + .signer_identity() + .sign_event_builder(response_builder, "test response") .expect("sign response"); let decrypted = nip44::decrypt( client_keys().secret_key(), diff --git a/tests/nip46_e2e.rs b/tests/nip46_e2e.rs @@ -6,8 +6,8 @@ use std::time::Duration; use futures_util::{SinkExt, StreamExt}; use myc::control; use myc::{ - MycConfig, MycConnectionApproval, MycDeliveryOutboxKind, MycDeliveryOutboxRecord, - MycDeliveryOutboxStatus, MycDiscoveryContext, MycDiscoveryLiveStatus, + MycActiveIdentity, MycConfig, MycConnectionApproval, MycDeliveryOutboxKind, + MycDeliveryOutboxRecord, MycDeliveryOutboxStatus, MycDiscoveryContext, MycDiscoveryLiveStatus, MycDiscoveryRelayFetchStatus, MycDiscoveryRepairOutcome, MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, MycRuntimeAuditBackend, MycSignerStateBackend, MycTransportDeliveryPolicy, diff_live_nip89, fetch_live_nip89, @@ -617,14 +617,17 @@ fn build_external_request_event( .expect("sign external request event") } -fn build_signer_noise_event(signer_identity: &RadrootsIdentity, created_at_unix: u64) -> Event { - EventBuilder::new( - Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), - "non-nip44-signer-noise", - ) - .custom_created_at(Timestamp::from(created_at_unix)) - .sign_with_keys(signer_identity.keys()) - .expect("sign noise event") +fn build_signer_noise_event(signer_identity: &MycActiveIdentity, created_at_unix: u64) -> Event { + signer_identity + .sign_event_builder( + RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + "non-nip44-signer-noise", + ) + .custom_created_at(Timestamp::from(created_at_unix)), + "signer noise event", + ) + .expect("sign noise event") } fn decrypt_response( @@ -1660,12 +1663,16 @@ async fn startup_recovery_republishes_queued_listener_connect_secret_job() -> Te .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::NotRequired), )?; let workflow = manager.begin_connect_secret_publish_finalization(&connection.connection_id)?; - let event = RadrootsNostrEventBuilder::new( - RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), - "startup-recovery", - ) - .sign_with_keys(runtime.signer_identity().keys()) - .map_err(|error| format!("failed to sign startup recovery event: {error}"))?; + let event = runtime + .signer_identity() + .sign_event_builder( + RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + "startup-recovery", + ), + "startup recovery", + ) + .map_err(|error| format!("failed to sign startup recovery event: {error}"))?; let outbox_record = MycDeliveryOutboxRecord::new( MycDeliveryOutboxKind::ListenerResponsePublish, event, @@ -1755,12 +1762,18 @@ async fn startup_recovery_republishes_queued_connect_accept_job() -> TestResult< .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::NotRequired), )?; let workflow = manager.begin_connect_secret_publish_finalization(&connection.connection_id)?; - let event = RadrootsNostrEventBuilder::new( - RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), - "startup-recovery-connect-accept", - ) - .sign_with_keys(runtime.signer_identity().keys()) - .map_err(|error| format!("failed to sign startup recovery connect-accept event: {error}"))?; + let event = runtime + .signer_identity() + .sign_event_builder( + RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + "startup-recovery-connect-accept", + ), + "startup recovery connect accept", + ) + .map_err(|error| { + format!("failed to sign startup recovery connect-accept event: {error}") + })?; let outbox_record = MycDeliveryOutboxRecord::new( MycDeliveryOutboxKind::ConnectAcceptPublish, event, @@ -1854,12 +1867,16 @@ async fn startup_recovery_republishes_queued_auth_replay_job() -> TestResult<()> ping_request_message("startup-recovery-auth"), )?; let workflow = manager.begin_auth_replay_publish_finalization(&connection.connection_id)?; - let event = RadrootsNostrEventBuilder::new( - RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), - "startup-recovery-auth-replay", - ) - .sign_with_keys(runtime.signer_identity().keys()) - .map_err(|error| format!("failed to sign startup recovery auth-replay event: {error}"))?; + let event = runtime + .signer_identity() + .sign_event_builder( + RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + "startup-recovery-auth-replay", + ), + "startup recovery auth replay", + ) + .map_err(|error| format!("failed to sign startup recovery auth-replay event: {error}"))?; let outbox_record = MycDeliveryOutboxRecord::new( MycDeliveryOutboxKind::AuthReplayPublish, event, diff --git a/tests/operability_cli.rs b/tests/operability_cli.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::process::Command; use myc::{ - MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycOperationAuditKind, + MycActiveIdentity, MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, }; use radroots_identity::RadrootsIdentity; @@ -53,9 +53,12 @@ MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=1\n", env_path } -fn signed_event(identity: &RadrootsIdentity) -> nostr::Event { - RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "operability") - .sign_with_keys(identity.keys()) +fn signed_event(identity: &MycActiveIdentity) -> nostr::Event { + identity + .sign_event_builder( + RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), "operability"), + "operability test event", + ) .expect("sign event") } diff --git a/tests/operability_e2e.rs b/tests/operability_e2e.rs @@ -3,9 +3,10 @@ use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use myc::{ - MycConfig, MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycOperationAuditKind, - MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, MycRuntimeAuditBackend, - MycRuntimeStatus, MycSignerStateBackend, MycTransportDeliveryPolicy, collect_status_full, + MycActiveIdentity, MycConfig, MycDeliveryOutboxKind, MycDeliveryOutboxRecord, + MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, + MycRuntimeAuditBackend, MycRuntimeStatus, MycSignerStateBackend, MycTransportDeliveryPolicy, + collect_status_full, }; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ @@ -122,9 +123,12 @@ fn write_test_identity(path: &Path, secret_key: &str) { .expect("write identity"); } -fn signed_delivery_event(identity: &RadrootsIdentity, content: &str) -> nostr::Event { - RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), content) - .sign_with_keys(identity.keys()) +fn signed_delivery_event(identity: &MycActiveIdentity, content: &str) -> nostr::Event { + identity + .sign_event_builder( + RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(24133), content), + "operability delivery test event", + ) .expect("sign event") }