signing.rs (8544B)
1 //! Concrete local Nostr implementation of the generic signing SPI. 2 //! 3 //! Signing is local and in-memory. Success returns a cryptographically verified 4 //! exact-plan receipt without persistence or publication. 5 6 use core::fmt; 7 use std::{ 8 time::{SystemTime, UNIX_EPOCH}, 9 vec, 10 }; 11 12 use radroots_identity::PublicKey; 13 use radroots_signing::{ 14 Error as SigningError, SignReceipt, SignRequest, Signer, SignerStatus, 15 capability::{CancellationSupport, SignerCapability, SignerKind}, 16 error::Kind, 17 recovery::ReplayCapability, 18 signer::BoxFuture, 19 status::{SignProgress, SignProgressStage, SignerAvailability}, 20 }; 21 22 use crate::{Error as NostrError, key::SecretKey}; 23 24 type Clock = fn() -> Result<u64, SigningError>; 25 26 /// A local Nostr key-backed signer adapter. 27 pub struct LocalSigner { 28 keys: nostr::Keys, 29 public_key: PublicKey, 30 clock: Clock, 31 } 32 33 impl LocalSigner { 34 pub fn new(secret_key: SecretKey) -> Result<Self, crate::Error> { 35 Self::with_clock(secret_key, system_time_unix_ms) 36 } 37 38 pub fn generate() -> Result<Self, crate::Error> { 39 Self::new(SecretKey::generate()) 40 } 41 42 #[must_use] 43 pub const fn public_key(&self) -> PublicKey { 44 self.public_key 45 } 46 47 fn with_clock(secret_key: SecretKey, clock: Clock) -> Result<Self, crate::Error> { 48 let public_key = secret_key.public_key()?; 49 Ok(Self { 50 keys: secret_key.into_keys(), 51 public_key, 52 clock, 53 }) 54 } 55 } 56 57 impl fmt::Debug for LocalSigner { 58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 59 formatter 60 .debug_struct("LocalSigner") 61 .field("public_key", &self.public_key) 62 .field("secret_key", &"[redacted]") 63 .finish() 64 } 65 } 66 67 impl Signer for LocalSigner { 68 fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> { 69 Box::pin(async { 70 Ok(SignerStatus::new( 71 SignerAvailability::Ready, 72 vec![SignerCapability::new( 73 SignerKind::Local, 74 ReplayCapability::LocalReplaySafe, 75 CancellationSupport::BeforePublication, 76 true, 77 false, 78 )], 79 None, 80 )) 81 }) 82 } 83 84 fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> { 85 Box::pin(async move { 86 request.ensure_active((self.clock)()?)?; 87 request.report_progress( 88 &SignProgress::stage(SignProgressStage::Validating) 89 .expect("validating has no challenge"), 90 ); 91 if request.expected_author() != &self.public_key { 92 return Err(SigningError::new(Kind::AuthorizationDenied)); 93 } 94 let signed_event = crate::plan_signing::sign_request(&self.keys, &request) 95 .map_err(normalize_nostr_error)?; 96 request.report_progress( 97 &SignProgress::stage(SignProgressStage::VerifyingOutput) 98 .expect("verification has no challenge"), 99 ); 100 let completed_at_unix_ms = (self.clock)()?; 101 request.ensure_active(completed_at_unix_ms)?; 102 let receipt = 103 SignReceipt::from_signed_event(&request, signed_event, completed_at_unix_ms)?; 104 request.report_progress( 105 &SignProgress::stage(SignProgressStage::Complete) 106 .expect("completion has no challenge"), 107 ); 108 Ok(receipt) 109 }) 110 } 111 } 112 113 fn system_time_unix_ms() -> Result<u64, SigningError> { 114 SystemTime::now() 115 .duration_since(UNIX_EPOCH) 116 .map_err(|source| SigningError::with_source(Kind::InternalError, source)) 117 .and_then(|duration| { 118 u64::try_from(duration.as_millis()) 119 .map_err(|source| SigningError::with_source(Kind::InternalError, source)) 120 }) 121 } 122 123 fn normalize_nostr_error(source: NostrError) -> SigningError { 124 let kind = match &source { 125 NostrError::ExternalSigningAuthorMismatch { .. } => Kind::AuthorizationDenied, 126 NostrError::ExternalSigningEventIdMismatch { .. } 127 | NostrError::ExternalSigningEventInvalid(_) 128 | NostrError::ExternalSigningPlanMismatch { .. } => Kind::SignerOutputInvalid, 129 _ => Kind::InternalError, 130 }; 131 SigningError::with_source(kind, source) 132 } 133 134 #[cfg(test)] 135 mod tests { 136 use super::*; 137 use radroots_event::{GenericEventDraft, contract::AuthorRole}; 138 use radroots_event_codec::authoring::AuthoredEventPlan; 139 use radroots_protocol::runtime::v1::OperationId; 140 use radroots_signing::{ 141 Actor, AuthoredArtifactId, SigningIntentId, SigningOperationId, 142 actor::ActorSource, 143 request::{CancellationPolicy, CancellationSignal, SignPolicy}, 144 }; 145 146 use crate::{ 147 key::parse_secret_key, 148 test_fixtures::{FIXTURE_ALICE, FIXTURE_BOB}, 149 }; 150 151 const CREATED_AT: u64 = 1_700_000_000; 152 const DEADLINE_MS: u64 = 1_700_000_100_000; 153 154 fn before_deadline() -> Result<u64, SigningError> { 155 Ok(DEADLINE_MS - 1) 156 } 157 158 fn at_deadline() -> Result<u64, SigningError> { 159 Ok(DEADLINE_MS) 160 } 161 162 fn fixture_secret(value: &str) -> SecretKey { 163 parse_secret_key(value).expect("secret fixture") 164 } 165 166 fn request() -> SignRequest { 167 let actor = Actor::from_public_key_hex( 168 FIXTURE_ALICE.public_key_hex, 169 ActorSource::ExplicitPublicKey, 170 [AuthorRole::Any], 171 ) 172 .unwrap(); 173 let draft = GenericEventDraft::new( 174 "radroots.social.geochat.v1", 175 20_000, 176 CREATED_AT, 177 Vec::new(), 178 "private-fixture-content", 179 FIXTURE_ALICE.public_key_hex, 180 ) 181 .unwrap(); 182 SignRequest::new( 183 OperationId::SyncPush, 184 SigningIntentId::new( 185 SigningOperationId::new([1; 16]).unwrap(), 186 AuthoredArtifactId::new([2; 16]).unwrap(), 187 ), 188 actor, 189 AuthoredEventPlan::from_generic(draft).unwrap(), 190 SignPolicy::new(DEADLINE_MS, CancellationPolicy::LocalCooperative).unwrap(), 191 ) 192 .unwrap() 193 } 194 195 #[tokio::test] 196 async fn local_signer_reports_safe_replay_and_returns_verified_exact_plan() { 197 let signer = LocalSigner::with_clock( 198 fixture_secret(FIXTURE_ALICE.secret_key_hex), 199 before_deadline, 200 ) 201 .unwrap(); 202 let status = signer.status().await.unwrap(); 203 assert_eq!( 204 status.capabilities()[0].replay(), 205 ReplayCapability::LocalReplaySafe 206 ); 207 let request = request(); 208 let expected_id = request.expected_event_id().to_hex(); 209 let receipt = signer.sign(request).await.unwrap(); 210 assert_eq!(receipt.signed_event().id_str(), expected_id); 211 assert_eq!(receipt.completed_at_unix_ms(), DEADLINE_MS - 1); 212 } 213 214 #[tokio::test] 215 async fn wrong_key_deadline_and_cancellation_fail_closed() { 216 let wrong = 217 LocalSigner::with_clock(fixture_secret(FIXTURE_BOB.secret_key_hex), before_deadline) 218 .unwrap(); 219 assert_eq!( 220 wrong.sign(request()).await.unwrap_err().kind(), 221 Kind::AuthorizationDenied 222 ); 223 let expired = 224 LocalSigner::with_clock(fixture_secret(FIXTURE_ALICE.secret_key_hex), at_deadline) 225 .unwrap(); 226 assert_eq!( 227 expired.sign(request()).await.unwrap_err().kind(), 228 Kind::DeadlineExceeded 229 ); 230 let signal = CancellationSignal::new(); 231 let cancelled = request().with_cancellation_signal(signal.clone()); 232 signal.cancel(); 233 assert_eq!( 234 LocalSigner::with_clock( 235 fixture_secret(FIXTURE_ALICE.secret_key_hex), 236 before_deadline, 237 ) 238 .unwrap() 239 .sign(cancelled) 240 .await 241 .unwrap_err() 242 .kind(), 243 Kind::SignerCancelled 244 ); 245 } 246 247 #[test] 248 fn debug_never_exposes_local_secret_material() { 249 let signer = LocalSigner::with_clock( 250 fixture_secret(FIXTURE_ALICE.secret_key_hex), 251 before_deadline, 252 ) 253 .unwrap(); 254 let debug = format!("{signer:?}"); 255 assert!(!debug.contains(FIXTURE_ALICE.secret_key_hex)); 256 assert!(!debug.contains(FIXTURE_ALICE.nsec)); 257 } 258 }