manager.rs (160929B)
1 use crate::error::RadrootsNostrSignerError; 2 use crate::evaluation::{ 3 RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal, 4 RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestEvaluation, 5 RadrootsNostrSignerSessionLookup, request_allowed_by_permissions, 6 required_permission_for_request, response_hint_for_request, 7 }; 8 use crate::model::{ 9 RADROOTS_NOSTR_SIGNER_STORE_VERSION, RadrootsNostrSignerApprovalRequirement, 10 RadrootsNostrSignerApprovalState, RadrootsNostrSignerAuthChallenge, 11 RadrootsNostrSignerAuthState, RadrootsNostrSignerAuthorizationOutcome, 12 RadrootsNostrSignerConnectSecretHash, RadrootsNostrSignerConnectionDraft, 13 RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord, 14 RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerPendingRequest, 15 RadrootsNostrSignerPermissionGrant, RadrootsNostrSignerPublishWorkflowKind, 16 RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerPublishWorkflowState, 17 RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision, 18 RadrootsNostrSignerRequestId, RadrootsNostrSignerStoreState, RadrootsNostrSignerWorkflowId, 19 }; 20 use crate::store::{RadrootsNostrMemorySignerStore, RadrootsNostrSignerStore}; 21 use nostr::{PublicKey, RelayUrl}; 22 use radroots_identity::RadrootsIdentityPublic; 23 use radroots_nostr_connect::prelude::{ 24 RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, 25 RadrootsNostrConnectRequestMessage, 26 }; 27 use std::sync::{Arc, RwLock}; 28 use std::time::{SystemTime, UNIX_EPOCH}; 29 30 #[derive(Clone)] 31 pub struct RadrootsNostrSignerManager { 32 store: Arc<dyn RadrootsNostrSignerStore>, 33 state: Arc<RwLock<RadrootsNostrSignerStoreState>>, 34 } 35 36 impl RadrootsNostrSignerManager { 37 pub fn new_in_memory() -> Self { 38 Self { 39 store: Arc::new(RadrootsNostrMemorySignerStore::new()), 40 state: Arc::new(RwLock::new(RadrootsNostrSignerStoreState::default())), 41 } 42 } 43 44 pub fn new(store: Arc<dyn RadrootsNostrSignerStore>) -> Result<Self, RadrootsNostrSignerError> { 45 let state = store.load()?; 46 if state.version != RADROOTS_NOSTR_SIGNER_STORE_VERSION { 47 return Err(RadrootsNostrSignerError::InvalidState(format!( 48 "unsupported signer schema version {}", 49 state.version 50 ))); 51 } 52 53 Ok(Self { 54 store, 55 state: Arc::new(RwLock::new(state)), 56 }) 57 } 58 59 pub fn signer_identity( 60 &self, 61 ) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> { 62 let guard = self 63 .state 64 .read() 65 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 66 Ok(guard.signer_identity.clone()) 67 } 68 69 pub fn set_signer_identity( 70 &self, 71 signer_identity: RadrootsIdentityPublic, 72 ) -> Result<(), RadrootsNostrSignerError> { 73 validate_public_identity(&signer_identity)?; 74 self.update_state(|state| { 75 state.signer_identity = Some(signer_identity); 76 Ok(()) 77 }) 78 } 79 80 pub fn list_connections( 81 &self, 82 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 83 let guard = self 84 .state 85 .read() 86 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 87 Ok(guard.connections.clone()) 88 } 89 90 pub fn get_connection( 91 &self, 92 connection_id: &RadrootsNostrSignerConnectionId, 93 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 94 let guard = self 95 .state 96 .read() 97 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 98 Ok(guard 99 .connections 100 .iter() 101 .find(|record| &record.connection_id == connection_id) 102 .cloned()) 103 } 104 105 pub fn list_publish_workflows( 106 &self, 107 ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 108 let guard = self 109 .state 110 .read() 111 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 112 Ok(guard.publish_workflows.clone()) 113 } 114 115 pub fn get_publish_workflow( 116 &self, 117 workflow_id: &RadrootsNostrSignerWorkflowId, 118 ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 119 let guard = self 120 .state 121 .read() 122 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 123 Ok(guard 124 .publish_workflows 125 .iter() 126 .find(|record| &record.workflow_id == workflow_id) 127 .cloned()) 128 } 129 130 pub fn find_connections_by_client_public_key( 131 &self, 132 client_public_key: &PublicKey, 133 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 134 let guard = self 135 .state 136 .read() 137 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 138 Ok(guard 139 .connections 140 .iter() 141 .filter(|record| &record.client_public_key == client_public_key) 142 .cloned() 143 .collect()) 144 } 145 146 pub fn find_connection_by_connect_secret( 147 &self, 148 connect_secret: &str, 149 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 150 let Some(connect_secret_hash) = 151 RadrootsNostrSignerConnectSecretHash::from_secret(connect_secret) 152 else { 153 return Ok(None); 154 }; 155 156 let guard = self 157 .state 158 .read() 159 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 160 Ok(guard 161 .connections 162 .iter() 163 .find(|record| { 164 record.connect_secret_hash.as_ref() == Some(&connect_secret_hash) 165 && (!record.is_terminal() || record.connect_secret_is_consumed()) 166 }) 167 .cloned()) 168 } 169 170 pub fn lookup_session( 171 &self, 172 client_public_key: &PublicKey, 173 connect_secret: Option<&str>, 174 ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> { 175 if let Some(connect_secret) = connect_secret 176 && let Some(connection) = self.find_connection_by_connect_secret(connect_secret)? 177 { 178 if &connection.client_public_key != client_public_key { 179 return Err(RadrootsNostrSignerError::InvalidState( 180 "connect secret is bound to a different client public key".into(), 181 )); 182 } 183 return Ok(RadrootsNostrSignerSessionLookup::Connection(Box::new( 184 connection, 185 ))); 186 } 187 188 let mut matches = self.find_connections_by_client_public_key(client_public_key)?; 189 matches.retain(|record| !record.is_terminal()); 190 Ok(match matches.len() { 191 0 => RadrootsNostrSignerSessionLookup::None, 192 1 => RadrootsNostrSignerSessionLookup::Connection(Box::new(matches.remove(0))), 193 _ => RadrootsNostrSignerSessionLookup::Ambiguous(matches), 194 }) 195 } 196 197 pub fn evaluate_connect_request( 198 &self, 199 client_public_key: PublicKey, 200 request: RadrootsNostrConnectRequest, 201 ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> { 202 let RadrootsNostrConnectRequest::Connect { 203 remote_signer_public_key, 204 secret, 205 requested_permissions, 206 } = request 207 else { 208 return Err(RadrootsNostrSignerError::InvalidState( 209 "connect evaluation requires a connect request".into(), 210 )); 211 }; 212 213 let (connect_secret, existing_connection) = 214 self.resolve_connect_request_context(remote_signer_public_key, secret)?; 215 if let Some(connection) = existing_connection { 216 if connection.client_public_key != client_public_key { 217 return Err(RadrootsNostrSignerError::InvalidState( 218 "connect secret is bound to a different client public key".into(), 219 )); 220 } 221 return Ok(RadrootsNostrSignerConnectEvaluation::ExistingConnection( 222 Box::new(connection), 223 )); 224 } 225 226 Ok(RadrootsNostrSignerConnectEvaluation::RegistrationRequired( 227 RadrootsNostrSignerConnectProposal { 228 client_public_key, 229 connect_secret, 230 requested_permissions: normalize_permissions(requested_permissions), 231 }, 232 )) 233 } 234 235 pub fn list_audit_records( 236 &self, 237 ) -> Result<Vec<RadrootsNostrSignerRequestAuditRecord>, RadrootsNostrSignerError> { 238 let guard = self 239 .state 240 .read() 241 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 242 Ok(guard.audit_records.clone()) 243 } 244 245 pub fn audit_records_for_connection( 246 &self, 247 connection_id: &RadrootsNostrSignerConnectionId, 248 ) -> Result<Vec<RadrootsNostrSignerRequestAuditRecord>, RadrootsNostrSignerError> { 249 let guard = self 250 .state 251 .read() 252 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 253 Ok(guard 254 .audit_records 255 .iter() 256 .filter(|record| &record.connection_id == connection_id) 257 .cloned() 258 .collect()) 259 } 260 261 pub fn register_connection( 262 &self, 263 draft: RadrootsNostrSignerConnectionDraft, 264 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 265 self.update_state_with(|state| { 266 let signer_identity = state 267 .signer_identity 268 .clone() 269 .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?; 270 validate_public_identity(&signer_identity)?; 271 validate_public_identity(&draft.user_identity)?; 272 273 let connect_secret_hash = draft 274 .connect_secret 275 .as_deref() 276 .and_then(RadrootsNostrSignerConnectSecretHash::from_secret); 277 if let Some(secret_hash) = connect_secret_hash.as_ref() 278 && state.connections.iter().any(|record| { 279 record.connect_secret_hash.as_ref() == Some(secret_hash) 280 && (!record.is_terminal() || record.connect_secret_is_consumed()) 281 }) 282 { 283 return Err(RadrootsNostrSignerError::ConnectSecretAlreadyInUse); 284 } 285 286 if state.connections.iter().any(|record| { 287 !record.is_terminal() 288 && record.client_public_key == draft.client_public_key 289 && record.user_identity.id == draft.user_identity.id 290 }) { 291 return Err(RadrootsNostrSignerError::ConnectionAlreadyExists { 292 client_public_key: draft.client_public_key.to_hex(), 293 user_identity_id: draft.user_identity.id.to_string(), 294 }); 295 } 296 297 let created_at_unix = now_unix_secs(); 298 let record = RadrootsNostrSignerConnectionRecord::new( 299 RadrootsNostrSignerConnectionId::new_v7(), 300 signer_identity, 301 RadrootsNostrSignerConnectionDraft { 302 client_public_key: draft.client_public_key, 303 user_identity: draft.user_identity, 304 connect_secret: draft.connect_secret, 305 requested_permissions: normalize_permissions(draft.requested_permissions), 306 relays: normalize_relays(draft.relays), 307 approval_requirement: draft.approval_requirement, 308 }, 309 created_at_unix, 310 ); 311 state.connections.push(record.clone()); 312 Ok(record) 313 }) 314 } 315 316 pub fn set_granted_permissions( 317 &self, 318 connection_id: &RadrootsNostrSignerConnectionId, 319 granted_permissions: RadrootsNostrConnectPermissions, 320 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 321 self.update_state_with(|state| { 322 let updated_at_unix = now_unix_secs(); 323 let record = find_connection_mut(state, connection_id)?; 324 if record.is_terminal() { 325 return Err(RadrootsNostrSignerError::InvalidState(format!( 326 "cannot update granted permissions for {} connection", 327 status_label(record.status) 328 ))); 329 } 330 331 let granted_permissions = normalize_permissions(granted_permissions); 332 validate_granted_permissions(&record.requested_permissions, &granted_permissions)?; 333 record.granted_permissions = granted_permissions 334 .as_slice() 335 .iter() 336 .cloned() 337 .map(|permission| { 338 RadrootsNostrSignerPermissionGrant::new(permission, updated_at_unix) 339 }) 340 .collect(); 341 record.touch_updated(updated_at_unix); 342 Ok(record.clone()) 343 }) 344 } 345 346 pub fn approve_connection( 347 &self, 348 connection_id: &RadrootsNostrSignerConnectionId, 349 granted_permissions: RadrootsNostrConnectPermissions, 350 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 351 self.update_state_with(|state| { 352 let updated_at_unix = now_unix_secs(); 353 let record = find_connection_mut(state, connection_id)?; 354 if record.approval_requirement != RadrootsNostrSignerApprovalRequirement::ExplicitUser { 355 return Err(RadrootsNostrSignerError::InvalidState( 356 "approval not required for connection".into(), 357 )); 358 } 359 if record.is_terminal() { 360 return Err(RadrootsNostrSignerError::InvalidState(format!( 361 "cannot approve {} connection", 362 status_label(record.status) 363 ))); 364 } 365 366 let granted_permissions = normalize_permissions(granted_permissions); 367 validate_granted_permissions(&record.requested_permissions, &granted_permissions)?; 368 record.granted_permissions = granted_permissions 369 .as_slice() 370 .iter() 371 .cloned() 372 .map(|permission| { 373 RadrootsNostrSignerPermissionGrant::new(permission, updated_at_unix) 374 }) 375 .collect(); 376 record.approval_state = RadrootsNostrSignerApprovalState::Approved; 377 record.status = RadrootsNostrSignerConnectionStatus::Active; 378 record.status_reason = None; 379 record.touch_updated(updated_at_unix); 380 Ok(record.clone()) 381 }) 382 } 383 384 pub fn reject_connection( 385 &self, 386 connection_id: &RadrootsNostrSignerConnectionId, 387 reason: Option<String>, 388 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 389 self.update_state_with(|state| { 390 let updated_at_unix = now_unix_secs(); 391 let record = find_connection_mut(state, connection_id)?; 392 if record.is_terminal() { 393 return Err(RadrootsNostrSignerError::InvalidState(format!( 394 "cannot reject {} connection", 395 status_label(record.status) 396 ))); 397 } 398 399 record.approval_state = RadrootsNostrSignerApprovalState::Rejected; 400 record.status = RadrootsNostrSignerConnectionStatus::Rejected; 401 record.status_reason = normalize_optional_string(reason); 402 record.touch_updated(updated_at_unix); 403 Ok(record.clone()) 404 }) 405 } 406 407 pub fn revoke_connection( 408 &self, 409 connection_id: &RadrootsNostrSignerConnectionId, 410 reason: Option<String>, 411 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 412 self.update_state_with(|state| { 413 let updated_at_unix = now_unix_secs(); 414 let record = find_connection_mut(state, connection_id)?; 415 if record.status == RadrootsNostrSignerConnectionStatus::Revoked { 416 return Err(RadrootsNostrSignerError::InvalidState( 417 "connection already revoked".into(), 418 )); 419 } 420 421 record.status = RadrootsNostrSignerConnectionStatus::Revoked; 422 record.status_reason = normalize_optional_string(reason); 423 record.touch_updated(updated_at_unix); 424 Ok(record.clone()) 425 }) 426 } 427 428 pub fn update_relays( 429 &self, 430 connection_id: &RadrootsNostrSignerConnectionId, 431 relays: Vec<RelayUrl>, 432 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 433 self.update_state_with(|state| { 434 let updated_at_unix = now_unix_secs(); 435 let record = find_connection_mut(state, connection_id)?; 436 if record.is_terminal() { 437 return Err(RadrootsNostrSignerError::InvalidState(format!( 438 "cannot update relays for {} connection", 439 status_label(record.status) 440 ))); 441 } 442 443 record.relays = normalize_relays(relays); 444 record.touch_updated(updated_at_unix); 445 Ok(record.clone()) 446 }) 447 } 448 449 pub fn require_auth_challenge( 450 &self, 451 connection_id: &RadrootsNostrSignerConnectionId, 452 auth_url: impl AsRef<str>, 453 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 454 self.update_state_with(|state| { 455 let required_at_unix = now_unix_secs(); 456 let record = find_connection_mut(state, connection_id)?; 457 if record.is_terminal() { 458 return Err(RadrootsNostrSignerError::InvalidState(format!( 459 "cannot require auth for {} connection", 460 status_label(record.status) 461 ))); 462 } 463 464 let challenge = 465 RadrootsNostrSignerAuthChallenge::new(auth_url.as_ref(), required_at_unix)?; 466 record.require_auth_challenge(challenge); 467 Ok(record.clone()) 468 }) 469 } 470 471 pub fn set_pending_request( 472 &self, 473 connection_id: &RadrootsNostrSignerConnectionId, 474 request_message: RadrootsNostrConnectRequestMessage, 475 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 476 self.update_state_with(|state| { 477 let record = find_connection_mut(state, connection_id)?; 478 if record.is_terminal() { 479 return Err(RadrootsNostrSignerError::InvalidState(format!( 480 "cannot set pending request for {} connection", 481 status_label(record.status) 482 ))); 483 } 484 if record.auth_state != RadrootsNostrSignerAuthState::Pending { 485 return Err(RadrootsNostrSignerError::InvalidState( 486 "auth challenge not pending for connection".into(), 487 )); 488 } 489 490 let pending_request = 491 RadrootsNostrSignerPendingRequest::new(request_message, now_unix_secs())?; 492 record.set_pending_request(pending_request); 493 Ok(record.clone()) 494 }) 495 } 496 497 pub fn authorize_auth_challenge( 498 &self, 499 connection_id: &RadrootsNostrSignerConnectionId, 500 ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> { 501 self.update_state_with(|state| { 502 let record = find_connection_mut(state, connection_id)?; 503 if record.is_terminal() { 504 return Err(RadrootsNostrSignerError::InvalidState(format!( 505 "cannot authorize auth challenge for {} connection", 506 status_label(record.status) 507 ))); 508 } 509 if record.auth_state != RadrootsNostrSignerAuthState::Pending { 510 return Err(RadrootsNostrSignerError::InvalidState( 511 "auth challenge not pending for connection".into(), 512 )); 513 } 514 515 let pending_request = record.authorize_auth_challenge(now_unix_secs()); 516 Ok(RadrootsNostrSignerAuthorizationOutcome::new( 517 record.clone(), 518 pending_request, 519 )) 520 }) 521 } 522 523 pub fn restore_pending_auth_challenge( 524 &self, 525 connection_id: &RadrootsNostrSignerConnectionId, 526 pending_request: RadrootsNostrSignerPendingRequest, 527 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 528 self.update_state_with(|state| { 529 let restored_at_unix = now_unix_secs(); 530 let record = find_connection_mut(state, connection_id)?; 531 if record.is_terminal() { 532 return Err(RadrootsNostrSignerError::InvalidState(format!( 533 "cannot restore auth challenge for {} connection", 534 status_label(record.status) 535 ))); 536 } 537 if record.auth_state != RadrootsNostrSignerAuthState::Authorized { 538 return Err(RadrootsNostrSignerError::InvalidState( 539 "auth challenge not authorized for connection".into(), 540 )); 541 } 542 if record.auth_challenge.is_none() { 543 return Err(RadrootsNostrSignerError::InvalidState( 544 "auth challenge missing for connection".into(), 545 )); 546 } 547 548 record.restore_pending_auth_challenge(pending_request, restored_at_unix); 549 Ok(record.clone()) 550 }) 551 } 552 553 pub fn begin_connect_secret_publish_finalization( 554 &self, 555 connection_id: &RadrootsNostrSignerConnectionId, 556 ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> { 557 self.update_state_with(|state| { 558 let connection_index = find_connection_index(state, connection_id)?; 559 let record = &state.connections[connection_index]; 560 if record.is_terminal() { 561 return Err(RadrootsNostrSignerError::InvalidState(format!( 562 "cannot begin connect secret finalization for {} connection", 563 status_label(record.status) 564 ))); 565 } 566 if record.connect_secret_hash.is_none() { 567 return Err(RadrootsNostrSignerError::InvalidState( 568 "connection does not have a connect secret".into(), 569 )); 570 } 571 if record.connect_secret_is_consumed() { 572 return Err(RadrootsNostrSignerError::InvalidState( 573 "connect secret already consumed for connection".into(), 574 )); 575 } 576 ensure_no_active_publish_workflow( 577 state, 578 connection_id, 579 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization, 580 )?; 581 582 let workflow = 583 RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization( 584 connection_id.clone(), 585 now_unix_secs(), 586 ); 587 state.publish_workflows.push(workflow.clone()); 588 Ok(workflow) 589 }) 590 } 591 592 pub fn begin_auth_replay_publish_finalization( 593 &self, 594 connection_id: &RadrootsNostrSignerConnectionId, 595 ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> { 596 self.update_state_with(|state| { 597 let authorized_at_unix = now_unix_secs(); 598 let connection_index = find_connection_index(state, connection_id)?; 599 let record = &state.connections[connection_index]; 600 if record.is_terminal() { 601 return Err(RadrootsNostrSignerError::InvalidState(format!( 602 "cannot begin auth replay finalization for {} connection", 603 status_label(record.status) 604 ))); 605 } 606 if record.auth_state != RadrootsNostrSignerAuthState::Pending { 607 return Err(RadrootsNostrSignerError::InvalidState( 608 "auth challenge not pending for connection".into(), 609 )); 610 } 611 if record.auth_challenge.is_none() { 612 return Err(RadrootsNostrSignerError::InvalidState( 613 "auth challenge missing for connection".into(), 614 )); 615 } 616 let pending_request = record.pending_request.clone().ok_or_else(|| { 617 RadrootsNostrSignerError::InvalidState( 618 "pending request missing for auth replay finalization".into(), 619 ) 620 })?; 621 ensure_no_active_publish_workflow( 622 state, 623 connection_id, 624 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization, 625 )?; 626 627 let workflow = RadrootsNostrSignerPublishWorkflowRecord::new_auth_replay_finalization( 628 connection_id.clone(), 629 pending_request, 630 authorized_at_unix, 631 ); 632 state.publish_workflows.push(workflow.clone()); 633 Ok(workflow) 634 }) 635 } 636 637 pub fn mark_publish_workflow_published( 638 &self, 639 workflow_id: &RadrootsNostrSignerWorkflowId, 640 ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> { 641 self.update_state_with(|state| { 642 let workflow = find_publish_workflow_mut(state, workflow_id)?; 643 workflow.mark_published(now_unix_secs()); 644 Ok(workflow.clone()) 645 }) 646 } 647 648 pub fn finalize_publish_workflow( 649 &self, 650 workflow_id: &RadrootsNostrSignerWorkflowId, 651 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 652 self.update_state_with(|state| { 653 let workflow_index = find_publish_workflow_index(state, workflow_id)?; 654 let workflow = state.publish_workflows[workflow_index].clone(); 655 if workflow.state != RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize { 656 return Err(RadrootsNostrSignerError::InvalidState( 657 "publish workflow has not reached published state".into(), 658 )); 659 } 660 661 let record = find_connection_mut(state, &workflow.connection_id)?; 662 let finalized = match workflow.kind { 663 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization => { 664 if record.connect_secret_hash.is_none() { 665 return Err(RadrootsNostrSignerError::InvalidState( 666 "connection does not have a connect secret".into(), 667 )); 668 } 669 if record.connect_secret_is_consumed() { 670 return Err(RadrootsNostrSignerError::InvalidState( 671 "connect secret already consumed for connection".into(), 672 )); 673 } 674 record.mark_connect_secret_consumed(now_unix_secs()); 675 record.clone() 676 } 677 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization => { 678 if record.auth_state != RadrootsNostrSignerAuthState::Pending { 679 return Err(RadrootsNostrSignerError::InvalidState( 680 "auth challenge not pending for connection".into(), 681 )); 682 } 683 if record.auth_challenge.is_none() { 684 return Err(RadrootsNostrSignerError::InvalidState( 685 "auth challenge missing for connection".into(), 686 )); 687 } 688 let expected_pending_request = 689 workflow.pending_request.clone().ok_or_else(|| { 690 RadrootsNostrSignerError::InvalidState( 691 "auth replay workflow missing pending request".into(), 692 ) 693 })?; 694 if record.pending_request.as_ref() != Some(&expected_pending_request) { 695 return Err(RadrootsNostrSignerError::InvalidState( 696 "pending request does not match auth replay workflow".into(), 697 )); 698 } 699 let authorized_at_unix = workflow.authorized_at_unix.ok_or_else(|| { 700 RadrootsNostrSignerError::InvalidState( 701 "auth replay workflow missing authorized timestamp".into(), 702 ) 703 })?; 704 let replay = record.authorize_auth_challenge(authorized_at_unix); 705 debug_assert_eq!( 706 replay.as_ref(), 707 Some(&expected_pending_request), 708 "auth replay finalization returned unexpected pending request" 709 ); 710 record.clone() 711 } 712 }; 713 714 state.publish_workflows.remove(workflow_index); 715 Ok(finalized) 716 }) 717 } 718 719 pub fn cancel_publish_workflow( 720 &self, 721 workflow_id: &RadrootsNostrSignerWorkflowId, 722 ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> { 723 self.update_state_with(|state| { 724 let workflow_index = find_publish_workflow_index(state, workflow_id)?; 725 Ok(state.publish_workflows.remove(workflow_index)) 726 }) 727 } 728 729 pub fn mark_authenticated( 730 &self, 731 connection_id: &RadrootsNostrSignerConnectionId, 732 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 733 self.update_state_with(|state| { 734 let authenticated_at_unix = now_unix_secs(); 735 let record = find_connection_mut(state, connection_id)?; 736 record.mark_authenticated(authenticated_at_unix); 737 Ok(record.clone()) 738 }) 739 } 740 741 pub fn mark_connect_secret_consumed( 742 &self, 743 connection_id: &RadrootsNostrSignerConnectionId, 744 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 745 self.update_state_with(|state| { 746 let consumed_at_unix = now_unix_secs(); 747 let record = find_connection_mut(state, connection_id)?; 748 if record.connect_secret_hash.is_none() { 749 return Err(RadrootsNostrSignerError::InvalidState( 750 "connection does not have a connect secret".into(), 751 )); 752 } 753 record.mark_connect_secret_consumed(consumed_at_unix); 754 Ok(record.clone()) 755 }) 756 } 757 758 pub fn evaluate_request( 759 &self, 760 connection_id: &RadrootsNostrSignerConnectionId, 761 request_message: RadrootsNostrConnectRequestMessage, 762 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 763 if matches!( 764 request_message.request, 765 RadrootsNostrConnectRequest::Connect { .. } 766 ) { 767 return Err(RadrootsNostrSignerError::InvalidState( 768 "connect requests must be evaluated via evaluate_connect_request".into(), 769 )); 770 } 771 772 self.update_state_with(|state| { 773 let request_at_unix = now_unix_secs(); 774 let request_id = RadrootsNostrSignerRequestId::parse(&request_message.id)?; 775 let record = find_connection_mut(state, connection_id)?; 776 let method = request_message.request.method(); 777 let action = evaluate_request_action(record, &request_message, request_at_unix)?; 778 record.mark_request(request_at_unix); 779 780 let audit = RadrootsNostrSignerRequestAuditRecord::new( 781 request_id.clone(), 782 connection_id.clone(), 783 method.clone(), 784 request_decision(&action), 785 action.audit_message(), 786 request_at_unix, 787 ); 788 let connection = record.clone(); 789 state.audit_records.push(audit.clone()); 790 791 Ok(RadrootsNostrSignerRequestEvaluation { 792 request_id, 793 method, 794 connection, 795 audit, 796 action, 797 }) 798 }) 799 } 800 801 pub fn evaluate_auth_replay_publish_workflow( 802 &self, 803 workflow_id: &RadrootsNostrSignerWorkflowId, 804 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 805 self.update_state_with(|state| { 806 let request_at_unix = now_unix_secs(); 807 let workflow = state 808 .publish_workflows 809 .iter() 810 .find(|record| &record.workflow_id == workflow_id) 811 .cloned() 812 .ok_or_else(|| { 813 RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string()) 814 })?; 815 if workflow.kind != RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization { 816 return Err(RadrootsNostrSignerError::InvalidState( 817 "publish workflow is not an auth replay finalization".into(), 818 )); 819 } 820 821 let pending_request = workflow.pending_request.clone().ok_or_else(|| { 822 RadrootsNostrSignerError::InvalidState( 823 "auth replay workflow missing pending request".into(), 824 ) 825 })?; 826 let request_message = pending_request.request_message(); 827 let request_id = pending_request.request_id(); 828 let method = request_message.request.method(); 829 830 let record = find_connection_mut(state, &workflow.connection_id)?; 831 if record.is_terminal() { 832 return Err(RadrootsNostrSignerError::InvalidState(format!( 833 "cannot evaluate auth replay workflow for {} connection", 834 status_label(record.status) 835 ))); 836 } 837 if record.auth_state != RadrootsNostrSignerAuthState::Pending { 838 return Err(RadrootsNostrSignerError::InvalidState( 839 "auth challenge not pending for connection".into(), 840 )); 841 } 842 if record.pending_request.as_ref() != Some(&pending_request) { 843 return Err(RadrootsNostrSignerError::InvalidState( 844 "pending request does not match auth replay workflow".into(), 845 )); 846 } 847 848 let mut effective_connection = record.clone(); 849 effective_connection.auth_state = RadrootsNostrSignerAuthState::Authorized; 850 effective_connection.pending_request = None; 851 if let Some(auth_challenge) = effective_connection.auth_challenge.as_mut() { 852 auth_challenge.authorized_at_unix = workflow.authorized_at_unix; 853 } 854 let request = &request_message; 855 let action = 856 evaluate_request_action(&mut effective_connection, request, request_at_unix)?; 857 effective_connection.mark_request(request_at_unix); 858 record.mark_request(request_at_unix); 859 860 let audit = RadrootsNostrSignerRequestAuditRecord::new( 861 request_id.clone(), 862 workflow.connection_id.clone(), 863 method.clone(), 864 request_decision(&action), 865 action.audit_message(), 866 request_at_unix, 867 ); 868 state.audit_records.push(audit.clone()); 869 870 Ok(RadrootsNostrSignerRequestEvaluation { 871 request_id, 872 method, 873 connection: effective_connection, 874 audit, 875 action, 876 }) 877 }) 878 } 879 880 pub fn record_request( 881 &self, 882 connection_id: &RadrootsNostrSignerConnectionId, 883 request_id: impl AsRef<str>, 884 method: RadrootsNostrConnectMethod, 885 decision: RadrootsNostrSignerRequestDecision, 886 message: Option<String>, 887 ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> { 888 self.update_state_with(|state| { 889 let created_at_unix = now_unix_secs(); 890 let request_id = RadrootsNostrSignerRequestId::parse(request_id.as_ref())?; 891 let record = find_connection_mut(state, connection_id)?; 892 record.mark_request(created_at_unix); 893 894 let audit = RadrootsNostrSignerRequestAuditRecord::new( 895 request_id, 896 connection_id.clone(), 897 method, 898 decision, 899 normalize_optional_string(message), 900 created_at_unix, 901 ); 902 state.audit_records.push(audit.clone()); 903 Ok(audit) 904 }) 905 } 906 907 #[cfg_attr(coverage_nightly, coverage(off))] 908 fn update_state( 909 &self, 910 update: impl FnOnce(&mut RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError>, 911 ) -> Result<(), RadrootsNostrSignerError> { 912 self.update_state_with(|state| { 913 update(state)?; 914 Ok(()) 915 }) 916 } 917 918 #[cfg_attr(coverage_nightly, coverage(off))] 919 fn update_state_with<T>( 920 &self, 921 update: impl FnOnce(&mut RadrootsNostrSignerStoreState) -> Result<T, RadrootsNostrSignerError>, 922 ) -> Result<T, RadrootsNostrSignerError> { 923 let mut guard = self 924 .state 925 .write() 926 .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?; 927 let mut next = guard.clone(); 928 let value = update(&mut next)?; 929 self.store.save(&next)?; 930 *guard = next; 931 Ok(value) 932 } 933 934 fn resolve_connect_request_context( 935 &self, 936 remote_signer_public_key: PublicKey, 937 secret: Option<String>, 938 ) -> Result< 939 (Option<String>, Option<RadrootsNostrSignerConnectionRecord>), 940 RadrootsNostrSignerError, 941 > { 942 let signer_identity = self 943 .signer_identity()? 944 .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?; 945 let signer_public_key = parse_identity_public_key(&signer_identity)?; 946 if remote_signer_public_key != signer_public_key { 947 return Err(RadrootsNostrSignerError::InvalidState( 948 "remote signer public key mismatch".into(), 949 )); 950 } 951 952 let connect_secret = normalize_optional_string(secret); 953 let existing_connection = 954 self.find_connection_by_connect_secret(connect_secret.as_deref().unwrap_or_default())?; 955 Ok((connect_secret, existing_connection)) 956 } 957 } 958 959 fn find_connection_mut<'a>( 960 state: &'a mut RadrootsNostrSignerStoreState, 961 connection_id: &RadrootsNostrSignerConnectionId, 962 ) -> Result<&'a mut RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 963 state 964 .connections 965 .iter_mut() 966 .find(|record| &record.connection_id == connection_id) 967 .ok_or_else(|| RadrootsNostrSignerError::ConnectionNotFound(connection_id.to_string())) 968 } 969 970 fn find_connection_index( 971 state: &RadrootsNostrSignerStoreState, 972 connection_id: &RadrootsNostrSignerConnectionId, 973 ) -> Result<usize, RadrootsNostrSignerError> { 974 for (index, record) in state.connections.iter().enumerate() { 975 if &record.connection_id == connection_id { 976 return Ok(index); 977 } 978 } 979 Err(RadrootsNostrSignerError::ConnectionNotFound( 980 connection_id.to_string(), 981 )) 982 } 983 984 fn find_publish_workflow_index( 985 state: &RadrootsNostrSignerStoreState, 986 workflow_id: &RadrootsNostrSignerWorkflowId, 987 ) -> Result<usize, RadrootsNostrSignerError> { 988 state 989 .publish_workflows 990 .iter() 991 .position(|record| &record.workflow_id == workflow_id) 992 .ok_or_else(|| RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string())) 993 } 994 995 fn find_publish_workflow_mut<'a>( 996 state: &'a mut RadrootsNostrSignerStoreState, 997 workflow_id: &RadrootsNostrSignerWorkflowId, 998 ) -> Result<&'a mut RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> { 999 state 1000 .publish_workflows 1001 .iter_mut() 1002 .find(|record| &record.workflow_id == workflow_id) 1003 .ok_or_else(|| RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string())) 1004 } 1005 1006 fn ensure_no_active_publish_workflow( 1007 state: &RadrootsNostrSignerStoreState, 1008 connection_id: &RadrootsNostrSignerConnectionId, 1009 kind: RadrootsNostrSignerPublishWorkflowKind, 1010 ) -> Result<(), RadrootsNostrSignerError> { 1011 if state 1012 .publish_workflows 1013 .iter() 1014 .any(|record| &record.connection_id == connection_id && record.kind == kind) 1015 { 1016 return Err(RadrootsNostrSignerError::InvalidState(format!( 1017 "publish workflow already active for {}", 1018 publish_workflow_kind_label(kind) 1019 ))); 1020 } 1021 Ok(()) 1022 } 1023 1024 fn validate_public_identity( 1025 identity: &RadrootsIdentityPublic, 1026 ) -> Result<(), RadrootsNostrSignerError> { 1027 if identity.id.as_str() != identity.public_key_hex { 1028 return Err(RadrootsNostrSignerError::InvalidState( 1029 "public identity id does not match public key".into(), 1030 )); 1031 } 1032 Ok(()) 1033 } 1034 1035 fn validate_granted_permissions( 1036 requested_permissions: &RadrootsNostrConnectPermissions, 1037 granted_permissions: &RadrootsNostrConnectPermissions, 1038 ) -> Result<(), RadrootsNostrSignerError> { 1039 if requested_permissions.is_empty() { 1040 return Ok(()); 1041 } 1042 1043 let requested = requested_permissions.as_slice(); 1044 if let Some(permission) = granted_permissions 1045 .as_slice() 1046 .iter() 1047 .find(|permission| !requested.contains(permission)) 1048 { 1049 return Err(RadrootsNostrSignerError::InvalidGrantedPermission( 1050 permission.to_string(), 1051 )); 1052 } 1053 Ok(()) 1054 } 1055 1056 fn evaluate_request_action( 1057 record: &mut RadrootsNostrSignerConnectionRecord, 1058 request_message: &RadrootsNostrConnectRequestMessage, 1059 request_at_unix: u64, 1060 ) -> Result<RadrootsNostrSignerRequestAction, RadrootsNostrSignerError> { 1061 if record.is_terminal() { 1062 return Ok(RadrootsNostrSignerRequestAction::Denied { 1063 reason: format!("connection is {}", status_label(record.status)), 1064 }); 1065 } 1066 if record.status != RadrootsNostrSignerConnectionStatus::Active { 1067 return Ok(RadrootsNostrSignerRequestAction::Denied { 1068 reason: format!("connection is {}", status_label(record.status)), 1069 }); 1070 } 1071 if record.auth_state == RadrootsNostrSignerAuthState::Pending { 1072 let auth_challenge = 1073 record 1074 .auth_challenge 1075 .clone() 1076 .ok_or(RadrootsNostrSignerError::InvalidState( 1077 "auth challenge missing for pending auth state".into(), 1078 ))?; 1079 let pending_request = 1080 RadrootsNostrSignerPendingRequest::new(request_message.clone(), request_at_unix)?; 1081 record.set_pending_request(pending_request.clone()); 1082 return Ok(RadrootsNostrSignerRequestAction::Challenged { 1083 auth_challenge, 1084 pending_request, 1085 }); 1086 } 1087 1088 let effective_permissions = record.effective_permissions(); 1089 if !request_allowed_by_permissions(&effective_permissions, &request_message.request) { 1090 return Ok(RadrootsNostrSignerRequestAction::Denied { 1091 reason: format!("unauthorized {}", request_message.request.method()), 1092 }); 1093 } 1094 1095 Ok(RadrootsNostrSignerRequestAction::Allowed { 1096 required_permission: required_permission_for_request(&request_message.request), 1097 response_hint: response_hint_for_request(record, &request_message.request)?, 1098 }) 1099 } 1100 1101 fn normalize_permissions( 1102 permissions: RadrootsNostrConnectPermissions, 1103 ) -> RadrootsNostrConnectPermissions { 1104 let mut permissions = permissions.into_vec(); 1105 permissions.sort(); 1106 permissions.dedup(); 1107 permissions.into() 1108 } 1109 1110 fn normalize_relays(relays: Vec<RelayUrl>) -> Vec<RelayUrl> { 1111 let mut relays = relays; 1112 relays.sort_by(|left, right| left.as_str().cmp(right.as_str())); 1113 relays.dedup_by(|left, right| left.as_str() == right.as_str()); 1114 relays 1115 } 1116 1117 fn normalize_optional_string(value: Option<String>) -> Option<String> { 1118 value.and_then(|value| { 1119 let trimmed = value.trim().to_owned(); 1120 if trimmed.is_empty() { 1121 None 1122 } else { 1123 Some(trimmed) 1124 } 1125 }) 1126 } 1127 1128 fn status_label(status: RadrootsNostrSignerConnectionStatus) -> &'static str { 1129 match status { 1130 RadrootsNostrSignerConnectionStatus::Pending => "pending", 1131 RadrootsNostrSignerConnectionStatus::Active => "active", 1132 RadrootsNostrSignerConnectionStatus::Rejected => "rejected", 1133 RadrootsNostrSignerConnectionStatus::Revoked => "revoked", 1134 } 1135 } 1136 1137 fn publish_workflow_kind_label(kind: RadrootsNostrSignerPublishWorkflowKind) -> &'static str { 1138 match kind { 1139 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization => { 1140 "connect_secret_finalization" 1141 } 1142 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization => { 1143 "auth_replay_finalization" 1144 } 1145 } 1146 } 1147 1148 fn request_decision( 1149 action: &RadrootsNostrSignerRequestAction, 1150 ) -> RadrootsNostrSignerRequestDecision { 1151 match action { 1152 RadrootsNostrSignerRequestAction::Allowed { .. } => { 1153 RadrootsNostrSignerRequestDecision::Allowed 1154 } 1155 RadrootsNostrSignerRequestAction::Denied { .. } => { 1156 RadrootsNostrSignerRequestDecision::Denied 1157 } 1158 RadrootsNostrSignerRequestAction::Challenged { .. } => { 1159 RadrootsNostrSignerRequestDecision::Challenged 1160 } 1161 } 1162 } 1163 1164 fn parse_identity_public_key( 1165 identity: &RadrootsIdentityPublic, 1166 ) -> Result<PublicKey, RadrootsNostrSignerError> { 1167 PublicKey::parse(identity.public_key_hex.as_str()) 1168 .or_else(|_| PublicKey::from_hex(identity.public_key_hex.as_str())) 1169 .map_err(|_| { 1170 RadrootsNostrSignerError::InvalidState("identity public key is invalid".into()) 1171 }) 1172 } 1173 1174 fn now_unix_secs() -> u64 { 1175 SystemTime::now() 1176 .duration_since(UNIX_EPOCH) 1177 .map(|duration| duration.as_secs()) 1178 .unwrap_or(0) 1179 } 1180 1181 #[cfg(test)] 1182 #[cfg_attr(coverage_nightly, coverage(off))] 1183 mod tests { 1184 use super::*; 1185 use crate::evaluation::{ 1186 RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestAction, 1187 RadrootsNostrSignerRequestResponseHint, RadrootsNostrSignerSessionLookup, 1188 }; 1189 use crate::store::RadrootsNostrSignerStore; 1190 use crate::test_support::{ 1191 api_primary_https, fixture_alice_identity, primary_relay, secondary_relay, 1192 synthetic_public_identity, synthetic_public_key, tertiary_relay, 1193 }; 1194 use nostr::{PublicKey, Timestamp, UnsignedEvent}; 1195 use radroots_identity::RadrootsIdentityPublic; 1196 use radroots_nostr_connect::prelude::RadrootsNostrConnectPermission; 1197 use serde_json::json; 1198 use std::sync::Arc; 1199 use std::thread; 1200 1201 fn public_identity(index: u32) -> RadrootsIdentityPublic { 1202 synthetic_public_identity(index) 1203 } 1204 1205 fn invalid_public_identity(index: u32) -> RadrootsIdentityPublic { 1206 let mut identity = public_identity(index); 1207 identity.id = 1208 radroots_identity::RadrootsIdentityId::parse(&public_key(0xff).to_hex()).expect("id"); 1209 identity 1210 } 1211 1212 fn public_key(index: u32) -> PublicKey { 1213 synthetic_public_key(index) 1214 } 1215 1216 fn permission( 1217 method: RadrootsNostrConnectMethod, 1218 parameter: Option<&str>, 1219 ) -> RadrootsNostrConnectPermission { 1220 match parameter { 1221 Some(parameter) => RadrootsNostrConnectPermission::with_parameter(method, parameter), 1222 None => RadrootsNostrConnectPermission::new(method), 1223 } 1224 } 1225 1226 fn request_message(id: &str) -> RadrootsNostrConnectRequestMessage { 1227 RadrootsNostrConnectRequestMessage::new( 1228 id, 1229 radroots_nostr_connect::prelude::RadrootsNostrConnectRequest::Ping, 1230 ) 1231 } 1232 1233 fn request_message_with_request( 1234 id: &str, 1235 request: RadrootsNostrConnectRequest, 1236 ) -> RadrootsNostrConnectRequestMessage { 1237 RadrootsNostrConnectRequestMessage::new(id, request) 1238 } 1239 1240 fn unsigned_event(kind: u16) -> UnsignedEvent { 1241 serde_json::from_value(json!({ 1242 "pubkey": public_key(0xa1).to_hex(), 1243 "created_at": Timestamp::from(1).as_secs(), 1244 "kind": kind, 1245 "tags": [], 1246 "content": "hello" 1247 })) 1248 .expect("unsigned event") 1249 } 1250 1251 #[cfg_attr(coverage_nightly, coverage(off))] 1252 fn expect_connection_lookup( 1253 lookup: RadrootsNostrSignerSessionLookup, 1254 ) -> RadrootsNostrSignerConnectionRecord { 1255 match lookup { 1256 RadrootsNostrSignerSessionLookup::Connection(found) => *found, 1257 other => panic!("unexpected lookup result: {other:?}"), 1258 } 1259 } 1260 1261 #[cfg_attr(coverage_nightly, coverage(off))] 1262 fn expect_ambiguous_lookup( 1263 lookup: RadrootsNostrSignerSessionLookup, 1264 ) -> Vec<RadrootsNostrSignerConnectionRecord> { 1265 match lookup { 1266 RadrootsNostrSignerSessionLookup::Ambiguous(found) => found, 1267 other => panic!("unexpected ambiguous lookup result: {other:?}"), 1268 } 1269 } 1270 1271 #[cfg_attr(coverage_nightly, coverage(off))] 1272 fn expect_existing_connect( 1273 evaluation: RadrootsNostrSignerConnectEvaluation, 1274 ) -> RadrootsNostrSignerConnectionRecord { 1275 match evaluation { 1276 RadrootsNostrSignerConnectEvaluation::ExistingConnection(found) => *found, 1277 other => panic!("unexpected existing connect result: {other:?}"), 1278 } 1279 } 1280 1281 #[cfg_attr(coverage_nightly, coverage(off))] 1282 fn expect_registration_connect( 1283 evaluation: RadrootsNostrSignerConnectEvaluation, 1284 ) -> crate::evaluation::RadrootsNostrSignerConnectProposal { 1285 match evaluation { 1286 RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => proposal, 1287 other => panic!("unexpected registration connect result: {other:?}"), 1288 } 1289 } 1290 1291 #[cfg_attr(coverage_nightly, coverage(off))] 1292 fn expect_none_lookup(lookup: RadrootsNostrSignerSessionLookup) { 1293 match lookup { 1294 RadrootsNostrSignerSessionLookup::None => {} 1295 other => panic!("unexpected non-empty lookup result: {other:?}"), 1296 } 1297 } 1298 1299 #[cfg_attr(coverage_nightly, coverage(off))] 1300 fn expect_allowed_user_public_key(action: &RadrootsNostrSignerRequestAction) { 1301 match action { 1302 RadrootsNostrSignerRequestAction::Allowed { 1303 required_permission: None, 1304 response_hint: RadrootsNostrSignerRequestResponseHint::UserPublicKey(_), 1305 } => {} 1306 other => panic!("unexpected allowed pubkey action: {other:?}"), 1307 } 1308 } 1309 1310 #[cfg_attr(coverage_nightly, coverage(off))] 1311 fn expect_allowed_without_response_hint(action: &RadrootsNostrSignerRequestAction) { 1312 match action { 1313 RadrootsNostrSignerRequestAction::Allowed { 1314 required_permission: Some(_), 1315 response_hint: RadrootsNostrSignerRequestResponseHint::None, 1316 } => {} 1317 other => panic!("unexpected allowed no-hint action: {other:?}"), 1318 } 1319 } 1320 1321 #[cfg_attr(coverage_nightly, coverage(off))] 1322 fn expect_challenged_action(action: &RadrootsNostrSignerRequestAction) { 1323 match action { 1324 RadrootsNostrSignerRequestAction::Challenged { .. } => {} 1325 other => panic!("unexpected challenged action: {other:?}"), 1326 } 1327 } 1328 1329 fn poison_manager_state(manager: &RadrootsNostrSignerManager) { 1330 let shared = manager.state.clone(); 1331 let _ = thread::spawn(move || { 1332 let _guard = shared.write().expect("write"); 1333 panic!("poison signer state"); 1334 }) 1335 .join(); 1336 } 1337 1338 fn assert_same_public_identity(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) { 1339 assert_eq!(left.id.as_str(), right.id.as_str()); 1340 assert_eq!(left.public_key_hex, right.public_key_hex); 1341 assert_eq!(left.public_key_npub, right.public_key_npub); 1342 } 1343 1344 fn assert_same_connection( 1345 left: &RadrootsNostrSignerConnectionRecord, 1346 right: &RadrootsNostrSignerConnectionRecord, 1347 ) { 1348 assert_eq!(left.connection_id, right.connection_id); 1349 assert_eq!(left.client_public_key, right.client_public_key); 1350 assert_same_public_identity(&left.signer_identity, &right.signer_identity); 1351 assert_same_public_identity(&left.user_identity, &right.user_identity); 1352 assert_eq!(left.connect_secret_hash, right.connect_secret_hash); 1353 assert_eq!( 1354 left.connect_secret_consumed_at_unix, 1355 right.connect_secret_consumed_at_unix 1356 ); 1357 assert_eq!(left.requested_permissions, right.requested_permissions); 1358 assert_eq!(left.granted_permissions, right.granted_permissions); 1359 assert_eq!(left.relays, right.relays); 1360 assert_eq!(left.approval_requirement, right.approval_requirement); 1361 assert_eq!(left.approval_state, right.approval_state); 1362 assert_eq!(left.auth_state, right.auth_state); 1363 assert_eq!(left.auth_challenge, right.auth_challenge); 1364 assert_eq!(left.pending_request, right.pending_request); 1365 assert_eq!(left.status, right.status); 1366 assert_eq!(left.status_reason, right.status_reason); 1367 assert_eq!(left.created_at_unix, right.created_at_unix); 1368 assert_eq!(left.updated_at_unix, right.updated_at_unix); 1369 assert_eq!( 1370 left.last_authenticated_at_unix, 1371 right.last_authenticated_at_unix 1372 ); 1373 assert_eq!(left.last_request_at_unix, right.last_request_at_unix); 1374 } 1375 1376 struct LoadErrorStore; 1377 1378 impl RadrootsNostrSignerStore for LoadErrorStore { 1379 fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> { 1380 Err(RadrootsNostrSignerError::Store("store load failed".into())) 1381 } 1382 1383 fn save( 1384 &self, 1385 _state: &RadrootsNostrSignerStoreState, 1386 ) -> Result<(), RadrootsNostrSignerError> { 1387 Ok(()) 1388 } 1389 } 1390 1391 struct SaveErrorStore { 1392 state: RwLock<RadrootsNostrSignerStoreState>, 1393 } 1394 1395 impl SaveErrorStore { 1396 fn new(state: RadrootsNostrSignerStoreState) -> Self { 1397 Self { 1398 state: RwLock::new(state), 1399 } 1400 } 1401 } 1402 1403 impl RadrootsNostrSignerStore for SaveErrorStore { 1404 fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> { 1405 self.state 1406 .read() 1407 .map(|guard| guard.clone()) 1408 .map_err(|_| RadrootsNostrSignerError::Store("save error store poisoned".into())) 1409 } 1410 1411 fn save( 1412 &self, 1413 _state: &RadrootsNostrSignerStoreState, 1414 ) -> Result<(), RadrootsNostrSignerError> { 1415 Err(RadrootsNostrSignerError::Store("store save failed".into())) 1416 } 1417 } 1418 1419 #[test] 1420 fn manager_new_in_memory_and_invalid_schema_paths() { 1421 let manager = RadrootsNostrSignerManager::new_in_memory(); 1422 assert!( 1423 manager 1424 .signer_identity() 1425 .expect("signer identity") 1426 .is_none() 1427 ); 1428 1429 let load_error_store = Arc::new(LoadErrorStore); 1430 load_error_store 1431 .save(&RadrootsNostrSignerStoreState::default()) 1432 .expect("load error store save"); 1433 let load_result = RadrootsNostrSignerManager::new(load_error_store); 1434 assert!(load_result.is_err()); 1435 let err = load_result.err().expect("load error"); 1436 assert!(err.to_string().contains("store load failed")); 1437 1438 let store = Arc::new(RadrootsNostrMemorySignerStore::new()); 1439 let mut state = RadrootsNostrSignerStoreState::default(); 1440 state.version = 2; 1441 store.save(&state).expect("save"); 1442 let version_result = RadrootsNostrSignerManager::new(store); 1443 assert!(version_result.is_err()); 1444 let err = version_result.err().expect("invalid version"); 1445 assert!( 1446 err.to_string() 1447 .contains("unsupported signer schema version") 1448 ); 1449 } 1450 1451 #[test] 1452 fn set_signer_identity_validates_and_persists() { 1453 let manager = RadrootsNostrSignerManager::new_in_memory(); 1454 let signer_identity = fixture_alice_identity(); 1455 manager 1456 .set_signer_identity(signer_identity.clone()) 1457 .expect("set signer"); 1458 1459 let loaded = manager 1460 .signer_identity() 1461 .expect("identity") 1462 .expect("loaded"); 1463 assert_same_public_identity(&loaded, &signer_identity); 1464 1465 let err = manager 1466 .set_signer_identity(invalid_public_identity(0x2)) 1467 .expect_err("invalid identity"); 1468 assert!( 1469 err.to_string() 1470 .contains("public identity id does not match public key") 1471 ); 1472 } 1473 1474 #[test] 1475 fn register_connection_requires_signer_identity_and_normalizes_inputs() { 1476 let manager = RadrootsNostrSignerManager::new_in_memory(); 1477 let err = manager 1478 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1479 public_key(0x3), 1480 public_identity(0x4), 1481 )) 1482 .expect_err("missing signer"); 1483 assert!(err.to_string().contains("missing signer identity")); 1484 1485 manager 1486 .set_signer_identity(public_identity(0x5)) 1487 .expect("set signer"); 1488 1489 let sign_event = permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")); 1490 let ping = permission(RadrootsNostrConnectMethod::Ping, None); 1491 let record = manager 1492 .register_connection( 1493 RadrootsNostrSignerConnectionDraft::new(public_key(0x6), public_identity(0x7)) 1494 .with_connect_secret(" secret ") 1495 .with_requested_permissions( 1496 vec![sign_event.clone(), ping.clone(), sign_event.clone()].into(), 1497 ) 1498 .with_relays(vec![primary_relay(), secondary_relay(), secondary_relay()]), 1499 ) 1500 .expect("register"); 1501 1502 assert!( 1503 record 1504 .connect_secret_hash 1505 .as_ref() 1506 .expect("connect secret hash") 1507 .matches_secret("secret") 1508 ); 1509 assert_eq!(record.status, RadrootsNostrSignerConnectionStatus::Active); 1510 assert_eq!( 1511 record.approval_state, 1512 RadrootsNostrSignerApprovalState::NotRequired 1513 ); 1514 assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::NotRequired); 1515 assert_eq!(record.requested_permissions.as_slice(), &[sign_event, ping]); 1516 assert_eq!(record.relays, vec![secondary_relay(), primary_relay()]); 1517 } 1518 1519 #[test] 1520 fn register_connection_enforces_identity_and_uniqueness_rules() { 1521 let manager = RadrootsNostrSignerManager::new_in_memory(); 1522 manager 1523 .set_signer_identity(public_identity(0x8)) 1524 .expect("set signer"); 1525 1526 let user_identity = public_identity(0x9); 1527 let client_public_key = public_key(0x10); 1528 let pending = manager 1529 .register_connection( 1530 RadrootsNostrSignerConnectionDraft::new(client_public_key, user_identity.clone()) 1531 .with_connect_secret("shared-secret") 1532 .with_approval_requirement( 1533 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 1534 ), 1535 ) 1536 .expect("register"); 1537 assert_eq!(pending.status, RadrootsNostrSignerConnectionStatus::Pending); 1538 1539 let duplicate_connection = manager 1540 .register_connection( 1541 RadrootsNostrSignerConnectionDraft::new(client_public_key, user_identity) 1542 .with_connect_secret("other-secret"), 1543 ) 1544 .expect_err("duplicate connection"); 1545 assert!( 1546 duplicate_connection 1547 .to_string() 1548 .contains("connection already exists") 1549 ); 1550 1551 let duplicate_secret = manager 1552 .register_connection( 1553 RadrootsNostrSignerConnectionDraft::new(public_key(0x11), public_identity(0x12)) 1554 .with_connect_secret("shared-secret"), 1555 ) 1556 .expect_err("duplicate secret"); 1557 assert!( 1558 duplicate_secret 1559 .to_string() 1560 .contains("connect secret already in use") 1561 ); 1562 1563 let invalid_user = manager 1564 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1565 public_key(0x13), 1566 invalid_public_identity(0x14), 1567 )) 1568 .expect_err("invalid user identity"); 1569 assert!( 1570 invalid_user 1571 .to_string() 1572 .contains("public identity id does not match public key") 1573 ); 1574 } 1575 1576 #[test] 1577 fn manager_query_helpers_find_connections() { 1578 let manager = RadrootsNostrSignerManager::new_in_memory(); 1579 manager 1580 .set_signer_identity(public_identity(0x15)) 1581 .expect("set signer"); 1582 1583 let client_public_key = public_key(0x16); 1584 let record = manager 1585 .register_connection( 1586 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x17)) 1587 .with_connect_secret("lookup-secret"), 1588 ) 1589 .expect("register"); 1590 1591 let by_id = manager 1592 .get_connection(&record.connection_id) 1593 .expect("get connection"); 1594 let by_client = manager 1595 .find_connections_by_client_public_key(&client_public_key) 1596 .expect("find by client"); 1597 let by_secret = manager 1598 .find_connection_by_connect_secret(" lookup-secret ") 1599 .expect("find by secret"); 1600 let empty_secret = manager 1601 .find_connection_by_connect_secret(" ") 1602 .expect("empty secret"); 1603 let all_connections = manager.list_connections().expect("list connections"); 1604 1605 assert_same_connection(&by_id.expect("by id"), &record); 1606 assert_eq!(by_client.len(), 1); 1607 assert_same_connection(&by_client[0], &record); 1608 assert_same_connection(&by_secret.expect("by secret"), &record); 1609 assert!(empty_secret.is_none()); 1610 assert_eq!(all_connections.len(), 1); 1611 assert_same_connection(&all_connections[0], &record); 1612 } 1613 1614 #[test] 1615 fn granted_permissions_and_approval_enforce_subset_rules() { 1616 let manager = RadrootsNostrSignerManager::new_in_memory(); 1617 manager 1618 .set_signer_identity(public_identity(0x18)) 1619 .expect("set signer"); 1620 let requested = vec![ 1621 permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")), 1622 permission(RadrootsNostrConnectMethod::Ping, None), 1623 ]; 1624 let granted = vec![requested[1].clone()]; 1625 let invalid = vec![permission( 1626 RadrootsNostrConnectMethod::Nip44Encrypt, 1627 Some("kind:1"), 1628 )]; 1629 let pending = manager 1630 .register_connection( 1631 RadrootsNostrSignerConnectionDraft::new(public_key(0x19), public_identity(0x20)) 1632 .with_requested_permissions(requested.clone().into()) 1633 .with_approval_requirement( 1634 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 1635 ), 1636 ) 1637 .expect("register"); 1638 1639 let invalid_set = manager 1640 .set_granted_permissions(&pending.connection_id, invalid.clone().into()) 1641 .expect_err("invalid set grants"); 1642 assert!( 1643 invalid_set 1644 .to_string() 1645 .contains("invalid granted permission") 1646 ); 1647 1648 let set_grants = manager 1649 .set_granted_permissions(&pending.connection_id, granted.clone().into()) 1650 .expect("set grants"); 1651 assert_eq!( 1652 set_grants.granted_permissions().as_slice(), 1653 granted.as_slice() 1654 ); 1655 assert_eq!( 1656 set_grants.status, 1657 RadrootsNostrSignerConnectionStatus::Pending 1658 ); 1659 1660 let approved = manager 1661 .approve_connection(&pending.connection_id, granted.clone().into()) 1662 .expect("approve"); 1663 assert_eq!(approved.status, RadrootsNostrSignerConnectionStatus::Active); 1664 assert_eq!( 1665 approved.approval_state, 1666 RadrootsNostrSignerApprovalState::Approved 1667 ); 1668 assert_eq!( 1669 approved.granted_permissions().as_slice(), 1670 granted.as_slice() 1671 ); 1672 1673 let reapprove = manager 1674 .approve_connection(&pending.connection_id, granted.into()) 1675 .expect("reapprove active"); 1676 assert_eq!( 1677 reapprove.status, 1678 RadrootsNostrSignerConnectionStatus::Active 1679 ); 1680 1681 let auto = manager 1682 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1683 public_key(0x21), 1684 public_identity(0x22), 1685 )) 1686 .expect("register auto"); 1687 let err = manager 1688 .approve_connection( 1689 &auto.connection_id, 1690 RadrootsNostrConnectPermissions::default(), 1691 ) 1692 .expect_err("approval not required"); 1693 assert!(err.to_string().contains("approval not required")); 1694 1695 let terminal_pending = manager 1696 .register_connection( 1697 RadrootsNostrSignerConnectionDraft::new(public_key(0x40), public_identity(0x41)) 1698 .with_connect_secret("terminal-secret") 1699 .with_approval_requirement( 1700 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 1701 ), 1702 ) 1703 .expect("register terminal"); 1704 manager 1705 .reject_connection(&terminal_pending.connection_id, Some("terminal".into())) 1706 .expect("reject terminal"); 1707 let terminal_approve = manager 1708 .approve_connection( 1709 &terminal_pending.connection_id, 1710 vec![requested[0].clone()].into(), 1711 ) 1712 .expect_err("approve rejected"); 1713 assert!( 1714 terminal_approve 1715 .to_string() 1716 .contains("cannot approve rejected connection") 1717 ); 1718 1719 let unrestricted = manager 1720 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1721 public_key(0x23), 1722 public_identity(0x24), 1723 )) 1724 .expect("register unrestricted"); 1725 let unrestricted_grants = manager 1726 .set_granted_permissions(&unrestricted.connection_id, invalid.into()) 1727 .expect("unrestricted grants"); 1728 assert_eq!(unrestricted_grants.granted_permissions.len(), 1); 1729 } 1730 1731 #[test] 1732 fn reject_revoke_and_relay_updates_cover_terminal_paths() { 1733 let manager = RadrootsNostrSignerManager::new_in_memory(); 1734 manager 1735 .set_signer_identity(public_identity(0x25)) 1736 .expect("set signer"); 1737 let rejected = manager 1738 .register_connection( 1739 RadrootsNostrSignerConnectionDraft::new(public_key(0x26), public_identity(0x27)) 1740 .with_connect_secret("shared-secret") 1741 .with_approval_requirement( 1742 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 1743 ), 1744 ) 1745 .expect("register reject"); 1746 let rejected = manager 1747 .reject_connection(&rejected.connection_id, Some("denied".into())) 1748 .expect("reject"); 1749 assert_eq!( 1750 rejected.status, 1751 RadrootsNostrSignerConnectionStatus::Rejected 1752 ); 1753 assert_eq!(rejected.status_reason.as_deref(), Some("denied")); 1754 1755 let reject_err = manager 1756 .reject_connection(&rejected.connection_id, None) 1757 .expect_err("reject terminal"); 1758 assert!( 1759 reject_err 1760 .to_string() 1761 .contains("cannot reject rejected connection") 1762 ); 1763 1764 let relay_err = manager 1765 .update_relays(&rejected.connection_id, vec![primary_relay()]) 1766 .expect_err("update rejected"); 1767 assert!( 1768 relay_err 1769 .to_string() 1770 .contains("cannot update relays for rejected connection") 1771 ); 1772 let rejected_lookup = manager 1773 .find_connection_by_connect_secret("shared-secret") 1774 .expect("lookup rejected secret"); 1775 assert!(rejected_lookup.is_none()); 1776 1777 let active = manager 1778 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1779 public_key(0x28), 1780 public_identity(0x29), 1781 )) 1782 .expect("register active"); 1783 let active = manager 1784 .update_relays( 1785 &active.connection_id, 1786 vec![tertiary_relay(), secondary_relay(), secondary_relay()], 1787 ) 1788 .expect("update relays"); 1789 assert_eq!(active.relays, vec![secondary_relay(), tertiary_relay()]); 1790 1791 let revoked = manager 1792 .revoke_connection(&active.connection_id, Some("manual".into())) 1793 .expect("revoke"); 1794 assert_eq!(revoked.status, RadrootsNostrSignerConnectionStatus::Revoked); 1795 assert_eq!(revoked.status_reason.as_deref(), Some("manual")); 1796 1797 let revoke_again = manager 1798 .revoke_connection(&active.connection_id, None) 1799 .expect_err("revoke twice"); 1800 assert!( 1801 revoke_again 1802 .to_string() 1803 .contains("connection already revoked") 1804 ); 1805 1806 let grants_err = manager 1807 .set_granted_permissions( 1808 &active.connection_id, 1809 vec![permission(RadrootsNostrConnectMethod::Ping, None)].into(), 1810 ) 1811 .expect_err("update grants revoked"); 1812 assert!( 1813 grants_err 1814 .to_string() 1815 .contains("cannot update granted permissions for revoked connection") 1816 ); 1817 1818 let require_auth_err = manager 1819 .require_auth_challenge(&active.connection_id, api_primary_https()) 1820 .expect_err("require auth revoked"); 1821 assert!( 1822 require_auth_err 1823 .to_string() 1824 .contains("cannot require auth for revoked connection") 1825 ); 1826 1827 let pending_request_err = manager 1828 .set_pending_request(&active.connection_id, request_message("req-terminal")) 1829 .expect_err("pending request revoked"); 1830 assert!( 1831 pending_request_err 1832 .to_string() 1833 .contains("cannot set pending request for revoked connection") 1834 ); 1835 1836 let authorize_auth_err = manager 1837 .authorize_auth_challenge(&active.connection_id) 1838 .expect_err("authorize auth revoked"); 1839 assert!( 1840 authorize_auth_err 1841 .to_string() 1842 .contains("cannot authorize auth challenge for revoked connection") 1843 ); 1844 } 1845 1846 #[test] 1847 fn authentication_and_request_audit_paths_are_recorded() { 1848 let manager = RadrootsNostrSignerManager::new_in_memory(); 1849 manager 1850 .set_signer_identity(public_identity(0x30)) 1851 .expect("set signer"); 1852 let record = manager 1853 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1854 public_key(0x31), 1855 public_identity(0x32), 1856 )) 1857 .expect("register"); 1858 1859 let authenticated = manager 1860 .mark_authenticated(&record.connection_id) 1861 .expect("auth"); 1862 assert!(authenticated.last_authenticated_at_unix.is_some()); 1863 1864 let consumed = manager 1865 .mark_connect_secret_consumed(&record.connection_id) 1866 .expect_err("consume missing secret"); 1867 assert!( 1868 consumed 1869 .to_string() 1870 .contains("connection does not have a connect secret") 1871 ); 1872 1873 let audit = manager 1874 .record_request( 1875 &record.connection_id, 1876 " request-1 ", 1877 RadrootsNostrConnectMethod::Ping, 1878 RadrootsNostrSignerRequestDecision::Challenged, 1879 Some(" challenge ".into()), 1880 ) 1881 .expect("record request"); 1882 assert_eq!(audit.request_id.as_str(), "request-1"); 1883 assert_eq!(audit.message.as_deref(), Some("challenge")); 1884 1885 let blank_message_audit = manager 1886 .record_request( 1887 &record.connection_id, 1888 "request-2", 1889 RadrootsNostrConnectMethod::Ping, 1890 RadrootsNostrSignerRequestDecision::Denied, 1891 Some(" ".into()), 1892 ) 1893 .expect("record blank message"); 1894 assert!(blank_message_audit.message.is_none()); 1895 1896 let all_audits = manager.list_audit_records().expect("list audits"); 1897 let connection_audits = manager 1898 .audit_records_for_connection(&record.connection_id) 1899 .expect("connection audits"); 1900 let stored = manager 1901 .get_connection(&record.connection_id) 1902 .expect("get") 1903 .expect("stored"); 1904 assert_eq!(all_audits, vec![audit.clone(), blank_message_audit.clone()]); 1905 assert_eq!(connection_audits, vec![audit, blank_message_audit]); 1906 assert!(stored.last_request_at_unix.is_some()); 1907 1908 let request_err = manager 1909 .record_request( 1910 &record.connection_id, 1911 " ", 1912 RadrootsNostrConnectMethod::Ping, 1913 RadrootsNostrSignerRequestDecision::Denied, 1914 None, 1915 ) 1916 .expect_err("invalid request id"); 1917 assert!(request_err.to_string().contains("invalid request id")); 1918 } 1919 1920 #[test] 1921 fn auth_challenge_and_pending_request_state_are_persisted_and_replayed() { 1922 let manager = RadrootsNostrSignerManager::new_in_memory(); 1923 manager 1924 .set_signer_identity(public_identity(0x34)) 1925 .expect("set signer"); 1926 let record = manager 1927 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1928 public_key(0x35), 1929 public_identity(0x36), 1930 )) 1931 .expect("register"); 1932 1933 let required = manager 1934 .require_auth_challenge( 1935 &record.connection_id, 1936 format!(" {}/flow ", api_primary_https()).as_str(), 1937 ) 1938 .expect("require auth"); 1939 assert_eq!(required.auth_state, RadrootsNostrSignerAuthState::Pending); 1940 assert_eq!( 1941 required 1942 .auth_challenge 1943 .as_ref() 1944 .expect("auth challenge") 1945 .auth_url, 1946 format!("{}/flow", api_primary_https()) 1947 ); 1948 assert!(required.pending_request.is_none()); 1949 1950 let pending = manager 1951 .set_pending_request(&record.connection_id, request_message(" req-auth ")) 1952 .expect("set pending request"); 1953 assert_eq!( 1954 pending 1955 .pending_request 1956 .as_ref() 1957 .expect("pending request") 1958 .request_id() 1959 .as_str(), 1960 "req-auth" 1961 ); 1962 1963 let authorized = manager 1964 .authorize_auth_challenge(&record.connection_id) 1965 .expect("authorize"); 1966 assert_eq!( 1967 authorized.connection.auth_state, 1968 RadrootsNostrSignerAuthState::Authorized 1969 ); 1970 assert!(authorized.connection.last_authenticated_at_unix.is_some()); 1971 assert!(authorized.connection.pending_request.is_none()); 1972 assert_eq!( 1973 authorized 1974 .pending_request 1975 .as_ref() 1976 .expect("replayed request") 1977 .request_message() 1978 .id, 1979 "req-auth" 1980 ); 1981 assert_eq!( 1982 authorized 1983 .connection 1984 .auth_challenge 1985 .as_ref() 1986 .expect("authorized challenge") 1987 .authorized_at_unix, 1988 authorized.connection.last_authenticated_at_unix 1989 ); 1990 1991 let invalid_url = manager 1992 .require_auth_challenge(&record.connection_id, "not-a-url") 1993 .expect_err("invalid auth url"); 1994 assert!(invalid_url.to_string().contains("invalid auth url")); 1995 1996 let no_pending_auth = manager 1997 .set_pending_request(&record.connection_id, request_message("req-again")) 1998 .expect_err("pending request without auth challenge"); 1999 assert!( 2000 no_pending_auth 2001 .to_string() 2002 .contains("auth challenge not pending for connection") 2003 ); 2004 2005 let no_authorize = manager 2006 .authorize_auth_challenge(&record.connection_id) 2007 .expect_err("authorize without pending auth challenge"); 2008 assert!( 2009 no_authorize 2010 .to_string() 2011 .contains("auth challenge not pending for connection") 2012 ); 2013 } 2014 2015 #[test] 2016 fn restored_authorized_auth_challenge_requeues_pending_request() { 2017 let manager = RadrootsNostrSignerManager::new_in_memory(); 2018 manager 2019 .set_signer_identity(public_identity(0x134)) 2020 .expect("set signer"); 2021 let record = manager 2022 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2023 public_key(0x135), 2024 public_identity(0x136), 2025 )) 2026 .expect("register"); 2027 2028 manager 2029 .require_auth_challenge( 2030 &record.connection_id, 2031 format!("{}/flow", api_primary_https()).as_str(), 2032 ) 2033 .expect("require auth"); 2034 manager 2035 .set_pending_request(&record.connection_id, request_message("req-replay")) 2036 .expect("set pending"); 2037 2038 let authorized = manager 2039 .authorize_auth_challenge(&record.connection_id) 2040 .expect("authorize"); 2041 let pending_request = authorized.pending_request.expect("pending request"); 2042 2043 let restored = manager 2044 .restore_pending_auth_challenge(&record.connection_id, pending_request.clone()) 2045 .expect("restore pending challenge"); 2046 assert_eq!(restored.auth_state, RadrootsNostrSignerAuthState::Pending); 2047 assert_eq!( 2048 restored 2049 .auth_challenge 2050 .as_ref() 2051 .expect("challenge") 2052 .authorized_at_unix, 2053 None 2054 ); 2055 assert!(restored.last_authenticated_at_unix.is_none()); 2056 assert_eq!( 2057 restored 2058 .pending_request 2059 .as_ref() 2060 .expect("pending request") 2061 .request_id() 2062 .as_str(), 2063 pending_request.request_id().as_str() 2064 ); 2065 } 2066 2067 #[test] 2068 fn connect_secret_consumption_persists_and_remains_idempotent() { 2069 let manager = RadrootsNostrSignerManager::new_in_memory(); 2070 manager 2071 .set_signer_identity(public_identity(0x37)) 2072 .expect("set signer"); 2073 let record = manager 2074 .register_connection( 2075 RadrootsNostrSignerConnectionDraft::new(public_key(0x38), public_identity(0x39)) 2076 .with_connect_secret("one-shot-secret"), 2077 ) 2078 .expect("register"); 2079 2080 let consumed = manager 2081 .mark_connect_secret_consumed(&record.connection_id) 2082 .expect("consume secret"); 2083 assert!(consumed.connect_secret_is_consumed()); 2084 assert!(consumed.connect_secret_consumed_at_unix.is_some()); 2085 2086 let consumed_again = manager 2087 .mark_connect_secret_consumed(&record.connection_id) 2088 .expect("consume secret again"); 2089 assert_eq!( 2090 consumed_again.connect_secret_consumed_at_unix, 2091 consumed.connect_secret_consumed_at_unix 2092 ); 2093 2094 let found = manager 2095 .find_connection_by_connect_secret("one-shot-secret") 2096 .expect("find consumed secret") 2097 .expect("stored secret"); 2098 assert!(found.connect_secret_is_consumed()); 2099 assert_eq!( 2100 found.connect_secret_consumed_at_unix, 2101 consumed.connect_secret_consumed_at_unix 2102 ); 2103 } 2104 2105 #[test] 2106 fn connect_secret_publish_workflow_is_persisted_and_finalized() { 2107 let manager = RadrootsNostrSignerManager::new_in_memory(); 2108 manager 2109 .set_signer_identity(public_identity(0x237)) 2110 .expect("set signer"); 2111 let record = manager 2112 .register_connection( 2113 RadrootsNostrSignerConnectionDraft::new(public_key(0x238), public_identity(0x239)) 2114 .with_connect_secret("workflow-secret"), 2115 ) 2116 .expect("register"); 2117 2118 let workflow = manager 2119 .begin_connect_secret_publish_finalization(&record.connection_id) 2120 .expect("begin workflow"); 2121 assert_eq!( 2122 workflow.kind, 2123 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization 2124 ); 2125 assert_eq!( 2126 workflow.state, 2127 RadrootsNostrSignerPublishWorkflowState::PendingPublish 2128 ); 2129 assert!(workflow.pending_request.is_none()); 2130 assert!( 2131 !manager 2132 .get_connection(&record.connection_id) 2133 .expect("get") 2134 .expect("stored") 2135 .connect_secret_is_consumed() 2136 ); 2137 assert_eq!( 2138 manager.list_publish_workflows().expect("list workflows"), 2139 vec![workflow.clone()] 2140 ); 2141 2142 let published = manager 2143 .mark_publish_workflow_published(&workflow.workflow_id) 2144 .expect("mark published"); 2145 assert_eq!( 2146 published.state, 2147 RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize 2148 ); 2149 2150 let finalized = manager 2151 .finalize_publish_workflow(&workflow.workflow_id) 2152 .expect("finalize workflow"); 2153 assert!(finalized.connect_secret_is_consumed()); 2154 assert!( 2155 manager 2156 .list_publish_workflows() 2157 .expect("list workflows") 2158 .is_empty() 2159 ); 2160 assert!( 2161 manager 2162 .find_connection_by_connect_secret("workflow-secret") 2163 .expect("find secret") 2164 .expect("stored") 2165 .connect_secret_is_consumed() 2166 ); 2167 } 2168 2169 #[test] 2170 fn auth_replay_publish_workflow_is_persisted_and_finalized() { 2171 let manager = RadrootsNostrSignerManager::new_in_memory(); 2172 manager 2173 .set_signer_identity(public_identity(0x23a)) 2174 .expect("set signer"); 2175 let record = manager 2176 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2177 public_key(0x23b), 2178 public_identity(0x23c), 2179 )) 2180 .expect("register"); 2181 2182 manager 2183 .require_auth_challenge( 2184 &record.connection_id, 2185 format!("{}/flow", api_primary_https()).as_str(), 2186 ) 2187 .expect("require auth"); 2188 let pending = manager 2189 .set_pending_request(&record.connection_id, request_message("req-auth-workflow")) 2190 .expect("set pending"); 2191 let pending_request = pending.pending_request.expect("pending request"); 2192 2193 let workflow = manager 2194 .begin_auth_replay_publish_finalization(&record.connection_id) 2195 .expect("begin auth replay workflow"); 2196 assert_eq!( 2197 workflow.kind, 2198 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization 2199 ); 2200 assert_eq!(workflow.pending_request.as_ref(), Some(&pending_request)); 2201 assert!(workflow.authorized_at_unix.is_some()); 2202 2203 let stored_before_publish = manager 2204 .get_connection(&record.connection_id) 2205 .expect("get") 2206 .expect("stored"); 2207 assert_eq!( 2208 stored_before_publish.auth_state, 2209 RadrootsNostrSignerAuthState::Pending 2210 ); 2211 assert_eq!( 2212 stored_before_publish.pending_request.as_ref(), 2213 Some(&pending_request) 2214 ); 2215 2216 manager 2217 .mark_publish_workflow_published(&workflow.workflow_id) 2218 .expect("mark published"); 2219 let finalized = manager 2220 .finalize_publish_workflow(&workflow.workflow_id) 2221 .expect("finalize auth replay"); 2222 assert_eq!( 2223 finalized.auth_state, 2224 RadrootsNostrSignerAuthState::Authorized 2225 ); 2226 assert!(finalized.pending_request.is_none()); 2227 assert_eq!( 2228 finalized 2229 .auth_challenge 2230 .as_ref() 2231 .expect("challenge") 2232 .authorized_at_unix, 2233 workflow.authorized_at_unix 2234 ); 2235 assert_eq!( 2236 finalized.last_authenticated_at_unix, 2237 workflow.authorized_at_unix 2238 ); 2239 assert!( 2240 manager 2241 .list_publish_workflows() 2242 .expect("list workflows") 2243 .is_empty() 2244 ); 2245 } 2246 2247 #[test] 2248 fn canceling_auth_replay_publish_workflow_preserves_pending_request() { 2249 let manager = RadrootsNostrSignerManager::new_in_memory(); 2250 manager 2251 .set_signer_identity(public_identity(0x23d)) 2252 .expect("set signer"); 2253 let record = manager 2254 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2255 public_key(0x23e), 2256 public_identity(0x23f), 2257 )) 2258 .expect("register"); 2259 2260 manager 2261 .require_auth_challenge( 2262 &record.connection_id, 2263 format!("{}/flow", api_primary_https()).as_str(), 2264 ) 2265 .expect("require auth"); 2266 let pending = manager 2267 .set_pending_request(&record.connection_id, request_message("req-auth-cancel")) 2268 .expect("set pending"); 2269 let pending_request = pending.pending_request.expect("pending request"); 2270 2271 let workflow = manager 2272 .begin_auth_replay_publish_finalization(&record.connection_id) 2273 .expect("begin auth replay workflow"); 2274 let canceled = manager 2275 .cancel_publish_workflow(&workflow.workflow_id) 2276 .expect("cancel workflow"); 2277 assert_eq!(canceled.workflow_id, workflow.workflow_id); 2278 2279 let stored = manager 2280 .get_connection(&record.connection_id) 2281 .expect("get") 2282 .expect("stored"); 2283 assert_eq!(stored.auth_state, RadrootsNostrSignerAuthState::Pending); 2284 assert_eq!(stored.pending_request.as_ref(), Some(&pending_request)); 2285 assert!( 2286 manager 2287 .list_publish_workflows() 2288 .expect("list workflows") 2289 .is_empty() 2290 ); 2291 } 2292 2293 #[test] 2294 fn evaluate_auth_replay_publish_workflow_uses_authorized_view_without_mutating_state() { 2295 let manager = RadrootsNostrSignerManager::new_in_memory(); 2296 manager 2297 .set_signer_identity(public_identity(0x240)) 2298 .expect("set signer"); 2299 let record = manager 2300 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2301 public_key(0x241), 2302 public_identity(0x242), 2303 )) 2304 .expect("register"); 2305 2306 manager 2307 .set_granted_permissions( 2308 &record.connection_id, 2309 vec!["get_public_key".parse().expect("permission")].into(), 2310 ) 2311 .expect("grant permissions"); 2312 manager 2313 .require_auth_challenge( 2314 &record.connection_id, 2315 format!("{}/flow", api_primary_https()).as_str(), 2316 ) 2317 .expect("require auth"); 2318 let pending = manager 2319 .set_pending_request( 2320 &record.connection_id, 2321 RadrootsNostrConnectRequestMessage::new( 2322 "req-auth-preview", 2323 RadrootsNostrConnectRequest::GetPublicKey, 2324 ), 2325 ) 2326 .expect("set pending"); 2327 let pending_request = pending.pending_request.expect("pending request"); 2328 2329 let workflow = manager 2330 .begin_auth_replay_publish_finalization(&record.connection_id) 2331 .expect("begin auth replay workflow"); 2332 let evaluation = manager 2333 .evaluate_auth_replay_publish_workflow(&workflow.workflow_id) 2334 .expect("evaluate auth replay workflow"); 2335 2336 assert_eq!( 2337 evaluation.request_id.as_str(), 2338 pending_request.request_id().as_str() 2339 ); 2340 assert_eq!( 2341 evaluation.connection.auth_state, 2342 RadrootsNostrSignerAuthState::Authorized 2343 ); 2344 assert!(evaluation.connection.pending_request.is_none()); 2345 assert!(matches!( 2346 evaluation.action, 2347 RadrootsNostrSignerRequestAction::Allowed { .. } 2348 )); 2349 2350 let stored = manager 2351 .get_connection(&record.connection_id) 2352 .expect("get") 2353 .expect("stored"); 2354 assert_eq!(stored.auth_state, RadrootsNostrSignerAuthState::Pending); 2355 assert_eq!(stored.pending_request.as_ref(), Some(&pending_request)); 2356 } 2357 2358 #[test] 2359 fn publish_workflow_duplicate_and_missing_paths_are_rejected() { 2360 let manager = RadrootsNostrSignerManager::new_in_memory(); 2361 manager 2362 .set_signer_identity(public_identity(0x240)) 2363 .expect("set signer"); 2364 let record = manager 2365 .register_connection( 2366 RadrootsNostrSignerConnectionDraft::new(public_key(0x241), public_identity(0x242)) 2367 .with_connect_secret("duplicate-secret"), 2368 ) 2369 .expect("register"); 2370 2371 let workflow = manager 2372 .begin_connect_secret_publish_finalization(&record.connection_id) 2373 .expect("begin workflow"); 2374 let duplicate = manager 2375 .begin_connect_secret_publish_finalization(&record.connection_id) 2376 .expect_err("duplicate workflow"); 2377 assert!( 2378 duplicate 2379 .to_string() 2380 .contains("publish workflow already active") 2381 ); 2382 2383 let missing_workflow_id = RadrootsNostrSignerWorkflowId::parse("wf-missing").expect("id"); 2384 let missing_mark = manager 2385 .mark_publish_workflow_published(&missing_workflow_id) 2386 .expect_err("missing mark"); 2387 let missing_finalize = manager 2388 .finalize_publish_workflow(&missing_workflow_id) 2389 .expect_err("missing finalize"); 2390 let missing_cancel = manager 2391 .cancel_publish_workflow(&missing_workflow_id) 2392 .expect_err("missing cancel"); 2393 2394 for err in [missing_mark, missing_finalize, missing_cancel] { 2395 assert!(err.to_string().contains("publish workflow not found")); 2396 } 2397 2398 let unpublished_finalize = manager 2399 .finalize_publish_workflow(&workflow.workflow_id) 2400 .expect_err("unpublished finalize"); 2401 assert!( 2402 unpublished_finalize 2403 .to_string() 2404 .contains("publish workflow has not reached published state") 2405 ); 2406 } 2407 2408 #[test] 2409 fn publish_workflow_entrypoints_reject_invalid_connection_states() { 2410 let manager = RadrootsNostrSignerManager::new_in_memory(); 2411 manager 2412 .set_signer_identity(public_identity(0x300)) 2413 .expect("set signer"); 2414 let missing_connection_id = 2415 RadrootsNostrSignerConnectionId::parse("conn-missing-publish").expect("connection id"); 2416 let restore_pending_request = 2417 RadrootsNostrSignerPendingRequest::new(request_message("req-restore-invalid"), 61) 2418 .expect("pending request"); 2419 2420 let missing_restore_err = manager 2421 .restore_pending_auth_challenge(&missing_connection_id, restore_pending_request.clone()) 2422 .expect_err("missing restore connection"); 2423 assert!( 2424 missing_restore_err 2425 .to_string() 2426 .contains("connection not found") 2427 ); 2428 2429 let terminal_restore = manager 2430 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2431 public_key(0x301), 2432 public_identity(0x302), 2433 )) 2434 .expect("register terminal restore"); 2435 manager 2436 .reject_connection(&terminal_restore.connection_id, Some("closed".into())) 2437 .expect("reject terminal restore"); 2438 let terminal_restore_err = manager 2439 .restore_pending_auth_challenge( 2440 &terminal_restore.connection_id, 2441 restore_pending_request.clone(), 2442 ) 2443 .expect_err("terminal restore error"); 2444 assert!( 2445 terminal_restore_err 2446 .to_string() 2447 .contains("cannot restore auth challenge for rejected connection") 2448 ); 2449 2450 let unauthorized_restore = manager 2451 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2452 public_key(0x303), 2453 public_identity(0x304), 2454 )) 2455 .expect("register unauthorized restore"); 2456 let unauthorized_restore_err = manager 2457 .restore_pending_auth_challenge( 2458 &unauthorized_restore.connection_id, 2459 restore_pending_request.clone(), 2460 ) 2461 .expect_err("unauthorized restore error"); 2462 assert!( 2463 unauthorized_restore_err 2464 .to_string() 2465 .contains("auth challenge not authorized for connection") 2466 ); 2467 2468 let missing_challenge_restore = manager 2469 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2470 public_key(0x305), 2471 public_identity(0x306), 2472 )) 2473 .expect("register missing challenge restore"); 2474 manager 2475 .require_auth_challenge( 2476 &missing_challenge_restore.connection_id, 2477 format!("{}/restore", api_primary_https()).as_str(), 2478 ) 2479 .expect("require auth"); 2480 manager 2481 .set_pending_request( 2482 &missing_challenge_restore.connection_id, 2483 request_message("req-restore-missing-challenge"), 2484 ) 2485 .expect("set pending"); 2486 let replay = manager 2487 .authorize_auth_challenge(&missing_challenge_restore.connection_id) 2488 .expect("authorize") 2489 .pending_request 2490 .expect("pending request"); 2491 { 2492 let mut state = manager.state.write().expect("write"); 2493 let record = state 2494 .connections 2495 .iter_mut() 2496 .find(|record| record.connection_id == missing_challenge_restore.connection_id) 2497 .expect("stored connection"); 2498 record.auth_challenge = None; 2499 } 2500 let missing_challenge_restore_err = manager 2501 .restore_pending_auth_challenge(&missing_challenge_restore.connection_id, replay) 2502 .expect_err("missing challenge restore error"); 2503 assert!( 2504 missing_challenge_restore_err 2505 .to_string() 2506 .contains("auth challenge missing for connection") 2507 ); 2508 2509 let terminal_connect = manager 2510 .register_connection( 2511 RadrootsNostrSignerConnectionDraft::new(public_key(0x307), public_identity(0x308)) 2512 .with_connect_secret("terminal-connect-secret"), 2513 ) 2514 .expect("register terminal connect"); 2515 manager 2516 .reject_connection(&terminal_connect.connection_id, Some("closed".into())) 2517 .expect("reject terminal connect"); 2518 let terminal_connect_err = manager 2519 .begin_connect_secret_publish_finalization(&terminal_connect.connection_id) 2520 .expect_err("terminal connect workflow"); 2521 assert!( 2522 terminal_connect_err 2523 .to_string() 2524 .contains("cannot begin connect secret finalization for rejected connection") 2525 ); 2526 2527 let no_secret_connect = manager 2528 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2529 public_key(0x309), 2530 public_identity(0x30a), 2531 )) 2532 .expect("register no secret connect"); 2533 let no_secret_connect_err = manager 2534 .begin_connect_secret_publish_finalization(&no_secret_connect.connection_id) 2535 .expect_err("missing secret workflow"); 2536 assert!( 2537 no_secret_connect_err 2538 .to_string() 2539 .contains("connection does not have a connect secret") 2540 ); 2541 2542 let consumed_connect = manager 2543 .register_connection( 2544 RadrootsNostrSignerConnectionDraft::new(public_key(0x30b), public_identity(0x30c)) 2545 .with_connect_secret("consumed-connect-secret"), 2546 ) 2547 .expect("register consumed connect"); 2548 manager 2549 .mark_connect_secret_consumed(&consumed_connect.connection_id) 2550 .expect("consume connect secret"); 2551 let consumed_connect_err = manager 2552 .begin_connect_secret_publish_finalization(&consumed_connect.connection_id) 2553 .expect_err("consumed secret workflow"); 2554 assert!( 2555 consumed_connect_err 2556 .to_string() 2557 .contains("connect secret already consumed for connection") 2558 ); 2559 2560 let missing_mark_consumed_err = manager 2561 .mark_connect_secret_consumed(&missing_connection_id) 2562 .expect_err("missing mark connect secret consumed"); 2563 assert!( 2564 missing_mark_consumed_err 2565 .to_string() 2566 .contains("connection not found") 2567 ); 2568 2569 let terminal_auth = manager 2570 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2571 public_key(0x30d), 2572 public_identity(0x30e), 2573 )) 2574 .expect("register terminal auth"); 2575 manager 2576 .reject_connection(&terminal_auth.connection_id, Some("closed".into())) 2577 .expect("reject terminal auth"); 2578 let terminal_auth_err = manager 2579 .begin_auth_replay_publish_finalization(&terminal_auth.connection_id) 2580 .expect_err("terminal auth workflow"); 2581 assert!( 2582 terminal_auth_err 2583 .to_string() 2584 .contains("cannot begin auth replay finalization for rejected connection") 2585 ); 2586 2587 let not_pending_auth = manager 2588 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2589 public_key(0x30f), 2590 public_identity(0x310), 2591 )) 2592 .expect("register not pending auth"); 2593 let not_pending_auth_err = manager 2594 .begin_auth_replay_publish_finalization(¬_pending_auth.connection_id) 2595 .expect_err("not pending auth workflow"); 2596 assert!( 2597 not_pending_auth_err 2598 .to_string() 2599 .contains("auth challenge not pending for connection") 2600 ); 2601 2602 let missing_challenge_auth = manager 2603 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2604 public_key(0x311), 2605 public_identity(0x312), 2606 )) 2607 .expect("register missing challenge auth"); 2608 manager 2609 .require_auth_challenge( 2610 &missing_challenge_auth.connection_id, 2611 format!("{}/auth-missing-challenge", api_primary_https()).as_str(), 2612 ) 2613 .expect("require auth"); 2614 { 2615 let mut state = manager.state.write().expect("write"); 2616 let record = state 2617 .connections 2618 .iter_mut() 2619 .find(|record| record.connection_id == missing_challenge_auth.connection_id) 2620 .expect("stored connection"); 2621 record.auth_challenge = None; 2622 } 2623 let missing_challenge_auth_err = manager 2624 .begin_auth_replay_publish_finalization(&missing_challenge_auth.connection_id) 2625 .expect_err("missing challenge auth workflow"); 2626 assert!( 2627 missing_challenge_auth_err 2628 .to_string() 2629 .contains("auth challenge missing for connection") 2630 ); 2631 2632 let missing_pending_auth = manager 2633 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2634 public_key(0x313), 2635 public_identity(0x314), 2636 )) 2637 .expect("register missing pending auth"); 2638 manager 2639 .require_auth_challenge( 2640 &missing_pending_auth.connection_id, 2641 format!("{}/auth-missing-pending", api_primary_https()).as_str(), 2642 ) 2643 .expect("require auth"); 2644 let missing_pending_auth_err = manager 2645 .begin_auth_replay_publish_finalization(&missing_pending_auth.connection_id) 2646 .expect_err("missing pending auth workflow"); 2647 assert!( 2648 missing_pending_auth_err 2649 .to_string() 2650 .contains("pending request missing for auth replay finalization") 2651 ); 2652 2653 let duplicate_auth = manager 2654 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2655 public_key(0x315), 2656 public_identity(0x316), 2657 )) 2658 .expect("register duplicate auth"); 2659 manager 2660 .require_auth_challenge( 2661 &duplicate_auth.connection_id, 2662 format!("{}/auth-duplicate", api_primary_https()).as_str(), 2663 ) 2664 .expect("require auth"); 2665 manager 2666 .set_pending_request( 2667 &duplicate_auth.connection_id, 2668 request_message("req-auth-duplicate"), 2669 ) 2670 .expect("set pending"); 2671 manager 2672 .begin_auth_replay_publish_finalization(&duplicate_auth.connection_id) 2673 .expect("begin auth workflow"); 2674 let duplicate_auth_err = manager 2675 .begin_auth_replay_publish_finalization(&duplicate_auth.connection_id) 2676 .expect_err("duplicate auth workflow"); 2677 assert!( 2678 duplicate_auth_err 2679 .to_string() 2680 .contains("publish workflow already active for auth_replay_finalization") 2681 ); 2682 } 2683 2684 #[test] 2685 fn publish_workflow_finalize_and_evaluate_reject_corrupted_states() { 2686 let manager = RadrootsNostrSignerManager::new_in_memory(); 2687 manager 2688 .set_signer_identity(public_identity(0x320)) 2689 .expect("set signer"); 2690 2691 let missing_workflow_id = 2692 RadrootsNostrSignerWorkflowId::parse("wf-evaluate-missing").expect("workflow id"); 2693 let missing_evaluate_err = manager 2694 .evaluate_auth_replay_publish_workflow(&missing_workflow_id) 2695 .expect_err("missing workflow evaluate"); 2696 assert!( 2697 missing_evaluate_err 2698 .to_string() 2699 .contains("publish workflow not found") 2700 ); 2701 2702 let connect_kind_record = manager 2703 .register_connection( 2704 RadrootsNostrSignerConnectionDraft::new(public_key(0x321), public_identity(0x322)) 2705 .with_connect_secret("evaluate-connect-kind"), 2706 ) 2707 .expect("register connect kind"); 2708 let connect_kind_workflow = manager 2709 .begin_connect_secret_publish_finalization(&connect_kind_record.connection_id) 2710 .expect("begin connect workflow"); 2711 let wrong_kind_err = manager 2712 .evaluate_auth_replay_publish_workflow(&connect_kind_workflow.workflow_id) 2713 .expect_err("wrong workflow kind"); 2714 assert!( 2715 wrong_kind_err 2716 .to_string() 2717 .contains("publish workflow is not an auth replay finalization") 2718 ); 2719 2720 let connect_missing_secret_record = manager 2721 .register_connection( 2722 RadrootsNostrSignerConnectionDraft::new(public_key(0x323), public_identity(0x324)) 2723 .with_connect_secret("missing-secret-finalize"), 2724 ) 2725 .expect("register connect missing secret"); 2726 let connect_missing_secret_workflow = manager 2727 .begin_connect_secret_publish_finalization(&connect_missing_secret_record.connection_id) 2728 .expect("begin connect missing secret workflow"); 2729 manager 2730 .mark_publish_workflow_published(&connect_missing_secret_workflow.workflow_id) 2731 .expect("mark connect missing secret workflow"); 2732 { 2733 let mut state = manager.state.write().expect("write"); 2734 let record = state 2735 .connections 2736 .iter_mut() 2737 .find(|record| record.connection_id == connect_missing_secret_record.connection_id) 2738 .expect("stored connection"); 2739 record.connect_secret_hash = None; 2740 record.connect_secret_consumed_at_unix = None; 2741 } 2742 let connect_missing_secret_err = manager 2743 .finalize_publish_workflow(&connect_missing_secret_workflow.workflow_id) 2744 .expect_err("missing connect secret finalize"); 2745 assert!( 2746 connect_missing_secret_err 2747 .to_string() 2748 .contains("connection does not have a connect secret") 2749 ); 2750 2751 let connect_consumed_record = manager 2752 .register_connection( 2753 RadrootsNostrSignerConnectionDraft::new(public_key(0x325), public_identity(0x326)) 2754 .with_connect_secret("consumed-secret-finalize"), 2755 ) 2756 .expect("register connect consumed"); 2757 let connect_consumed_workflow = manager 2758 .begin_connect_secret_publish_finalization(&connect_consumed_record.connection_id) 2759 .expect("begin connect consumed workflow"); 2760 manager 2761 .mark_publish_workflow_published(&connect_consumed_workflow.workflow_id) 2762 .expect("mark connect consumed workflow"); 2763 { 2764 let mut state = manager.state.write().expect("write"); 2765 let record = state 2766 .connections 2767 .iter_mut() 2768 .find(|record| record.connection_id == connect_consumed_record.connection_id) 2769 .expect("stored connection"); 2770 record.connect_secret_consumed_at_unix = Some(88); 2771 } 2772 let connect_consumed_err = manager 2773 .finalize_publish_workflow(&connect_consumed_workflow.workflow_id) 2774 .expect_err("consumed connect secret finalize"); 2775 assert!( 2776 connect_consumed_err 2777 .to_string() 2778 .contains("connect secret already consumed for connection") 2779 ); 2780 2781 let start_auth_replay_workflow = |suffix: u32, 2782 request_id: &str| 2783 -> ( 2784 RadrootsNostrSignerConnectionRecord, 2785 RadrootsNostrSignerPublishWorkflowRecord, 2786 RadrootsNostrSignerPendingRequest, 2787 ) { 2788 let record = manager 2789 .register_connection(RadrootsNostrSignerConnectionDraft::new( 2790 public_key(0x330 + suffix), 2791 public_identity(0x340 + suffix), 2792 )) 2793 .expect("register auth workflow"); 2794 manager 2795 .require_auth_challenge( 2796 &record.connection_id, 2797 format!("{}/auth-workflow-{suffix}", api_primary_https()).as_str(), 2798 ) 2799 .expect("require auth"); 2800 let pending = manager 2801 .set_pending_request(&record.connection_id, request_message(request_id)) 2802 .expect("set pending"); 2803 let pending_request = pending.pending_request.expect("pending request"); 2804 let workflow = manager 2805 .begin_auth_replay_publish_finalization(&record.connection_id) 2806 .expect("begin auth workflow"); 2807 (record, workflow, pending_request) 2808 }; 2809 2810 let (missing_pending_record, missing_pending_workflow, _) = 2811 start_auth_replay_workflow(0, "req-eval-missing-pending"); 2812 { 2813 let mut state = manager.state.write().expect("write"); 2814 let workflow = state 2815 .publish_workflows 2816 .iter_mut() 2817 .find(|workflow| workflow.workflow_id == missing_pending_workflow.workflow_id) 2818 .expect("stored workflow"); 2819 workflow.pending_request = None; 2820 } 2821 let missing_pending_eval_err = manager 2822 .evaluate_auth_replay_publish_workflow(&missing_pending_workflow.workflow_id) 2823 .expect_err("missing pending evaluate"); 2824 assert!( 2825 missing_pending_eval_err 2826 .to_string() 2827 .contains("auth replay workflow missing pending request") 2828 ); 2829 { 2830 let mut state = manager.state.write().expect("write"); 2831 state 2832 .publish_workflows 2833 .retain(|workflow| workflow.workflow_id != missing_pending_workflow.workflow_id); 2834 state 2835 .connections 2836 .retain(|record| record.connection_id != missing_pending_record.connection_id); 2837 } 2838 2839 let (missing_challenge_eval_record, missing_challenge_eval_workflow, pending_request) = 2840 start_auth_replay_workflow(1, "req-eval-no-challenge"); 2841 { 2842 let mut state = manager.state.write().expect("write"); 2843 let record = state 2844 .connections 2845 .iter_mut() 2846 .find(|record| record.connection_id == missing_challenge_eval_record.connection_id) 2847 .expect("stored connection"); 2848 record.auth_challenge = None; 2849 } 2850 let evaluation = manager 2851 .evaluate_auth_replay_publish_workflow(&missing_challenge_eval_workflow.workflow_id) 2852 .expect("evaluate without challenge"); 2853 assert_eq!( 2854 evaluation.request_id.as_str(), 2855 pending_request.request_id().as_str() 2856 ); 2857 assert_eq!( 2858 evaluation.connection.auth_state, 2859 RadrootsNostrSignerAuthState::Authorized 2860 ); 2861 assert!(evaluation.connection.pending_request.is_none()); 2862 2863 let (invalid_identity_eval_record, invalid_identity_eval_workflow, _) = 2864 start_auth_replay_workflow(10, "req-eval-invalid-identity"); 2865 { 2866 let mut state = manager.state.write().expect("write"); 2867 let pending_request = RadrootsNostrSignerPendingRequest::new( 2868 request_message_with_request( 2869 "req-eval-invalid-identity", 2870 RadrootsNostrConnectRequest::GetSessionCapability, 2871 ), 2872 81, 2873 ) 2874 .expect("pending request"); 2875 let workflow = state 2876 .publish_workflows 2877 .iter_mut() 2878 .find(|workflow| workflow.workflow_id == invalid_identity_eval_workflow.workflow_id) 2879 .expect("stored workflow"); 2880 workflow.pending_request = Some(pending_request.clone()); 2881 let record = state 2882 .connections 2883 .iter_mut() 2884 .find(|record| record.connection_id == invalid_identity_eval_record.connection_id) 2885 .expect("stored connection"); 2886 record.pending_request = Some(pending_request); 2887 record.user_identity.public_key_hex = "invalid".into(); 2888 } 2889 let invalid_identity_eval_err = manager 2890 .evaluate_auth_replay_publish_workflow(&invalid_identity_eval_workflow.workflow_id) 2891 .expect_err("invalid identity evaluate"); 2892 assert!( 2893 invalid_identity_eval_err 2894 .to_string() 2895 .contains("user identity public key is invalid") 2896 ); 2897 2898 let (terminal_eval_record, terminal_eval_workflow, _) = 2899 start_auth_replay_workflow(2, "req-eval-terminal"); 2900 { 2901 let mut state = manager.state.write().expect("write"); 2902 let record = state 2903 .connections 2904 .iter_mut() 2905 .find(|record| record.connection_id == terminal_eval_record.connection_id) 2906 .expect("stored connection"); 2907 record.status = RadrootsNostrSignerConnectionStatus::Rejected; 2908 } 2909 let terminal_eval_err = manager 2910 .evaluate_auth_replay_publish_workflow(&terminal_eval_workflow.workflow_id) 2911 .expect_err("terminal evaluate"); 2912 assert!( 2913 terminal_eval_err 2914 .to_string() 2915 .contains("cannot evaluate auth replay workflow for rejected connection") 2916 ); 2917 2918 let (not_pending_eval_record, not_pending_eval_workflow, _) = 2919 start_auth_replay_workflow(3, "req-eval-not-pending"); 2920 { 2921 let mut state = manager.state.write().expect("write"); 2922 let record = state 2923 .connections 2924 .iter_mut() 2925 .find(|record| record.connection_id == not_pending_eval_record.connection_id) 2926 .expect("stored connection"); 2927 record.auth_state = RadrootsNostrSignerAuthState::Authorized; 2928 } 2929 let not_pending_eval_err = manager 2930 .evaluate_auth_replay_publish_workflow(¬_pending_eval_workflow.workflow_id) 2931 .expect_err("not pending evaluate"); 2932 assert!( 2933 not_pending_eval_err 2934 .to_string() 2935 .contains("auth challenge not pending for connection") 2936 ); 2937 2938 let (mismatch_eval_record, mismatch_eval_workflow, _) = 2939 start_auth_replay_workflow(4, "req-eval-mismatch"); 2940 { 2941 let mut state = manager.state.write().expect("write"); 2942 let record = state 2943 .connections 2944 .iter_mut() 2945 .find(|record| record.connection_id == mismatch_eval_record.connection_id) 2946 .expect("stored connection"); 2947 record.pending_request = Some( 2948 RadrootsNostrSignerPendingRequest::new( 2949 request_message("req-eval-mismatch-other"), 2950 77, 2951 ) 2952 .expect("mismatched pending request"), 2953 ); 2954 } 2955 let mismatch_eval_err = manager 2956 .evaluate_auth_replay_publish_workflow(&mismatch_eval_workflow.workflow_id) 2957 .expect_err("mismatch evaluate"); 2958 assert!( 2959 mismatch_eval_err 2960 .to_string() 2961 .contains("pending request does not match auth replay workflow") 2962 ); 2963 2964 let start_published_auth_workflow = |suffix: u32, request_id: &str| { 2965 let (record, workflow, pending_request) = 2966 start_auth_replay_workflow(suffix, request_id); 2967 let published = manager 2968 .mark_publish_workflow_published(&workflow.workflow_id) 2969 .expect("mark published"); 2970 (record, published, pending_request) 2971 }; 2972 2973 let (auth_not_pending_record, auth_not_pending_workflow, _) = 2974 start_published_auth_workflow(5, "req-finalize-not-pending"); 2975 { 2976 let mut state = manager.state.write().expect("write"); 2977 let record = state 2978 .connections 2979 .iter_mut() 2980 .find(|record| record.connection_id == auth_not_pending_record.connection_id) 2981 .expect("stored connection"); 2982 record.auth_state = RadrootsNostrSignerAuthState::Authorized; 2983 } 2984 let auth_not_pending_err = manager 2985 .finalize_publish_workflow(&auth_not_pending_workflow.workflow_id) 2986 .expect_err("not pending finalize"); 2987 assert!( 2988 auth_not_pending_err 2989 .to_string() 2990 .contains("auth challenge not pending for connection") 2991 ); 2992 2993 let (missing_connection_finalize_record, missing_connection_finalize_workflow, _) = 2994 start_published_auth_workflow(11, "req-finalize-missing-connection"); 2995 { 2996 let mut state = manager.state.write().expect("write"); 2997 let workflow = state 2998 .publish_workflows 2999 .iter_mut() 3000 .find(|workflow| { 3001 workflow.workflow_id == missing_connection_finalize_workflow.workflow_id 3002 }) 3003 .expect("stored workflow"); 3004 workflow.connection_id = 3005 RadrootsNostrSignerConnectionId::parse("conn-finalize-missing") 3006 .expect("connection id"); 3007 } 3008 let missing_connection_finalize_err = manager 3009 .finalize_publish_workflow(&missing_connection_finalize_workflow.workflow_id) 3010 .expect_err("missing connection finalize"); 3011 assert!( 3012 missing_connection_finalize_err 3013 .to_string() 3014 .contains("connection not found") 3015 ); 3016 { 3017 let mut state = manager.state.write().expect("write"); 3018 state.publish_workflows.retain(|workflow| { 3019 workflow.workflow_id != missing_connection_finalize_workflow.workflow_id 3020 }); 3021 state.connections.retain(|record| { 3022 record.connection_id != missing_connection_finalize_record.connection_id 3023 }); 3024 } 3025 3026 let (auth_missing_challenge_record, auth_missing_challenge_workflow, _) = 3027 start_published_auth_workflow(6, "req-finalize-missing-challenge"); 3028 { 3029 let mut state = manager.state.write().expect("write"); 3030 let record = state 3031 .connections 3032 .iter_mut() 3033 .find(|record| record.connection_id == auth_missing_challenge_record.connection_id) 3034 .expect("stored connection"); 3035 record.auth_challenge = None; 3036 } 3037 let auth_missing_challenge_err = manager 3038 .finalize_publish_workflow(&auth_missing_challenge_workflow.workflow_id) 3039 .expect_err("missing challenge finalize"); 3040 assert!( 3041 auth_missing_challenge_err 3042 .to_string() 3043 .contains("auth challenge missing for connection") 3044 ); 3045 3046 let (workflow_missing_pending_record, workflow_missing_pending_workflow, _) = 3047 start_published_auth_workflow(7, "req-finalize-workflow-missing-pending"); 3048 { 3049 let mut state = manager.state.write().expect("write"); 3050 let workflow = state 3051 .publish_workflows 3052 .iter_mut() 3053 .find(|workflow| { 3054 workflow.workflow_id == workflow_missing_pending_workflow.workflow_id 3055 }) 3056 .expect("stored workflow"); 3057 workflow.pending_request = None; 3058 } 3059 let workflow_missing_pending_err = manager 3060 .finalize_publish_workflow(&workflow_missing_pending_workflow.workflow_id) 3061 .expect_err("workflow missing pending finalize"); 3062 assert!( 3063 workflow_missing_pending_err 3064 .to_string() 3065 .contains("auth replay workflow missing pending request") 3066 ); 3067 { 3068 let mut state = manager.state.write().expect("write"); 3069 state.publish_workflows.retain(|workflow| { 3070 workflow.workflow_id != workflow_missing_pending_workflow.workflow_id 3071 }); 3072 state.connections.retain(|record| { 3073 record.connection_id != workflow_missing_pending_record.connection_id 3074 }); 3075 } 3076 3077 let (mismatch_finalize_record, mismatch_finalize_workflow, _) = 3078 start_published_auth_workflow(8, "req-finalize-mismatch"); 3079 { 3080 let mut state = manager.state.write().expect("write"); 3081 let record = state 3082 .connections 3083 .iter_mut() 3084 .find(|record| record.connection_id == mismatch_finalize_record.connection_id) 3085 .expect("stored connection"); 3086 record.pending_request = Some( 3087 RadrootsNostrSignerPendingRequest::new( 3088 request_message("req-finalize-mismatch-other"), 3089 78, 3090 ) 3091 .expect("mismatched pending request"), 3092 ); 3093 } 3094 let mismatch_finalize_err = manager 3095 .finalize_publish_workflow(&mismatch_finalize_workflow.workflow_id) 3096 .expect_err("mismatch finalize"); 3097 assert!( 3098 mismatch_finalize_err 3099 .to_string() 3100 .contains("pending request does not match auth replay workflow") 3101 ); 3102 3103 let (missing_authorized_record, missing_authorized_workflow, _) = 3104 start_published_auth_workflow(9, "req-finalize-missing-authorized"); 3105 { 3106 let mut state = manager.state.write().expect("write"); 3107 let workflow = state 3108 .publish_workflows 3109 .iter_mut() 3110 .find(|workflow| workflow.workflow_id == missing_authorized_workflow.workflow_id) 3111 .expect("stored workflow"); 3112 workflow.authorized_at_unix = None; 3113 } 3114 let missing_authorized_err = manager 3115 .finalize_publish_workflow(&missing_authorized_workflow.workflow_id) 3116 .expect_err("missing authorized finalize"); 3117 assert!( 3118 missing_authorized_err 3119 .to_string() 3120 .contains("auth replay workflow missing authorized timestamp") 3121 ); 3122 { 3123 let mut state = manager.state.write().expect("write"); 3124 state 3125 .publish_workflows 3126 .retain(|workflow| workflow.workflow_id != missing_authorized_workflow.workflow_id); 3127 state 3128 .connections 3129 .retain(|record| record.connection_id != missing_authorized_record.connection_id); 3130 } 3131 3132 let (missing_connection_eval_record, missing_connection_eval_workflow, _) = 3133 start_auth_replay_workflow(12, "req-eval-missing-connection"); 3134 { 3135 let mut state = manager.state.write().expect("write"); 3136 let workflow = state 3137 .publish_workflows 3138 .iter_mut() 3139 .find(|workflow| { 3140 workflow.workflow_id == missing_connection_eval_workflow.workflow_id 3141 }) 3142 .expect("stored workflow"); 3143 workflow.connection_id = 3144 RadrootsNostrSignerConnectionId::parse("conn-evaluate-missing") 3145 .expect("connection id"); 3146 } 3147 let missing_connection_eval_err = manager 3148 .evaluate_auth_replay_publish_workflow(&missing_connection_eval_workflow.workflow_id) 3149 .expect_err("missing connection evaluate"); 3150 assert!( 3151 missing_connection_eval_err 3152 .to_string() 3153 .contains("connection not found") 3154 ); 3155 { 3156 let mut state = manager.state.write().expect("write"); 3157 state.publish_workflows.retain(|workflow| { 3158 workflow.workflow_id != missing_connection_eval_workflow.workflow_id 3159 }); 3160 state.connections.retain(|record| { 3161 record.connection_id != missing_connection_eval_record.connection_id 3162 }); 3163 } 3164 } 3165 3166 #[test] 3167 fn manager_reports_missing_connections_and_save_failures() { 3168 let manager = RadrootsNostrSignerManager::new_in_memory(); 3169 let missing_id = RadrootsNostrSignerConnectionId::parse("missing").expect("id"); 3170 let missing_get = manager.get_connection(&missing_id).expect("missing get"); 3171 assert!(missing_get.is_none()); 3172 3173 let mark_err = manager 3174 .mark_authenticated(&missing_id) 3175 .expect_err("missing auth"); 3176 assert!(mark_err.to_string().contains("connection not found")); 3177 3178 let save_error_store = 3179 Arc::new(SaveErrorStore::new(RadrootsNostrSignerStoreState::default())); 3180 let loaded_state = save_error_store.load().expect("load save error store"); 3181 assert_eq!(loaded_state.version, RADROOTS_NOSTR_SIGNER_STORE_VERSION); 3182 let manager = RadrootsNostrSignerManager::new(save_error_store).expect("manager"); 3183 let err = manager 3184 .set_signer_identity(public_identity(0x33)) 3185 .expect_err("save error"); 3186 assert!(err.to_string().contains("store save failed")); 3187 3188 let signer_identity = public_identity(0x243); 3189 let connection = RadrootsNostrSignerConnectionRecord::new( 3190 RadrootsNostrSignerConnectionId::parse("conn-save-error").expect("id"), 3191 signer_identity.clone(), 3192 RadrootsNostrSignerConnectionDraft::new(public_key(0x244), public_identity(0x245)) 3193 .with_connect_secret("save-error-secret"), 3194 1, 3195 ); 3196 let manager = RadrootsNostrSignerManager::new(Arc::new(SaveErrorStore::new( 3197 RadrootsNostrSignerStoreState { 3198 version: RADROOTS_NOSTR_SIGNER_STORE_VERSION, 3199 signer_identity: Some(signer_identity), 3200 connections: vec![connection.clone()], 3201 audit_records: Vec::new(), 3202 publish_workflows: Vec::new(), 3203 }, 3204 ))) 3205 .expect("manager with preloaded state"); 3206 let workflow_err = manager 3207 .begin_connect_secret_publish_finalization(&connection.connection_id) 3208 .expect_err("workflow save error"); 3209 assert!(workflow_err.to_string().contains("store save failed")); 3210 } 3211 3212 #[test] 3213 fn mutation_methods_cover_remaining_error_paths() { 3214 let manager = RadrootsNostrSignerManager::new_in_memory(); 3215 manager 3216 .set_signer_identity(public_identity(0x51)) 3217 .expect("set signer"); 3218 3219 let missing_id = RadrootsNostrSignerConnectionId::parse("missing-2").expect("id"); 3220 let missing_permissions: RadrootsNostrConnectPermissions = 3221 vec![permission(RadrootsNostrConnectMethod::Ping, None)].into(); 3222 3223 let missing_grants = manager 3224 .set_granted_permissions(&missing_id, missing_permissions.clone()) 3225 .expect_err("missing grants"); 3226 let missing_approve = manager 3227 .approve_connection(&missing_id, RadrootsNostrConnectPermissions::default()) 3228 .expect_err("missing approve"); 3229 let missing_reject = manager 3230 .reject_connection(&missing_id, None) 3231 .expect_err("missing reject"); 3232 let missing_revoke = manager 3233 .revoke_connection(&missing_id, None) 3234 .expect_err("missing revoke"); 3235 let missing_relays = manager 3236 .update_relays(&missing_id, vec![primary_relay()]) 3237 .expect_err("missing relays"); 3238 let missing_require_auth = manager 3239 .require_auth_challenge(&missing_id, api_primary_https()) 3240 .expect_err("missing require auth"); 3241 let missing_pending_request = manager 3242 .set_pending_request(&missing_id, request_message("req-missing-2")) 3243 .expect_err("missing pending request"); 3244 let missing_begin_connect_workflow = manager 3245 .begin_connect_secret_publish_finalization(&missing_id) 3246 .expect_err("missing connect workflow"); 3247 let missing_begin_auth_workflow = manager 3248 .begin_auth_replay_publish_finalization(&missing_id) 3249 .expect_err("missing auth workflow"); 3250 let missing_authorize_auth = manager 3251 .authorize_auth_challenge(&missing_id) 3252 .expect_err("missing authorize auth"); 3253 let missing_request = manager 3254 .record_request( 3255 &missing_id, 3256 "req-missing", 3257 RadrootsNostrConnectMethod::Ping, 3258 RadrootsNostrSignerRequestDecision::Denied, 3259 None, 3260 ) 3261 .expect_err("missing request"); 3262 3263 for err in [ 3264 missing_grants, 3265 missing_approve, 3266 missing_reject, 3267 missing_revoke, 3268 missing_relays, 3269 missing_require_auth, 3270 missing_pending_request, 3271 missing_begin_connect_workflow, 3272 missing_begin_auth_workflow, 3273 missing_authorize_auth, 3274 missing_request, 3275 ] { 3276 assert!(err.to_string().contains("connection not found")); 3277 } 3278 3279 let requested = vec![permission(RadrootsNostrConnectMethod::Ping, None)]; 3280 let pending = manager 3281 .register_connection( 3282 RadrootsNostrSignerConnectionDraft::new(public_key(0x52), public_identity(0x53)) 3283 .with_requested_permissions(requested.into()) 3284 .with_approval_requirement( 3285 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 3286 ), 3287 ) 3288 .expect("register pending"); 3289 let invalid_approve = manager 3290 .approve_connection( 3291 &pending.connection_id, 3292 vec![permission( 3293 RadrootsNostrConnectMethod::Nip44Encrypt, 3294 Some("kind:1"), 3295 )] 3296 .into(), 3297 ) 3298 .expect_err("invalid approve grants"); 3299 assert!( 3300 invalid_approve 3301 .to_string() 3302 .contains("invalid granted permission") 3303 ); 3304 3305 let auth_required = manager 3306 .require_auth_challenge(&pending.connection_id, api_primary_https()) 3307 .expect("require auth"); 3308 assert_eq!( 3309 auth_required.auth_state, 3310 RadrootsNostrSignerAuthState::Pending 3311 ); 3312 3313 let invalid_pending_request = manager 3314 .set_pending_request(&pending.connection_id, request_message(" ")) 3315 .expect_err("invalid pending request id"); 3316 assert!( 3317 invalid_pending_request 3318 .to_string() 3319 .contains("invalid request id") 3320 ); 3321 3322 let update_state_err = manager 3323 .update_state(|_| Err(RadrootsNostrSignerError::InvalidState("manual".into()))) 3324 .expect_err("update_state error"); 3325 assert!(update_state_err.to_string().contains("manual")); 3326 } 3327 3328 #[test] 3329 fn register_connection_rejects_invalid_persisted_signer_identity() { 3330 let store = Arc::new(RadrootsNostrMemorySignerStore::new()); 3331 let mut state = RadrootsNostrSignerStoreState::default(); 3332 state.signer_identity = Some(invalid_public_identity(0x54)); 3333 store.save(&state).expect("seed state"); 3334 3335 let manager = RadrootsNostrSignerManager::new(store).expect("manager"); 3336 let err = manager 3337 .register_connection(RadrootsNostrSignerConnectionDraft::new( 3338 public_key(0x55), 3339 public_identity(0x56), 3340 )) 3341 .expect_err("invalid signer identity"); 3342 assert!( 3343 err.to_string() 3344 .contains("public identity id does not match public key") 3345 ); 3346 } 3347 3348 #[test] 3349 fn manager_reports_poisoned_state_lock() { 3350 let manager = RadrootsNostrSignerManager::new_in_memory(); 3351 poison_manager_state(&manager); 3352 3353 let identity = manager.signer_identity().expect_err("poisoned read"); 3354 assert!(identity.to_string().contains("signer state lock poisoned")); 3355 } 3356 3357 #[test] 3358 fn read_helpers_report_poisoned_state_lock() { 3359 let manager = RadrootsNostrSignerManager::new_in_memory(); 3360 poison_manager_state(&manager); 3361 3362 let connection_id = RadrootsNostrSignerConnectionId::parse("conn-1").expect("id"); 3363 let client_public_key = public_key(0x47); 3364 3365 let get_err = manager 3366 .get_connection(&connection_id) 3367 .expect_err("poisoned get"); 3368 let list_err = manager.list_connections().expect_err("poisoned list"); 3369 let audit_list_err = manager 3370 .list_audit_records() 3371 .expect_err("poisoned audit list"); 3372 let audit_for_connection_err = manager 3373 .audit_records_for_connection(&connection_id) 3374 .expect_err("poisoned audit connection"); 3375 let workflow_list_err = manager 3376 .list_publish_workflows() 3377 .expect_err("poisoned workflow list"); 3378 let workflow_get_err = manager 3379 .get_publish_workflow(&RadrootsNostrSignerWorkflowId::parse("wf-poison").expect("id")) 3380 .expect_err("poisoned workflow get"); 3381 let find_secret_err = manager 3382 .find_connection_by_connect_secret("secret") 3383 .expect_err("poisoned secret lookup"); 3384 let find_client_err = manager 3385 .find_connections_by_client_public_key(&client_public_key) 3386 .expect_err("poisoned client lookup"); 3387 let lookup_secret_err = manager 3388 .lookup_session(&client_public_key, Some("secret")) 3389 .expect_err("poisoned session secret lookup"); 3390 let lookup_client_err = manager 3391 .lookup_session(&client_public_key, None) 3392 .expect_err("poisoned session client lookup"); 3393 3394 for err in [ 3395 get_err, 3396 list_err, 3397 audit_list_err, 3398 audit_for_connection_err, 3399 workflow_list_err, 3400 workflow_get_err, 3401 find_secret_err, 3402 find_client_err, 3403 lookup_secret_err, 3404 lookup_client_err, 3405 ] { 3406 assert!(err.to_string().contains("signer state lock poisoned")); 3407 } 3408 } 3409 3410 #[test] 3411 fn evaluate_connect_request_reports_poisoned_state_lock() { 3412 let store = Arc::new(RadrootsNostrMemorySignerStore::new()); 3413 let signer_identity = public_identity(0x57); 3414 let mut state = RadrootsNostrSignerStoreState::default(); 3415 state.signer_identity = Some(signer_identity.clone()); 3416 store.save(&state).expect("save state"); 3417 3418 let manager = RadrootsNostrSignerManager::new(store).expect("manager"); 3419 poison_manager_state(&manager); 3420 3421 let err = manager 3422 .evaluate_connect_request( 3423 public_key(0x58), 3424 RadrootsNostrConnectRequest::Connect { 3425 remote_signer_public_key: PublicKey::parse( 3426 signer_identity.public_key_hex.as_str(), 3427 ) 3428 .expect("signer public key"), 3429 secret: Some("secret".into()), 3430 requested_permissions: RadrootsNostrConnectPermissions::default(), 3431 }, 3432 ) 3433 .expect_err("poisoned connect evaluation"); 3434 assert!(err.to_string().contains("signer state lock poisoned")); 3435 } 3436 3437 #[test] 3438 fn mutation_helpers_report_poisoned_state_lock() { 3439 let manager = RadrootsNostrSignerManager::new_in_memory(); 3440 poison_manager_state(&manager); 3441 3442 let signer_identity = public_identity(0x48); 3443 let connection_id = RadrootsNostrSignerConnectionId::parse("conn-2").expect("id"); 3444 let workflow_id = RadrootsNostrSignerWorkflowId::parse("wf-2").expect("id"); 3445 let connect_draft = 3446 RadrootsNostrSignerConnectionDraft::new(public_key(0x49), public_identity(0x50)); 3447 3448 let set_signer_err = manager 3449 .set_signer_identity(signer_identity) 3450 .expect_err("poisoned set signer"); 3451 let register_err = manager 3452 .register_connection(connect_draft) 3453 .expect_err("poisoned register"); 3454 let grants_err = manager 3455 .set_granted_permissions( 3456 &connection_id, 3457 vec![permission(RadrootsNostrConnectMethod::Ping, None)].into(), 3458 ) 3459 .expect_err("poisoned set grants"); 3460 let approve_err = manager 3461 .approve_connection(&connection_id, RadrootsNostrConnectPermissions::default()) 3462 .expect_err("poisoned approve"); 3463 let reject_err = manager 3464 .reject_connection(&connection_id, Some("reason".into())) 3465 .expect_err("poisoned reject"); 3466 let revoke_err = manager 3467 .revoke_connection(&connection_id, Some("reason".into())) 3468 .expect_err("poisoned revoke"); 3469 let update_relays_err = manager 3470 .update_relays(&connection_id, vec![primary_relay()]) 3471 .expect_err("poisoned relays"); 3472 let require_auth_err = manager 3473 .require_auth_challenge(&connection_id, api_primary_https()) 3474 .expect_err("poisoned require auth"); 3475 let set_pending_request_err = manager 3476 .set_pending_request(&connection_id, request_message("req-2")) 3477 .expect_err("poisoned set pending request"); 3478 let authorize_auth_err = manager 3479 .authorize_auth_challenge(&connection_id) 3480 .expect_err("poisoned authorize auth"); 3481 let begin_connect_workflow_err = manager 3482 .begin_connect_secret_publish_finalization(&connection_id) 3483 .expect_err("poisoned connect workflow"); 3484 let begin_auth_workflow_err = manager 3485 .begin_auth_replay_publish_finalization(&connection_id) 3486 .expect_err("poisoned auth workflow"); 3487 let mark_workflow_err = manager 3488 .mark_publish_workflow_published(&workflow_id) 3489 .expect_err("poisoned mark workflow"); 3490 let finalize_workflow_err = manager 3491 .finalize_publish_workflow(&workflow_id) 3492 .expect_err("poisoned finalize workflow"); 3493 let cancel_workflow_err = manager 3494 .cancel_publish_workflow(&workflow_id) 3495 .expect_err("poisoned cancel workflow"); 3496 let auth_err = manager 3497 .mark_authenticated(&connection_id) 3498 .expect_err("poisoned auth"); 3499 let request_err = manager 3500 .record_request( 3501 &connection_id, 3502 "req-1", 3503 RadrootsNostrConnectMethod::Ping, 3504 RadrootsNostrSignerRequestDecision::Allowed, 3505 None, 3506 ) 3507 .expect_err("poisoned request"); 3508 3509 for err in [ 3510 set_signer_err, 3511 register_err, 3512 grants_err, 3513 approve_err, 3514 reject_err, 3515 revoke_err, 3516 update_relays_err, 3517 require_auth_err, 3518 set_pending_request_err, 3519 authorize_auth_err, 3520 begin_connect_workflow_err, 3521 begin_auth_workflow_err, 3522 mark_workflow_err, 3523 finalize_workflow_err, 3524 cancel_workflow_err, 3525 auth_err, 3526 request_err, 3527 ] { 3528 assert!(err.to_string().contains("signer state lock poisoned")); 3529 } 3530 } 3531 3532 #[test] 3533 fn save_error_store_reports_poisoned_load_lock() { 3534 let store = SaveErrorStore::new(RadrootsNostrSignerStoreState::default()); 3535 let shared = Arc::new(store); 3536 let poison = shared.clone(); 3537 let _ = thread::spawn(move || { 3538 let _guard = poison.state.write().expect("write"); 3539 panic!("poison save error store"); 3540 }) 3541 .join(); 3542 3543 let err = shared.load().expect_err("poisoned load"); 3544 assert!(err.to_string().contains("save error store poisoned")); 3545 } 3546 3547 #[test] 3548 fn helpers_cover_status_labels_and_consumed_secret_reuse_rules() { 3549 assert_eq!( 3550 status_label(RadrootsNostrSignerConnectionStatus::Pending), 3551 "pending" 3552 ); 3553 assert_eq!( 3554 status_label(RadrootsNostrSignerConnectionStatus::Active), 3555 "active" 3556 ); 3557 assert_eq!( 3558 status_label(RadrootsNostrSignerConnectionStatus::Rejected), 3559 "rejected" 3560 ); 3561 assert_eq!( 3562 status_label(RadrootsNostrSignerConnectionStatus::Revoked), 3563 "revoked" 3564 ); 3565 3566 let manager = RadrootsNostrSignerManager::new_in_memory(); 3567 manager 3568 .set_signer_identity(public_identity(0x42)) 3569 .expect("set signer"); 3570 3571 let initial = manager 3572 .register_connection( 3573 RadrootsNostrSignerConnectionDraft::new(public_key(0x43), public_identity(0x44)) 3574 .with_connect_secret("reusable-secret") 3575 .with_approval_requirement( 3576 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 3577 ), 3578 ) 3579 .expect("register initial"); 3580 manager 3581 .reject_connection(&initial.connection_id, Some("closed".into())) 3582 .expect("reject initial"); 3583 3584 let reused = manager 3585 .register_connection( 3586 RadrootsNostrSignerConnectionDraft::new(public_key(0x45), public_identity(0x46)) 3587 .with_connect_secret("reusable-secret"), 3588 ) 3589 .expect("register reused secret"); 3590 3591 assert!( 3592 reused 3593 .connect_secret_hash 3594 .as_ref() 3595 .expect("connect secret hash") 3596 .matches_secret("reusable-secret") 3597 ); 3598 3599 let consumed = manager 3600 .mark_connect_secret_consumed(&reused.connection_id) 3601 .expect("consume secret"); 3602 assert!(consumed.connect_secret_is_consumed()); 3603 manager 3604 .reject_connection(&reused.connection_id, Some("closed".into())) 3605 .expect("reject consumed"); 3606 3607 let blocked_reuse = manager 3608 .register_connection( 3609 RadrootsNostrSignerConnectionDraft::new(public_key(0x47), public_identity(0x48)) 3610 .with_connect_secret("reusable-secret"), 3611 ) 3612 .expect_err("block consumed secret reuse"); 3613 assert!(matches!( 3614 blocked_reuse, 3615 RadrootsNostrSignerError::ConnectSecretAlreadyInUse 3616 )); 3617 } 3618 3619 #[test] 3620 fn session_lookup_and_connect_evaluation_cover_new_paths() { 3621 let manager = RadrootsNostrSignerManager::new_in_memory(); 3622 let signer_identity = public_identity(0x60); 3623 let signer_public_key = 3624 PublicKey::parse(signer_identity.public_key_hex.as_str()).expect("signer public key"); 3625 manager 3626 .set_signer_identity(signer_identity) 3627 .expect("set signer"); 3628 3629 let client_public_key = public_key(0x61); 3630 let primary = manager 3631 .register_connection( 3632 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x62)) 3633 .with_connect_secret("connect-secret"), 3634 ) 3635 .expect("register primary"); 3636 3637 let single_lookup = manager 3638 .lookup_session(&client_public_key, None) 3639 .expect("lookup single"); 3640 assert_same_connection(&expect_connection_lookup(single_lookup), &primary); 3641 3642 let secret_lookup = manager 3643 .lookup_session(&client_public_key, Some("connect-secret")) 3644 .expect("lookup by secret"); 3645 assert_same_connection(&expect_connection_lookup(secret_lookup), &primary); 3646 let missing_secret_lookup = manager 3647 .lookup_session(&client_public_key, Some("missing-secret")) 3648 .expect("lookup missing secret"); 3649 assert_same_connection(&expect_connection_lookup(missing_secret_lookup), &primary); 3650 3651 let second = manager 3652 .register_connection( 3653 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x63)) 3654 .with_connect_secret("second-secret"), 3655 ) 3656 .expect("register second"); 3657 3658 let ambiguous_by_missing_secret = manager 3659 .lookup_session(&client_public_key, Some("missing-secret")) 3660 .expect("lookup missing secret after second"); 3661 let found = expect_ambiguous_lookup(ambiguous_by_missing_secret); 3662 assert_eq!(found.len(), 2); 3663 assert_same_connection(&found[0], &primary); 3664 assert_same_connection(&found[1], &second); 3665 let ambiguous_lookup = manager 3666 .lookup_session(&client_public_key, None) 3667 .expect("lookup ambiguous"); 3668 let found = expect_ambiguous_lookup(ambiguous_lookup); 3669 assert_eq!(found.len(), 2); 3670 assert_same_connection(&found[0], &primary); 3671 assert_same_connection(&found[1], &second); 3672 3673 let mismatch_secret = manager 3674 .lookup_session(&public_key(0x64), Some("connect-secret")) 3675 .expect_err("secret mismatch"); 3676 assert!( 3677 mismatch_secret 3678 .to_string() 3679 .contains("different client public key") 3680 ); 3681 3682 let none_lookup = manager 3683 .lookup_session(&public_key(0x65), None) 3684 .expect("lookup none"); 3685 expect_none_lookup(none_lookup); 3686 3687 let non_connect_err = manager 3688 .evaluate_connect_request(client_public_key, RadrootsNostrConnectRequest::Ping) 3689 .expect_err("non-connect evaluation"); 3690 assert!( 3691 non_connect_err 3692 .to_string() 3693 .contains("connect evaluation requires a connect request") 3694 ); 3695 3696 let missing_signer_err = RadrootsNostrSignerManager::new_in_memory() 3697 .evaluate_connect_request( 3698 client_public_key, 3699 RadrootsNostrConnectRequest::Connect { 3700 remote_signer_public_key: signer_public_key, 3701 secret: None, 3702 requested_permissions: RadrootsNostrConnectPermissions::default(), 3703 }, 3704 ) 3705 .expect_err("missing signer"); 3706 assert_eq!(missing_signer_err.to_string(), "missing signer identity"); 3707 3708 let signer_mismatch_err = manager 3709 .evaluate_connect_request( 3710 client_public_key, 3711 RadrootsNostrConnectRequest::Connect { 3712 remote_signer_public_key: public_key(0x66), 3713 secret: None, 3714 requested_permissions: RadrootsNostrConnectPermissions::default(), 3715 }, 3716 ) 3717 .expect_err("signer mismatch"); 3718 assert!( 3719 signer_mismatch_err 3720 .to_string() 3721 .contains("remote signer public key mismatch") 3722 ); 3723 3724 let existing_connect = manager 3725 .evaluate_connect_request( 3726 client_public_key, 3727 RadrootsNostrConnectRequest::Connect { 3728 remote_signer_public_key: signer_public_key, 3729 secret: Some(" connect-secret ".into()), 3730 requested_permissions: vec![ 3731 permission(RadrootsNostrConnectMethod::Ping, None), 3732 permission(RadrootsNostrConnectMethod::Ping, None), 3733 ] 3734 .into(), 3735 }, 3736 ) 3737 .expect("existing connect request"); 3738 assert_same_connection(&expect_existing_connect(existing_connect), &primary); 3739 3740 let registration_connect = manager 3741 .evaluate_connect_request( 3742 public_key(0x67), 3743 RadrootsNostrConnectRequest::Connect { 3744 remote_signer_public_key: signer_public_key, 3745 secret: Some(" fresh-secret ".into()), 3746 requested_permissions: vec![ 3747 permission(RadrootsNostrConnectMethod::Ping, None), 3748 permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")), 3749 permission(RadrootsNostrConnectMethod::Ping, None), 3750 ] 3751 .into(), 3752 }, 3753 ) 3754 .expect("registration connect request"); 3755 let proposal = expect_registration_connect(registration_connect); 3756 assert_eq!(proposal.client_public_key, public_key(0x67)); 3757 assert_eq!(proposal.connect_secret.as_deref(), Some("fresh-secret")); 3758 assert_eq!( 3759 proposal.requested_permissions.as_slice(), 3760 &[ 3761 permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")), 3762 permission(RadrootsNostrConnectMethod::Ping, None), 3763 ] 3764 ); 3765 3766 let existing_secret_mismatch = manager 3767 .evaluate_connect_request( 3768 public_key(0x68), 3769 RadrootsNostrConnectRequest::Connect { 3770 remote_signer_public_key: signer_public_key, 3771 secret: Some("connect-secret".into()), 3772 requested_permissions: RadrootsNostrConnectPermissions::default(), 3773 }, 3774 ) 3775 .expect_err("existing secret mismatch"); 3776 assert!( 3777 existing_secret_mismatch 3778 .to_string() 3779 .contains("different client public key") 3780 ); 3781 3782 let store = Arc::new(RadrootsNostrMemorySignerStore::new()); 3783 let mut invalid_state = RadrootsNostrSignerStoreState::default(); 3784 let mut invalid_identity = public_identity(0x69); 3785 invalid_identity.public_key_hex = "invalid".into(); 3786 invalid_state.signer_identity = Some(invalid_identity); 3787 store 3788 .save(&invalid_state) 3789 .expect("save invalid signer state"); 3790 let invalid_manager = RadrootsNostrSignerManager::new(store).expect("invalid manager"); 3791 let invalid_signer_err = invalid_manager 3792 .evaluate_connect_request( 3793 public_key(0x70), 3794 RadrootsNostrConnectRequest::Connect { 3795 remote_signer_public_key: signer_public_key, 3796 secret: None, 3797 requested_permissions: RadrootsNostrConnectPermissions::default(), 3798 }, 3799 ) 3800 .expect_err("invalid signer public key"); 3801 assert!( 3802 invalid_signer_err 3803 .to_string() 3804 .contains("identity public key is invalid") 3805 ); 3806 } 3807 3808 #[test] 3809 fn evaluate_request_covers_allowed_denied_and_challenged_paths() { 3810 let manager = RadrootsNostrSignerManager::new_in_memory(); 3811 manager 3812 .set_signer_identity(public_identity(0x71)) 3813 .expect("set signer"); 3814 3815 let active = manager 3816 .register_connection( 3817 RadrootsNostrSignerConnectionDraft::new(public_key(0x72), public_identity(0x73)) 3818 .with_requested_permissions( 3819 vec![permission( 3820 RadrootsNostrConnectMethod::SignEvent, 3821 Some("kind:1"), 3822 )] 3823 .into(), 3824 ), 3825 ) 3826 .expect("register active"); 3827 3828 let get_public_key = manager 3829 .evaluate_request( 3830 &active.connection_id, 3831 request_message_with_request("req-get", RadrootsNostrConnectRequest::GetPublicKey), 3832 ) 3833 .expect("evaluate get_public_key"); 3834 expect_allowed_user_public_key(&get_public_key.action); 3835 assert_eq!( 3836 get_public_key.audit.decision, 3837 RadrootsNostrSignerRequestDecision::Allowed 3838 ); 3839 assert!(get_public_key.denied_reason().is_none()); 3840 3841 let allowed_sign = manager 3842 .evaluate_request( 3843 &active.connection_id, 3844 request_message_with_request( 3845 "req-sign-1", 3846 RadrootsNostrConnectRequest::SignEvent(unsigned_event(1)), 3847 ), 3848 ) 3849 .expect("evaluate sign allowed"); 3850 expect_allowed_without_response_hint(&allowed_sign.action); 3851 3852 let denied_sign = manager 3853 .evaluate_request( 3854 &active.connection_id, 3855 request_message_with_request( 3856 "req-sign-2", 3857 RadrootsNostrConnectRequest::SignEvent(unsigned_event(2)), 3858 ), 3859 ) 3860 .expect("evaluate sign denied"); 3861 assert_eq!(denied_sign.denied_reason(), Some("unauthorized sign_event")); 3862 assert_eq!( 3863 denied_sign.audit.decision, 3864 RadrootsNostrSignerRequestDecision::Denied 3865 ); 3866 3867 let pending = manager 3868 .register_connection( 3869 RadrootsNostrSignerConnectionDraft::new(public_key(0x74), public_identity(0x75)) 3870 .with_approval_requirement( 3871 RadrootsNostrSignerApprovalRequirement::ExplicitUser, 3872 ), 3873 ) 3874 .expect("register pending"); 3875 let pending_eval = manager 3876 .evaluate_request(&pending.connection_id, request_message("req-pending")) 3877 .expect("evaluate pending"); 3878 assert_eq!(pending_eval.denied_reason(), Some("connection is pending")); 3879 3880 let challenged = manager 3881 .register_connection(RadrootsNostrSignerConnectionDraft::new( 3882 public_key(0x76), 3883 public_identity(0x77), 3884 )) 3885 .expect("register challenged"); 3886 manager 3887 .require_auth_challenge(&challenged.connection_id, api_primary_https()) 3888 .expect("require auth challenge"); 3889 let challenged_eval = manager 3890 .evaluate_request(&challenged.connection_id, request_message("req-auth")) 3891 .expect("evaluate challenged"); 3892 expect_challenged_action(&challenged_eval.action); 3893 assert_eq!( 3894 challenged_eval.audit.decision, 3895 RadrootsNostrSignerRequestDecision::Challenged 3896 ); 3897 assert_eq!( 3898 challenged_eval 3899 .connection 3900 .pending_request 3901 .as_ref() 3902 .expect("pending request") 3903 .request_id() 3904 .as_str(), 3905 "req-auth" 3906 ); 3907 3908 let rejected = manager 3909 .reject_connection(&challenged.connection_id, Some("closed".into())) 3910 .expect("reject challenged"); 3911 let rejected_eval = manager 3912 .evaluate_request(&rejected.connection_id, request_message("req-rejected")) 3913 .expect("evaluate rejected"); 3914 assert_eq!( 3915 rejected_eval.denied_reason(), 3916 Some("connection is rejected") 3917 ); 3918 3919 let connect_eval_err = manager 3920 .evaluate_request( 3921 &active.connection_id, 3922 request_message_with_request( 3923 "req-connect", 3924 RadrootsNostrConnectRequest::Connect { 3925 remote_signer_public_key: active.client_public_key, 3926 secret: None, 3927 requested_permissions: RadrootsNostrConnectPermissions::default(), 3928 }, 3929 ), 3930 ) 3931 .expect_err("connect through evaluate_request"); 3932 assert!( 3933 connect_eval_err 3934 .to_string() 3935 .contains("evaluate_connect_request") 3936 ); 3937 } 3938 3939 #[test] 3940 fn evaluate_request_reports_invalid_corrupted_auth_state() { 3941 let store = Arc::new(RadrootsNostrMemorySignerStore::new()); 3942 let signer_identity = public_identity(0x78); 3943 let mut state = RadrootsNostrSignerStoreState::default(); 3944 state.signer_identity = Some(signer_identity.clone()); 3945 let mut record = RadrootsNostrSignerConnectionRecord::new( 3946 RadrootsNostrSignerConnectionId::new_v7(), 3947 signer_identity, 3948 RadrootsNostrSignerConnectionDraft::new(public_key(0x79), public_identity(0x80)), 3949 1, 3950 ); 3951 record.auth_state = RadrootsNostrSignerAuthState::Pending; 3952 record.auth_challenge = None; 3953 state.connections.push(record.clone()); 3954 store.save(&state).expect("save corrupted auth state"); 3955 3956 let manager = RadrootsNostrSignerManager::new(store).expect("manager"); 3957 let err = manager 3958 .evaluate_request(&record.connection_id, request_message("req-corrupt")) 3959 .expect_err("corrupted auth evaluation"); 3960 assert!(err.to_string().contains("auth challenge missing")); 3961 } 3962 3963 #[test] 3964 fn evaluate_request_reports_invalid_request_id_and_missing_connection() { 3965 let manager = RadrootsNostrSignerManager::new_in_memory(); 3966 manager 3967 .set_signer_identity(public_identity(0x81)) 3968 .expect("set signer"); 3969 3970 let active = manager 3971 .register_connection(RadrootsNostrSignerConnectionDraft::new( 3972 public_key(0x82), 3973 public_identity(0x83), 3974 )) 3975 .expect("register active"); 3976 3977 let invalid_request_id = manager 3978 .evaluate_request( 3979 &active.connection_id, 3980 request_message_with_request(" ", RadrootsNostrConnectRequest::Ping), 3981 ) 3982 .expect_err("invalid request id"); 3983 assert!( 3984 invalid_request_id 3985 .to_string() 3986 .contains("invalid request id") 3987 ); 3988 3989 let missing_connection = manager 3990 .evaluate_request( 3991 &RadrootsNostrSignerConnectionId::new_v7(), 3992 request_message("req-missing"), 3993 ) 3994 .expect_err("missing connection"); 3995 assert!( 3996 missing_connection 3997 .to_string() 3998 .contains("connection not found") 3999 ); 4000 } 4001 4002 #[test] 4003 fn evaluate_request_action_reports_pending_request_and_response_hint_errors() { 4004 let mut pending_record = RadrootsNostrSignerConnectionRecord::new( 4005 RadrootsNostrSignerConnectionId::new_v7(), 4006 public_identity(0x84), 4007 RadrootsNostrSignerConnectionDraft::new(public_key(0x85), public_identity(0x86)), 4008 1, 4009 ); 4010 pending_record.status = RadrootsNostrSignerConnectionStatus::Active; 4011 pending_record.auth_state = RadrootsNostrSignerAuthState::Pending; 4012 pending_record.auth_challenge = 4013 Some(RadrootsNostrSignerAuthChallenge::new(api_primary_https(), 1).expect("challenge")); 4014 let invalid_pending = evaluate_request_action( 4015 &mut pending_record, 4016 &request_message_with_request(" ", RadrootsNostrConnectRequest::Ping), 4017 1, 4018 ) 4019 .expect_err("invalid pending request"); 4020 assert!(invalid_pending.to_string().contains("invalid request id")); 4021 4022 let mut invalid_user_record = RadrootsNostrSignerConnectionRecord::new( 4023 RadrootsNostrSignerConnectionId::new_v7(), 4024 public_identity(0x87), 4025 RadrootsNostrSignerConnectionDraft::new(public_key(0x88), public_identity(0x89)), 4026 1, 4027 ); 4028 invalid_user_record.status = RadrootsNostrSignerConnectionStatus::Active; 4029 invalid_user_record.user_identity.public_key_hex = "invalid".into(); 4030 let response_hint_err = evaluate_request_action( 4031 &mut invalid_user_record, 4032 &request_message_with_request("req-get", RadrootsNostrConnectRequest::GetPublicKey), 4033 1, 4034 ) 4035 .expect_err("invalid response hint"); 4036 assert!( 4037 response_hint_err 4038 .to_string() 4039 .contains("user identity public key is invalid") 4040 ); 4041 } 4042 }